diff --git a/hassio/src/addon-view/config/hassio-addon-network.ts b/hassio/src/addon-view/config/hassio-addon-network.ts index d90a68ffd389..99f871be9949 100644 --- a/hassio/src/addon-view/config/hassio-addon-network.ts +++ b/hassio/src/addon-view/config/hassio-addon-network.ts @@ -88,8 +88,8 @@ class HassioAddonNetwork extends LitElement {
- Reset to defaults > + Reset to defaults + Save diff --git a/hassio/src/dashboard/hassio-update.ts b/hassio/src/dashboard/hassio-update.ts index 3bb15444ed52..9bb6cc73ece1 100644 --- a/hassio/src/dashboard/hassio-update.ts +++ b/hassio/src/dashboard/hassio-update.ts @@ -164,8 +164,9 @@ export class HassioUpdate extends LitElement { try { await this.hass.callApi>("POST", item.apiPath); } catch (err) { - // Only show an error if the status code was not 504, or no status at all (connection terminated) - if (err.status_code && err.status_code !== 504) { + // Only show an error if the status code was not expected (user behind proxy) + // or no status at all(connection terminated) + if (err.status_code && ![502, 503, 504].includes(err.status_code)) { showAlertDialog(this, { title: "Update failed", text: extractApiErrorMessage(err), diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index fd043751de48..12af46d091c3 100644 --- a/hassio/src/system/hassio-supervisor-info.ts +++ b/hassio/src/system/hassio-supervisor-info.ts @@ -18,6 +18,7 @@ import { setSupervisorOption, SupervisorOptions, updateSupervisor, + fetchHassioSupervisorInfo, } from "../../../src/data/hassio/supervisor"; import { showAlertDialog, @@ -176,10 +177,11 @@ class HassioSupervisorInfo extends LitElement { try { const data: Partial = { - channel: this.supervisorInfo.channel !== "stable" ? "beta" : "stable", + channel: this.supervisorInfo.channel === "stable" ? "beta" : "stable", }; await setSupervisorOption(this.hass, data); await reloadSupervisor(this.hass); + this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass); } catch (err) { showAlertDialog(this, { title: "Failed to set supervisor option", @@ -195,6 +197,7 @@ class HassioSupervisorInfo extends LitElement { try { await reloadSupervisor(this.hass); + this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass); } catch (err) { showAlertDialog(this, { title: "Failed to reload the supervisor", diff --git a/package.json b/package.json index 1410d6bf3b0c..1db467294c02 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "@polymer/polymer": "3.1.0", "@thomasloven/round-slider": "0.5.0", "@types/chromecast-caf-sender": "^1.0.3", + "@types/sortablejs": "^1.10.6", "@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-date-picker": "^4.0.7", "@vue/web-component-wrapper": "^1.2.0", diff --git a/setup.py b/setup.py index 8ca7a0f6d63f..b1a1ef7a73d7 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="home-assistant-frontend", - version="20200901.0", + version="20200907.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/common/decorators/local-storage.ts b/src/common/decorators/local-storage.ts index 99cdebdf3d1c..d4034ae25fa0 100644 --- a/src/common/decorators/local-storage.ts +++ b/src/common/decorators/local-storage.ts @@ -1,7 +1,33 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { PropertyDeclaration, UpdatingElement } from "lit-element"; import type { ClassElement } from "../../types"; +type Callback = (oldValue: any, newValue: any) => void; + class Storage { - private _storage: any = {}; + constructor() { + window.addEventListener("storage", (ev: StorageEvent) => { + if (ev.key && this.hasKey(ev.key)) { + this._storage[ev.key] = ev.newValue + ? JSON.parse(ev.newValue) + : ev.newValue; + if (this._listeners[ev.key]) { + this._listeners[ev.key].forEach((listener) => + listener( + ev.oldValue ? JSON.parse(ev.oldValue) : ev.oldValue, + this._storage[ev.key!] + ) + ); + } + } + }); + } + + private _storage: { [storageKey: string]: any } = {}; + + private _listeners: { + [storageKey: string]: Callback[]; + } = {}; public addFromStorage(storageKey: any): void { if (!this._storage[storageKey]) { @@ -12,6 +38,30 @@ class Storage { } } + public subscribeChanges( + storageKey: string, + callback: Callback + ): UnsubscribeFunc { + if (this._listeners[storageKey]) { + this._listeners[storageKey].push(callback); + } else { + this._listeners[storageKey] = [callback]; + } + return () => { + this.unsubscribeChanges(storageKey, callback); + }; + } + + public unsubscribeChanges(storageKey: string, callback: Callback) { + if (!(storageKey in this._listeners)) { + return; + } + const index = this._listeners[storageKey].indexOf(callback); + if (index !== -1) { + this._listeners[storageKey].splice(index, 1); + } + } + public hasKey(storageKey: string): any { return storageKey in this._storage; } @@ -32,30 +82,49 @@ class Storage { const storage = new Storage(); -export const LocalStorage = (key?: string) => { - return (element: ClassElement, propName: string) => { - const storageKey = key || propName; - const initVal = element.initializer ? element.initializer() : undefined; +export const LocalStorage = ( + storageKey?: string, + property?: boolean, + propertyOptions?: PropertyDeclaration +): any => { + return (clsElement: ClassElement) => { + const key = String(clsElement.key); + storageKey = storageKey || String(clsElement.key); + const initVal = clsElement.initializer + ? clsElement.initializer() + : undefined; storage.addFromStorage(storageKey); + const subscribe = (el: UpdatingElement): UnsubscribeFunc => + storage.subscribeChanges(storageKey!, (oldValue) => { + el.requestUpdate(clsElement.key, oldValue); + }); + const getValue = (): any => { - return storage.hasKey(storageKey) - ? storage.getValue(storageKey) + return storage.hasKey(storageKey!) + ? storage.getValue(storageKey!) : initVal; }; - const setValue = (val: any) => { - storage.setValue(storageKey, val); + const setValue = (el: UpdatingElement, value: any) => { + let oldValue: unknown | undefined; + if (property) { + oldValue = getValue(); + } + storage.setValue(storageKey!, value); + if (property) { + el.requestUpdate(clsElement.key, oldValue); + } }; return { kind: "method", - placement: "own", - key: element.key, + placement: "prototype", + key: clsElement.key, descriptor: { - set(value) { - setValue(value); + set(this: UpdatingElement, value: unknown) { + setValue(this, value); }, get() { return getValue(); @@ -63,6 +132,24 @@ export const LocalStorage = (key?: string) => { enumerable: true, configurable: true, }, + finisher(cls: typeof UpdatingElement) { + if (property) { + const connectedCallback = cls.prototype.connectedCallback; + const disconnectedCallback = cls.prototype.disconnectedCallback; + cls.prototype.connectedCallback = function () { + connectedCallback.call(this); + this[`__unbsubLocalStorage${key}`] = subscribe(this); + }; + cls.prototype.disconnectedCallback = function () { + disconnectedCallback.call(this); + this[`__unbsubLocalStorage${key}`](); + }; + cls.createProperty(clsElement.key, { + noAccessor: true, + ...propertyOptions, + }); + } + }, }; }; }; diff --git a/src/components/entity/ha-entity-attribute-picker.ts b/src/components/entity/ha-entity-attribute-picker.ts new file mode 100644 index 000000000000..ee2958323b46 --- /dev/null +++ b/src/components/entity/ha-entity-attribute-picker.ts @@ -0,0 +1,178 @@ +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-item/paper-item"; +import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; +import { HassEntity } from "home-assistant-js-websocket"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + PropertyValues, + query, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../common/dom/fire_event"; +import { PolymerChangedEvent } from "../../polymer-types"; +import { HomeAssistant } from "../../types"; +import "../ha-icon-button"; +import "./state-badge"; + +export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; + +const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => { + if (!root.firstElementChild) { + root.innerHTML = ` + + + `; + } + root.querySelector("paper-item")!.textContent = model.item; +}; + +@customElement("ha-entity-attribute-picker") +class HaEntityAttributePicker extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public entityId?: string; + + @property({ type: Boolean }) public autofocus = false; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean, attribute: "allow-custom-value" }) + public allowCustomValue; + + @property() public label?: string; + + @property() public value?: string; + + @property({ type: Boolean }) private _opened = false; + + @query("vaadin-combo-box-light") private _comboBox!: HTMLElement; + + protected shouldUpdate(changedProps: PropertyValues) { + return !(!changedProps.has("_opened") && this._opened); + } + + protected updated(changedProps: PropertyValues) { + if (changedProps.has("_opened") && this._opened) { + const state = this.entityId ? this.hass.states[this.entityId] : undefined; + (this._comboBox as any).items = state + ? Object.keys(state.attributes) + : []; + } + } + + protected render(): TemplateResult { + if (!this.hass) { + return html``; + } + + return html` + + + ${this.value + ? html` + + Clear + + ` + : ""} + + + Toggle + + + + `; + } + + private _clearValue(ev: Event) { + ev.stopPropagation(); + this._setValue(""); + } + + private get _value() { + return this.value || ""; + } + + private _openedChanged(ev: PolymerChangedEvent) { + this._opened = ev.detail.value; + } + + private _valueChanged(ev: PolymerChangedEvent) { + const newValue = ev.detail.value; + if (newValue !== this._value) { + this._setValue(newValue); + } + } + + private _setValue(value: string) { + this.value = value; + setTimeout(() => { + fireEvent(this, "value-changed", { value }); + fireEvent(this, "change"); + }, 0); + } + + static get styles(): CSSResult { + return css` + paper-input > ha-icon-button { + --mdc-icon-button-size: 24px; + padding: 0px 2px; + color: var(--secondary-text-color); + } + [hidden] { + display: none; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-entity-attribute-picker": HaEntityAttributePicker; + } +} diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 300ae57a7924..51ddf0d30f05 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -1,4 +1,3 @@ -import "../ha-icon-button"; import "@polymer/paper-input/paper-input"; import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; @@ -20,6 +19,7 @@ import { computeDomain } from "../../common/entity/compute_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; import { PolymerChangedEvent } from "../../polymer-types"; import { HomeAssistant } from "../../types"; +import "../ha-icon-button"; import "./state-badge"; export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; @@ -95,6 +95,8 @@ class HaEntityPicker extends LitElement { @query("vaadin-combo-box-light") private _comboBox!: HTMLElement; + private _initedStates = false; + private _getStates = memoizeOne( ( _opened: boolean, @@ -148,11 +150,18 @@ class HaEntityPicker extends LitElement { ); protected shouldUpdate(changedProps: PropertyValues) { + if ( + changedProps.has("value") || + changedProps.has("label") || + changedProps.has("disabled") + ) { + return true; + } return !(!changedProps.has("_opened") && this._opened); } protected updated(changedProps: PropertyValues) { - if (changedProps.has("_opened") && this._opened) { + if (!this._initedStates || (changedProps.has("_opened") && this._opened)) { const states = this._getStates( this._opened, this.hass, @@ -162,6 +171,7 @@ class HaEntityPicker extends LitElement { this.includeDeviceClasses ); (this._comboBox as any).items = states; + this._initedStates = true; } } @@ -169,7 +179,6 @@ class HaEntityPicker extends LitElement { if (!this.hass) { return html``; } - return html` ` : html` - + .hass=${this.hass} + .url=${this._url!} + > `} `; } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - - const stateObjChanged = changedProps.has("stateObj"); - const attachedChanged = changedProps.has("_attached"); - - const oldState = changedProps.get("stateObj") as this["stateObj"]; - const oldEntityId = oldState ? oldState.entity_id : undefined; - const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined; - - if ( - (!stateObjChanged && !attachedChanged) || - (stateObjChanged && oldEntityId === curEntityId) - ) { - return; - } - - // If we are no longer attached, destroy polyfill. - if (attachedChanged && !this._attached) { - this._destroyPolyfill(); - return; - } - - // Nothing to do if we are render MJPEG. - if (this._shouldRenderMJPEG) { - return; - } - - // Tear down existing polyfill, if available - this._destroyPolyfill(); - - if (curEntityId) { - this._startHls(); + protected updated(changedProps: PropertyValues): void { + if (changedProps.has("stateObj")) { + this._forceMJPEG = undefined; + this._getStreamUrl(); } } @@ -125,136 +80,35 @@ class HaCameraStream extends LitElement { ); } - private get _videoEl(): HTMLVideoElement { - return this.shadowRoot!.querySelector("video")!; - } - - private async _getUseExoPlayer(): Promise { - if (!this.hass!.auth.external) { - return false; - } - const externalConfig = await getExternalConfig(this.hass!.auth.external); - return externalConfig && externalConfig.hasExoPlayer; - } - - private async _startHls(): Promise { - // eslint-disable-next-line - let hls; - const videoEl = this._videoEl; - this._useExoPlayer = await this._getUseExoPlayer(); - if (!this._useExoPlayer) { - hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any) - .default as HLSModule; - let hlsSupported = hls.isSupported(); - - if (!hlsSupported) { - hlsSupported = - videoEl.canPlayType("application/vnd.apple.mpegurl") !== ""; - } - - if (!hlsSupported) { - this._forceMJPEG = this.stateObj!.entity_id; - return; - } - } - + private async _getStreamUrl(): Promise { try { const { url } = await fetchStreamUrl( this.hass!, this.stateObj!.entity_id ); - if (this._useExoPlayer) { - this._renderHLSExoPlayer(url); - } else if (hls.isSupported()) { - this._renderHLSPolyfill(videoEl, hls, url); - } else { - this._renderHLSNative(videoEl, url); - } - return; + this._url = url; } catch (err) { // Fails if we were unable to get a stream // eslint-disable-next-line console.error(err); + this._forceMJPEG = this.stateObj!.entity_id; } } - private async _renderHLSExoPlayer(url: string) { - window.addEventListener("resize", this._resizeExoPlayer); - this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer); - this._videoEl.style.visibility = "hidden"; - await this.hass!.auth.external!.sendMessage({ - type: "exoplayer/play_hls", - payload: new URL(url, window.location.href).toString(), - }); - } - - private _resizeExoPlayer = () => { - const rect = this._videoEl.getBoundingClientRect(); - this.hass!.auth.external!.fireMessage({ - type: "exoplayer/resize", - payload: { - left: rect.left, - top: rect.top, - right: rect.right, - bottom: rect.bottom, - }, - }); - }; - - private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) { - videoEl.src = url; - await new Promise((resolve) => - videoEl.addEventListener("loadedmetadata", resolve) - ); - videoEl.play(); - } - - private async _renderHLSPolyfill( - videoEl: HTMLVideoElement, - // eslint-disable-next-line - Hls: HLSModule, - url: string - ) { - const hls = new Hls({ - liveBackBufferLength: 60, - fragLoadingTimeOut: 30000, - manifestLoadingTimeOut: 30000, - levelLoadingTimeOut: 30000, - }); - this._hlsPolyfillInstance = hls; - hls.attachMedia(videoEl); - hls.on(Hls.Events.MEDIA_ATTACHED, () => { - hls.loadSource(url); - }); - } - private _elementResized() { fireEvent(this, "iron-resize"); } - private _destroyPolyfill() { - if (this._hlsPolyfillInstance) { - this._hlsPolyfillInstance.destroy(); - this._hlsPolyfillInstance = undefined; - } - if (this._useExoPlayer) { - window.removeEventListener("resize", this._resizeExoPlayer); - this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" }); - } - } - static get styles(): CSSResult { return css` :host, - img, - video { + img { display: block; } - img, - video { + img { width: 100%; } `; diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index 04dcbb1155eb..8c34add0e384 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -10,7 +10,7 @@ import "./ha-icon-button"; const MwcDialog = customElements.get("mwc-dialog") as Constructor; export const createCloseHeading = (hass: HomeAssistant, title: string) => html` - ${title} + ${title} + - + `; } @@ -74,7 +74,7 @@ export class HaFormSelect extends LitElement implements HaFormElement { static get styles(): CSSResult { return css` - paper-dropdown-menu { + ha-paper-dropdown-menu { display: block; } `; diff --git a/src/components/ha-header-bar.ts b/src/components/ha-header-bar.ts index 946c1979de37..0677e96673c9 100644 --- a/src/components/ha-header-bar.ts +++ b/src/components/ha-header-bar.ts @@ -1,6 +1,6 @@ -import { customElement, LitElement, html, unsafeCSS, css } from "lit-element"; // @ts-ignore import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css"; +import { css, customElement, html, LitElement, unsafeCSS } from "lit-element"; @customElement("ha-header-bar") export class HaHeaderBar extends LitElement { diff --git a/src/components/ha-hls-player.ts b/src/components/ha-hls-player.ts new file mode 100644 index 000000000000..8f47b088707f --- /dev/null +++ b/src/components/ha-hls-player.ts @@ -0,0 +1,216 @@ +import { + css, + CSSResult, + customElement, + html, + internalProperty, + LitElement, + property, + PropertyValues, + query, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../common/dom/fire_event"; +import { nextRender } from "../common/util/render-status"; +import { getExternalConfig } from "../external_app/external_config"; +import type { HomeAssistant } from "../types"; + +type HLSModule = typeof import("hls.js"); + +@customElement("ha-hls-player") +class HaHLSPlayer extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public url!: string; + + @property({ type: Boolean, attribute: "controls" }) + public controls = false; + + @property({ type: Boolean, attribute: "muted" }) + public muted = false; + + @property({ type: Boolean, attribute: "autoplay" }) + public autoPlay = false; + + @property({ type: Boolean, attribute: "playsinline" }) + public playsInline = false; + + @query("video") private _videoEl!: HTMLVideoElement; + + @internalProperty() private _attached = false; + + private _hlsPolyfillInstance?: Hls; + + private _useExoPlayer = false; + + public connectedCallback() { + super.connectedCallback(); + this._attached = true; + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this._attached = false; + } + + protected render(): TemplateResult { + if (!this._attached) { + return html``; + } + + return html` + + `; + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + + const attachedChanged = changedProps.has("_attached"); + const urlChanged = changedProps.has("url"); + + if (!urlChanged && !attachedChanged) { + return; + } + + // If we are no longer attached, destroy polyfill + if (attachedChanged && !this._attached) { + // Tear down existing polyfill, if available + this._destroyPolyfill(); + return; + } + + this._destroyPolyfill(); + this._startHls(); + } + + private async _getUseExoPlayer(): Promise { + if (!this.hass!.auth.external) { + return false; + } + const externalConfig = await getExternalConfig(this.hass!.auth.external); + return externalConfig && externalConfig.hasExoPlayer; + } + + private async _startHls(): Promise { + let hls: any; + const videoEl = this._videoEl; + this._useExoPlayer = await this._getUseExoPlayer(); + if (!this._useExoPlayer) { + hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any) + .default as HLSModule; + let hlsSupported = hls.isSupported(); + + if (!hlsSupported) { + hlsSupported = + videoEl.canPlayType("application/vnd.apple.mpegurl") !== ""; + } + + if (!hlsSupported) { + this._videoEl.innerHTML = this.hass.localize( + "ui.components.media-browser.video_not_supported" + ); + return; + } + } + + const url = this.url; + + if (this._useExoPlayer) { + this._renderHLSExoPlayer(url); + } else if (hls.isSupported()) { + this._renderHLSPolyfill(videoEl, hls, url); + } else { + this._renderHLSNative(videoEl, url); + } + } + + private async _renderHLSExoPlayer(url: string) { + window.addEventListener("resize", this._resizeExoPlayer); + this.updateComplete.then(() => nextRender()).then(this._resizeExoPlayer); + this._videoEl.style.visibility = "hidden"; + await this.hass!.auth.external!.sendMessage({ + type: "exoplayer/play_hls", + payload: new URL(url, window.location.href).toString(), + }); + } + + private _resizeExoPlayer = () => { + const rect = this._videoEl.getBoundingClientRect(); + this.hass!.auth.external!.fireMessage({ + type: "exoplayer/resize", + payload: { + left: rect.left, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + }, + }); + }; + + private async _renderHLSPolyfill( + videoEl: HTMLVideoElement, + Hls: HLSModule, + url: string + ) { + const hls = new Hls({ + liveBackBufferLength: 60, + fragLoadingTimeOut: 30000, + manifestLoadingTimeOut: 30000, + levelLoadingTimeOut: 30000, + }); + this._hlsPolyfillInstance = hls; + hls.attachMedia(videoEl); + hls.on(Hls.Events.MEDIA_ATTACHED, () => { + hls.loadSource(url); + }); + } + + private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) { + videoEl.src = url; + await new Promise((resolve) => + videoEl.addEventListener("loadedmetadata", resolve) + ); + videoEl.play(); + } + + private _elementResized() { + fireEvent(this, "iron-resize"); + } + + private _destroyPolyfill() { + if (this._hlsPolyfillInstance) { + this._hlsPolyfillInstance.destroy(); + this._hlsPolyfillInstance = undefined; + } + if (this._useExoPlayer) { + window.removeEventListener("resize", this._resizeExoPlayer); + this.hass!.auth.external!.fireMessage({ type: "exoplayer/stop" }); + } + } + + static get styles(): CSSResult { + return css` + :host, + video { + display: block; + } + + video { + width: 100%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-hls-player": HaHLSPlayer; + } +} diff --git a/src/components/ha-sidebar-sort-styles.ts b/src/components/ha-sidebar-sort-styles.ts deleted file mode 100644 index b87b3c92ba4f..000000000000 --- a/src/components/ha-sidebar-sort-styles.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { html } from "lit-element"; - -export const sortStyles = html` - -`; diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 4f86c9f20291..1226fffbc455 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -23,7 +23,6 @@ import { LitElement, property, PropertyValues, - TemplateResult, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; import { guard } from "lit-html/directives/guard"; @@ -31,6 +30,7 @@ import memoizeOne from "memoize-one"; import { LocalStorage } from "../common/decorators/local-storage"; import { fireEvent } from "../common/dom/fire_event"; import { computeDomain } from "../common/entity/compute_domain"; +import { navigate } from "../common/navigate"; import { compare } from "../common/string/compare"; import { computeRTL } from "../common/util/compute_rtl"; import { ActionHandlerDetail } from "../data/lovelace"; @@ -43,6 +43,7 @@ import { getExternalConfig, } from "../external_app/external_config"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; +import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant, PanelInfo } from "../types"; import "./ha-icon"; import "./ha-menu-button"; @@ -159,7 +160,7 @@ const computePanels = memoizeOne( let Sortable; -let sortStyles: TemplateResult; +let sortStyles: CSSResult; @customElement("ha-sidebar") class HaSidebar extends LitElement { @@ -190,11 +191,15 @@ class HaSidebar extends LitElement { private _recentKeydownActiveUntil = 0; // @ts-ignore - @LocalStorage("sidebarPanelOrder") + @LocalStorage("sidebarPanelOrder", true, { + attribute: false, + }) private _panelOrder: string[] = []; // @ts-ignore - @LocalStorage("sidebarHiddenPanels") + @LocalStorage("sidebarHiddenPanels", true, { + attribute: false, + }) private _hiddenPanels: string[] = []; private _sortable?; @@ -223,7 +228,13 @@ class HaSidebar extends LitElement { } return html` - ${this._editMode ? sortStyles : ""} + ${this._editMode + ? html` + + ` + : ""} @@ -677,294 +696,283 @@ class HaSidebar extends LitElement { `; } - static get styles(): CSSResult { - return css` - :host { - height: 100%; - display: block; - overflow: hidden; - -ms-user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - border-right: 1px solid var(--divider-color); - background-color: var(--sidebar-background-color); - width: 64px; - } - :host([expanded]) { - width: calc(256px + env(safe-area-inset-left)); - } - :host([rtl]) { - border-right: 0; - border-left: 1px solid var(--divider-color); - } - .menu { - box-sizing: border-box; - height: 65px; - display: flex; - padding: 0 8.5px; - border-bottom: 1px solid transparent; - white-space: nowrap; - font-weight: 400; - color: var(--primary-text-color); - border-bottom: 1px solid var(--divider-color); - background-color: var(--primary-background-color); - font-size: 20px; - align-items: center; - padding-left: calc(8.5px + env(safe-area-inset-left)); - } - :host([rtl]) .menu { - padding-left: 8.5px; - padding-right: calc(8.5px + env(safe-area-inset-right)); - } - :host([expanded]) .menu { - width: calc(256px + env(safe-area-inset-left)); - } - :host([rtl][expanded]) .menu { - width: calc(256px + env(safe-area-inset-right)); - } - .menu mwc-icon-button { - color: var(--sidebar-icon-color); - } - :host([expanded]) .menu mwc-icon-button { - margin-right: 23px; - } - :host([expanded][rtl]) .menu mwc-icon-button { - margin-right: 0px; - margin-left: 23px; - } - - .title { - width: 100%; - display: none; - } - :host([expanded]) .title { - display: initial; - } - .title mwc-button { - width: 100%; - } - - paper-listbox::-webkit-scrollbar { - width: 0.4rem; - height: 0.4rem; - } - - paper-listbox::-webkit-scrollbar-thumb { - -webkit-border-radius: 4px; - border-radius: 4px; - background: var(--scrollbar-thumb-color); - } - - paper-listbox { - padding: 4px 0; - display: flex; - flex-direction: column; - box-sizing: border-box; - height: calc(100% - 196px - env(safe-area-inset-bottom)); - overflow-y: auto; - overflow-x: hidden; - scrollbar-color: var(--scrollbar-thumb-color) transparent; - scrollbar-width: thin; - background: none; - margin-left: env(safe-area-inset-left); - } - - :host([rtl]) paper-listbox { - margin-left: initial; - margin-right: env(safe-area-inset-right); - } - - a { - text-decoration: none; - color: var(--sidebar-text-color); - font-weight: 500; - font-size: 14px; - position: relative; - display: block; - outline: 0; - } - - paper-icon-item { - box-sizing: border-box; - margin: 4px 8px; - padding-left: 12px; - border-radius: 4px; - --paper-item-min-height: 40px; - width: 48px; - } - :host([expanded]) paper-icon-item { - width: 240px; - } - :host([rtl]) paper-icon-item { - padding-left: auto; - padding-right: 12px; - } - - ha-icon[slot="item-icon"], - ha-svg-icon[slot="item-icon"] { - color: var(--sidebar-icon-color); - } - - .iron-selected paper-icon-item::before, - a:not(.iron-selected):focus::before { - border-radius: 4px; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - pointer-events: none; - content: ""; - transition: opacity 15ms linear; - will-change: opacity; - } - .iron-selected paper-icon-item::before { - background-color: var(--sidebar-selected-icon-color); - opacity: 0.12; - } - a:not(.iron-selected):focus::before { - background-color: currentColor; - opacity: var(--dark-divider-opacity); - margin: 4px 8px; - } - .iron-selected paper-icon-item:focus::before, - .iron-selected:focus paper-icon-item::before { - opacity: 0.2; - } - - .iron-selected paper-icon-item[pressed]:before { - opacity: 0.37; - } - - paper-icon-item span { - color: var(--sidebar-text-color); - font-weight: 500; - font-size: 14px; - } - - a.iron-selected paper-icon-item ha-icon, - a.iron-selected paper-icon-item ha-svg-icon { - color: var(--sidebar-selected-icon-color); - } - - a.iron-selected .item-text { - color: var(--sidebar-selected-text-color); - } - - paper-icon-item .item-text { - display: none; - max-width: calc(100% - 56px); - } - :host([expanded]) paper-icon-item .item-text { - display: block; - } - - .divider { - bottom: 112px; - padding: 10px 0; - } - .divider::before { - content: " "; - display: block; - height: 1px; - background-color: var(--divider-color); - } - .notifications-container { - display: flex; - margin-left: env(safe-area-inset-left); - } - :host([rtl]) .notifications-container { - margin-left: initial; - margin-right: env(safe-area-inset-right); - } - .notifications { - cursor: pointer; - } - .notifications .item-text { - flex: 1; - } - .profile { - margin-left: env(safe-area-inset-left); - } - :host([rtl]) .profile { - margin-left: initial; - margin-right: env(safe-area-inset-right); - } - .profile paper-icon-item { - padding-left: 4px; - } - :host([rtl]) .profile paper-icon-item { - padding-left: auto; - padding-right: 4px; - } - .profile .item-text { - margin-left: 8px; - } - :host([rtl]) .profile .item-text { - margin-right: 8px; - } - - .notification-badge { - min-width: 20px; - box-sizing: border-box; - border-radius: 50%; - font-weight: 400; - background-color: var(--accent-color); - line-height: 20px; - text-align: center; - padding: 0px 6px; - color: var(--text-accent-color, var(--text-primary-color)); - } - ha-svg-icon + .notification-badge { - position: absolute; - bottom: 14px; - left: 26px; - font-size: 0.65em; - } - - .spacer { - flex: 1; - pointer-events: none; - } - - .subheader { - color: var(--sidebar-text-color); - font-weight: 500; - font-size: 14px; - padding: 16px; - white-space: nowrap; - } - - .dev-tools { - display: flex; - flex-direction: row; - justify-content: space-between; - padding: 0 8px; - width: 256px; - box-sizing: border-box; - } - - .dev-tools a { - color: var(--sidebar-icon-color); - } - - .tooltip { - display: none; - position: absolute; - opacity: 0.9; - border-radius: 2px; - white-space: nowrap; - color: var(--sidebar-background-color); - background-color: var(--sidebar-text-color); - padding: 4px; - font-weight: 500; - } - - :host([rtl]) .menu mwc-icon-button { - -webkit-transform: scaleX(-1); - transform: scaleX(-1); - } - `; + static get styles(): CSSResult[] { + return [ + haStyleScrollbar, + css` + :host { + height: 100%; + display: block; + overflow: hidden; + -ms-user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + border-right: 1px solid var(--divider-color); + background-color: var(--sidebar-background-color); + width: 64px; + } + :host([expanded]) { + width: calc(256px + env(safe-area-inset-left)); + } + :host([rtl]) { + border-right: 0; + border-left: 1px solid var(--divider-color); + } + .menu { + box-sizing: border-box; + height: 65px; + display: flex; + padding: 0 8.5px; + border-bottom: 1px solid transparent; + white-space: nowrap; + font-weight: 400; + color: var(--primary-text-color); + border-bottom: 1px solid var(--divider-color); + background-color: var(--primary-background-color); + font-size: 20px; + align-items: center; + padding-left: calc(8.5px + env(safe-area-inset-left)); + } + :host([rtl]) .menu { + padding-left: 8.5px; + padding-right: calc(8.5px + env(safe-area-inset-right)); + } + :host([expanded]) .menu { + width: calc(256px + env(safe-area-inset-left)); + } + :host([rtl][expanded]) .menu { + width: calc(256px + env(safe-area-inset-right)); + } + .menu mwc-icon-button { + color: var(--sidebar-icon-color); + } + :host([expanded]) .menu mwc-icon-button { + margin-right: 23px; + } + :host([expanded][rtl]) .menu mwc-icon-button { + margin-right: 0px; + margin-left: 23px; + } + + .title { + width: 100%; + display: none; + } + :host([expanded]) .title { + display: initial; + } + .title mwc-button { + width: 100%; + } + + paper-listbox { + padding: 4px 0; + display: flex; + flex-direction: column; + box-sizing: border-box; + height: calc(100% - 196px - env(safe-area-inset-bottom)); + overflow-x: hidden; + background: none; + margin-left: env(safe-area-inset-left); + } + + :host([rtl]) paper-listbox { + margin-left: initial; + margin-right: env(safe-area-inset-right); + } + + a { + text-decoration: none; + color: var(--sidebar-text-color); + font-weight: 500; + font-size: 14px; + position: relative; + display: block; + outline: 0; + } + + paper-icon-item { + box-sizing: border-box; + margin: 4px 8px; + padding-left: 12px; + border-radius: 4px; + --paper-item-min-height: 40px; + width: 48px; + } + :host([expanded]) paper-icon-item { + width: 240px; + } + :host([rtl]) paper-icon-item { + padding-left: auto; + padding-right: 12px; + } + + ha-icon[slot="item-icon"], + ha-svg-icon[slot="item-icon"] { + color: var(--sidebar-icon-color); + } + + .iron-selected paper-icon-item::before, + a:not(.iron-selected):focus::before { + border-radius: 4px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + content: ""; + transition: opacity 15ms linear; + will-change: opacity; + } + .iron-selected paper-icon-item::before { + background-color: var(--sidebar-selected-icon-color); + opacity: 0.12; + } + a:not(.iron-selected):focus::before { + background-color: currentColor; + opacity: var(--dark-divider-opacity); + margin: 4px 8px; + } + .iron-selected paper-icon-item:focus::before, + .iron-selected:focus paper-icon-item::before { + opacity: 0.2; + } + + .iron-selected paper-icon-item[pressed]:before { + opacity: 0.37; + } + + paper-icon-item span { + color: var(--sidebar-text-color); + font-weight: 500; + font-size: 14px; + } + + a.iron-selected paper-icon-item ha-icon, + a.iron-selected paper-icon-item ha-svg-icon { + color: var(--sidebar-selected-icon-color); + } + + a.iron-selected .item-text { + color: var(--sidebar-selected-text-color); + } + + paper-icon-item .item-text { + display: none; + max-width: calc(100% - 56px); + } + :host([expanded]) paper-icon-item .item-text { + display: block; + } + + .divider { + bottom: 112px; + padding: 10px 0; + } + .divider::before { + content: " "; + display: block; + height: 1px; + background-color: var(--divider-color); + } + .notifications-container { + display: flex; + margin-left: env(safe-area-inset-left); + } + :host([rtl]) .notifications-container { + margin-left: initial; + margin-right: env(safe-area-inset-right); + } + .notifications { + cursor: pointer; + } + .notifications .item-text { + flex: 1; + } + .profile { + margin-left: env(safe-area-inset-left); + } + :host([rtl]) .profile { + margin-left: initial; + margin-right: env(safe-area-inset-right); + } + .profile paper-icon-item { + padding-left: 4px; + } + :host([rtl]) .profile paper-icon-item { + padding-left: auto; + padding-right: 4px; + } + .profile .item-text { + margin-left: 8px; + } + :host([rtl]) .profile .item-text { + margin-right: 8px; + } + + .notification-badge { + min-width: 20px; + box-sizing: border-box; + border-radius: 50%; + font-weight: 400; + background-color: var(--accent-color); + line-height: 20px; + text-align: center; + padding: 0px 6px; + color: var(--text-accent-color, var(--text-primary-color)); + } + ha-svg-icon + .notification-badge { + position: absolute; + bottom: 14px; + left: 26px; + font-size: 0.65em; + } + + .spacer { + flex: 1; + pointer-events: none; + } + + .subheader { + color: var(--sidebar-text-color); + font-weight: 500; + font-size: 14px; + padding: 16px; + white-space: nowrap; + } + + .dev-tools { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 0 8px; + width: 256px; + box-sizing: border-box; + } + + .dev-tools a { + color: var(--sidebar-icon-color); + } + + .tooltip { + display: none; + position: absolute; + opacity: 0.9; + border-radius: 2px; + white-space: nowrap; + color: var(--sidebar-background-color); + background-color: var(--sidebar-text-color); + padding: 4px; + font-weight: 500; + } + + :host([rtl]) .menu mwc-icon-button { + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + } + `, + ]; } } diff --git a/src/components/media-player/dialog-media-player-browse.ts b/src/components/media-player/dialog-media-player-browse.ts index 5f4d14649ad4..311536844fcc 100644 --- a/src/components/media-player/dialog-media-player-browse.ts +++ b/src/components/media-player/dialog-media-player-browse.ts @@ -8,7 +8,7 @@ import { property, TemplateResult, } from "lit-element"; -import { HASSDomEvent } from "../../common/dom/fire_event"; +import { fireEvent, HASSDomEvent } from "../../common/dom/fire_event"; import type { MediaPickedEvent, MediaPlayerBrowseAction, @@ -33,16 +33,17 @@ class DialogMediaPlayerBrowse extends LitElement { @internalProperty() private _params?: MediaPlayerBrowseDialogParams; - public async showDialog( - params: MediaPlayerBrowseDialogParams - ): Promise { + public showDialog(params: MediaPlayerBrowseDialogParams): void { this._params = params; this._entityId = this._params.entityId; this._mediaContentId = this._params.mediaContentId; this._mediaContentType = this._params.mediaContentType; this._action = this._params.action || "play"; + } - await this.updateComplete; + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render(): TemplateResult { @@ -57,7 +58,7 @@ class DialogMediaPlayerBrowse extends LitElement { escapeKeyAction hideActions flexContent - @closed=${this._closeDialog} + @closed=${this.closeDialog} > `; } - private _closeDialog() { - this._params = undefined; - } - private _mediaPicked(ev: HASSDomEvent): void { this._params!.mediaPickedCallback(ev.detail); if (this._action !== "play") { - this._closeDialog(); + this.closeDialog(); } } @@ -93,17 +90,12 @@ class DialogMediaPlayerBrowse extends LitElement { --dialog-content-padding: 0; } - ha-header-bar { - --mdc-theme-on-primary: var(--primary-text-color); - --mdc-theme-primary: var(--mdc-theme-surface); - flex-shrink: 0; - border-bottom: 1px solid - var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); - } - @media (min-width: 800px) { ha-dialog { --mdc-dialog-max-width: 800px; + --dialog-surface-position: fixed; + --dialog-surface-top: 40px; + --mdc-dialog-max-height: calc(100% - 72px); } ha-media-player-browse { width: 700px; diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts index c485a9645393..0d9cde1946d7 100644 --- a/src/components/media-player/ha-media-player-browse.ts +++ b/src/components/media-player/ha-media-player-browse.ts @@ -18,12 +18,21 @@ import { } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; import { ifDefined } from "lit-html/directives/if-defined"; +import { styleMap } from "lit-html/directives/style-map"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; +import { compare } from "../../common/string/compare"; import { computeRTLDirection } from "../../common/util/compute_rtl"; import { debounce } from "../../common/util/debounce"; -import { browseMediaPlayer, MediaPickedEvent } from "../../data/media-player"; +import { + browseLocalMediaPlayer, + browseMediaPlayer, + BROWSER_SOURCE, + MediaPickedEvent, + MediaPlayerBrowseAction, +} from "../../data/media-player"; import type { MediaPlayerItem } from "../../data/media-player"; +import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer"; import { haStyle } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; @@ -50,11 +59,7 @@ export class HaMediaPlayerBrowse extends LitElement { @property() public mediaContentType?: string; - @property() public action: "pick" | "play" = "play"; - - @property({ type: Boolean }) public hideBack = false; - - @property({ type: Boolean }) public hideTitle = false; + @property() public action: MediaPlayerBrowseAction = "play"; @property({ type: Boolean }) public dialog = false; @@ -63,6 +68,8 @@ export class HaMediaPlayerBrowse extends LitElement { @internalProperty() private _loading = false; + @internalProperty() private _error?: { message: string; code: string }; + @internalProperty() private _mediaPlayerItems: MediaPlayerItem[] = []; private _resizeObserver?: ResizeObserver; @@ -87,53 +94,103 @@ export class HaMediaPlayerBrowse extends LitElement { this._navigate(item); } - protected render(): TemplateResult { - if (!this._mediaPlayerItems.length) { - return html``; + private _renderError(err: { message: string; code: string }) { + if (err.message === "Media directory does not exist.") { + return html` +

No local media found.

+

+ It looks like you have not yet created a media directory. +
Create a directory with the name "media" in the + configuration directory of Home Assistant + (${this.hass.config.config_dir}).
Place your video, audio and + image files in this directory to be able to browse and play them in + the browser or on supported media players. +

+ +

+ Check the + documentation + for more info +

+ `; } + return err.message; + } + protected render(): TemplateResult { if (this._loading) { return html``; } - const mostRecentItem = this._mediaPlayerItems[ + if (this._error && !this._mediaPlayerItems.length) { + if (this.dialog) { + this._closeDialogAction(); + showAlertDialog(this, { + title: this.hass.localize( + "ui.components.media-browser.media_browsing_error" + ), + text: this._renderError(this._error), + }); + } else { + return html`
+ ${this._renderError(this._error)} +
`; + } + } + + if (!this._mediaPlayerItems.length) { + return html``; + } + + const currentItem = this._mediaPlayerItems[ this._mediaPlayerItems.length - 1 ]; - const previousItem = + + const previousItem: MediaPlayerItem | undefined = this._mediaPlayerItems.length > 1 ? this._mediaPlayerItems[this._mediaPlayerItems.length - 2] : undefined; const hasExpandableChildren: | MediaPlayerItem - | undefined = this._hasExpandableChildren(mostRecentItem.children); + | undefined = this._hasExpandableChildren(currentItem.children); - const showImages = mostRecentItem.children?.some( - (child) => child.thumbnail && child.thumbnail !== mostRecentItem.thumbnail + const showImages: boolean | undefined = currentItem.children?.some( + (child) => child.thumbnail && child.thumbnail !== currentItem.thumbnail ); const mediaType = this.hass.localize( - `ui.components.media-browser.content-type.${mostRecentItem.media_content_type}` + `ui.components.media-browser.content-type.${currentItem.media_content_type}` ); return html`
- ${mostRecentItem.thumbnail + ${currentItem.thumbnail ? html`
- ${this._narrow && mostRecentItem?.can_play + ${this._narrow && currentItem?.can_play ? html` - ${this.hideTitle && (this._narrow || !mostRecentItem.thumbnail) - ? "" - : html``} - ${mostRecentItem?.can_play && - (!mostRecentItem.thumbnail || !this._narrow) + + ${currentItem.can_play && (!currentItem.thumbnail || !this._narrow) ? html` - + ` : ""}
- ${mostRecentItem.children?.length + ${this._error + ? html`
+ ${this._renderError(this._error)} +
` + : currentItem.children?.length ? hasExpandableChildren ? html`
- ${mostRecentItem.children?.length - ? html` - ${mostRecentItem.children.map( - (child) => html` -
-
- html` +
+
+ + ${child.can_expand && !child.thumbnail + ? html` + + ` + : ""} + + ${child.can_play + ? html` + - ${child.can_expand && !child.thumbnail - ? html` - - ` - : ""} - - ${child.can_play - ? html` - - - - ` - : ""} -
-
${child.title}
-
- ${this.hass.localize( - `ui.components.media-browser.content-type.${child.media_content_type}` - )} -
-
- ` - )} - ` - : ""} + + + ` + : ""} +
+
${child.title}
+
+ ${this.hass.localize( + `ui.components.media-browser.content-type.${child.media_content_type}` + )} +
+
+ ` + )}
` : html` - ${mostRecentItem.children.map( + ${currentItem.children.map( (child) => html` ` - : this.hass.localize("ui.components.media-browser.no_items")} + : html`
+ ${this.hass.localize("ui.components.media-browser.no_items")} +
`} `; } @@ -338,11 +395,22 @@ export class HaMediaPlayerBrowse extends LitElement { return; } - this._fetchData(this.mediaContentId, this.mediaContentType).then( - (itemData) => { + if (changedProps.has("entityId")) { + this._error = undefined; + this._mediaPlayerItems = []; + } + + this._fetchData(this.mediaContentId, this.mediaContentType) + .then((itemData) => { + if (!itemData) { + return; + } + this._mediaPlayerItems = [itemData]; - } - ); + }) + .catch((err) => { + this._error = err; + }); } private _actionClicked(ev: MouseEvent): void { @@ -353,27 +421,44 @@ export class HaMediaPlayerBrowse extends LitElement { } private _runAction(item: MediaPlayerItem): void { - fireEvent(this, "media-picked", { - media_content_id: item.media_content_id, - media_content_type: item.media_content_type, - }); + fireEvent(this, "media-picked", { item }); } - private async _navigateForward(ev: MouseEvent): Promise { + private async _childClicked(ev: MouseEvent): Promise { const target = ev.currentTarget as any; const item: MediaPlayerItem = target.item; if (!item) { return; } + + if (!item.can_expand) { + this._runAction(item); + return; + } + this._navigate(item); } private async _navigate(item: MediaPlayerItem) { - const itemData = await this._fetchData( - item.media_content_id, - item.media_content_type - ); + this._error = undefined; + + let itemData: MediaPlayerItem; + + try { + itemData = await this._fetchData( + item.media_content_id, + item.media_content_type + ); + } catch (err) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.components.media-browser.media_browsing_error" + ), + text: this._renderError(err), + }); + return; + } this.scrollTo(0, 0); this._mediaPlayerItems = [...this._mediaPlayerItems, itemData]; @@ -383,11 +468,21 @@ export class HaMediaPlayerBrowse extends LitElement { mediaContentId?: string, mediaContentType?: string ): Promise { - const itemData = await browseMediaPlayer( - this.hass, - this.entityId, - !mediaContentId ? undefined : mediaContentId, - mediaContentType + const itemData = + this.entityId !== BROWSER_SOURCE + ? await browseMediaPlayer( + this.hass, + this.entityId, + mediaContentId, + mediaContentType + ) + : await browseLocalMediaPlayer(this.hass, mediaContentId); + itemData.children = itemData.children?.sort((first, second) => + !first.can_expand && second.can_expand + ? 1 + : first.can_expand && !second.can_expand + ? -1 + : compare(first.title, second.title) ); return itemData; @@ -416,8 +511,8 @@ export class HaMediaPlayerBrowse extends LitElement { this._resizeObserver.observe(this); } - private _hasExpandableChildren = memoizeOne((children) => - children.find((item: MediaPlayerItem) => item.can_expand) + private _hasExpandableChildren = memoizeOne((children?: MediaPlayerItem[]) => + children?.find((item: MediaPlayerItem) => item.can_expand) ); private _closeDialogAction(): void { @@ -436,21 +531,20 @@ export class HaMediaPlayerBrowse extends LitElement { flex-direction: column; } + .container { + padding: 16px; + } + .header { display: flex; justify-content: space-between; border-bottom: 1px solid var(--divider-color); } - .header_button { - position: relative; - top: 14px; - right: -8px; - } - .header { background-color: var(--card-background-color); position: sticky; + position: -webkit-sticky; top: 0; z-index: 5; padding: 20px 24px 10px; @@ -485,12 +579,6 @@ export class HaMediaPlayerBrowse extends LitElement { display: block; } - .breadcrumb-overflow { - display: flex; - flex-grow: 1; - justify-content: space-between; - } - .breadcrumb { display: flex; flex-direction: column; @@ -552,9 +640,6 @@ export class HaMediaPlayerBrowse extends LitElement { ); grid-gap: 16px; margin: 8px 0px; - } - - :host(:not([narrow])) .children { padding: 0px 24px; } @@ -657,6 +742,10 @@ export class HaMediaPlayerBrowse extends LitElement { padding: 0; } + :host([narrow]) .header.no-dialog { + display: block; + } + :host([narrow]) .header_button { position: absolute; top: 14px; @@ -696,8 +785,7 @@ export class HaMediaPlayerBrowse extends LitElement { padding: 20px 24px 10px; } - :host([narrow]) .media-source, - :host([narrow]) .children { + :host([narrow]) .media-source { padding: 0 24px; } @@ -716,6 +804,10 @@ export class HaMediaPlayerBrowse extends LitElement { -webkit-line-clamp: 1; } + :host(:not([narrow])[scroll]) .header:not(.no-img) mwc-icon-button { + align-self: center; + } + :host([scroll]) .header-info mwc-button, .no-img .header-info mwc-button { padding-right: 4px; diff --git a/src/components/user/ha-person-badge.ts b/src/components/user/ha-person-badge.ts new file mode 100644 index 000000000000..70c47d60234b --- /dev/null +++ b/src/components/user/ha-person-badge.ts @@ -0,0 +1,71 @@ +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; +import { styleMap } from "lit-html/directives/style-map"; +import { Person } from "../../data/person"; +import { computeInitials } from "./ha-user-badge"; + +@customElement("ha-person-badge") +class PersonBadge extends LitElement { + @property({ attribute: false }) public person?: Person; + + protected render(): TemplateResult { + if (!this.person) { + return html``; + } + + const picture = this.person.picture; + + if (picture) { + return html`
`; + } + const initials = computeInitials(this.person.name); + return html`
+ ${initials} +
`; + } + + static get styles(): CSSResult { + return css` + .picture { + width: 40px; + height: 40px; + background-size: cover; + border-radius: 50%; + } + .initials { + display: inline-block; + box-sizing: border-box; + width: 40px; + line-height: 40px; + border-radius: 50%; + text-align: center; + background-color: var(--light-primary-color); + text-decoration: none; + color: var(--text-light-primary-color, var(--primary-text-color)); + overflow: hidden; + } + .initials.long { + font-size: 80%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-person-badge": PersonBadge; + } +} diff --git a/src/components/user/ha-user-badge.ts b/src/components/user/ha-user-badge.ts index c7fd114ae4a8..09972237fcae 100644 --- a/src/components/user/ha-user-badge.ts +++ b/src/components/user/ha-user-badge.ts @@ -8,15 +8,15 @@ import { property, TemplateResult, } from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; import { styleMap } from "lit-html/directives/style-map"; -import { toggleAttribute } from "../../common/dom/toggle_attribute"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { User } from "../../data/user"; import { CurrentUser, HomeAssistant } from "../../types"; -const computeInitials = (name: string) => { +export const computeInitials = (name: string) => { if (!name) { - return "user"; + return "?"; } return ( name @@ -31,7 +31,7 @@ const computeInitials = (name: string) => { }; @customElement("ha-user-badge") -class StateBadge extends LitElement { +class UserBadge extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public user?: User | CurrentUser; @@ -44,14 +44,6 @@ class StateBadge extends LitElement { super.updated(changedProps); if (changedProps.has("user")) { this._getPersonPicture(); - if (!this._personPicture) { - toggleAttribute( - this, - "long", - (this.hass.user ? computeInitials(this.hass.user.name) : "?").length > - 2 - ); - } return; } const oldHass = changedProps.get("hass"); @@ -67,6 +59,8 @@ class StateBadge extends LitElement { } else { this._getPersonPicture(); } + } else if (!this._personEntityId && oldHass) { + this._getPersonPicture(); } } @@ -74,22 +68,20 @@ class StateBadge extends LitElement { if (!this.hass || !this.user) { return html``; } - const user = this.user; const picture = this._personPicture; - return html` - ${picture - ? html`
` - : html`
2} - > - ${computeInitials(user?.name!)} -
`} - `; + if (picture) { + return html`
`; + } + const initials = computeInitials(this.user.name); + return html`
+ ${initials} +
`; } private _getPersonPicture() { @@ -100,7 +92,7 @@ class StateBadge extends LitElement { } for (const entity of Object.values(this.hass.states)) { if ( - entity.attributes.user_id === this.user!.id && + entity.attributes.user_id === this.user.id && computeStateDomain(entity) === "person" ) { this._personEntityId = entity.entity_id; @@ -130,7 +122,7 @@ class StateBadge extends LitElement { color: var(--text-light-primary-color, var(--primary-text-color)); overflow: hidden; } - :host([long]) .initials { + .initials.long { font-size: 80%; } `; @@ -139,6 +131,6 @@ class StateBadge extends LitElement { declare global { interface HTMLElementTagNameMap { - "ha-user-badge": StateBadge; + "ha-user-badge": UserBadge; } } diff --git a/src/data/automation.ts b/src/data/automation.ts index 97395806dbc4..fe29dd8b35a1 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -3,7 +3,7 @@ import { HassEntityBase, } from "home-assistant-js-websocket"; import { navigate } from "../common/navigate"; -import { HomeAssistant, Context } from "../types"; +import { Context, HomeAssistant } from "../types"; import { DeviceCondition, DeviceTrigger } from "./device_automation"; import { Action } from "./script"; @@ -15,6 +15,7 @@ export interface AutomationEntity extends HassEntityBase { } export interface AutomationConfig { + id?: string; alias: string; description: string; trigger: Trigger[]; @@ -32,7 +33,8 @@ export interface ForDict { export interface StateTrigger { platform: "state"; - entity_id?: string; + entity_id: string; + attribute?: string; from?: string | number; to?: string | number; for?: string | number | ForDict; @@ -59,6 +61,7 @@ export interface HassTrigger { export interface NumericStateTrigger { platform: "numeric_state"; entity_id: string; + attribute?: string; above?: number; below?: number; value_template?: string; @@ -136,12 +139,14 @@ export interface LogicalCondition { export interface StateCondition { condition: "state"; entity_id: string; + attribute?: string; state: string | number; } export interface NumericStateCondition { condition: "numeric_state"; entity_id: string; + attribute?: string; above?: number; below?: number; value_template?: string; @@ -165,6 +170,7 @@ export interface TimeCondition { condition: "time"; after: string; before: string; + weekday: string[]; } export interface TemplateCondition { diff --git a/src/data/config_flow.ts b/src/data/config_flow.ts index a06f8f42ecff..94cce83ba8ea 100644 --- a/src/data/config_flow.ts +++ b/src/data/config_flow.ts @@ -13,6 +13,8 @@ export const DISCOVERY_SOURCES = [ "discovery", ]; +export const ATTENTION_SOURCES = ["reauth"]; + export const createConfigFlow = (hass: HomeAssistant, handler: string) => hass.callApi("POST", "config/config_entries/flow", { handler, diff --git a/src/data/hassio/common.ts b/src/data/hassio/common.ts index b3681d72a845..4ab432b85595 100644 --- a/src/data/hassio/common.ts +++ b/src/data/hassio/common.ts @@ -9,7 +9,7 @@ export const hassioApiResultExtractor = (response: HassioResponse) => export const extractApiErrorMessage = (error: any): string => { return typeof error === "object" ? typeof error.body === "object" - ? error.body.message || "Unkown error, see logs" - : error.body || "Unkown error, see logs" + ? error.body.message || "Unknown error, see logs" + : error.body || "Unknown error, see logs" : error; }; diff --git a/src/data/media-player.ts b/src/data/media-player.ts index 0a3d289259d2..af11824211bd 100644 --- a/src/data/media-player.ts +++ b/src/data/media-player.ts @@ -20,9 +20,10 @@ export const CONTRAST_RATIO = 4.5; export type MediaPlayerBrowseAction = "pick" | "play"; +export const BROWSER_SOURCE = "browser"; + export interface MediaPickedEvent { - media_content_id: string; - media_content_type: string; + item: MediaPlayerItem; } export interface MediaPlayerThumbnail { @@ -58,6 +59,15 @@ export const browseMediaPlayer = ( media_content_type: mediaContentType, }); +export const browseLocalMediaPlayer = ( + hass: HomeAssistant, + mediaContentId?: string +): Promise => + hass.callWS({ + type: "media_source/browse_media", + media_content_id: mediaContentId, + }); + export const getCurrentProgress = (stateObj: HassEntity): number => { let progress = stateObj.attributes.media_position; diff --git a/src/data/ozw.ts b/src/data/ozw.ts index b24692c01f7f..80ad9882b7b7 100644 --- a/src/data/ozw.ts +++ b/src/data/ozw.ts @@ -14,6 +14,8 @@ export interface OZWDevice { is_zwave_plus: boolean; ozw_instance: number; event: string; + node_manufacturer_name: string; + node_product_name: string; } export interface OZWDeviceMetaDataResponse { @@ -147,6 +149,15 @@ export const fetchOZWNetworkStatistics = ( ozw_instance: ozw_instance, }); +export const fetchOZWNodes = ( + hass: HomeAssistant, + ozw_instance: number +): Promise => + hass.callWS({ + type: "ozw/get_nodes", + ozw_instance: ozw_instance, + }); + export const fetchOZWNodeStatus = ( hass: HomeAssistant, ozw_instance: number, diff --git a/src/data/script.ts b/src/data/script.ts index 7a8e83d71721..240472f7434b 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -5,7 +5,7 @@ import { import { computeObjectId } from "../common/entity/compute_object_id"; import { navigate } from "../common/navigate"; import { HomeAssistant } from "../types"; -import { Condition } from "./automation"; +import { Condition, Trigger } from "./automation"; export const MODES = ["single", "restart", "queued", "parallel"]; export const MODES_MAX = ["queued", "parallel"]; @@ -56,6 +56,13 @@ export interface SceneAction { export interface WaitAction { wait_template: string; timeout?: number; + continue_on_timeout?: boolean; +} + +export interface WaitForTriggerAction { + wait_for_trigger: Trigger[]; + timeout?: number; + continue_on_timeout?: boolean; } export interface RepeatAction { @@ -91,6 +98,7 @@ export type Action = | DelayAction | SceneAction | WaitAction + | WaitForTriggerAction | RepeatAction | ChooseAction; diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts index b8fcea9a706b..7d63d3dd5322 100644 --- a/src/dialogs/generic/dialog-box.ts +++ b/src/dialogs/generic/dialog-box.ts @@ -5,19 +5,19 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; +import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-dialog"; import "../../components/ha-switch"; import { PolymerChangedEvent } from "../../polymer-types"; import { haStyleDialog } from "../../resources/styles"; import { HomeAssistant } from "../../types"; import { DialogParams } from "./show-dialog-box"; -import { fireEvent } from "../../common/dom/fire_event"; @customElement("dialog-box") class DialogBox extends LitElement { @@ -114,8 +114,8 @@ class DialogBox extends LitElement { } private _dismiss(): void { - if (this._params!.cancel) { - this._params!.cancel(); + if (this._params?.cancel) { + this._params.cancel(); } this._close(); } diff --git a/src/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts index 0e7a445c0b4f..45dee3e224cb 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.ts +++ b/src/dialogs/more-info/controls/more-info-media_player.ts @@ -130,7 +130,7 @@ class MoreInfoMediaPlayer extends LitElement {
` : ""} - ${stateObj.state !== "off" && + ${![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state) && supportsFeature(stateObj, SUPPORT_SELECT_SOURCE) && stateObj.attributes.source_list?.length ? html` @@ -409,8 +409,8 @@ class MoreInfoMediaPlayer extends LitElement { entityId: this.stateObj!.entity_id, mediaPickedCallback: (pickedMedia: MediaPickedEvent) => this._playMedia( - pickedMedia.media_content_id, - pickedMedia.media_content_type + pickedMedia.item.media_content_id, + pickedMedia.item.media_content_type ), }); } diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index 56d429836d01..7738ee7349f6 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -11,6 +11,7 @@ import { LitElement, property, } from "lit-element"; +import { cache } from "lit-html/directives/cache"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const"; import { fireEvent } from "../../common/dom/fire_event"; @@ -147,47 +148,52 @@ export class MoreInfoDialog extends LitElement { : ""}
- ${this._currTabIndex === 0 - ? html` - ${DOMAINS_NO_INFO.includes(domain) - ? "" - : html` - - `} - - ${stateObj.attributes.restored - ? html` -

- ${this.hass.localize( - "ui.dialogs.more_info_control.restored.not_provided" - )} -

-

- ${this.hass.localize( - "ui.dialogs.more_info_control.restored.remove_intro" - )} -

- - ${this.hass.localize( - "ui.dialogs.more_info_control.restored.remove_action" - )} - - ` - : ""} - ` - : html` - - `} + ${cache( + this._currTabIndex === 0 + ? html` + ${DOMAINS_NO_INFO.includes(domain) + ? "" + : html` + + `} + + ${stateObj.attributes.restored + ? html` +

+ ${this.hass.localize( + "ui.dialogs.more_info_control.restored.not_provided" + )} +

+

+ ${this.hass.localize( + "ui.dialogs.more_info_control.restored.remove_intro" + )} +

+ + ${this.hass.localize( + "ui.dialogs.more_info_control.restored.remove_action" + )} + + ` + : ""} + ` + : html` + + ` + )}
`; @@ -268,6 +274,7 @@ export class MoreInfoDialog extends LitElement { --mdc-theme-on-primary: var(--primary-text-color); --mdc-theme-primary: var(--mdc-theme-surface); flex-shrink: 0; + display: block; } @media all and (max-width: 450px), all and (max-height: 500px) { diff --git a/src/dialogs/more-info/ha-more-info-tab-history.ts b/src/dialogs/more-info/ha-more-info-tab-history.ts index 036d66bdb623..18624a11ef4f 100644 --- a/src/dialogs/more-info/ha-more-info-tab-history.ts +++ b/src/dialogs/more-info/ha-more-info-tab-history.ts @@ -8,7 +8,7 @@ import { PropertyValues, TemplateResult, } from "lit-element"; -import { classMap } from "lit-html/directives/class-map"; +import { styleMap } from "lit-html/directives/style-map"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import "../../components/ha-circular-progress"; import "../../components/state-history-charts"; @@ -57,19 +57,21 @@ export class MoreInfoTabHistoryDialog extends LitElement { alt=${this.hass.localize("ui.common.loading")} > ` - : html` + : this._entries.length + ? html` - `} + ` + : ""} `; } @@ -144,8 +146,8 @@ export class MoreInfoTabHistoryDialog extends LitElement { margin-bottom: 16px; } - ha-logbook.has-entries { - height: 360px; + ha-logbook { + max-height: 360px; } ha-circular-progress { diff --git a/src/html/index.html.template b/src/html/index.html.template index 5ec5dd1dc1fe..fe4271c0fb6a 100644 --- a/src/html/index.html.template +++ b/src/html/index.html.template @@ -48,7 +48,7 @@ } @media (prefers-color-scheme: dark) { html { - background-color: var(--primary-background-color, #111111); + background-color: #111111; } #ha-init-skeleton::before { background-color: #1c1c1c; diff --git a/src/html/onboarding.html.template b/src/html/onboarding.html.template index cf95d093621e..0a605baba1c4 100644 --- a/src/html/onboarding.html.template +++ b/src/html/onboarding.html.template @@ -5,6 +5,15 @@ <%= renderTemplate('_header') %> diff --git a/src/layouts/hass-error-screen.ts b/src/layouts/hass-error-screen.ts index eab453b29eb8..877618449848 100644 --- a/src/layouts/hass-error-screen.ts +++ b/src/layouts/hass-error-screen.ts @@ -63,6 +63,7 @@ class HassErrorScreen extends LitElement { pointer-events: auto; } .content { + color: var(--primary-text-color); height: calc(100% - 64px); display: flex; align-items: center; diff --git a/src/layouts/partial-panel-resolver.ts b/src/layouts/partial-panel-resolver.ts index 280a32eb1689..ad04160caa07 100644 --- a/src/layouts/partial-panel-resolver.ts +++ b/src/layouts/partial-panel-resolver.ts @@ -1,6 +1,13 @@ import { PolymerElement } from "@polymer/polymer"; +import { + STATE_NOT_RUNNING, + STATE_RUNNING, + STATE_STARTING, +} from "home-assistant-js-websocket"; import { customElement, property, PropertyValues } from "lit-element"; +import { deepActiveElement } from "../common/dom/deep-active-element"; import { deepEqual } from "../common/util/deep-equal"; +import { CustomPanelInfo } from "../data/panel_custom"; import { HomeAssistant, Panels } from "../types"; import { removeInitSkeleton } from "../util/init-skeleton"; import { @@ -8,13 +15,6 @@ import { RouteOptions, RouterOptions, } from "./hass-router-page"; -import { - STATE_STARTING, - STATE_NOT_RUNNING, - STATE_RUNNING, -} from "home-assistant-js-websocket"; -import { CustomPanelInfo } from "../data/panel_custom"; -import { deepActiveElement } from "../common/dom/deep-active-element"; const CACHE_URL_PATHS = ["lovelace", "developer-tools"]; const COMPONENTS = { @@ -64,6 +64,10 @@ const COMPONENTS = { import( /* webpackChunkName: "panel-shopping-list" */ "../panels/shopping-list/ha-panel-shopping-list" ), + "media-browser": () => + import( + /* webpackChunkName: "panel-media-browser" */ "../panels/media-browser/ha-panel-media-browser" + ), }; const getRoutes = (panels: Panels): RouterOptions => { diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index 4a40b0ee51e9..420d339e281d 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -1,9 +1,8 @@ -import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; -import "@material/mwc-list/mwc-list-item"; import "@material/mwc-icon-button"; -import "../../../../components/ha-button-menu"; -import "../../../../components/ha-svg-icon"; -import { mdiDotsVertical, mdiArrowUp, mdiArrowDown } from "@mdi/js"; +import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; +import "@material/mwc-list/mwc-list-item"; +import { mdiArrowDown, mdiArrowUp, mdiDotsVertical } from "@mdi/js"; +import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox"; @@ -12,29 +11,31 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, } from "lit-element"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; +import "../../../../components/ha-svg-icon"; import type { Action } from "../../../../data/script"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; +import { handleStructError } from "../../../lovelace/common/structs/handle-errors"; +import "./types/ha-automation-action-choose"; import "./types/ha-automation-action-condition"; import "./types/ha-automation-action-delay"; import "./types/ha-automation-action-device_id"; import "./types/ha-automation-action-event"; +import "./types/ha-automation-action-repeat"; import "./types/ha-automation-action-scene"; import "./types/ha-automation-action-service"; +import "./types/ha-automation-action-wait_for_trigger"; import "./types/ha-automation-action-wait_template"; -import "./types/ha-automation-action-repeat"; -import "./types/ha-automation-action-choose"; -import { handleStructError } from "../../../lovelace/common/structs/handle-errors"; -import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; -import { haStyle } from "../../../../resources/styles"; const OPTIONS = [ "condition", @@ -44,6 +45,7 @@ const OPTIONS = [ "scene", "service", "wait_template", + "wait_for_trigger", "repeat", "choose", ]; @@ -166,7 +168,7 @@ export default class HaAutomationActionRow extends LitElement { "ui.panel.config.automation.editor.edit_yaml" )} - + ${this.hass.localize( "ui.panel.config.automation.editor.actions.duplicate" )} @@ -261,6 +263,7 @@ export default class HaAutomationActionRow extends LitElement { this._switchYamlMode(); break; case 1: + fireEvent(this, "duplicate"); break; case 2: this._onDelete(); diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts index d6e815ea63c6..77db982c3b95 100644 --- a/src/panels/config/automation/action/ha-automation-action.ts +++ b/src/panels/config/automation/action/ha-automation-action.ts @@ -28,6 +28,7 @@ export default class HaAutomationAction extends LitElement { .index=${idx} .totalActions=${this.actions.length} .action=${action} + @duplicate=${this._duplicateAction} @move-action=${this._move} @value-changed=${this._actionChanged} .hass=${this.hass} @@ -78,6 +79,14 @@ export default class HaAutomationAction extends LitElement { fireEvent(this, "value-changed", { value: actions }); } + private _duplicateAction(ev: CustomEvent) { + ev.stopPropagation(); + const index = (ev.target as any).index; + fireEvent(this, "value-changed", { + value: this.actions.concat(this.actions[index]), + }); + } + static get styles(): CSSResult { return css` ha-automation-action-row, diff --git a/src/panels/config/automation/action/types/ha-automation-action-choose.ts b/src/panels/config/automation/action/types/ha-automation-action-choose.ts index 0bca89bf91de..fa1f52f349c0 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-choose.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-choose.ts @@ -1,22 +1,21 @@ +import { mdiDelete } from "@mdi/js"; import "@polymer/paper-input/paper-input"; +import "@polymer/paper-listbox/paper-listbox"; import { + css, + CSSResult, customElement, LitElement, property, - CSSResult, - css, } from "lit-element"; import { html } from "lit-html"; -import { Action, ChooseAction } from "../../../../../data/script"; -import { HomeAssistant } from "../../../../../types"; -import { ActionElement } from "../ha-automation-action-row"; -import "../../condition/ha-automation-condition-editor"; -import "@polymer/paper-listbox/paper-listbox"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import "../ha-automation-action"; import { Condition } from "../../../../../data/automation"; +import { Action, ChooseAction } from "../../../../../data/script"; import { haStyle } from "../../../../../resources/styles"; -import { mdiDelete } from "@mdi/js"; +import { HomeAssistant } from "../../../../../types"; +import "../ha-automation-action"; +import { ActionElement } from "../ha-automation-action-row"; @customElement("ha-automation-action-choose") export class HaChooseAction extends LitElement implements ActionElement { diff --git a/src/panels/config/automation/action/types/ha-automation-action-repeat.ts b/src/panels/config/automation/action/types/ha-automation-action-repeat.ts index 3fd2cd95cb82..13adc9aa7496 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-repeat.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-repeat.ts @@ -1,22 +1,21 @@ import "@polymer/paper-input/paper-input"; -import { customElement, LitElement, property, CSSResult } from "lit-element"; +import type { PaperListboxElement } from "@polymer/paper-listbox"; +import "@polymer/paper-listbox/paper-listbox"; +import { CSSResult, customElement, LitElement, property } from "lit-element"; import { html } from "lit-html"; +import { fireEvent } from "../../../../../common/dom/fire_event"; import { - RepeatAction, Action, CountRepeat, - WhileRepeat, + RepeatAction, UntilRepeat, + WhileRepeat, } from "../../../../../data/script"; +import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import { ActionElement } from "../ha-automation-action-row"; -import "../../condition/ha-automation-condition-editor"; -import type { PaperListboxElement } from "@polymer/paper-listbox"; -import "@polymer/paper-listbox/paper-listbox"; -import { fireEvent } from "../../../../../common/dom/fire_event"; -import "../ha-automation-action"; import { Condition } from "../../../../lovelace/common/validate-condition"; -import { haStyle } from "../../../../../resources/styles"; +import "../ha-automation-action"; +import { ActionElement } from "../ha-automation-action-row"; const OPTIONS = ["count", "while", "until"]; diff --git a/src/panels/config/automation/action/types/ha-automation-action-service.ts b/src/panels/config/automation/action/types/ha-automation-action-service.ts index 5ef46a6875c2..c7e564bcb869 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-service.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-service.ts @@ -8,6 +8,7 @@ import { } from "lit-element"; import { html } from "lit-html"; import memoizeOne from "memoize-one"; +import { any, assert, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { computeDomain } from "../../../../../common/entity/compute_domain"; import { computeObjectId } from "../../../../../common/entity/compute_object_id"; @@ -18,14 +19,13 @@ import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor"; import { ServiceAction } from "../../../../../data/script"; import type { PolymerChangedEvent } from "../../../../../polymer-types"; import type { HomeAssistant } from "../../../../../types"; -import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; -import { assert, optional, object, string } from "superstruct"; import { EntityId } from "../../../../lovelace/common/structs/is-entity-id"; +import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; const actionStruct = object({ service: optional(string()), entity_id: optional(EntityId), - data: optional(object()), + data: optional(any()), }); @customElement("ha-automation-action-service") diff --git a/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts b/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts new file mode 100644 index 000000000000..2d99185f5a03 --- /dev/null +++ b/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts @@ -0,0 +1,70 @@ +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-input/paper-textarea"; +import { customElement, LitElement, property } from "lit-element"; +import { html } from "lit-html"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-formfield"; +import { WaitForTriggerAction } from "../../../../../data/script"; +import { HomeAssistant } from "../../../../../types"; +import "../../trigger/ha-automation-trigger"; +import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; + +@customElement("ha-automation-action-wait_for_trigger") +export class HaWaitForTriggerAction extends LitElement + implements ActionElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public action!: WaitForTriggerAction; + + public static get defaultConfig() { + return { wait_for_trigger: [], timeout: "" }; + } + + protected render() { + const { wait_for_trigger, continue_on_timeout, timeout } = this.action; + + return html` + +
+ + + + + `; + } + + private _continueChanged(ev) { + fireEvent(this, "value-changed", { + value: { ...this.action, continue_on_timeout: ev.target.checked }, + }); + } + + private _valueChanged(ev: CustomEvent): void { + handleChangeEvent(this, ev); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-action-wait_for_trigger": HaWaitForTriggerAction; + } +} diff --git a/src/panels/config/automation/action/types/ha-automation-action-wait_template.ts b/src/panels/config/automation/action/types/ha-automation-action-wait_template.ts index e585d5203203..2cf6a86299a1 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-wait_template.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-wait_template.ts @@ -2,6 +2,7 @@ import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-textarea"; import { customElement, LitElement, property } from "lit-element"; import { html } from "lit-html"; +import { fireEvent } from "../../../../../common/dom/fire_event"; import { WaitAction } from "../../../../../data/script"; import { HomeAssistant } from "../../../../../types"; import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; @@ -13,11 +14,11 @@ export class HaWaitAction extends LitElement implements ActionElement { @property() public action!: WaitAction; public static get defaultConfig() { - return { wait_template: "", timeout: "" }; + return { wait_template: "" }; } protected render() { - const { wait_template, timeout } = this.action; + const { wait_template, timeout, continue_on_timeout } = this.action; return html` +
+ + + `; } + private _continueChanged(ev) { + fireEvent(this, "value-changed", { + value: { ...this.action, continue_on_timeout: ev.target.checked }, + }); + } + private _valueChanged(ev: CustomEvent): void { handleChangeEvent(this, ev); } diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts index 1afc44967c83..1f51b47f13d4 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -1,24 +1,24 @@ -import "../../../../components/ha-icon-button"; -import "@polymer/paper-item/paper-item"; +import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; -import "../../../../components/ha-button-menu"; import { mdiDotsVertical } from "@mdi/js"; +import "@polymer/paper-item/paper-item"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, } from "lit-element"; import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; +import "../../../../components/ha-icon-button"; import { Condition } from "../../../../data/automation"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { HomeAssistant } from "../../../../types"; import "./ha-automation-condition-editor"; -import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; export interface ConditionElement extends LitElement { condition: Condition; @@ -81,7 +81,7 @@ export default class HaAutomationConditionRow extends LitElement { "ui.panel.config.automation.editor.edit_yaml" )}
- + ${this.hass.localize( "ui.panel.config.automation.editor.actions.duplicate" )} @@ -109,6 +109,7 @@ export default class HaAutomationConditionRow extends LitElement { this._switchYamlMode(); break; case 1: + fireEvent(this, "duplicate"); break; case 2: this._onDelete(); diff --git a/src/panels/config/automation/condition/ha-automation-condition.ts b/src/panels/config/automation/condition/ha-automation-condition.ts index 05367bd6a259..b4529c46503e 100644 --- a/src/panels/config/automation/condition/ha-automation-condition.ts +++ b/src/panels/config/automation/condition/ha-automation-condition.ts @@ -6,6 +6,7 @@ import { html, LitElement, property, + PropertyValues, } from "lit-element"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-card"; @@ -20,13 +21,43 @@ export default class HaAutomationCondition extends LitElement { @property() public conditions!: Condition[]; + protected updated(changedProperties: PropertyValues) { + if (!changedProperties.has("conditions")) { + return; + } + let updatedConditions: Condition[] | undefined; + if (!Array.isArray(this.conditions)) { + updatedConditions = [this.conditions]; + } + + (updatedConditions || this.conditions).forEach((condition, index) => { + if (typeof condition === "string") { + updatedConditions = updatedConditions || [...this.conditions]; + updatedConditions[index] = { + condition: "template", + value_template: condition, + }; + } + }); + + if (updatedConditions) { + fireEvent(this, "value-changed", { + value: updatedConditions, + }); + } + } + protected render() { + if (!Array.isArray(this.conditions)) { + return html``; + } return html` ${this.conditions.map( (cond, idx) => html` @@ -68,6 +99,14 @@ export default class HaAutomationCondition extends LitElement { fireEvent(this, "value-changed", { value: conditions }); } + private _duplicateCondition(ev: CustomEvent) { + ev.stopPropagation(); + const index = (ev.target as any).index; + fireEvent(this, "value-changed", { + value: this.conditions.concat(this.conditions[index]), + }); + } + static get styles(): CSSResult { return css` ha-automation-condition-row, diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-numeric_state.ts b/src/panels/config/automation/condition/types/ha-automation-condition-numeric_state.ts index 656fc999e616..71259efe5d94 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-numeric_state.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-numeric_state.ts @@ -1,7 +1,6 @@ import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-textarea"; import { customElement, html, LitElement, property } from "lit-element"; -import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/entity/ha-entity-picker"; import { NumericStateCondition } from "../../../../../data/automation"; import { HomeAssistant } from "../../../../../types"; @@ -19,16 +18,34 @@ export default class HaNumericStateCondition extends LitElement { }; } - protected render() { - const { value_template, entity_id, below, above } = this.condition; + public render() { + const { + value_template, + entity_id, + attribute, + below, + above, + } = this.condition; return html` + + ) { - ev.stopPropagation(); - fireEvent(this, "value-changed", { - value: { ...this.condition, entity_id: ev.detail.value }, - }); - } } declare global { diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-time.ts b/src/panels/config/automation/condition/types/ha-automation-condition-time.ts index 7f790194fc65..2e8a75141a07 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-time.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-time.ts @@ -1,5 +1,14 @@ +import { Radio } from "@material/mwc-radio"; import "@polymer/paper-input/paper-input"; -import { customElement, html, LitElement, property } from "lit-element"; +import { + customElement, + html, + internalProperty, + LitElement, + property, +} from "lit-element"; +import "../../../../../components/ha-formfield"; +import "../../../../../components/ha-radio"; import { TimeCondition } from "../../../../../data/automation"; import { HomeAssistant } from "../../../../../types"; import { @@ -7,11 +16,31 @@ import { handleChangeEvent, } from "../ha-automation-condition-row"; +const includeDomains = ["input_datetime"]; + +interface WeekdayHaSwitch extends HaSwitch { + day: string; +} + @customElement("ha-automation-condition-time") export class HaTimeCondition extends LitElement implements ConditionElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public condition!: TimeCondition; + @property({ attribute: false }) public condition!: TimeCondition; + + private _days: Weekday[] = [ + { name: "mon", order: 1 }, + { name: "tue", order: 2 }, + { name: "wed", order: 3 }, + { name: "thu", order: 4 }, + { name: "fri", order: 5 }, + { name: "sat", order: 6 }, + { name: "sun", order: 7 }, + ]; + + @internalProperty() private _inputModeBefore?: boolean; + + @internalProperty() private _inputModeAfter?: boolean; public static get defaultConfig() { return {}; @@ -19,27 +48,150 @@ export class HaTimeCondition extends LitElement implements ConditionElement { protected render() { const { after, before } = this.condition; + + const inputModeBefore = + this._inputModeBefore ?? before?.startsWith("input_datetime."); + const inputModeAfter = + this._inputModeAfter ?? after?.startsWith("input_datetime."); + return html` - - + + + + + + ${inputModeAfter + ? html`` + : html``} + + + > + + + + + + ${inputModeBefore + ? html`` + : html``} `; } + private _handleModeChanged(ev: Event) { + const target = ev.target as Radio; + if (target.getAttribute("name") === "mode_after") { + this._inputModeAfter = target.value === "input"; + } else { + this._inputModeBefore = target.value === "input"; + } + } + private _valueChanged(ev: CustomEvent): void { handleChangeEvent(this, ev); } + + private _dayValueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + + const daySwitch = ev.currentTarget as WeekdayHaSwitch; + + let days = this.condition.weekday || []; + + if (daySwitch.checked) { + days.push(daySwitch.day); + } else { + days = days.filter((d) => d !== daySwitch.day); + } + + days.sort((a: string, b: string) => DAYS[a] - DAYS[b]); + + this.condition.weekday = days; + + fireEvent(this, "value-changed", { + value: this.condition, + }); + } + + static get styles(): CSSResult { + return css` + .flex { + display: flex; + height: 40px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-condition-time": HaTimeCondition; + } } diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 255edefff7a8..1c6f0c125c98 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -1,28 +1,32 @@ +import "@material/mwc-fab"; +import { mdiContentDuplicate, mdiContentSave, mdiDelete } from "@mdi/js"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; import "@polymer/paper-input/paper-textarea"; -import "../../../components/ha-icon-button"; +import { PaperListboxElement } from "@polymer/paper-listbox"; import { css, CSSResult, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; import { navigate } from "../../../common/navigate"; import "../../../components/ha-card"; +import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; -import "@material/mwc-fab"; import { AutomationConfig, AutomationEntity, Condition, deleteAutomation, getAutomationEditorInitData, + showAutomationEditor, Trigger, triggerAutomation, } from "../../../data/automation"; @@ -42,9 +46,6 @@ import { HaDeviceAction } from "./action/types/ha-automation-action-device_id"; import "./condition/ha-automation-condition"; import "./trigger/ha-automation-trigger"; import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device"; -import { mdiContentSave } from "@mdi/js"; -import { PaperListboxElement } from "@polymer/paper-listbox"; -import { classMap } from "lit-html/directives/class-map"; const MODES = ["single", "restart", "queued", "parallel"]; const MODES_MAX = ["queued", "parallel"]; @@ -53,6 +54,7 @@ declare global { // for fire event interface HASSDomEvents { "ui-mode-not-available": Error; + duplicate: undefined; } } @@ -92,14 +94,24 @@ export class HaAutomationEditor extends LitElement { ${!this.automationId ? "" : html` - + +
+ + > + + `} ${this._config ? html` @@ -473,6 +485,31 @@ export class HaAutomationEditor extends LitElement { } } + private async _duplicate() { + if (this._dirty) { + if ( + !(await showConfirmationDialog(this, { + text: this.hass!.localize( + "ui.panel.config.automation.editor.unsaved_confirm" + ), + confirmText: this.hass!.localize("ui.common.yes"), + dismissText: this.hass!.localize("ui.common.no"), + })) + ) { + return; + } + // Wait for dialog to complate closing + await new Promise((resolve) => setTimeout(resolve, 0)); + } + showAutomationEditor(this, { + ...this._config, + id: undefined, + alias: `${this._config?.alias} (${this.hass.localize( + "ui.panel.config.automation.picker.duplicate" + )})`, + }); + } + private async _deleteConfirm() { showConfirmationDialog(this, { text: this.hass.localize( diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index a50373bbc8e1..feeda917738f 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -1,25 +1,27 @@ +import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; +import "@material/mwc-list/mwc-list-item"; +import { mdiDotsVertical } from "@mdi/js"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; -import "../../../../components/ha-icon-button"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; -import "@material/mwc-list/mwc-list-item"; -import "../../../../components/ha-button-menu"; -import { mdiDotsVertical } from "@mdi/js"; import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, } from "lit-element"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; +import "../../../../components/ha-icon-button"; import type { Trigger } from "../../../../data/automation"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import "./types/ha-automation-trigger-device"; import "./types/ha-automation-trigger-event"; @@ -29,14 +31,12 @@ import "./types/ha-automation-trigger-mqtt"; import "./types/ha-automation-trigger-numeric_state"; import "./types/ha-automation-trigger-state"; import "./types/ha-automation-trigger-sun"; +import "./types/ha-automation-trigger-tag"; import "./types/ha-automation-trigger-template"; import "./types/ha-automation-trigger-time"; import "./types/ha-automation-trigger-time_pattern"; import "./types/ha-automation-trigger-webhook"; import "./types/ha-automation-trigger-zone"; -import "./types/ha-automation-trigger-tag"; -import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; -import { haStyle } from "../../../../resources/styles"; const OPTIONS = [ "device", @@ -113,7 +113,7 @@ export default class HaAutomationTriggerRow extends LitElement { "ui.panel.config.automation.editor.edit_yaml" )} - + ${this.hass.localize( "ui.panel.config.automation.editor.actions.duplicate" )} @@ -183,6 +183,7 @@ export default class HaAutomationTriggerRow extends LitElement { this._switchYamlMode(); break; case 1: + fireEvent(this, "duplicate"); break; case 2: this._onDelete(); diff --git a/src/panels/config/automation/trigger/ha-automation-trigger.ts b/src/panels/config/automation/trigger/ha-automation-trigger.ts index a074cbb881e6..953ec670379e 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger.ts @@ -27,6 +27,7 @@ export default class HaAutomationTrigger extends LitElement { @@ -68,6 +69,14 @@ export default class HaAutomationTrigger extends LitElement { fireEvent(this, "value-changed", { value: triggers }); } + private _duplicateTrigger(ev: CustomEvent) { + ev.stopPropagation(); + const index = (ev.target as any).index; + fireEvent(this, "value-changed", { + value: this.triggers.concat(this.triggers[index]), + }); + } + static get styles(): CSSResult { return css` ha-automation-trigger-row, diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts index 068dbd685697..0c54205b2b2b 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state.ts @@ -1,7 +1,6 @@ import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-textarea"; import { customElement, html, LitElement, property } from "lit-element"; -import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/entity/ha-entity-picker"; import { ForDict, NumericStateTrigger } from "../../../../../data/automation"; import { HomeAssistant } from "../../../../../types"; @@ -19,8 +18,8 @@ export default class HaNumericStateTrigger extends LitElement { }; } - protected render() { - const { value_template, entity_id, below, above } = this.trigger; + public render() { + const { value_template, entity_id, attribute, below, above } = this.trigger; let trgFor = this.trigger.for; if ( @@ -41,10 +40,22 @@ export default class HaNumericStateTrigger extends LitElement { return html` + + ) { - ev.stopPropagation(); - fireEvent(this, "value-changed", { - value: { ...this.trigger, entity_id: ev.detail.value }, - }); - } } declare global { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts index 7ea3043660be..1d9e6fa67704 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts @@ -1,5 +1,14 @@ import "@polymer/paper-input/paper-input"; -import { customElement, html, LitElement, property } from "lit-element"; +import { + customElement, + html, + internalProperty, + LitElement, + property, +} from "lit-element"; +import "../../../../../components/entity/ha-entity-picker"; +import "../../../../../components/ha-formfield"; +import "../../../../../components/ha-radio"; import { TimeTrigger } from "../../../../../data/automation"; import { HomeAssistant } from "../../../../../types"; import { @@ -7,31 +16,81 @@ import { TriggerElement, } from "../ha-automation-trigger-row"; +const includeDomains = ["input_datetime"]; + @customElement("ha-automation-trigger-time") export class HaTimeTrigger extends LitElement implements TriggerElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public trigger!: TimeTrigger; + @internalProperty() private _inputMode?: boolean; + public static get defaultConfig() { return { at: "" }; } protected render() { const { at } = this.trigger; + const inputMode = this._inputMode ?? at?.startsWith("input_datetime."); return html` - + > + + + + + + ${inputMode + ? html`` + : html``} `; } + private _handleModeChanged(ev: Event) { + this._inputMode = (ev.target as any).value === "input"; + } + private _valueChanged(ev: CustomEvent): void { handleChangeEvent(this, ev); } } + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-trigger-time": HaTimeTrigger; + } +} diff --git a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts new file mode 100644 index 000000000000..7f0cac03f2c2 --- /dev/null +++ b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts @@ -0,0 +1,88 @@ +import { + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, + css, + PropertyValues, +} from "lit-element"; +import { DeviceRegistryEntry } from "../../../../../../data/device_registry"; +import { haStyle } from "../../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../../types"; +import { + getIdentifiersFromDevice, + OZWNodeIdentifiers, +} from "../../../../../../data/ozw"; +import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node"; +import { navigate } from "../../../../../../common/navigate"; +import "@material/mwc-button/mwc-button"; + +@customElement("ha-device-actions-ozw") +export class HaDeviceActionsOzw extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public device!: DeviceRegistryEntry; + + @property() + private node_id = 0; + + @property() + private ozw_instance = 1; + + + protected updated(changedProperties: PropertyValues) { + if (changedProperties.has("device")) { + const identifiers: + | OZWNodeIdentifiers + | undefined = getIdentifiersFromDevice(this.device); + if (!identifiers) { + return; + } + this.ozw_instance = identifiers.ozw_instance; + this.node_id = identifiers.node_id; + } + } + + protected render(): TemplateResult { + if (!this.ozw_instance || !this.node_id) { + return html``; + } + return html` + + ${this.hass.localize("ui.panel.config.ozw.node.button")} + + + ${this.hass.localize("ui.panel.config.ozw.refresh_node.button")} + + `; + } + + private async _refreshNodeClicked() { + showOZWRefreshNodeDialog(this, { + node_id: this.node_id, + ozw_instance: this.ozw_instance, + }); + } + + private async _nodeDetailsClicked() { + navigate( + this, + `/config/ozw/network/${this.ozw_instance}/node/${this.node_id}/dashboard` + ); + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + :host { + display: flex; + flex-direction: column; + align-items: flex-start; + } + `, + ]; + } +} diff --git a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts index 2197fe689b80..ce639c75b82b 100644 --- a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts +++ b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-info-ozw.ts @@ -18,7 +18,6 @@ import { getIdentifiersFromDevice, OZWNodeIdentifiers, } from "../../../../../../data/ozw"; -import { showOZWRefreshNodeDialog } from "../../../../integrations/integration-panels/ozw/show-dialog-ozw-refresh-node"; @customElement("ha-device-info-ozw") export class HaDeviceInfoOzw extends LitElement { @@ -83,19 +82,9 @@ export class HaDeviceInfoOzw extends LitElement { ? this.hass.localize("ui.common.yes") : this.hass.localize("ui.common.no")}
- - Refresh Node - `; } - private async _refreshNodeClicked() { - showOZWRefreshNodeDialog(this, { - node_id: this.node_id, - ozw_instance: this.ozw_instance, - }); - } - static get styles(): CSSResult[] { return [ haStyle, diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 0287b084cd01..22c2ea190af7 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -517,12 +517,19 @@ export class HaConfigDevicePage extends LitElement { `); } if (integrations.includes("ozw")) { + import("./device-detail/integration-elements/ozw/ha-device-actions-ozw"); import("./device-detail/integration-elements/ozw/ha-device-info-ozw"); templates.push(html` +
+ +
`); } if (integrations.includes("zha")) { diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index dd738133f7f0..99d7f6b26629 100644 --- a/src/panels/config/integrations/ha-config-integrations.ts +++ b/src/panels/config/integrations/ha-config-integrations.ts @@ -10,12 +10,13 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; import memoizeOne from "memoize-one"; import { HASSDomEvent } from "../../../common/dom/fire_event"; import "../../../common/search/search-input"; @@ -32,6 +33,7 @@ import { getConfigEntries, } from "../../../data/config_entries"; import { + ATTENTION_SOURCES, DISCOVERY_SOURCES, getConfigFlowInProgressCollection, ignoreConfigFlow, @@ -355,52 +357,67 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { : ""} ${configEntriesInProgress.length ? configEntriesInProgress.map( - (flow: DataEntryFlowProgressExtended) => html` - -
- ${this.hass.localize( - "ui.panel.config.integrations.discovered" - )} -
-
-
- + (flow: DataEntryFlowProgressExtended) => { + const attention = ATTENTION_SOURCES.includes( + flow.context.source + ); + return html` + +
+ ${this.hass.localize( + `ui.panel.config.integrations.${ + attention ? "attention" : "discovered" + }` + )}
-

- ${flow.localized_title} -

-
- - ${this.hass.localize( - "ui.panel.config.integrations.configure" - )} - - ${DISCOVERY_SOURCES.includes(flow.context.source) && - flow.context.unique_id - ? html` - - ${this.hass.localize( - "ui.panel.config.integrations.ignore.ignore" - )} - - ` - : ""} +
+
+ +
+

+ ${flow.localized_title} +

+
+ + ${this.hass.localize( + `ui.panel.config.integrations.${ + attention ? "reconfigure" : "configure" + }` + )} + + ${DISCOVERY_SOURCES.includes(flow.context.source) && + flow.context.unique_id + ? html` + + ${this.hass.localize( + "ui.panel.config.integrations.ignore.ignore" + )} + + ` + : ""} +
-
-
- ` + + `; + } ) : ""} ${groupedConfigEntries.size @@ -639,6 +656,18 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) { flex-direction: column; justify-content: space-between; } + .attention { + --ha-card-border-color: var(--error-color); + } + .attention .header { + background: var(--error-color); + color: var(--text-primary-color); + padding: 8px; + text-align: center; + } + .attention mwc-button { + --mdc-theme-primary: var(--error-color); + } .discovered { --ha-card-border-color: var(--primary-color); } diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index dddeaf67542f..a1625c7c2c43 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -137,6 +137,7 @@ export class HaIntegrationCard extends LitElement { private _renderSingleEntry(item: ConfigEntryExtended): TemplateResult { const devices = this._getDevices(item); + const services = this._getServices(item); const entities = this._getEntities(item); return html` @@ -168,7 +169,7 @@ export class HaIntegrationCard extends LitElement {

${item.localized_domain_name === item.title ? "" : item.title}

- ${devices.length || entities.length + ${devices.length || services.length || entities.length ? html`
${devices.length @@ -180,10 +181,22 @@ export class HaIntegrationCard extends LitElement { "count", devices.length )}${services.length ? "," : ""} + ` + : ""} + ${services.length + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.services", + "count", + services.length + )} ` : ""} - ${devices.length && entities.length + ${(devices.length || services.length) && entities.length ? this.hass.localize("ui.common.and") : ""} ${entities.length @@ -304,8 +317,21 @@ export class HaIntegrationCard extends LitElement { if (!this.deviceRegistryEntries) { return []; } - return this.deviceRegistryEntries.filter((device) => - device.config_entries.includes(configEntry.entry_id) + return this.deviceRegistryEntries.filter( + (device) => + device.config_entries.includes(configEntry.entry_id) && + device.entry_type !== "service" + ); + } + + private _getServices(configEntry: ConfigEntry): DeviceRegistryEntry[] { + if (!this.deviceRegistryEntries) { + return []; + } + return this.deviceRegistryEntries.filter( + (device) => + device.config_entries.includes(configEntry.entry_id) && + device.entry_type === "service" ); } diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts index f23d7a711451..478ede0f8ee5 100644 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-config-dashboard.ts @@ -1,6 +1,8 @@ +import "@material/mwc-button/mwc-button"; +import "@material/mwc-fab"; +import { mdiCheckCircle, mdiCircle, mdiCloseCircle, mdiZWave } from "@mdi/js"; import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; -import "@material/mwc-fab"; import { css, CSSResultArray, @@ -14,20 +16,18 @@ import { import { navigate } from "../../../../../common/navigate"; import "../../../../../components/ha-card"; import "../../../../../components/ha-icon-next"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; -import { mdiCircle, mdiCheckCircle, mdiCloseCircle, mdiZWave } from "@mdi/js"; -import "../../../../../layouts/hass-tabs-subpage"; -import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; -import "@material/mwc-button/mwc-button"; import { - OZWInstance, fetchOZWInstances, - networkOnlineStatuses, networkOfflineStatuses, + networkOnlineStatuses, networkStartingStatuses, + OZWInstance, } from "../../../../../data/ozw"; +import "../../../../../layouts/hass-tabs-subpage"; +import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant, Route } from "../../../../../types"; +import "../../../ha-config-section"; export const ozwTabs: PageNavigation[] = []; @@ -45,22 +45,8 @@ class OZWConfigDashboard extends LitElement { @internalProperty() private _instances: OZWInstance[] = []; - public connectedCallback(): void { - super.connectedCallback(); - if (this.hass) { - this._fetchData(); - } - } - - private async _fetchData() { - this._instances = await fetchOZWInstances(this.hass!); - if (this._instances.length === 1) { - navigate( - this, - `/config/ozw/network/${this._instances[0].ozw_instance}`, - true - ); - } + protected firstUpdated() { + this._fetchData(); } protected render(): TemplateResult { @@ -142,12 +128,23 @@ class OZWConfigDashboard extends LitElement { `; })} ` - : ``} + : ""} `; } + private async _fetchData() { + this._instances = await fetchOZWInstances(this.hass!); + if (this._instances.length === 1) { + navigate( + this, + `/config/ozw/network/${this._instances[0].ozw_instance}`, + true + ); + } + } + static get styles(): CSSResultArray { return [ haStyle, diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts index 28402cbe4c4f..804e2073afb2 100644 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-config-router.ts @@ -1,10 +1,23 @@ import { customElement, property } from "lit-element"; +import memoizeOne from "memoize-one"; import { HassRouterPage, RouterOptions, } from "../../../../../layouts/hass-router-page"; -import { HomeAssistant } from "../../../../../types"; -import { navigate } from "../../../../../common/navigate"; +import { HomeAssistant, Route } from "../../../../../types"; + +export const computeTail = memoizeOne((route: Route) => { + const dividerPos = route.path.indexOf("/", 1); + return dividerPos === -1 + ? { + prefix: route.prefix + route.path, + path: "", + } + : { + prefix: route.prefix + route.path.substr(0, dividerPos), + path: route.path.substr(dividerPos), + }; +}); @customElement("ozw-config-router") class OZWConfigRouter extends HassRouterPage { @@ -30,10 +43,10 @@ class OZWConfigRouter extends HassRouterPage { ), }, network: { - tag: "ozw-config-network", + tag: "ozw-network-router", load: () => import( - /* webpackChunkName: "ozw-config-network" */ "./ozw-config-network" + /* webpackChunkName: "ozw-network-router" */ "./ozw-network-router" ), }, }, @@ -46,19 +59,9 @@ class OZWConfigRouter extends HassRouterPage { el.narrow = this.narrow; el.configEntryId = this._configEntry; if (this._currentPage === "network") { - el.ozw_instance = this.routeTail.path.substr(1); - } - - const searchParams = new URLSearchParams(window.location.search); - if (this._configEntry && !searchParams.has("config_entry")) { - searchParams.append("config_entry", this._configEntry); - navigate( - this, - `${this.routeTail.prefix}${ - this.routeTail.path - }?${searchParams.toString()}`, - true - ); + const path = this.routeTail.path.split("/"); + el.ozwInstance = path[1]; + el.route = computeTail(this.routeTail); } } } diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-config-network.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts similarity index 83% rename from src/panels/config/integrations/integration-panels/ozw/ozw-config-network.ts rename to src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts index 6d67ed779e8e..98ae8a28a635 100644 --- a/src/panels/config/integrations/integration-panels/ozw/ozw-config-network.ts +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-network-dashboard.ts @@ -1,4 +1,6 @@ +import "@material/mwc-button/mwc-button"; import "@material/mwc-fab"; +import { mdiCheckCircle, mdiCircle, mdiCloseCircle } from "@mdi/js"; import { css, CSSResultArray, @@ -9,31 +11,28 @@ import { property, TemplateResult, } from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; import { navigate } from "../../../../../common/navigate"; +import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; import "../../../../../components/ha-icon-next"; -import "../../../../../components/buttons/ha-call-service-button"; -import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; -import "../../../ha-config-section"; -import { mdiCircle, mdiCheckCircle, mdiCloseCircle } from "@mdi/js"; -import "../../../../../layouts/hass-tabs-subpage"; -import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; -import "@material/mwc-button/mwc-button"; import { - OZWInstance, - fetchOZWNetworkStatus, fetchOZWNetworkStatistics, - networkOnlineStatuses, + fetchOZWNetworkStatus, networkOfflineStatuses, + networkOnlineStatuses, networkStartingStatuses, + OZWInstance, OZWNetworkStatistics, } from "../../../../../data/ozw"; +import "../../../../../layouts/hass-tabs-subpage"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant, Route } from "../../../../../types"; +import "../../../ha-config-section"; +import { ozwNetworkTabs } from "./ozw-network-router"; -export const ozwTabs: PageNavigation[] = []; - -@customElement("ozw-config-network") -class OZWConfigNetwork extends LitElement { +@customElement("ozw-network-dashboard") +class OZWNetworkDashboard extends LitElement { @property({ type: Object }) public hass!: HomeAssistant; @property({ type: Object }) public route!: Route; @@ -44,7 +43,7 @@ class OZWConfigNetwork extends LitElement { @property() public configEntryId?: string; - @property() public ozw_instance = 0; + @property() public ozwInstance?: number; @internalProperty() private _network?: OZWInstance; @@ -54,54 +53,21 @@ class OZWConfigNetwork extends LitElement { @internalProperty() private _icon = mdiCircle; - public connectedCallback(): void { - super.connectedCallback(); - if (this.ozw_instance <= 0) { + protected firstUpdated() { + if (!this.ozwInstance) { navigate(this, "/config/ozw/dashboard", true); - } - if (this.hass) { + } else if (this.hass) { this._fetchData(); } } - private async _fetchData() { - this._network = await fetchOZWNetworkStatus(this.hass!, this.ozw_instance); - this._statistics = await fetchOZWNetworkStatistics( - this.hass!, - this.ozw_instance - ); - if (networkOnlineStatuses.includes(this._network.Status)) { - this._status = "online"; - this._icon = mdiCheckCircle; - } - if (networkStartingStatuses.includes(this._network.Status)) { - this._status = "starting"; - } - if (networkOfflineStatuses.includes(this._network.Status)) { - this._status = "offline"; - this._icon = mdiCloseCircle; - } - } - - private _generateServiceButton(service: string) { - return html` - - ${this.hass!.localize("ui.panel.config.ozw.services." + service)} - - `; - } - protected render(): TemplateResult { return html`
@@ -118,20 +84,21 @@ class OZWConfigNetwork extends LitElement {
${this.hass.localize( "ui.panel.config.ozw.common.network" )} ${this.hass.localize( - "ui.panel.config.ozw.network_status." + this._status + `ui.panel.config.ozw.network_status.${this._status}` )}
${this.hass.localize( - "ui.panel.config.ozw.network_status.details." + - this._network.Status.toLowerCase() + `ui.panel.config.ozw.network_status.details.${this._network.Status.toLowerCase()}` )}
@@ -171,6 +138,38 @@ class OZWConfigNetwork extends LitElement { `; } + private async _fetchData() { + if (!this.ozwInstance) return; + this._network = await fetchOZWNetworkStatus(this.hass!, this.ozwInstance); + this._statistics = await fetchOZWNetworkStatistics( + this.hass!, + this.ozwInstance + ); + if (networkOnlineStatuses.includes(this._network!.Status)) { + this._status = "online"; + this._icon = mdiCheckCircle; + } + if (networkStartingStatuses.includes(this._network!.Status)) { + this._status = "starting"; + } + if (networkOfflineStatuses.includes(this._network!.Status)) { + this._status = "offline"; + this._icon = mdiCloseCircle; + } + } + + private _generateServiceButton(service: string) { + return html` + + ${this.hass!.localize(`ui.panel.config.ozw.services.${service}`)} + + `; + } + static get styles(): CSSResultArray { return [ haStyle, @@ -248,6 +247,6 @@ class OZWConfigNetwork extends LitElement { declare global { interface HTMLElementTagNameMap { - "ozw-config-network": OZWConfigNetwork; + "ozw-network-dashboard": OZWNetworkDashboard; } } diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts new file mode 100644 index 000000000000..11d6a3c2a391 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts @@ -0,0 +1,144 @@ +import "@material/mwc-button/mwc-button"; +import "@material/mwc-fab"; +import { mdiAlert, mdiCheck } from "@mdi/js"; +import { + CSSResult, + customElement, + html, + internalProperty, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import memoizeOne from "memoize-one"; +import { navigate } from "../../../../../common/navigate"; +import "../../../../../components/buttons/ha-call-service-button"; +import { HASSDomEvent } from "../../../../../common/dom/fire_event"; +import { + DataTableColumnContainer, + RowClickedEvent, +} from "../../../../../components/data-table/ha-data-table"; +import "../../../../../components/ha-card"; +import "../../../../../components/ha-icon-next"; +import { fetchOZWNodes, OZWDevice } from "../../../../../data/ozw"; +import "../../../../../layouts/hass-tabs-subpage"; +import "../../../../../layouts/hass-tabs-subpage-data-table"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant, Route } from "../../../../../types"; +import "../../../ha-config-section"; +import { ozwNetworkTabs } from "./ozw-network-router"; + +export interface NodeRowData extends OZWDevice { + node?: NodeRowData; + id?: number; +} + +@customElement("ozw-network-nodes") +class OZWNetworkNodes extends LitElement { + @property({ type: Object }) public hass!: HomeAssistant; + + @property({ type: Object }) public route!: Route; + + @property({ type: Boolean }) public narrow!: boolean; + + @property({ type: Boolean }) public isWide!: boolean; + + @property() public configEntryId?: string; + + @property() public ozwInstance = 0; + + @internalProperty() private _nodes: OZWDevice[] = []; + + private _columns = memoizeOne( + (narrow: boolean): DataTableColumnContainer => { + return { + node_id: { + title: this.hass.localize("ui.panel.config.ozw.nodes_table.id"), + sortable: true, + type: "numeric", + width: "72px", + filterable: true, + direction: "asc", + }, + node_product_name: { + title: this.hass.localize("ui.panel.config.ozw.nodes_table.model"), + sortable: true, + width: narrow ? "75%" : "25%", + }, + node_manufacturer_name: { + title: this.hass.localize( + "ui.panel.config.ozw.nodes_table.manufacturer" + ), + sortable: true, + hidden: narrow, + width: "25%", + }, + node_query_stage: { + title: this.hass.localize( + "ui.panel.config.ozw.nodes_table.query_stage" + ), + sortable: true, + width: narrow ? "25%" : "15%", + }, + is_zwave_plus: { + title: this.hass.localize( + "ui.panel.config.ozw.nodes_table.zwave_plus" + ), + hidden: narrow, + template: (value: boolean) => + value ? html` ` : "", + }, + is_failed: { + title: this.hass.localize("ui.panel.config.ozw.nodes_table.failed"), + hidden: narrow, + template: (value: boolean) => + value ? html` ` : "", + }, + }; + } + ); + + protected firstUpdated() { + if (!this.ozwInstance) { + navigate(this, "/config/ozw/dashboard", true); + } else if (this.hass) { + this._fetchData(); + } + } + + protected render(): TemplateResult { + return html` + + + `; + } + + private async _fetchData() { + this._nodes = await fetchOZWNodes(this.hass!, this.ozwInstance!); + } + + private _handleRowClicked(ev: HASSDomEvent) { + const nodeId = ev.detail.id; + navigate(this, `/config/ozw/network/${this.ozwInstance}/node/${nodeId}`); + } + + static get styles(): CSSResult { + return haStyle; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ozw-network-nodes": OZWNetworkNodes; + } +} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-network-router.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-network-router.ts new file mode 100644 index 000000000000..f98660bed0ee --- /dev/null +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-network-router.ts @@ -0,0 +1,83 @@ +import { customElement, property } from "lit-element"; +import { + HassRouterPage, + RouterOptions, +} from "../../../../../layouts/hass-router-page"; +import { HomeAssistant } from "../../../../../types"; +import { computeTail } from "./ozw-config-router"; +import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage"; +import { mdiServerNetwork, mdiNetwork } from "@mdi/js"; + +export const ozwNetworkTabs = (instance: number): PageNavigation[] => { + return [ + { + translationKey: "ui.panel.config.ozw.navigation.network", + path: `/config/ozw/network/${instance}/dashboard`, + iconPath: mdiServerNetwork, + }, + { + translationKey: "ui.panel.config.ozw.navigation.nodes", + path: `/config/ozw/network/${instance}/nodes`, + iconPath: mdiNetwork, + }, + ]; +}; + +@customElement("ozw-network-router") +class OZWNetworkRouter extends HassRouterPage { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public isWide!: boolean; + + @property() public narrow!: boolean; + + @property() public ozwInstance!: number; + + private _configEntry = new URLSearchParams(window.location.search).get( + "config_entry" + ); + + protected routerOptions: RouterOptions = { + defaultPage: "dashboard", + showLoading: true, + routes: { + dashboard: { + tag: "ozw-network-dashboard", + load: () => + import( + /* webpackChunkName: "ozw-network-dashboard" */ "./ozw-network-dashboard" + ), + }, + nodes: { + tag: "ozw-network-nodes", + load: () => + import( + /* webpackChunkName: "ozw-network-nodes" */ "./ozw-network-nodes" + ), + }, + node: { + tag: "ozw-node-router", + load: () => + import(/* webpackChunkName: "ozw-node-router" */ "./ozw-node-router"), + }, + }, + }; + + protected updatePageEl(el): void { + el.route = computeTail(this.routeTail); + el.hass = this.hass; + el.isWide = this.isWide; + el.narrow = this.narrow; + el.configEntryId = this._configEntry; + el.ozwInstance = this.ozwInstance; + if (this._currentPage === "node") { + el.nodeId = this.routeTail.path.split("/")[1]; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "ozw-network-router": OZWNetworkRouter; + } +} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts new file mode 100644 index 000000000000..bd2fa6af3e75 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts @@ -0,0 +1,231 @@ +import "@material/mwc-button/mwc-button"; +import "@material/mwc-fab"; +import { + css, + CSSResultArray, + customElement, + html, + LitElement, + internalProperty, + property, + TemplateResult, +} from "lit-element"; +import { navigate } from "../../../../../common/navigate"; +import "../../../../../components/buttons/ha-call-service-button"; +import "../../../../../components/ha-card"; +import "../../../../../components/ha-icon-next"; +import "../../../../../layouts/hass-tabs-subpage"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant, Route } from "../../../../../types"; +import "../../../ha-config-section"; +import { + fetchOZWNodeStatus, + fetchOZWNodeMetadata, + OZWDevice, + OZWDeviceMetaDataResponse, +} from "../../../../../data/ozw"; +import { ERR_NOT_FOUND } from "../../../../../data/websocket_api"; +import { showOZWRefreshNodeDialog } from "./show-dialog-ozw-refresh-node"; +import { ozwNetworkTabs } from "./ozw-network-router"; + +@customElement("ozw-node-dashboard") +class OZWNodeDashboard extends LitElement { + @property({ type: Object }) public hass!: HomeAssistant; + + @property({ type: Object }) public route!: Route; + + @property({ type: Boolean }) public narrow!: boolean; + + @property({ type: Boolean }) public isWide!: boolean; + + @property() public configEntryId?: string; + + @property() public ozwInstance?; + + @property() public nodeId?; + + @internalProperty() private _node?: OZWDevice; + + @internalProperty() private _metadata?: OZWDeviceMetaDataResponse; + + @internalProperty() private _not_found = false; + + protected firstUpdated() { + if (!this.ozwInstance) { + navigate(this, "/config/ozw/dashboard", true); + } else if (!this.nodeId) { + navigate(this, `/config/ozw/network/${this.ozwInstance}/nodes`, true); + } else if (this.hass) { + this._fetchData(); + } + } + + protected render(): TemplateResult { + if (this._not_found) { + return html` + + `; + } + + return html` + + +
+ Node Management +
+ +
+ View the status of a node and manage its configuration. +
+ ${this._node + ? html` + +
+ ${this._node.node_manufacturer_name} + ${this._node.node_product_name}
+ Node ID: ${this._node.node_id}
+ Query Stage: ${this._node.node_query_stage} + ${this._metadata?.metadata.ProductManualURL + ? html` +

Product Manual

+
` + : ``} +
+
+ + Refresh Node + +
+
+ + ${this._metadata + ? html` + +
+ ${this._metadata.metadata.Description} +
+
+ +
+ ${this._metadata.metadata.InclusionHelp} +
+
+ +
+ ${this._metadata.metadata.ExclusionHelp} +
+
+ +
+ ${this._metadata.metadata.ResetHelp} +
+
+ +
+ ${this._metadata.metadata.WakeupHelp} +
+
+ ` + : ``} + ` + : ``} +
+
+ `; + } + + private async _fetchData() { + if (!this.ozwInstance || !this.nodeId) { + return; + } + + try { + this._node = await fetchOZWNodeStatus( + this.hass!, + this.ozwInstance, + this.nodeId + ); + this._metadata = await fetchOZWNodeMetadata( + this.hass!, + this.ozwInstance, + this.nodeId + ); + } catch (err) { + if (err.code === ERR_NOT_FOUND) { + this._not_found = true; + return; + } + throw err; + } + } + + private async _refreshNodeClicked() { + showOZWRefreshNodeDialog(this, { + node_id: this.nodeId, + ozw_instance: this.ozwInstance, + }); + } + + static get styles(): CSSResultArray { + return [ + haStyle, + css` + .secondary { + color: var(--secondary-text-color); + } + + .content { + margin-top: 24px; + } + + .sectionHeader { + position: relative; + padding-right: 40px; + } + + ha-card { + margin: 0 auto; + max-width: 600px; + } + + .card-actions.warning ha-call-service-button { + color: var(--error-color); + } + + .toggle-help-icon { + position: absolute; + top: -6px; + right: 0; + color: var(--primary-color); + } + + ha-service-description { + display: block; + color: grey; + padding: 0 8px 12px; + } + + [hidden] { + display: none; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ozw-node-dashboard": OZWNodeDashboard; + } +} diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts new file mode 100644 index 000000000000..baf68ddc969a --- /dev/null +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts @@ -0,0 +1,66 @@ +import { customElement, property } from "lit-element"; +import { navigate } from "../../../../../common/navigate"; +import { + HassRouterPage, + RouterOptions, +} from "../../../../../layouts/hass-router-page"; +import { HomeAssistant } from "../../../../../types"; + +@customElement("ozw-node-router") +class OZWNodeRouter extends HassRouterPage { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public isWide!: boolean; + + @property() public narrow!: boolean; + + @property() public ozwInstance!: number; + + @property() public nodeId!: number; + + private _configEntry = new URLSearchParams(window.location.search).get( + "config_entry" + ); + + protected routerOptions: RouterOptions = { + defaultPage: "dashboard", + showLoading: true, + routes: { + dashboard: { + tag: "ozw-node-dashboard", + load: () => + import( + /* webpackChunkName: "ozw-node-dashboard" */ "./ozw-node-dashboard" + ), + }, + }, + }; + + protected updatePageEl(el): void { + el.route = this.routeTail; + el.hass = this.hass; + el.isWide = this.isWide; + el.narrow = this.narrow; + el.configEntryId = this._configEntry; + el.ozwInstance = this.ozwInstance; + el.nodeId = this.nodeId; + + const searchParams = new URLSearchParams(window.location.search); + if (this._configEntry && !searchParams.has("config_entry")) { + searchParams.append("config_entry", this._configEntry); + navigate( + this, + `${this.routeTail.prefix}${ + this.routeTail.path + }?${searchParams.toString()}`, + true + ); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "ozw-node-router": OZWNodeRouter; + } +} diff --git a/src/panels/config/person/ha-config-person.ts b/src/panels/config/person/ha-config-person.ts index 24f794b4e8ec..3d1de87bd02c 100644 --- a/src/panels/config/person/ha-config-person.ts +++ b/src/panels/config/person/ha-config-person.ts @@ -1,4 +1,3 @@ -import "@material/mwc-fab"; import { mdiPlus } from "@mdi/js"; import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; @@ -11,11 +10,10 @@ import { property, TemplateResult, } from "lit-element"; -import { styleMap } from "lit-html/directives/style-map"; import { compare } from "../../../common/string/compare"; import "../../../components/ha-card"; import "../../../components/ha-svg-icon"; -import "../../../components/user/ha-user-badge"; +import "../../../components/user/ha-person-badge"; import { createPerson, deletePerson, @@ -87,15 +85,10 @@ class HaConfigPerson extends LitElement { ${this._storageItems.map((entry) => { return html` - ${entry.picture - ? html`
` - : ""} + ${entry.name} @@ -123,15 +116,10 @@ class HaConfigPerson extends LitElement { ${this._configItems.map((entry) => { return html` - ${entry.picture - ? html`
` - : ""} + ${entry.name} @@ -248,12 +236,6 @@ class HaConfigPerson extends LitElement { margin: 16px auto; overflow: hidden; } - .picture { - width: 40px; - height: 40px; - background-size: cover; - border-radius: 50%; - } .empty { text-align: center; padding: 8px; diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts index 173dc592317b..1270db9619f1 100644 --- a/src/panels/logbook/ha-logbook.ts +++ b/src/panels/logbook/ha-logbook.ts @@ -21,6 +21,7 @@ import { computeRTL, emitRTLDirection } from "../../common/util/compute_rtl"; import "../../components/ha-circular-progress"; import "../../components/ha-icon"; import { LogbookEntry } from "../../data/logbook"; +import { haStyleScrollbar } from "../../resources/styles"; import { HomeAssistant } from "../../types"; @customElement("ha-logbook") @@ -73,7 +74,7 @@ class HaLogbook extends LitElement { return html`
+
${index === 0 || (item?.when && previous?.when && @@ -184,86 +185,106 @@ class HaLogbook extends LitElement { }); } - static get styles(): CSSResult { - return css` - :host { - display: block; - height: 100%; - } - - .rtl { - direction: ltr; - } - - .entry { - display: flex; - line-height: 2em; - padding-bottom: 8px; - } - - .time { - width: 65px; - flex-shrink: 0; - font-size: 12px; - color: var(--secondary-text-color); - } - - .rtl .date { - direction: rtl; - } - - .icon-message { - display: flex; - align-items: center; - } - - .no-entries { - text-align: center; - } - - ha-icon { - margin: 0 8px 0 16px; - flex-shrink: 0; - color: var(--primary-text-color); - } - - .message { - color: var(--primary-text-color); - } - - .no-name .item-message { - text-transform: capitalize; - } - - a { - color: var(--primary-color); - } - - .container { - padding: 0 16px; - } - - .uni-virtualizer-host { - display: block; - position: relative; - contain: strict; - height: 100%; - overflow: auto; - } - - .uni-virtualizer-host > * { - box-sizing: border-box; - } - - .narrow .entry { - flex-direction: column-reverse; - line-height: 1.5; - } - - .narrow .icon-message ha-icon { - margin-left: 0; - } - `; + static get styles(): CSSResult[] { + return [ + haStyleScrollbar, + css` + :host { + display: block; + height: 100%; + } + + .rtl { + direction: ltr; + } + + .entry-container { + width: 100%; + } + + .entry { + display: flex; + width: 100%; + line-height: 2em; + padding: 8px 16px; + box-sizing: border-box; + border-top: 1px solid + var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); + } + + .time { + display: flex; + justify-content: center; + flex-direction: column; + width: 70px; + flex-shrink: 0; + font-size: 12px; + color: var(--secondary-text-color); + } + + .date { + margin: 8px 0; + padding: 0 16px; + } + + .narrow .date { + padding: 0 8px; + } + + .rtl .date { + direction: rtl; + } + + .icon-message { + display: flex; + align-items: center; + } + + .no-entries { + text-align: center; + } + + ha-icon { + margin: 0 8px 0 16px; + flex-shrink: 0; + color: var(--primary-text-color); + } + + .message { + color: var(--primary-text-color); + } + + .no-name .item-message { + text-transform: capitalize; + } + + a { + color: var(--primary-color); + } + + .uni-virtualizer-host { + display: block; + position: relative; + contain: strict; + height: 100%; + overflow: auto; + } + + .uni-virtualizer-host > * { + box-sizing: border-box; + } + + .narrow .entry { + flex-direction: column; + line-height: 1.5; + padding: 8px; + } + + .narrow .icon-message ha-icon { + margin-left: 0; + } + `, + ]; } } diff --git a/src/panels/lovelace/cards/hui-calendar-card.ts b/src/panels/lovelace/cards/hui-calendar-card.ts index 235145482945..ee94fb807ce1 100644 --- a/src/panels/lovelace/cards/hui-calendar-card.ts +++ b/src/panels/lovelace/cards/hui-calendar-card.ts @@ -76,11 +76,11 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { private _resizeObserver?: ResizeObserver; public setConfig(config: CalendarCardConfig): void { - if (!config.entities) { + if (!config.entities?.length) { throw new Error("Entities must be defined"); } - if (config.entities && !Array.isArray(config.entities)) { + if (!Array.isArray(config.entities)) { throw new Error("Entities need to be an array"); } diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts index a176fc251c18..0aa6389e218f 100644 --- a/src/panels/lovelace/cards/hui-media-control-card.ts +++ b/src/panels/lovelace/cards/hui-media-control-card.ts @@ -667,8 +667,8 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { entityId: this._config!.entity, mediaPickedCallback: (pickedMedia: MediaPickedEvent) => this._playMedia( - pickedMedia.media_content_id, - pickedMedia.media_content_type + pickedMedia.item.media_content_id, + pickedMedia.item.media_content_type ), }); } diff --git a/src/panels/lovelace/common/directives/action-handler-directive.ts b/src/panels/lovelace/common/directives/action-handler-directive.ts index 09dc3c43516d..0c245a476413 100644 --- a/src/panels/lovelace/common/directives/action-handler-directive.ts +++ b/src/panels/lovelace/common/directives/action-handler-directive.ts @@ -44,6 +44,8 @@ class ActionHandler extends HTMLElement implements ActionHandler { protected held = false; + private cancelled = false; + private dblClickTimeout?: number; constructor() { @@ -76,9 +78,12 @@ class ActionHandler extends HTMLElement implements ActionHandler { document.addEventListener( ev, () => { - clearTimeout(this.timer); - this.stopAnimation(); - this.timer = undefined; + this.cancelled = true; + if (this.timer) { + this.stopAnimation(); + clearTimeout(this.timer); + this.timer = undefined; + } }, { passive: true } ); @@ -124,7 +129,7 @@ class ActionHandler extends HTMLElement implements ActionHandler { } element.actionHandler.start = (ev: Event) => { - this.held = false; + this.cancelled = false; let x; let y; if ((ev as TouchEvent).touches) { @@ -136,6 +141,7 @@ class ActionHandler extends HTMLElement implements ActionHandler { } if (options.hasHold) { + this.held = false; this.timer = window.setTimeout(() => { this.startAnimation(x, y); this.held = true; @@ -144,24 +150,20 @@ class ActionHandler extends HTMLElement implements ActionHandler { }; element.actionHandler.end = (ev: Event) => { - // Don't respond on our own generated click - if (!ev.isTrusted) { + // Don't respond when moved or scrolled while touch + if (["touchend", "touchcancel"].includes(ev.type) && this.cancelled) { return; } // Prevent mouse event if touch event - ev.preventDefault(); + if (ev.cancelable) { + ev.preventDefault(); + } if (options.hasHold) { - if ( - ["touchend", "touchcancel"].includes(ev.type) && - this.timer === undefined - ) { - return; - } clearTimeout(this.timer); this.stopAnimation(); this.timer = undefined; } - if (this.held) { + if (options.hasHold && this.held) { fireEvent(element, "action", { action: "hold" }); } else if (options.hasDoubleClick) { if ( @@ -179,8 +181,6 @@ class ActionHandler extends HTMLElement implements ActionHandler { } } else { fireEvent(element, "action", { action: "tap" }); - // Fire the click we prevented the action for - (ev.target as HTMLElement)?.click(); } }; diff --git a/src/panels/lovelace/components/hui-entity-editor.ts b/src/panels/lovelace/components/hui-entity-editor.ts index 23ab6eb6f108..b22bc775fdef 100644 --- a/src/panels/lovelace/components/hui-entity-editor.ts +++ b/src/panels/lovelace/components/hui-entity-editor.ts @@ -1,27 +1,51 @@ -import "../../../components/ha-icon-button"; +import { mdiDrag } from "@mdi/js"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, + PropertyValues, TemplateResult, } from "lit-element"; +import { guard } from "lit-html/directives/guard"; +import type { SortableEvent } from "sortablejs"; +import Sortable, { + AutoScroll, + OnSpill, +} from "sortablejs/modular/sortable.core.esm"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/entity/ha-entity-picker"; +import "../../../components/ha-icon-button"; +import { sortableStyles } from "../../../resources/ha-sortable-style"; import { HomeAssistant } from "../../../types"; import { EditorTarget } from "../editor/types"; import { EntityConfig } from "../entity-rows/types"; @customElement("hui-entity-editor") export class HuiEntityEditor extends LitElement { - @property() protected hass?: HomeAssistant; + @property({ attribute: false }) protected hass?: HomeAssistant; - @property() protected entities?: EntityConfig[]; + @property({ attribute: false }) protected entities?: EntityConfig[]; @property() protected label?: string; + @internalProperty() private _attached = false; + + private _sortable?; + + public connectedCallback() { + super.connectedCallback(); + this._attached = true; + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this._attached = false; + } + protected render(): TemplateResult { if (!this.entities) { return html``; @@ -36,42 +60,73 @@ export class HuiEntityEditor extends LitElement { ")"}
- ${this.entities.map((entityConf, index) => { - return html` -
- - - -
- `; - })} - + ${guard([this.entities], () => + this.entities!.map((entityConf, index) => { + return html` +
+ + +
+ `; + }) + )}
+ `; } - private _addEntity(ev: Event): void { + protected firstUpdated(): void { + Sortable.mount(OnSpill); + Sortable.mount(new AutoScroll()); + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + + const attachedChanged = changedProps.has("_attached"); + const entitiesChanged = changedProps.has("entities"); + + if (!entitiesChanged && !attachedChanged) { + return; + } + + if (attachedChanged && !this._attached) { + // Tear down sortable, if available + this._sortable?.destroy(); + this._sortable = undefined; + return; + } + + if (!this._sortable && this.entities) { + this._createSortable(); + return; + } + + if (entitiesChanged) { + this._sortable.sort(this.entities?.map((entity) => entity.entity)); + } + } + + private _createSortable() { + this._sortable = new Sortable(this.shadowRoot!.querySelector(".entities"), { + animation: 150, + fallbackClass: "sortable-fallback", + handle: "ha-svg-icon", + dataIdAttr: "data-entity-id", + onEnd: async (evt: SortableEvent) => this._entityMoved(evt), + }); + } + + private async _addEntity(ev: Event): Promise { const target = ev.target! as EditorTarget; if (target.value === "") { return; @@ -83,26 +138,14 @@ export class HuiEntityEditor extends LitElement { fireEvent(this, "entities-changed", { entities: newConfigEntities }); } - private _entityUp(ev: Event): void { - const target = ev.target! as EditorTarget; - const newEntities = this.entities!.concat(); - - [newEntities[target.index! - 1], newEntities[target.index!]] = [ - newEntities[target.index!], - newEntities[target.index! - 1], - ]; - - fireEvent(this, "entities-changed", { entities: newEntities }); - } + private _entityMoved(ev: SortableEvent): void { + if (ev.oldIndex === ev.newIndex) { + return; + } - private _entityDown(ev: Event): void { - const target = ev.target! as EditorTarget; const newEntities = this.entities!.concat(); - [newEntities[target.index! + 1], newEntities[target.index!]] = [ - newEntities[target.index!], - newEntities[target.index! + 1], - ]; + newEntities.splice(ev.newIndex!, 0, newEntities.splice(ev.oldIndex!, 1)[0]); fireEvent(this, "entities-changed", { entities: newEntities }); } @@ -123,16 +166,23 @@ export class HuiEntityEditor extends LitElement { fireEvent(this, "entities-changed", { entities: newConfigEntities }); } - static get styles(): CSSResult { - return css` - .entity { - display: flex; - align-items: flex-end; - } - .entity ha-entity-picker { - flex-grow: 1; - } - `; + static get styles(): CSSResult[] { + return [ + sortableStyles, + css` + .entity { + display: flex; + align-items: center; + } + .entity ha-svg-icon { + padding-right: 8px; + cursor: move; + } + .entity ha-entity-picker { + flex-grow: 1; + } + `, + ]; } } diff --git a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts index 54e108a46f62..f68b99e1f5cf 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts @@ -1,48 +1,45 @@ import "@material/mwc-tab-bar/mwc-tab-bar"; import "@material/mwc-tab/mwc-tab"; import Fuse from "fuse.js"; -import memoizeOne from "memoize-one"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; import { styleMap } from "lit-html/directives/style-map"; import { until } from "lit-html/directives/until"; - +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../common/search/search-input"; +import "../../../../components/ha-circular-progress"; import { UNAVAILABLE_STATES } from "../../../../data/entity"; +import type { + LovelaceCardConfig, + LovelaceConfig, +} from "../../../../data/lovelace"; import { CustomCardEntry, customCards, CUSTOM_TYPE_PREFIX, getCustomCardEntry, } from "../../../../data/lovelace_custom_cards"; +import type { HomeAssistant } from "../../../../types"; import { - computeUsedEntities, calcUnusedEntities, + computeUsedEntities, } from "../../common/compute-unused-entities"; import { tryCreateCardElement } from "../../create-element/create-card-element"; +import type { LovelaceCard } from "../../types"; import { getCardStubConfig } from "../get-card-stub-config"; import { coreCards } from "../lovelace-cards"; - -import type { CardPickTarget, Card } from "../types"; -import type { LovelaceCard } from "../../types"; -import type { HomeAssistant } from "../../../../types"; -import type { - LovelaceCardConfig, - LovelaceConfig, -} from "../../../../data/lovelace"; - -import "../../../../components/ha-circular-progress"; -import "../../../../common/search/search-input"; +import type { Card, CardPickTarget } from "../types"; interface CardElement { card: Card; @@ -107,7 +104,7 @@ export class HuiCardPicker extends LitElement { no-label-float @value-changed=${this._handleSearchChange} .label=${this.hass.localize( - "ui.panel.lovelace.editor.card.generic.search" + "ui.panel.lovelace.editor.edit_card.search_cards" )} >
{ + public async showDialog(params: CreateCardDialogParams): Promise { this._params = params; const [view] = params.path; this._viewConfig = params.lovelaceConfig.views[view]; @@ -76,10 +76,11 @@ export class HuiCreateDialogCard extends LitElement implements HassDialog { @keydown=${this._ignoreKeydown} @closed=${this._cancel} .heading=${true} + class=${classMap({ table: this._currTabIndex === 1 })} >
-
+ ${this._viewConfig.title ? this.hass!.localize( "ui.panel.lovelace.editor.edit_card.pick_card_view_title", @@ -89,7 +90,7 @@ export class HuiCreateDialogCard extends LitElement implements HassDialog { : this.hass!.localize( "ui.panel.lovelace.editor.edit_card.pick_card" )} -
+
- ${this._currTabIndex === 0 - ? html` - - ` - : html` -
- -
- `} + @config-changed=${this._handleCardPicked} + > + ` + : html` +
+ +
+ ` + )}
@@ -167,6 +170,12 @@ export class HuiCreateDialogCard extends LitElement implements HassDialog { ha-dialog { --mdc-dialog-max-width: 845px; + --dialog-content-padding: 2px 24px 20px 24px; + --dialog-z-index: 5; + } + + ha-dialog.table { + --dialog-content-padding: 0; } ha-header-bar { @@ -190,7 +199,8 @@ export class HuiCreateDialogCard extends LitElement implements HassDialog { } mwc-tab-bar { - padding-top: 8px; + border-bottom: 1px solid + var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); } .entity-picker-container { diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts index c8caba2a4821..87d5ea311555 100755 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts @@ -366,6 +366,7 @@ export class HuiDialogEditCard extends LitElement implements HassDialog { ha-dialog { --mdc-dialog-max-width: 845px; + --dialog-z-index: 5; } ha-header-bar { diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts index 00e7d8b5a027..0a23b164e69e 100755 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts @@ -140,6 +140,7 @@ export class HuiDialogSuggestCard extends LitElement { } ha-paper-dialog { max-width: 845px; + --dialog-z-index: 5; } mwc-button ha-circular-progress { width: 14px; diff --git a/src/panels/lovelace/editor/card-editor/show-edit-card-dialog.ts b/src/panels/lovelace/editor/card-editor/show-edit-card-dialog.ts index 18b0d820e3ff..46525f1c4311 100644 --- a/src/panels/lovelace/editor/card-editor/show-edit-card-dialog.ts +++ b/src/panels/lovelace/editor/card-editor/show-edit-card-dialog.ts @@ -5,7 +5,6 @@ export interface EditCardDialogParams { lovelaceConfig: LovelaceConfig; saveConfig: (config: LovelaceConfig) => void; path: [number] | [number, number]; - entities?: string[]; // We can pass entity id's that will be added to the config when a card is picked cardConfig?: LovelaceCardConfig; } diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts index cd230be53511..49a9db601cc9 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts @@ -2,13 +2,15 @@ import "@polymer/paper-input/paper-input"; import { customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; +import { assert, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import { stateIcon } from "../../../../common/entity/state_icon"; +import "../../../../components/entity/ha-entity-attribute-picker"; import "../../../../components/ha-icon-input"; import { HomeAssistant } from "../../../../types"; import { EntityCardConfig } from "../../cards/types"; @@ -19,7 +21,6 @@ import { headerFooterConfigStructs } from "../../header-footer/types"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; -import { string, object, optional, assert } from "superstruct"; const cardConfigStruct = object({ type: string(), @@ -113,7 +114,9 @@ export class HuiEntityCardEditor extends LitElement >
- + > + + + +
+ ${title || ""}${this.hass.localize( + "ui.components.media-browser.media-player-browser" + )} +
+ + + +
+
+
+ +
+ + `; + } + + private _showSelectMediaPlayerDialog(): void { + showSelectMediaPlayerDialog(this, { + mediaSources: this._mediaPlayerEntities, + sourceSelectedCallback: (entityId) => { + this._entityId = entityId; + }, + }); + } + + private async _mediaPicked( + ev: HASSDomEvent + ): Promise { + const item = ev.detail.item; + if (this._entityId === BROWSER_SOURCE) { + const resolvedUrl: any = await this.hass.callWS({ + type: "media_source/resolve_media", + media_content_id: item.media_content_id, + }); + + showWebBrowserPlayMediaDialog(this, { + sourceUrl: resolvedUrl.url, + sourceType: resolvedUrl.mime_type, + title: item.title, + }); + return; + } + + this.hass!.callService("media_player", "play_media", { + entity_id: this._entityId, + media_content_id: item.media_content_id, + media_content_type: item.media_content_type, + }); + } + + private get _mediaPlayerEntities() { + return Object.values(this.hass!.states).filter((entity) => { + if ( + computeStateDomain(entity) === "media_player" && + supportsFeature(entity, SUPPORT_BROWSE_MEDIA) + ) { + return true; + } + + return false; + }); + } + + static get styles(): CSSResultArray { + return [ + haStyle, + css` + ha-media-player-browse { + height: calc(100vh - 84px); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-panel-media-browser": PanelMediaBrowser; + } +} diff --git a/src/panels/media-browser/hui-dialog-select-media-player.ts b/src/panels/media-browser/hui-dialog-select-media-player.ts new file mode 100644 index 000000000000..0c45026e81fe --- /dev/null +++ b/src/panels/media-browser/hui-dialog-select-media-player.ts @@ -0,0 +1,97 @@ +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-listbox/paper-listbox"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../common/dom/fire_event"; +import { createCloseHeading } from "../../components/ha-dialog"; +import { BROWSER_SOURCE } from "../../data/media-player"; +import type { HomeAssistant } from "../../types"; +import { haStyleDialog } from "../../resources/styles"; +import type { SelectMediaPlayerDialogParams } from "./show-select-media-source-dialog"; + +@customElement("hui-dialog-select-media-player") +export class HuiDialogSelectMediaPlayer extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) + private _params?: SelectMediaPlayerDialogParams; + + public showDialog(params: SelectMediaPlayerDialogParams): void { + this._params = params; + } + + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + + return html` + + ${this.hass.localize( + "ui.components.media-browser.web-browser" + )} + ${this._params.mediaSources.map( + (source) => html` + ${source.attributes.friendly_name} + ` + )} + + + `; + } + + private _selectSource(ev: CustomEvent): void { + const entityId = ev.detail.item.itemName; + this._params!.sourceSelectedCallback(entityId); + this.closeDialog(); + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + ha-dialog { + --dialog-content-padding: 0 24px 20px; + } + paper-item { + cursor: pointer; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-dialog-select-media-player": HuiDialogSelectMediaPlayer; + } +} diff --git a/src/panels/media-browser/hui-dialog-web-browser-play-media.ts b/src/panels/media-browser/hui-dialog-web-browser-play-media.ts new file mode 100644 index 000000000000..d528bec1e41b --- /dev/null +++ b/src/panels/media-browser/hui-dialog-web-browser-play-media.ts @@ -0,0 +1,122 @@ +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../common/dom/fire_event"; +import { createCloseHeading } from "../../components/ha-dialog"; +import "../../components/ha-hls-player"; +import type { HomeAssistant } from "../../types"; +import { haStyleDialog } from "../../resources/styles"; +import { WebBrowserPlayMediaDialogParams } from "./show-media-player-dialog"; + +@customElement("hui-dialog-web-browser-play-media") +export class HuiDialogWebBrowserPlayMedia extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) + private _params?: WebBrowserPlayMediaDialogParams; + + public showDialog(params: WebBrowserPlayMediaDialogParams): void { + this._params = params; + } + + public closeDialog() { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._params || !this._params.sourceType || !this._params.sourceUrl) { + return html``; + } + + const mediaType = this._params.sourceType.split("/", 1)[0]; + + return html` + + ${mediaType === "audio" + ? html` + + ` + : mediaType === "video" + ? html` + + ` + : this._params.sourceType === "application/x-mpegURL" + ? html` + + ` + : mediaType === "image" + ? html`` + : html`${this.hass.localize( + "ui.components.media-browser.media_not_supported" + )}`} + + `; + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + @media (min-width: 800px) { + ha-dialog { + --mdc-dialog-max-width: 800px; + --mdc-dialog-min-width: 400px; + } + } + + video, + audio, + img { + outline: none; + width: 100%; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-dialog-web-browser-play-media": HuiDialogWebBrowserPlayMedia; + } +} diff --git a/src/panels/media-browser/show-media-player-dialog.ts b/src/panels/media-browser/show-media-player-dialog.ts new file mode 100644 index 000000000000..161e97fd96b2 --- /dev/null +++ b/src/panels/media-browser/show-media-player-dialog.ts @@ -0,0 +1,21 @@ +import { fireEvent } from "../../common/dom/fire_event"; + +export interface WebBrowserPlayMediaDialogParams { + sourceUrl: string; + sourceType: string; + title?: string; +} + +export const showWebBrowserPlayMediaDialog = ( + element: HTMLElement, + webBrowserPlayMediaDialogParams: WebBrowserPlayMediaDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "hui-dialog-web-browser-play-media", + dialogImport: () => + import( + /* webpackChunkName: "hui-dialog-media-player" */ "./hui-dialog-web-browser-play-media" + ), + dialogParams: webBrowserPlayMediaDialogParams, + }); +}; diff --git a/src/panels/media-browser/show-select-media-source-dialog.ts b/src/panels/media-browser/show-select-media-source-dialog.ts new file mode 100644 index 000000000000..ec68a67a5ba8 --- /dev/null +++ b/src/panels/media-browser/show-select-media-source-dialog.ts @@ -0,0 +1,21 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { fireEvent } from "../../common/dom/fire_event"; + +export interface SelectMediaPlayerDialogParams { + mediaSources: HassEntity[]; + sourceSelectedCallback: (entityId: string) => void; +} + +export const showSelectMediaPlayerDialog = ( + element: HTMLElement, + selectMediaPlayereDialogParams: SelectMediaPlayerDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "hui-dialog-select-media-player", + dialogImport: () => + import( + /* webpackChunkName: "hui-dialog-select-media-player" */ "./hui-dialog-select-media-player" + ), + dialogParams: selectMediaPlayereDialogParams, + }); +}; diff --git a/src/resources/ha-sortable-style.ts b/src/resources/ha-sortable-style.ts new file mode 100644 index 000000000000..aa5ffe778947 --- /dev/null +++ b/src/resources/ha-sortable-style.ts @@ -0,0 +1,75 @@ +import { css } from "lit-element"; + +export const sortableStyles = css` + #sortable a:nth-of-type(2n) paper-icon-item { + animation-name: keyframes1; + animation-iteration-count: infinite; + transform-origin: 50% 10%; + animation-delay: -0.75s; + animation-duration: 0.25s; + } + + #sortable a:nth-of-type(2n-1) paper-icon-item { + animation-name: keyframes2; + animation-iteration-count: infinite; + animation-direction: alternate; + transform-origin: 30% 5%; + animation-delay: -0.5s; + animation-duration: 0.33s; + } + + #sortable { + outline: none; + display: flex; + flex-direction: column; + } + + .sortable-ghost { + opacity: 0.4; + } + + .sortable-fallback { + opacity: 0; + } + + @keyframes keyframes1 { + 0% { + transform: rotate(-1deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(1.5deg); + animation-timing-function: ease-out; + } + } + + @keyframes keyframes2 { + 0% { + transform: rotate(1deg); + animation-timing-function: ease-in; + } + + 50% { + transform: rotate(-1.5deg); + animation-timing-function: ease-out; + } + } + + .hide-panel { + display: none; + position: absolute; + right: 8px; + } + + :host([expanded]) .hide-panel { + display: inline-flex; + } + + paper-icon-item.hidden-panel, + paper-icon-item.hidden-panel span, + paper-icon-item.hidden-panel ha-icon[slot="item-icon"] { + color: var(--secondary-text-color); + cursor: pointer; + } +`; diff --git a/src/resources/styles.ts b/src/resources/styles.ts index 8054abe4753e..ef04c15548b3 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -320,3 +320,22 @@ export const haStyleDialog = css` color: var(--error-color); } `; + +export const haStyleScrollbar = css` + .ha-scrollbar::-webkit-scrollbar { + width: 0.4rem; + height: 0.4rem; + } + + .ha-scrollbar::-webkit-scrollbar-thumb { + -webkit-border-radius: 4px; + border-radius: 4px; + background: var(--scrollbar-thumb-color); + } + + .ha-scrollbar { + overflow-y: auto; + scrollbar-color: var(--scrollbar-thumb-color) transparent; + scrollbar-width: thin; + } +`; diff --git a/src/state/themes-mixin.ts b/src/state/themes-mixin.ts index b4470a237ae8..0815f4391a27 100644 --- a/src/state/themes-mixin.ts +++ b/src/state/themes-mixin.ts @@ -88,9 +88,15 @@ export default >(superClass: T) => } const themeMeta = document.querySelector("meta[name=theme-color]"); - const headerColor = getComputedStyle( - document.documentElement - ).getPropertyValue("--app-header-background-color"); + const computedStyles = getComputedStyle(document.documentElement); + const headerColor = computedStyles.getPropertyValue( + "--app-header-background-color" + ); + + document.documentElement.style.backgroundColor = computedStyles.getPropertyValue( + "--primary-background-color" + ); + if (themeMeta) { if (!themeMeta.hasAttribute("default-content")) { themeMeta.setAttribute( diff --git a/src/state/url-sync-mixin.ts b/src/state/url-sync-mixin.ts index 12919204e5c2..92892f1359a4 100644 --- a/src/state/url-sync-mixin.ts +++ b/src/state/url-sync-mixin.ts @@ -1,14 +1,14 @@ /* eslint-disable no-console */ +import { UpdatingElement } from "lit-element"; +import { HASSDomEvent } from "../common/dom/fire_event"; import { closeDialog, - showDialog, - DialogState, DialogClosedParams, + DialogState, + showDialog, } from "../dialogs/make-dialog-manager"; -import { Constructor } from "../types"; -import { HASSDomEvent } from "../common/dom/fire_event"; -import { UpdatingElement } from "lit-element"; import { ProvideHassElement } from "../mixins/provide-hass-lit-mixin"; +import { Constructor } from "../types"; const DEBUG = false; diff --git a/src/translations/en.json b/src/translations/en.json index 3aeafd0ec1ee..37d49b43e8f2 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -9,6 +9,7 @@ "mailbox": "Mailbox", "shopping_list": "Shopping list", "developer_tools": "Developer Tools", + "media_browser": "Media Browser", "profile": "Profile" }, "state": { @@ -285,6 +286,10 @@ "entity": "Entity", "clear": "Clear", "show_entities": "Show entities" + }, + "entity-attribute-picker": { + "attribute": "Attribute", + "show_attributes": "Show attributes" } }, "device-picker": { @@ -357,8 +362,14 @@ "play-media": "Play Media", "pick-media": "Pick Media", "no_items": "No items", - "choose-source": "Choose Source", + "choose_player": "Choose Player", "media-player-browser": "Media Player Browser", + "web-browser": "Web Browser", + "media_player": "Media Player", + "audio_not_supported": "Your browser does not support the audio element.", + "video_not_supported": "Your browser does not support the video element.", + "media_not_supported": "The Browser Media Player does not support this type of media", + "media_browsing_error": "Media Browsing Error", "content-type": { "server": "Server", "library": "Library", @@ -445,7 +456,7 @@ "delete": "Delete", "confirm_delete": "Are you sure you want to delete this entry?", "update": "Update", - "note": "Note: this might not work yet with all integrations." + "note": "Note: This might not work yet with all integrations." } }, "helper_settings": { @@ -908,6 +919,8 @@ "show_info_automation": "Show info about automation", "delete_automation": "Delete automation", "delete_confirm": "Are you sure you want to delete this automation?", + "duplicate_automation": "Duplicate automation", + "duplicate": "Duplicate", "headers": { "name": "Name" } @@ -978,6 +991,7 @@ }, "state": { "label": "State", + "attribute": "Attribute (Optional)", "from": "From", "for": "For", "to": "To" @@ -1014,8 +1028,10 @@ "value_template": "Value template" }, "time": { + "type_value": "Fixed time", + "type_input": "Value of a date/time helper", "label": "Time", - "at": "At" + "at": "At time" }, "time_pattern": { "label": "Time Pattern", @@ -1091,9 +1107,21 @@ "value_template": "[%key:ui::panel::config::automation::editor::triggers::type::template::value_template%]" }, "time": { + "type_value": "[%key:ui::panel::config::automation::editor::triggers::type::time::type_value%]", + "type_input": "[%key:ui::panel::config::automation::editor::triggers::type::time::type_input%]", "label": "[%key:ui::panel::config::automation::editor::triggers::type::time::label%]", "after": "After", - "before": "Before" + "before": "Before", + "days": "Days", + "weekdays": { + "mon": "Monday", + "tue": "Tuesday", + "wed": "Wednesday", + "thu": "Thursday", + "fri": "Friday", + "sat": "Saturday", + "sun": "Sunday" + } }, "zone": { "label": "[%key:ui::panel::config::automation::editor::triggers::type::zone::label%]", @@ -1125,7 +1153,13 @@ "wait_template": { "label": "Wait", "wait_template": "Wait Template", - "timeout": "Timeout (optional)" + "timeout": "Timeout (optional)", + "continue_timeout": "Continue on timeout" + }, + "wait_for_trigger": { + "label": "Wait for trigger", + "timeout": "[%key:ui::panel::config::automation::editor::actions::type::wait_template::timeout%]", + "continue_timeout": "[%key:ui::panel::config::automation::editor::actions::type::wait_template::continue_timeout%]" }, "condition": { "label": "Condition" @@ -1603,6 +1637,7 @@ "description": "Manage integrations", "integration": "integration", "discovered": "Discovered", + "attention": "Attention required", "configured": "Configured", "new": "Set up a new integration", "add_integration": "Add integration", @@ -1611,6 +1646,7 @@ "note_about_website_reference": "More are available on the ", "home_assistant_website": "Home Assistant website", "configure": "Configure", + "reconfigure": "Reconfigure", "none": "Nothing configured yet", "none_found": "No integrations found", "none_found_detail": "Adjust your search criteria.", @@ -1633,6 +1669,7 @@ "config_entry": { "devices": "{count} {count, plural,\n one {device}\n other {devices}\n}", "entities": "{count} {count, plural,\n one {entity}\n other {entities}\n}", + "services": "{count} {count, plural,\n one {service}\n other {services}\n}", "rename": "Rename", "options": "Options", "system_options": "System options", @@ -1671,7 +1708,7 @@ "users": { "caption": "Users", "description": "Manage users", - "users_privileges_note": "The users group is a work in progress. The user will be unable to administer the instance via the UI. We're still auditing all management API endpoints to ensure that they correctly limit access to administrators.", + "users_privileges_note": "The user group feature is a work in progress. The user will be unable to administer the instance via the UI. We're still auditing all management API endpoints to ensure that they correctly limit access to administrators.", "picker": { "headers": { "name": "Name", @@ -1759,6 +1796,7 @@ "complete": "Interview process is complete" }, "refresh_node": { + "button": "Refresh Node", "title": "Refresh Node Information", "complete": "Node Refresh Complete", "description": "This will tell OpenZWave to re-interview a node and update the node's command classes, capabilities, and values.", @@ -1804,6 +1842,18 @@ "introduction": "Manage network-wide functions.", "node_count": "{count} nodes" }, + "nodes_table": { + "id": "ID", + "manufacturer": "Manufacturer", + "model": "Model", + "query_stage": "Query Stage", + "zwave_plus": "Z-Wave Plus", + "failed": "Failed" + }, + "node": { + "button": "Node Details", + "not_found": "Node not found" + }, "services": { "add_node": "Add Node", "remove_node": "Remove Node" @@ -1849,7 +1899,7 @@ "clusters": { "header": "Clusters", "help_cluster_dropdown": "Select a cluster to view attributes and commands.", - "introduction": "Clusters are the building blocks for Zigbee functionality. They seperate functionality into logical units. There are client and server types and that are comprised of attributes and commands." + "introduction": "Clusters are the building blocks for Zigbee functionality. They separate functionality into logical units. There are client and server types and that are comprised of attributes and commands." }, "cluster_attributes": { "header": "Cluster Attributes", @@ -1940,10 +1990,10 @@ "node_group_associations": "Node group associations", "group": "Group", "node_to_control": "Node to control", - "nodes_in_group": "Other Nodes in this group:", + "nodes_in_group": "Other nodes in this group:", "max_associations": "Max Associations:", - "add_to_group": "Add To Group", - "remove_from_group": "Remove From Group", + "add_to_group": "Add to Group", + "remove_from_group": "Remove from Group", "remove_broadcast": "Remove Broadcast" }, "ozw_log": { @@ -2137,7 +2187,8 @@ "delete": "Delete Card", "duplicate": "Duplicate Card", "move": "Move to View", - "options": "More options" + "options": "More options", + "search_cards": "Search cards" }, "move_card": { "header": "Choose a view to move the card to" diff --git a/translations/frontend/ar.json b/translations/frontend/ar.json index 15f0aca0eae3..bb67ab7519df 100644 --- a/translations/frontend/ar.json +++ b/translations/frontend/ar.json @@ -1469,12 +1469,34 @@ "next": "التالى", "providers": { "command_line": { + "abort": { + "login_expired": "" + }, + "error": { + "invalid_auth": "" + }, "step": { "init": { "data": { "password": "كلمة السر", "username": "اسم المستخدم" } + }, + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + }, + "legacy_api_password": { + "step": { + "mfa": { + "data": { + "code": "" + }, + "description": "" } } }, diff --git a/translations/frontend/bs.json b/translations/frontend/bs.json index f3a2c84456b4..e85972507cdf 100644 --- a/translations/frontend/bs.json +++ b/translations/frontend/bs.json @@ -36,7 +36,8 @@ "unknown": "Nepoznat" }, "device_tracker": { - "home": "Kod kuće" + "home": "Kod kuće", + "not_home": "" }, "person": { "home": "Kod kuće" @@ -93,6 +94,7 @@ "on": "Otvoren" }, "presence": { + "off": "", "on": "Kod kuće" }, "problem": { @@ -159,6 +161,7 @@ "closing": "Zatvoreno", "home": "Kod kuće", "locked": "Zaključan", + "not_home": "", "off": "Isključen", "ok": "OK", "on": "Uključen", @@ -242,6 +245,13 @@ "config": { "automation": { "editor": { + "conditions": { + "type": { + "zone": { + "entity": "" + } + } + }, "triggers": { "type": { "mqtt": { @@ -293,6 +303,45 @@ "empty": "Nemate poruke", "playback_title": "Poruku preslušati" }, + "page-authorize": { + "form": { + "providers": { + "command_line": { + "abort": { + "login_expired": "" + }, + "error": { + "invalid_auth": "", + "invalid_code": "" + }, + "step": { + "init": { + "data": { + "password": "", + "username": "" + } + }, + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + }, + "legacy_api_password": { + "step": { + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + } + } + } + }, "shopping-list": { "add_item": "Dodajte objekat", "clear_completed": "Čišćenje završeno", diff --git a/translations/frontend/ca.json b/translations/frontend/ca.json index 5e49c1fdd106..9e2888ab081e 100644 --- a/translations/frontend/ca.json +++ b/translations/frontend/ca.json @@ -19,6 +19,7 @@ "logbook": "Diari de registre", "mailbox": "Bústia", "map": "Mapa", + "media_browser": "Navegador multimèdia", "profile": "Perfil", "shopping_list": "Llista de compres", "states": "Visualització general" @@ -505,6 +506,7 @@ "back": "Torna", "cancel": "Cancel·la", "close": "Tanca", + "continue": "Continua", "delete": "Elimina", "error_required": "Obligatori", "loading": "Carregant", @@ -562,6 +564,8 @@ "no_history_found": "No s'ha trobat cap historial d'estats." }, "media-browser": { + "audio_not_supported": "El teu navegador no és compatible amb l'element d'àudio.", + "choose_player": "Tria el reproductor", "choose-source": "Tria la font", "content-type": { "album": "Àlbum", @@ -570,12 +574,17 @@ "playlist": "Llista de reproducció", "server": "Servidor" }, + "media_browsing_error": "Error de navegació multimèdia", + "media_not_supported": "El reproductor multimèdia de navegador no és compatible amb aquest tipus de mitjà", + "media_player": "Reproductor multimèdia", "media-player-browser": "Navegador del reproductor multimèdia", "no_items": "Sense elements", "pick": "Escull", "pick-media": "Tria mitjans", "play": "Reprodueix", - "play-media": "Reprodueix mitjans" + "play-media": "Reprodueix mitjans", + "video_not_supported": "El teu navegador no és compatible amb l'element de vídeo.", + "web-browser": "Navegador web" }, "picture-upload": { "label": "Imatge", @@ -689,8 +698,10 @@ "crop": "Retalla" }, "more_info_control": { + "controls": "Controls", "dismiss": "Desestimar el diàleg", "edit": "Edita entitat", + "history": "Historial", "person": { "create_zone": "Crea una zona a partir de la ubicació actual" }, @@ -1543,6 +1554,7 @@ "reload_restart_confirm": "Reinicia Home Assistant per acabar de carregar aquesta integració", "rename": "Canvia el nom", "restart_confirm": "Reinicia Home Assistant per acabar d'eliminar aquesta integració", + "services": "{count} {count, plural,\n one {servei}\n other {serveis}\n}", "settings_button": "Edita la configuració de {integration}", "system_options": "Opcions de sistema", "system_options_button": "Opcions de sistema de {integration}", @@ -1762,8 +1774,21 @@ "versions": "Obtenint informació de programari i versions de classes de comandes", "wakeup": "Configurant el suport per a cues i missatges" }, + "node": { + "button": "Detalls del node", + "not_found": "No s'ha trobat el node" + }, + "nodes_table": { + "failed": "Ha fallat", + "id": "ID", + "manufacturer": "Fabricant", + "model": "Model", + "query_stage": "Fase de consulta", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { "battery_note": "Si el node funciona amb bateria, assegura't de que estigui actiu abans de continuar", + "button": "Actualitza node", "complete": "Actualització del node completa", "description": "Això farà que OpenZWave torni a consultar el node i n'actualitzi les classes de comandes, funcions i valors.", "node_status": "Estat del node", @@ -1912,7 +1937,7 @@ "filter": "Torna a carregar entitats de filtre", "generic": "Torna a carregar entitats genèriques de càmera IP", "generic_thermostat": "Torna a carregar entitats genèriques de termòstat", - "group": "Actualitza grups", + "group": "Torna a carregar grups, grups d'entitats i serveis de notificació", "heading": "Tornant a carregar la configuració", "history_stats": "Torna a carregar entitats d'estadístiques històriques", "homekit": "Torna a carregar HomeKit", @@ -1923,12 +1948,17 @@ "input_text": "Actualitza entrades de text", "introduction": "Algunes parts de Home Assistant es poden actualitzar sense necessitat reiniciar-lo. Si prems actualitza s'esborrarà la configuració YAML actual i se'n carregarà la nova.", "min_max": "Torna a carregar entitats min/max", + "mqtt": "Torna a carregar entitats MQTT", "person": "Actualitza persones", "ping": "Torna a carregar entitats de sensors binaris de ping", - "rest": "Torna a carregar entitats de repòs", + "reload": "Torna a carregar {domain}", + "rest": "Torna a carregar entitats de repòs i serveis de notificació", + "rpi_gpio": "Torna a carregar entitats GPIO de la Raspberry Pi", "scene": "Actualitza escenes", "script": "Actualitza programes", + "smtp": "Torna a carregar serveis de notificació SMTP", "statistics": "Torna a carregar entitats estadístiques", + "telegram": "Torna a carregar serveis de notificació de Telegram", "template": "Torna a carregar entitats de plantilla", "trend": "Torna a carregar entitats de tendència", "universal": "Torna a carregar entitats del reproductor universal", @@ -2549,7 +2579,11 @@ } }, "cardpicker": { + "by_card": "Per targeta", + "by_entity": "Per entitat", "custom_card": "Personalitzada", + "domain": "Domini", + "entity": "Entitat", "no_description": "No hi ha cap descripció disponible." }, "edit_card": { @@ -2563,6 +2597,7 @@ "options": "Més opcions", "pick_card": "Quina targeta vols afegir?", "pick_card_view_title": "Quina targeta vols afegir a la visualització {name}?", + "search_cards": "Cerca targetes", "show_code_editor": "Mostra l'editor de codi", "show_visual_editor": "Mostra l'editor visual", "toggle_editor": "Commutar l'editor", diff --git a/translations/frontend/cs.json b/translations/frontend/cs.json index ca77ec6015d9..e8048e50a0fc 100644 --- a/translations/frontend/cs.json +++ b/translations/frontend/cs.json @@ -19,6 +19,7 @@ "logbook": "Záznamy", "mailbox": "Schránka", "map": "Mapa", + "media_browser": "Prohlížeč médií", "profile": "Profil", "shopping_list": "Nákupní seznam", "states": "Přehled" @@ -505,6 +506,7 @@ "back": "Zpět", "cancel": "Zrušit", "close": "Zavřít", + "continue": "Pokračovat", "delete": "Smazat", "error_required": "Povinné", "loading": "Načítání", @@ -562,6 +564,8 @@ "no_history_found": "Historie stavu chybí." }, "media-browser": { + "audio_not_supported": "Váš prohlížeč nepodporuje element \"audio\".", + "choose_player": "Vyberte přehrávač", "choose-source": "Zvolte zdroj", "content-type": { "album": "Album", @@ -570,12 +574,17 @@ "playlist": "Seznam skladeb", "server": "Server" }, + "media_browsing_error": "Chyba při procházení médií", + "media_not_supported": "Přehrávač médií v prohlížeči nepodporuje tento typ média", + "media_player": "Přehrávač médií", "media-player-browser": "Prohlížeč přehrávače médií", "no_items": "Žádné položky", "pick": "Vybrat", "pick-media": "Vybrat média", "play": "Přehrát", - "play-media": "Přehrát média" + "play-media": "Přehrát média", + "video_not_supported": "Váš prohlížeč nepodporuje element \"video\".", + "web-browser": "Webový prohlížeč" }, "picture-upload": { "label": "Obrázek", @@ -633,7 +642,7 @@ "icon": "Nahrazení ikony", "icon_error": "Ikony by měly být ve formátu 'prefix:nazevikony', např. 'mdi:home'", "name": "Přepsání názvu", - "note": "Poznámka: to nemusí fungovat se všemi integracemi.", + "note": "Poznámka: U všech integrací to ještě nemusí fungovat.", "unavailable": "Tato entita není momentálně k dispozici.", "update": "Aktualizovat" }, @@ -689,8 +698,10 @@ "crop": "Oříznout" }, "more_info_control": { + "controls": "Ovládací prvky", "dismiss": "Zavřít dialog", "edit": "Upravit entitu", + "history": "Historie", "person": { "create_zone": "Vytvořit zónu z aktuálního umístění" }, @@ -1543,6 +1554,7 @@ "reload_restart_confirm": "Restartujte Home Assistant pro nové načtení této integrace", "rename": "Přejmenovat", "restart_confirm": "Restartujte Home Assistant pro odstranění této integrace", + "services": "{count} {count, plural,\n one {služba}\n few {služby}\n other {služeb}\n}", "settings_button": "Upravit nastavení pro {integration}", "system_options": "Více možností", "system_options_button": "Upravit nastavení pro {integration}", @@ -1762,8 +1774,21 @@ "versions": "Získávám informace o verzích firmwaru a typech příkazů", "wakeup": "Nastavuji podporu pro probouzecí fronty a zprávy" }, + "node": { + "button": "Podrobnosti uzlu", + "not_found": "Uzel nenalezen" + }, + "nodes_table": { + "failed": "Selhalo", + "id": "ID", + "manufacturer": "Výrobce", + "model": "Model", + "query_stage": "Fáze dotazu", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { "battery_note": "Pokud je uzel napájen z baterie, nezapomeňte jej probudit, než budete pokračovat", + "button": "Obnovit uzel", "complete": "Obnova uzlu dokončena", "description": "Toto řekne OpenZWave, aby znovu provedl komunikaci s uzlem a aktualizoval typy příkazů, schopnosti a hodnoty uzlu.", "node_status": "Stav uzlu", @@ -1912,7 +1937,7 @@ "filter": "Nově načíst entity integrace Filter", "generic": "Nově načíst entity integrace Generic IP camera", "generic_thermostat": "Nově načíst entity integrace Generic thermostat", - "group": "Nově načíst skupiny", + "group": "Nově načíst skupiny, skupiny entit a notifikační služby", "heading": "Konfigurace se načítá", "history_stats": "Nově načíst entity integrace History stats", "homekit": "Nově načíst entity integrace HomeKit", @@ -1923,12 +1948,17 @@ "input_text": "Nově načíst pomocníky - texty", "introduction": "Některé části Home Assistant lze nově načíst bez nutnosti restartování. Nové načtení zahodí jejich aktuální konfiguraci a načte novou.", "min_max": "Nově načíst entity integrace Min/Max", + "mqtt": "Nově načíst entity integrace MQTT", "person": "Nově načíst osoby", "ping": "Nově načíst entity integrace Ping", - "rest": "Nově načíst entity integrace Rest", + "reload": "Nově načíst integraci {domain}", + "rest": "Nově načíst entity a notifikační služby integrace Rest", + "rpi_gpio": "Nově načíst entity integrace Raspberry Pi GPIO", "scene": "Nově načíst scény", "script": "Nově načíst skripty", + "smtp": "Nově načíst notifikační služby integrace SMTP", "statistics": "Nově načíst entity integrace Statistics", + "telegram": "Nově načíst notifikační služby integrace Telegram", "template": "Nově načíst entity integrace Template", "trend": "Nově načíst entity integrace Trend", "universal": "Nově načíst entity integrace Universal media player", @@ -2017,7 +2047,7 @@ "system": "Systémový" } }, - "users_privileges_note": "Skupina uživatelů je v přípravě. Uživatel nebude moci spravovat instanci prostřednictvím uživatelského rozhraní. Stále kontrolujeme všechny koncové body API pro správu, abychom zajistili, že správně omezují přístup." + "users_privileges_note": "Skupiny uživatelů jsou v přípravě. Uživatel je nebude moci spravovat prostřednictvím uživatelského rozhraní. Stále kontrolujeme API pro správu, abychom zajistili, že správně omezuje přístup pouze pro administrátory." }, "zha": { "add_device_page": { @@ -2549,7 +2579,11 @@ } }, "cardpicker": { + "by_card": "Podle karty", + "by_entity": "Podle entity", "custom_card": "Vlastní", + "domain": "Doména", + "entity": "Entita", "no_description": "Žádný popis není k dispozici." }, "edit_card": { @@ -2563,6 +2597,7 @@ "options": "Více možností", "pick_card": "Kterou kartu chcete přidat?", "pick_card_view_title": "Kterou kartu byste chtěli přidat do svého {name} pohledu?", + "search_cards": "Vyhledat karty", "show_code_editor": "Zobrazit editor kódu", "show_visual_editor": "Zobrazit vizuální editor", "toggle_editor": "Přepnout Editor", diff --git a/translations/frontend/cy.json b/translations/frontend/cy.json index 15f461bf9188..abd54bda5237 100644 --- a/translations/frontend/cy.json +++ b/translations/frontend/cy.json @@ -1283,7 +1283,18 @@ "mfa": { "data": { "code": "Cod dilysu dwy-ffactor" - } + }, + "description": "" + } + } + }, + "legacy_api_password": { + "step": { + "mfa": { + "data": { + "code": "" + }, + "description": "" } } } diff --git a/translations/frontend/en.json b/translations/frontend/en.json index 482cf10f2382..250666624b49 100644 --- a/translations/frontend/en.json +++ b/translations/frontend/en.json @@ -19,6 +19,7 @@ "logbook": "Logbook", "mailbox": "Mailbox", "map": "Map", + "media_browser": "Media Browser", "profile": "Profile", "shopping_list": "Shopping list", "states": "Overview" @@ -552,6 +553,10 @@ "toggle": "Toggle" }, "entity": { + "entity-attribute-picker": { + "attribute": "Attribute", + "show_attributes": "Show attributes" + }, "entity-picker": { "clear": "Clear", "entity": "Entity", @@ -563,6 +568,8 @@ "no_history_found": "No state history found." }, "media-browser": { + "audio_not_supported": "Your browser does not support the audio element.", + "choose_player": "Choose Player", "choose-source": "Choose Source", "content-type": { "album": "Album", @@ -571,12 +578,17 @@ "playlist": "Playlist", "server": "Server" }, + "media_browsing_error": "Media Browsing Error", + "media_not_supported": "The Browser Media Player does not support this type of media", + "media_player": "Media Player", "media-player-browser": "Media Player Browser", "no_items": "No items", "pick": "Pick", "pick-media": "Pick Media", "play": "Play", - "play-media": "Play Media" + "play-media": "Play Media", + "video_not_supported": "Your browser does not support the video element.", + "web-browser": "Web Browser" }, "picture-upload": { "label": "Picture", @@ -634,7 +646,7 @@ "icon": "Icon Override", "icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'", "name": "Name Override", - "note": "Note: this might not work yet with all integrations.", + "note": "Note: This might not work yet with all integrations.", "unavailable": "This entity is not currently available.", "update": "Update" }, @@ -921,7 +933,13 @@ "label": "Call service", "service_data": "Service data" }, + "wait_for_trigger": { + "continue_timeout": "Continue on timeout", + "label": "Wait for trigger", + "timeout": "Timeout (optional)" + }, "wait_template": { + "continue_timeout": "Continue on timeout", "label": "Wait", "timeout": "Timeout (optional)", "wait_template": "Wait Template" @@ -985,7 +1003,9 @@ "time": { "after": "After", "before": "Before", - "label": "Time" + "label": "Time", + "type_input": "Value of a date/time helper", + "type_value": "Fixed time" }, "zone": { "entity": "Entity with location", @@ -1073,6 +1093,7 @@ "value_template": "Value template (optional)" }, "state": { + "attribute": "Attribute (Optional)", "for": "For", "from": "From", "label": "State", @@ -1099,8 +1120,10 @@ "seconds": "Seconds" }, "time": { - "at": "At", - "label": "Time" + "at": "At time", + "label": "Time", + "type_input": "Value of a date/time helper", + "type_value": "Fixed time" }, "webhook": { "label": "Webhook", @@ -1123,6 +1146,8 @@ "add_automation": "Add automation", "delete_automation": "Delete automation", "delete_confirm": "Are you sure you want to delete this automation?", + "duplicate": "Duplicate", + "duplicate_automation": "Duplicate automation", "edit_automation": "Edit automation", "header": "Automation Editor", "headers": { @@ -1523,6 +1548,7 @@ }, "integrations": { "add_integration": "Add integration", + "attention": "Attention required", "caption": "Integrations", "config_entry": { "area": "In {area}", @@ -1546,6 +1572,7 @@ "reload_restart_confirm": "Restart Home Assistant to finish reloading this integration", "rename": "Rename", "restart_confirm": "Restart Home Assistant to finish removing this integration", + "services": "{count} {count, plural,\n one {service}\n other {services}\n}", "settings_button": "Edit settings for {integration}", "system_options": "System options", "system_options_button": "System options for {integration}", @@ -1592,6 +1619,7 @@ "none_found_detail": "Adjust your search criteria.", "note_about_integrations": "Not all integrations can be configured via the UI yet.", "note_about_website_reference": "More are available on the ", + "reconfigure": "Reconfigure", "rename_dialog": "Edit the name of this config entry", "rename_input_label": "Entry name", "search": "Search integrations" @@ -1765,8 +1793,21 @@ "versions": "Obtaining information about firmware and command class versions", "wakeup": "Setting up support for wakeup queues and messages" }, + "node": { + "button": "Node Details", + "not_found": "Node not found" + }, + "nodes_table": { + "failed": "Failed", + "id": "ID", + "manufacturer": "Manufacturer", + "model": "Model", + "query_stage": "Query Stage", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { "battery_note": "If the node is battery powered, be sure to wake it before proceeding", + "button": "Refresh Node", "complete": "Node Refresh Complete", "description": "This will tell OpenZWave to re-interview a node and update the node's command classes, capabilities, and values.", "node_status": "Node Status", @@ -2025,7 +2066,7 @@ "system": "System" } }, - "users_privileges_note": "The users group is a work in progress. The user will be unable to administer the instance via the UI. We're still auditing all management API endpoints to ensure that they correctly limit access to administrators." + "users_privileges_note": "The user group feature is a work in progress. The user will be unable to administer the instance via the UI. We're still auditing all management API endpoints to ensure that they correctly limit access to administrators." }, "zha": { "add_device_page": { @@ -2063,7 +2104,7 @@ "clusters": { "header": "Clusters", "help_cluster_dropdown": "Select a cluster to view attributes and commands.", - "introduction": "Clusters are the building blocks for Zigbee functionality. They seperate functionality into logical units. There are client and server types and that are comprised of attributes and commands." + "introduction": "Clusters are the building blocks for Zigbee functionality. They separate functionality into logical units. There are client and server types and that are comprised of attributes and commands." }, "common": { "add_devices": "Add Devices", @@ -2196,7 +2237,7 @@ "true": "True" }, "node_management": { - "add_to_group": "Add To Group", + "add_to_group": "Add to Group", "entities": "Entities of this node", "entity_info": "Entity Information", "exclude_entity": "Exclude this entity from Home Assistant", @@ -2209,11 +2250,11 @@ "node_to_control": "Node to control", "nodes": "Nodes", "nodes_hint": "Select node to view per-node options", - "nodes_in_group": "Other Nodes in this group:", + "nodes_in_group": "Other nodes in this group:", "pooling_intensity": "Polling intensity", "protection": "Protection", "remove_broadcast": "Remove Broadcast", - "remove_from_group": "Remove From Group", + "remove_from_group": "Remove from Group", "set_protection": "Set Protection" }, "ozw_log": { @@ -2575,6 +2616,7 @@ "options": "More options", "pick_card": "Which card would you like to add?", "pick_card_view_title": "Which card would you like to add to your {name} view?", + "search_cards": "Search cards", "show_code_editor": "Show Code Editor", "show_visual_editor": "Show Visual Editor", "toggle_editor": "Toggle Editor", diff --git a/translations/frontend/eo.json b/translations/frontend/eo.json index 23fc03e9a944..8b0548932878 100644 --- a/translations/frontend/eo.json +++ b/translations/frontend/eo.json @@ -1,4 +1,49 @@ { + "state_badge": { + "device_tracker": { + "home": "" + } + }, + "state": { + "automation": { + "off": "" + }, + "binary_sensor": { + "default": { + "on": "" + }, + "presence": { + "on": "" + } + }, + "calendar": { + "on": "" + }, + "group": { + "home": "", + "off": "", + "on": "" + }, + "input_boolean": { + "on": "" + }, + "light": { + "off": "", + "on": "" + }, + "media_player": { + "off": "" + }, + "script": { + "off": "" + }, + "sensor": { + "off": "" + }, + "switch": { + "on": "" + } + }, "ui": { "card": { "climate": { @@ -79,7 +124,12 @@ "edit_ui": "Redakti kun UI", "edit_yaml": "Redakti kiel YAML", "triggers": { - "name": "Ellasilo" + "name": "Ellasilo", + "type": { + "mqtt": { + "label": "" + } + } } }, "picker": { diff --git a/translations/frontend/es-419.json b/translations/frontend/es-419.json index 62e01af2d696..ced016f2a1ce 100644 --- a/translations/frontend/es-419.json +++ b/translations/frontend/es-419.json @@ -13,9 +13,15 @@ }, "panel": { "calendar": "Calendario", + "config": "", "developer_tools": "Herramientas para desarrolladores", + "history": "", + "logbook": "", + "mailbox": "", + "map": "", "profile": "Perfil", - "shopping_list": "Lista de compras" + "shopping_list": "Lista de compras", + "states": "" }, "state_attributes": { "climate": { @@ -994,6 +1000,7 @@ "start": "Inicio" }, "mqtt": { + "label": "", "payload": "Payload (opcional)", "topic": "Topic" }, @@ -1609,6 +1616,7 @@ "start_listening": "Comenzar a escuchar", "stop_listening": "Deja de escuchar", "subscribe_to": "Tema para suscribirse", + "title": "", "topic": "tema" }, "person": { @@ -1939,6 +1947,7 @@ }, "zwave": { "button": "Configurar", + "caption": "", "common": { "index": "Índice", "instance": "Instancia", diff --git a/translations/frontend/es.json b/translations/frontend/es.json index 1b45b587648c..9b22dc0cde9e 100644 --- a/translations/frontend/es.json +++ b/translations/frontend/es.json @@ -19,6 +19,7 @@ "logbook": "Registro", "mailbox": "Buzón", "map": "Mapa", + "media_browser": "Navegador de medios", "profile": "Perfil", "shopping_list": "Lista de la compra", "states": "Resumen" @@ -505,6 +506,7 @@ "back": "Volver", "cancel": "Cancelar", "close": "Cerrar", + "continue": "Continuar", "delete": "Eliminar", "error_required": "Obligatorio", "loading": "Cargando", @@ -562,6 +564,8 @@ "no_history_found": "No se encontró historial de estado." }, "media-browser": { + "audio_not_supported": "Tu navegador no es compatible con el elemento de audio.", + "choose_player": "Elige reproductor", "choose-source": "Elige la fuente", "content-type": { "album": "Álbum", @@ -570,12 +574,17 @@ "playlist": "Lista de reproducción", "server": "Servidor" }, + "media_browsing_error": "Error de navegación de medios", + "media_not_supported": "El Reproductor multimedia del navegador no es compatible con este tipo de medio", + "media_player": "Reproductor multimedia", "media-player-browser": "Navegador del Reproductor Multimedia", "no_items": "No hay elementos", "pick": "Elegir", "pick-media": "Elegir medio", "play": "Reproducir", - "play-media": "Reproducir medio" + "play-media": "Reproducir medio", + "video_not_supported": "Tu navegador no es compatible con el elemento de vídeo.", + "web-browser": "Navegador web" }, "picture-upload": { "label": "Imagen", @@ -689,8 +698,10 @@ "crop": "Recortar" }, "more_info_control": { + "controls": "Controles", "dismiss": "Descartar diálogo", "edit": "Editar entidad", + "history": "Historial", "person": { "create_zone": "Crear zona a partir de la ubicación actual" }, @@ -1543,6 +1554,7 @@ "reload_restart_confirm": "Reinicia Home Assistant para terminar de recargar esta integración", "rename": "Renombrar", "restart_confirm": "Reinicia Home Assistant para terminar de eliminar esta integración.", + "services": "{count} {count, plural,\n one {servicio}\n other {servicios}\n}", "settings_button": "Editar configuración para {integration}", "system_options": "Opciones del sistema", "system_options_button": "Opciones del sistema para {integration}", @@ -1762,8 +1774,21 @@ "versions": "Obteniendo información sobre versiones de firmware y clases de órdenes", "wakeup": "Configurando soporte para colas de despertador y mensajes" }, + "node": { + "button": "Detalles del nodo", + "not_found": "Nodo no encontrado" + }, + "nodes_table": { + "failed": "Ha fallado", + "id": "ID", + "manufacturer": "Fabricante", + "model": "Modelo", + "query_stage": "Etapa de consulta", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { "battery_note": "Si el nodo funciona con batería, asegúrate de despertarlo antes de continuar", + "button": "Actualizar nodo", "complete": "Refresco del Nodo Finalizado", "description": "Esto le indicará a OpenZWave que vuelva a entrevistar un nodo y actualice las clases de órdenes, las capacidades y los valores del mismo.", "node_status": "Estado del Nodo", @@ -1912,7 +1937,7 @@ "filter": "Recargar entidades de filtro", "generic": "Recargar entidades de cámara IP genéricas", "generic_thermostat": "Recargar entidades de termostato genéricas", - "group": "Recargar grupos", + "group": "Recargar grupos, entidades de grupo, y notificar servicios", "heading": "Recargando la configuración YAML", "history_stats": "Recargar entidades de estadísticas del historial", "homekit": "Recargar HomeKit", @@ -1923,12 +1948,17 @@ "input_text": "Recargar los campos de texto", "introduction": "Algunas partes de Home Assistant pueden recargarse sin necesidad de reiniciar. Al pulsar en recargar se descartará la configuración YAML actual y se cargará la nueva.", "min_max": "Recargar entidades min/max", + "mqtt": "Recargar entidades mqtt", "person": "Recargar personas", "ping": "Recargar entidades de sensor binario de ping", - "rest": "Recargar entidades rest", + "reload": "Recargar {domain}", + "rest": "Recargar entidades rest y notificar servicios", + "rpi_gpio": "Recargar entidades GPIO de Raspberry Pi", "scene": "Recargar escenas", "script": "Recargar scripts", + "smtp": "Recargar servicios de notificación smtp", "statistics": "Recargar entidades de estadísticas", + "telegram": "Recargar servicios de notificación de telegram", "template": "Recargar entidades de plantilla", "trend": "Recargar entidades de tendencia", "universal": "Recargar entidades de reproductor multimedia universal", @@ -2017,7 +2047,7 @@ "system": "Sistema" } }, - "users_privileges_note": "El grupo de usuarios es un trabajo en progreso. El usuario no podrá administrar la instancia a través de la interfaz de usuario. Todavía estamos auditando todos los endpoints de la API de administración para garantizar que se limita correctamente el acceso sólo a los administradores." + "users_privileges_note": "El grupo de usuarios es un trabajo en progreso. El usuario no podrá administrar la instancia a través de la IU. Todavía estamos auditando todos los endpoints de la API de administración para garantizar que se limita correctamente el acceso sólo a los administradores." }, "zha": { "add_device_page": { @@ -2055,7 +2085,7 @@ "clusters": { "header": "Clústeres", "help_cluster_dropdown": "Selecciona un clúster para ver atributos y comandos.", - "introduction": "Los clústeres son los bloques de construcción para la funcionalidad de Zigbee. Separan la funcionalidad en unidades lógicas. Hay tipos de cliente y servidor y se componen de atributos y comandos." + "introduction": "Los clústeres son los bloques de construcción para la funcionalidad de Zigbee. Separan la funcionalidad en unidades lógicas. Hay tipos de cliente y de servidor y se componen de atributos y comandos." }, "common": { "add_devices": "Añadir dispositivos", @@ -2188,7 +2218,7 @@ "true": "Verdadero" }, "node_management": { - "add_to_group": "Añadir al grupo", + "add_to_group": "Añadir al Grupo", "entities": "Entidades de este nodo", "entity_info": "Información de la entidad", "exclude_entity": "Excluir esta entidad de Home Assistant", @@ -2205,7 +2235,7 @@ "pooling_intensity": "Intensidad de sondeo", "protection": "Protección", "remove_broadcast": "Eliminar difusión", - "remove_from_group": "Eliminar del grupo", + "remove_from_group": "Eliminar del Grupo", "set_protection": "Establecer protección" }, "ozw_log": { @@ -2549,7 +2579,11 @@ } }, "cardpicker": { + "by_card": "Por tarjeta", + "by_entity": "Por entidad", "custom_card": "Personalizado", + "domain": "Dominio", + "entity": "Entidad", "no_description": "No hay descripción disponible." }, "edit_card": { @@ -2563,6 +2597,7 @@ "options": "Más opciones", "pick_card": "¿Qué tarjeta te gustaría añadir?", "pick_card_view_title": "¿Qué tarjeta te gustaría añadir a tu vista {name} ?", + "search_cards": "Buscar tarjetas", "show_code_editor": "Mostrar editor de código", "show_visual_editor": "Mostrar editor visual", "toggle_editor": "Alternar editor", diff --git a/translations/frontend/et.json b/translations/frontend/et.json index 4b3c342d9b5c..51e3f2f62040 100644 --- a/translations/frontend/et.json +++ b/translations/frontend/et.json @@ -923,6 +923,10 @@ "at": "Kell", "label": "Aeg" }, + "webhook": { + "label": "", + "webhook_id": "" + }, "zone": { "enter": "Sisenemine", "entity": "Asukohaga olem", diff --git a/translations/frontend/fa.json b/translations/frontend/fa.json index 28992b49cbce..2fc44ddc5d98 100644 --- a/translations/frontend/fa.json +++ b/translations/frontend/fa.json @@ -1689,6 +1689,9 @@ }, "step": { "mfa": { + "data": { + "code": "" + }, "description": "باز کردن **{mfa_module_name}** * * * در دستگاه خود را برای مشاهده شما دو فاکتور تأیید هویت کد و هویت خود را تایید کنید:" } } diff --git a/translations/frontend/fr.json b/translations/frontend/fr.json index 939706d0e1ae..8af22d4ab643 100644 --- a/translations/frontend/fr.json +++ b/translations/frontend/fr.json @@ -19,6 +19,7 @@ "logbook": "Journal", "mailbox": "Boîtes aux lettres", "map": "Carte", + "media_browser": "Navigateur multimédia", "profile": "Profil", "shopping_list": "Liste de courses", "states": "Aperçu" @@ -79,7 +80,7 @@ "default": { "entity_not_found": "Entité introuvable", "error": "Erreur", - "unavailable": "Indisponible", + "unavailable": "Indispo.", "unknown": "Inconnu" }, "device_tracker": { @@ -170,7 +171,7 @@ "on": "Problème" }, "safety": { - "off": "Sécurisé", + "off": "Sûr", "on": "Dangereux" }, "smoke": { @@ -318,7 +319,7 @@ "fog": "Brouillard", "hail": "Grêle", "lightning": "Orage", - "lightning-rainy": "Orage / Pluie", + "lightning-rainy": "Orage / Pluvieux", "partlycloudy": "Partiellement nuageux", "pouring": "Averses", "rainy": "Pluvieux", @@ -351,7 +352,7 @@ "alarm_control_panel": { "arm_away": "Armer (absent)", "arm_custom_bypass": "Bypass personnalisé", - "arm_home": "Armer (domicile)", + "arm_home": "Armer (présent)", "arm_night": "Armer nuit", "clear_code": "Effacer", "code": "Code", @@ -505,6 +506,7 @@ "back": "Retour", "cancel": "Annuler", "close": "Fermer", + "continue": "Continuer", "delete": "Supprimer", "error_required": "Obligatoire", "loading": "Chargement", @@ -562,6 +564,8 @@ "no_history_found": "Aucun historique des valeurs trouvé." }, "media-browser": { + "audio_not_supported": "Votre navigateur ne prend pas en charge l'élément audio.", + "choose_player": "Choisissez le lecteur", "choose-source": "Choisissez la source", "content-type": { "album": "Album", @@ -570,12 +574,17 @@ "playlist": "Liste de lecture", "server": "Serveur" }, + "media_browsing_error": "Erreur de navigation multimédia", + "media_not_supported": "Le Browser Media Player ne prend pas en charge ce type de média", + "media_player": "Lecteur multimédia", "media-player-browser": "Lecteur multimédia", "no_items": "Aucun éléments", "pick": "Choisir", "pick-media": "Choisissez un média", "play": "Lecture", - "play-media": "Lire le média" + "play-media": "Lire le média", + "video_not_supported": "Votre navigateur ne prend pas en charge l'élément vidéo.", + "web-browser": "Navigateur web" }, "picture-upload": { "label": "Image", @@ -689,8 +698,10 @@ "crop": "Recadrer" }, "more_info_control": { + "controls": "Contrôles", "dismiss": "Fermer la fenêtre de dialogue", "edit": "Modifier l'entité", + "history": "Historique", "person": { "create_zone": "Créer une zone à partir de l'emplacement actuel" }, @@ -1442,7 +1453,7 @@ "name": "Nom", "status": "Statut" }, - "introduction": "Home Assistant tient un registre de chaque entité qu'il a déjà vu au moins une fois et qui peut être identifié de manière unique. Chacune de ces entités se verra attribuer un identifiant qui sera réservé à cette seule entité.", + "introduction": "Home Assistant tient un registre de chaque entité qu'il a déjà vu au moins une fois et qui peut être identifiée de manière unique. Chacune de ces entités se verra attribuer un identifiant qui sera réservé à cette seule entité.", "introduction2": "Utilisé le registre des entités pour remplacer le nom, modifier l'ID de l'entité ou supprimer l'entrée de Home Assistant.", "remove_selected": { "button": "Supprimer la sélection", @@ -1534,7 +1545,7 @@ "firmware": "Firmware: {version}", "hub": "Connecté via", "manuf": "par {manufacturer}", - "no_area": "Pas de pièce", + "no_area": "Pas de zone", "no_device": "Entités sans appareils", "no_devices": "Cette intégration n'a pas d'appareils.", "options": "Options", @@ -1740,7 +1751,7 @@ }, "network": { "header": "Gestion du réseau", - "introduction": "Διαχείρηση λειτουργιών δικτύου", + "introduction": "Gérez les fonctions de niveau réseau.", "node_count": "{count} nœuds" }, "node_query_stages": { @@ -1762,8 +1773,21 @@ "versions": "Obtention d'informations sur les versions des microprogrammes et des classes de commande", "wakeup": "Configuration de la prise en charge des files d'attente et des messages de réveil" }, + "node": { + "button": "Détails du nœud", + "not_found": "Nœud introuvable" + }, + "nodes_table": { + "failed": "Echec", + "id": "ID", + "manufacturer": "Fabricant", + "model": "Modèle", + "query_stage": "Étape de requête", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { "battery_note": "Si le nœud est alimenté par batterie, assurez-vous de l'activer avant de continuer", + "button": "Actualiser le nœud", "complete": "Actualisation du nœud terminée", "description": "Cela indiquera à OpenZWave de réinterroger le nœud et de le mettre à jour (commandes, possibilités et valeurs).", "node_status": "État du nœud", @@ -1912,7 +1936,7 @@ "filter": "Recharger les entités de filtre", "generic": "Recharger les entités de caméra IP générique", "generic_thermostat": "Recharger les entités de thermostat générique", - "group": "Recharger les groupes", + "group": "Recharger les groupes, les entités de groupe et notifier les services", "heading": "Rechargement de la configuration", "history_stats": "Recharger les entités des statistiques historiques", "homekit": "Recharger HomeKit", @@ -1923,12 +1947,17 @@ "input_text": "Recharger les entrées de texte (input text)", "introduction": "Certaines parties de Home Assistant peuvent être rechargées sans nécessiter de redémarrage. Le fait de cliquer sur recharger déchargera leur configuration actuelle et chargera la nouvelle.", "min_max": "Recharger les entités min/max", + "mqtt": "Recharger les entités mqtt", "person": "Recharger les personnes", "ping": "Recharger les entités de capteur binaire ping", - "rest": "Recharger les entités REST", + "reload": "Recharger {domain}", + "rest": "Recharger les entités REST et notifier les services", + "rpi_gpio": "Recharger les entités GPIO du Raspberry Pi", "scene": "Recharger les scènes", "script": "Recharger les scripts", + "smtp": "Recharger les services de notification smtp", "statistics": "Recharger les entités de statistiques", + "telegram": "Recharger les services de notification de telegram", "template": "Recharger les entités modèles", "trend": "Recharger les entités de tendance", "universal": "Recharger les entités de lecteur média universel", @@ -2017,7 +2046,7 @@ "system": "Système" } }, - "users_privileges_note": "Le groupe d'utilisateurs est en cours de développement. L'utilisateur ne pourra pas gérer l'instance via l'interface. Nous vérifions les entrées de l'interface de gestion pour assurer que les accès soient limités aux administrateurs." + "users_privileges_note": "La fonctionnalité de groupe d'utilisateurs est en cours de développement. L'utilisateur ne pourra pas gérer l'instance via l'interface. Nous vérifions toujours tous les points terminaisons d'API pour assurer que les accès soient limités aux administrateurs." }, "zha": { "add_device_page": { @@ -2055,7 +2084,7 @@ "clusters": { "header": "Clusters", "help_cluster_dropdown": "Sélectionnez un cluster pour afficher les attributs et les commandes.", - "introduction": "Les clusters sont les blocs de construction de la fonctionnalité Zigbee. Ils séparent les fonctionnalités en unités logiques. Il existe des types de clients et de serveurs qui sont composés d'attributs et de commandes." + "introduction": "Les grappes sont les éléments de construction de la fonctionnalité Zigbee. Ils séparent les fonctionnalités en unités logiques. Il en existe de types client et serveur et sont composés d'attributs et de commandes." }, "common": { "add_devices": "Ajouter des appareils", @@ -2496,7 +2525,7 @@ }, "markdown": { "content": "Contenu", - "description": "La carte Markdown est utilisée pour afficher du Markdown.", + "description": "La carte Markdown est utilisée pour le rendu du Markdown.", "name": "Markdown" }, "media-control": { @@ -2549,7 +2578,11 @@ } }, "cardpicker": { + "by_card": "Par carte", + "by_entity": "Par entité", "custom_card": "Personnalisé", + "domain": "Domaine", + "entity": "Entité", "no_description": "Aucune description disponible." }, "edit_card": { @@ -2563,6 +2596,7 @@ "options": "Plus d'options", "pick_card": "Quelle carte aimeriez-vous ajouter ?", "pick_card_view_title": "Quelle carte souhaitez-vous ajouter à votre vue {name} ?", + "search_cards": "Rechercher des cartes", "show_code_editor": "Afficher l'éditeur de code", "show_visual_editor": "Afficher l'éditeur visuel", "toggle_editor": "Permuter l’éditeur", @@ -2765,7 +2799,7 @@ "data": { "code": "Code d'authentification à deux facteurs" }, - "description": "Ouvrez le **{mfa_module_name}** sur votre appareil pour afficher votre code d'authentification à deux facteurs et vérifier votre identité:" + "description": "Ouvrez le ** {mfa_module_name} ** sur votre appareil pour afficher votre code d'authentification à deux facteurs et confirmer votre identité:" } } }, @@ -2785,7 +2819,7 @@ } }, "start_over": "Recommencer", - "unknown_error": "Quelque chose a mal tourné", + "unknown_error": "Un problème est survenu", "working": "Veuillez patienter" }, "initializing": "Initialisation", diff --git a/translations/frontend/fy.json b/translations/frontend/fy.json index 1f8841d202f7..918eb995c3d0 100644 --- a/translations/frontend/fy.json +++ b/translations/frontend/fy.json @@ -195,8 +195,10 @@ }, "triggers": { "add": "Trigger tafoegje", + "header": "", "type": { "homeassistant": { + "label": "", "shutdown": "Ofslúte", "start": "Opstarte" }, diff --git a/translations/frontend/gl.json b/translations/frontend/gl.json index 9e26dfeeb6e6..15018eec6540 100644 --- a/translations/frontend/gl.json +++ b/translations/frontend/gl.json @@ -1 +1,19 @@ -{} \ No newline at end of file +{ + "ui": { + "panel": { + "config": { + "automation": { + "editor": { + "triggers": { + "type": { + "mqtt": { + "label": "" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/translations/frontend/gsw.json b/translations/frontend/gsw.json index b4e756236a6b..2e0d098575c8 100644 --- a/translations/frontend/gsw.json +++ b/translations/frontend/gsw.json @@ -37,7 +37,8 @@ "unknown": "Unbekannt" }, "device_tracker": { - "home": "Dahei" + "home": "Dahei", + "not_home": "" }, "person": { "home": "Dahei" @@ -96,6 +97,7 @@ "on": "Offä" }, "presence": { + "off": "", "on": "Dahei" }, "problem": { @@ -417,6 +419,9 @@ "time": { "after": "Nachhär", "before": "Vorhär" + }, + "zone": { + "entity": "" } } }, @@ -510,6 +515,45 @@ "mailbox": { "delete_button": "Lösche" }, + "page-authorize": { + "form": { + "providers": { + "command_line": { + "abort": { + "login_expired": "" + }, + "error": { + "invalid_auth": "", + "invalid_code": "" + }, + "step": { + "init": { + "data": { + "password": "", + "username": "" + } + }, + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + }, + "legacy_api_password": { + "step": { + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + } + } + } + }, "profile": { "change_password": { "confirm_new_password": "Nöis Passwort bestätige", diff --git a/translations/frontend/hi.json b/translations/frontend/hi.json index f71427a68363..0f06beb774ec 100644 --- a/translations/frontend/hi.json +++ b/translations/frontend/hi.json @@ -19,6 +19,9 @@ }, "state_badge": { "alarm_control_panel": { + "armed_away": "", + "armed_custom_bypass": "", + "armed_night": "", "pending": "अपूर्ण" }, "default": { @@ -28,7 +31,8 @@ "unknown": "अज्ञात" }, "device_tracker": { - "home": "घर" + "home": "घर", + "not_home": "" }, "person": { "home": "घर" @@ -75,16 +79,27 @@ "off": "विशद", "on": "अनुसन्धानित" }, + "occupancy": { + "off": "", + "on": "" + }, "opening": { "on": "खुला" }, "presence": { + "off": "", "on": "घर" }, "safety": { "off": "सुरक्षित", "on": "असुरक्षित" }, + "smoke": { + "off": "" + }, + "vibration": { + "on": "" + }, "window": { "off": "बंद", "on": "खुली" @@ -115,10 +130,15 @@ "on": "चालू" }, "group": { + "closing": "", "home": "घर", + "not_home": "", "off": "बंद", + "ok": "", "on": "चालू", - "problem": "समस्या" + "open": "", + "problem": "समस्या", + "stopped": "" }, "input_boolean": { "off": "बंद", @@ -234,6 +254,9 @@ "time": { "after": "बाद", "before": "पहले" + }, + "zone": { + "entity": "" } } }, @@ -405,6 +428,39 @@ "form": { "next": "अगला", "providers": { + "command_line": { + "abort": { + "login_expired": "" + }, + "error": { + "invalid_auth": "", + "invalid_code": "" + }, + "step": { + "init": { + "data": { + "password": "", + "username": "" + } + }, + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + }, + "legacy_api_password": { + "step": { + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + }, "trusted_networks": { "step": { "init": { diff --git a/translations/frontend/ja.json b/translations/frontend/ja.json index f0d6e4647557..19aaf17e50e5 100644 --- a/translations/frontend/ja.json +++ b/translations/frontend/ja.json @@ -65,7 +65,10 @@ }, "state_badge": { "alarm_control_panel": { + "armed_away": "", + "armed_custom_bypass": "", "armed_home": "アームしました", + "armed_night": "", "disarmed": "解除", "disarming": "解除", "pending": "保留", diff --git a/translations/frontend/lt.json b/translations/frontend/lt.json index 567cd131d24e..08fc0028b04e 100644 --- a/translations/frontend/lt.json +++ b/translations/frontend/lt.json @@ -189,6 +189,7 @@ "ok": "Ok", "on": "Įjungta", "open": "Atidarytas", + "problem": "", "stopped": "Sustabdytas", "unlocked": "Atrakinta" }, @@ -408,6 +409,7 @@ "label": "Laikas" }, "zone": { + "entity": "", "label": "Vieta", "zone": "Vieta" } @@ -688,6 +690,9 @@ "form": { "providers": { "command_line": { + "abort": { + "login_expired": "" + }, "error": { "invalid_auth": "Netinkamas vartotojo vardas arba slaptažodis", "invalid_code": "Netinkamas autentifikacijos kodas" @@ -702,7 +707,18 @@ "mfa": { "data": { "code": "Dvieju lygiu autentifikacija" - } + }, + "description": "" + } + } + }, + "legacy_api_password": { + "step": { + "mfa": { + "data": { + "code": "" + }, + "description": "" } } } diff --git a/translations/frontend/nb.json b/translations/frontend/nb.json index 235f6930b83b..523a9a271812 100644 --- a/translations/frontend/nb.json +++ b/translations/frontend/nb.json @@ -19,6 +19,7 @@ "logbook": "Loggbok", "mailbox": "Postkasse", "map": "Kart", + "media_browser": "Medieleser", "profile": "Profil", "shopping_list": "Handleliste", "states": "Oversikt" @@ -26,6 +27,7 @@ "state_attributes": { "climate": { "fan_mode": { + "auto": "", "off": "Av", "on": "På" }, @@ -50,11 +52,14 @@ }, "humidifier": { "mode": { + "auto": "", "away": "Borte", + "baby": "", "boost": "Øke", "comfort": "Komfort", "eco": "Øko", "home": "Hjem", + "normal": "", "sleep": "Sove" } } @@ -110,6 +115,7 @@ "on": "Lavt" }, "cold": { + "off": "", "on": "Kald" }, "connectivity": { @@ -160,6 +166,10 @@ "off": "Borte", "on": "Hjemme" }, + "problem": { + "off": "", + "on": "" + }, "safety": { "off": "Sikker", "on": "Usikker" @@ -229,6 +239,7 @@ "locked": "Låst", "not_home": "Borte", "off": "Av", + "ok": "", "on": "På", "open": "Åpen", "opening": "Åpner", @@ -260,6 +271,7 @@ "home": "Hjemme" }, "plant": { + "ok": "", "problem": "Problem" }, "remote": { @@ -365,6 +377,7 @@ "low": "lav", "on_off": "På / av", "operation": "Operasjon", + "preset_mode": "", "swing_mode": "Svingmodus", "target_humidity": "Ønsket luftfuktighet", "target_temperature": "Ønsket temperatur", @@ -409,7 +422,7 @@ "media_player": { "browse_media": "Bla gjennom medier", "media_next_track": "Neste", - "media_play": "Spille", + "media_play": "Spill av", "media_play_pause": "Spill av/pause", "media_previous_track": "Forrige", "sound_mode": "Lydmodus", @@ -435,7 +448,9 @@ "timer": { "actions": { "cancel": "Avbryt", - "finish": "Ferdig" + "finish": "Ferdig", + "pause": "", + "start": "" } }, "vacuum": { @@ -467,10 +482,12 @@ "e": "Ø", "ene": "ØNØ", "ese": "ØSØ", + "n": "", "ne": "NØ", "nne": "NNØ", "nnw": "NNV", "nw": "NV", + "s": "", "se": "SØ", "sse": "SSØ", "ssw": "SSV", @@ -489,6 +506,7 @@ "back": "Tilbake", "cancel": "Avbryt", "close": "Lukk", + "continue": "Fortsette", "delete": "Slett", "error_required": "Nødvendig", "loading": "Laster", @@ -546,6 +564,8 @@ "no_history_found": "Ingen statushistorikk funnet." }, "media-browser": { + "audio_not_supported": "Nettleseren din støtter ikke lydelementet.", + "choose_player": "Velg spiller", "choose-source": "Velg kilde", "content-type": { "album": "Album", @@ -554,12 +574,17 @@ "playlist": "Spilleliste", "server": "Server" }, + "media_browsing_error": "Feil ved medievisning", + "media_not_supported": "Browser Media Player støtter ikke denne typen medier", + "media_player": "Mediaspiller", "media-player-browser": "Nettleser for Mediespiller", "no_items": "Ingen elementer", "pick": "Velg", "pick-media": "Velg Media", - "play": "Spille", - "play-media": "Spill media" + "play": "Spill av", + "play-media": "Spill media", + "video_not_supported": "Nettleseren din støtter ikke videoelementet.", + "web-browser": "Nettleser" }, "picture-upload": { "label": "Bilde", @@ -617,7 +642,7 @@ "icon": "Overstyring av ikon", "icon_error": "Ikoner bør være i formatet 'prefiks:ikonnavn', f.eks 'mdi:home'", "name": "Overstyr Navn", - "note": "Merk: dette fungerer kanskje ikke med alle integrasjoner ennå .", + "note": "Merk: Dette fungerer kanskje ikke ennå med alle integrasjoner.", "unavailable": "Denne entiteten er ikke tilgjengelig for øyeblikket.", "update": "Oppdater" }, @@ -628,7 +653,8 @@ "generic": { "cancel": "Avbryt", "close": "Lukk", - "default_confirmation_title": "Er du sikker?" + "default_confirmation_title": "Er du sikker?", + "ok": "" }, "helper_settings": { "generic": { @@ -672,8 +698,10 @@ "crop": "Beskjære" }, "more_info_control": { + "controls": "Kontroller", "dismiss": "Avvis dialogboksen", "edit": "Redigér entitet", + "history": "Historie", "person": { "create_zone": "Opprett sone fra gjeldende plassering" }, @@ -702,8 +730,11 @@ "commands": "Støvsugerkommandoer:", "fan_speed": "Viftehastighet", "locate": "Lokaliser", + "pause": "", "return_home": "Returner hjem", + "start": "", "start_pause": "Start / Pause", + "status": "", "stop": "Stopp" } }, @@ -750,6 +781,7 @@ "manuf": "av {manufacturer}", "no_area": "Intet område", "power_source": "Strømkilde", + "quirk": "", "services": { "reconfigure": "Rekonfigurer ZHA-enhet (heal enhet). Bruk dette hvis du har problemer med enheten. Hvis den aktuelle enheten er en batteridrevet enhet, sørg for at den er våken og aksepterer kommandoer når du bruker denne tjenesten.", "remove": "Fjern en enhet fra Zigbee-nettverket.", @@ -878,7 +910,7 @@ "type_select": "Gjenta type", "type": { "count": { - "label": "Telle" + "label": "Antall" }, "until": { "conditions": "Inntil forholdene", @@ -1033,7 +1065,9 @@ }, "homeassistant": { "event": "Hendelse:", - "shutdown": "Slå av" + "label": "", + "shutdown": "Slå av", + "start": "" }, "mqtt": { "label": "MQTT", @@ -1047,6 +1081,7 @@ "value_template": "Verdi fra mal (valgfritt)" }, "state": { + "for": "", "from": "Fra", "label": "Tilstand", "to": "Til" @@ -1075,6 +1110,10 @@ "at": "Klokken", "label": "Tid" }, + "webhook": { + "label": "", + "webhook_id": "" + }, "zone": { "enter": "Ankommer", "entity": "Entitet med posisjon", @@ -1118,7 +1157,8 @@ "manage_entities": "Håndtér entiteter", "state_reporting_error": "Kan ikke {enable_disable} rapportere status.", "sync_entities": "Synkronisér entiteter", - "sync_entities_error": "Kunne ikke synkronisere entiteter:" + "sync_entities_error": "Kunne ikke synkronisere entiteter:", + "title": "" }, "connected": "Tilkoblet", "connection_status": "Status for skytilkobling", @@ -1136,7 +1176,8 @@ "manage_entities": "Håndtér entiteter", "security_devices": "Sikkerhetsenheter", "sync_entities": "Synkronisér entiteter til Google", - "sync_entities_404_message": "Kunne ikke synkronisere enhetene dine med Google, be Google 'Hei Google, synkroniser enhetene mine' for å synkronisere enhetene dine." + "sync_entities_404_message": "Kunne ikke synkronisere enhetene dine med Google, be Google 'Hei Google, synkroniser enhetene mine' for å synkronisere enhetene dine.", + "title": "" }, "integrations": "Integrasjoner", "integrations_introduction": "Integrasjoner for Home Assistant Cloud lar deg koble til tjenester i skyen uten å måtte avsløre Home Assistant-forekomsten offentlig på internett.", @@ -1165,7 +1206,8 @@ "no_hooks_yet": "Ser ut som du ikke har noen webhooks ennå. Kom i gang ved å konfigurere en ", "no_hooks_yet_link_automation": "webhook-automasjon", "no_hooks_yet_link_integration": "webhook-basert integrasjon", - "no_hooks_yet2": " eller ved å opprette en " + "no_hooks_yet2": " eller ved å opprette en ", + "title": "" } }, "alexa": { @@ -1178,8 +1220,10 @@ "follow_domain": "Følg domenet", "manage_domains": "Administrer domener", "not_exposed": "{selected} ikke eksponert", - "not_exposed_entities": "Ikke eksponerte enheter" + "not_exposed_entities": "Ikke eksponerte enheter", + "title": "" }, + "caption": "", "description_features": "Kontroller borte fra hjemmet, integrer med Alexa og Google Assistant.", "description_login": "Logget inn som {email}", "description_not_login": "Ikke pålogget", @@ -1222,7 +1266,8 @@ "manage_domains": "Administrer domener", "not_exposed": "{selected} ikke eksponert", "not_exposed_entities": "Ikke eksponerte enheter", - "sync_to_google": "Synkroniserer endringer til Google." + "sync_to_google": "Synkroniserer endringer til Google.", + "title": "" }, "login": { "alert_email_confirm_necessary": "Du må bekrefte e-posten din før du logger inn.", @@ -1396,6 +1441,7 @@ "confirm_title": "Vil du aktivere {number} enheter?" }, "filter": { + "filter": "", "show_disabled": "Vis deaktiverte entiteter", "show_readonly": "Vis skrivebeskyttede enheter", "show_unavailable": "Vis utilgjengelige enheter" @@ -1404,7 +1450,8 @@ "headers": { "entity_id": "Entitets-ID", "integration": "Integrasjon", - "name": "Navn" + "name": "Navn", + "status": "" }, "introduction": "Home Assistant bygger opp et register over hver entitet den har sett som kan identifiseres unikt. Hver av disse entitetene vil ha en ID som er reservert kun til denne.", "introduction2": "Bruk entitetsregistret til å overstyre navnet, endre id-en eller fjerne den fra Home Assistant.", @@ -1419,6 +1466,7 @@ "selected": "{number} valgte", "status": { "disabled": "Deaktivert", + "ok": "", "readonly": "Skrivebeskyttet", "restored": "Gjennopprettet", "unavailable": "Utilgjengelig" @@ -1443,7 +1491,8 @@ "headers": { "editable": "Redigerbare", "entity_id": "Entitets-ID", - "name": "Navn" + "name": "Navn", + "type": "" }, "no_helpers": "Det ser ut som om du ikke har noen hjelpere ennå!" }, @@ -1451,6 +1500,7 @@ "input_boolean": "Veksle", "input_datetime": "Dato og/eller klokkeslett", "input_number": "Nummer", + "input_select": "", "input_text": "Tekst" } }, @@ -1463,6 +1513,7 @@ "documentation": "Dokumentasjon", "frontend": "frontend", "frontend_version": "Brukergrensesnittet-versjon: {version} - {type}", + "home_assistant_logo": "", "icons_by": "Ikoner fra", "integrations": "Integrasjoner", "issues": "Problemer", @@ -1503,6 +1554,7 @@ "reload_restart_confirm": "Start Home Assistant på nytt for å fullføre omlastingen av denne integrasjonen", "rename": "Gi nytt navn", "restart_confirm": "Start Home Assistant på nytt for å fullføre fjerningen av denne integrasjonen", + "services": "{count} {count, plural,\n one {service}\n other {services}\n}", "settings_button": "Rediger innstillinger for {integration}", "system_options": "Systemalternativer", "system_options_button": "Systemalternativer for {integration}", @@ -1593,6 +1645,7 @@ "title": "Tittel", "title_required": "Tittel er påkrevd.", "update": "Oppdater", + "url": "", "url_error_msg": "URLen skal inneholde en - og kan ikke inneholde mellomrom eller spesialtegn, bortsett fra _ og -" }, "picker": { @@ -1620,12 +1673,17 @@ "new_resource": "Legg til ny ressurs", "type": "Ressurstype", "update": "Oppdater", + "url": "", "url_error_msg": "URL-adresse er et obligatorisk felt", "warning_header": "Vær forsiktig!", "warning_text": "Det kan være farlig å legge til ressurser, sørg for at du kjenner kilden til ressursen og stoler på dem. Dårlige ressurser kan skade systemet ditt alvorlig." }, "picker": { "add_resource": "Legg til ressurs", + "headers": { + "type": "", + "url": "" + }, "no_resources": "Ingen ressurser" }, "refresh_body": "Du må oppdatere siden for å fullføre fjerningen, vil du oppdatere nå?", @@ -1716,8 +1774,21 @@ "versions": "Hente informasjon om fastvare- og kommandoklasseversjoner", "wakeup": "Sette opp støtte for vekkingskøer og meldinger" }, + "node": { + "button": "Node Detaljer", + "not_found": "Finner ikke noden" + }, + "nodes_table": { + "failed": "Mislyktes", + "id": "ID", + "manufacturer": "Produsent", + "model": "Modell", + "query_stage": "Spørringsstadiet", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { "battery_note": "Hvis noden er batteridrevet, må du passe på å vekke den før du fortsetter", + "button": "Oppdater node", "complete": "Node oppdatering fullført", "description": "Dette vil fortelle OpenZWave å re-intervjue en node og oppdatere nodens kommandoklasser, evner og verdier.", "node_status": "Node-status", @@ -1866,7 +1937,7 @@ "filter": "Last inn filter entiteter på nytt", "generic": "Last inn generiske IP-kamera entiteter på nytt", "generic_thermostat": "Last inn generiske termostat entiteter på nytt", - "group": "Last inn grupper på nytt", + "group": "Laste inn grupper, gruppere enheter og varsle tjenester på nytt", "heading": "YAML -Konfigurasjon lastes på nytt", "history_stats": "Last inn historiske tilstander på nytt", "homekit": "Last inn HomeKit på nytt", @@ -1877,12 +1948,17 @@ "input_text": "Last inn inndata tekst på nytt", "introduction": "Noen deler av Home Assistant kan laste inn uten å kreve omstart. Hvis du trykker last på nytt, vil du bytte den nåværende konfigurasjonen med den nye.", "min_max": "Last inn min/maks entiteter på nytt", + "mqtt": "Last inn mqtt-enheter på nytt", "person": "Last inn personer på nytt", "ping": "Last inn ping binære sensor entiteter på nytt", - "rest": "Last inn REST entiteter på nytt", + "reload": "Last inn {domain} på nytt", + "rest": "Last inn hvileenheter på nytt og varsle tjenester", + "rpi_gpio": "Last inn Raspberry Pi GPIO-enheter på nytt", "scene": "Last inn scener på nytt", "script": "Last inn skript på nytt", + "smtp": "Last inn smtp-varslingstjenester på nytt", "statistics": "Last inn statistiske entiteter på nytt", + "telegram": "Last inn telegram varslingstjenester på nytt", "template": "Laste inn mal entiteter på nytt", "trend": "Laste inn trend entiteter på nytt", "universal": "Laste inn universelle mediespiller entiteter på nytt", @@ -1946,6 +2022,7 @@ "editor": { "activate_user": "Aktiver bruker", "active": "Aktiv", + "admin": "", "caption": "Vis bruker", "change_password": "Endre passord", "confirm_user_deletion": "Er du sikker på at du vil slette {name} ?", @@ -1966,10 +2043,11 @@ "picker": { "headers": { "group": "Gruppe", - "name": "Navn" + "name": "Navn", + "system": "" } }, - "users_privileges_note": "Brukere-gruppen er et pågående arbeid. Brukeren kan ikke administrere forekomsten via brukergrensesnittet. Vi reviderer fortsatt alle API-endepunkter for å sikre at de begrenser tilgangen til administratorer på riktig måte." + "users_privileges_note": "Brukergruppefunksjonen er et pågående arbeid. Brukeren vil ikke kunne administrere forekomsten via brukergrensesnittet. Vi overvåker fortsatt alle administrasjons-API-endepunkter for å sikre at de begrenser tilgangen til administratorer på riktig måte." }, "zha": { "add_device_page": { @@ -1986,6 +2064,7 @@ "description": "Legg til enheter i Zigbee-nettverket" }, "button": "Konfigurer", + "caption": "", "cluster_attributes": { "attributes_of_cluster": "Attributter for den valgte klyngen", "get_zigbee_attribute": "Hent ZigBee-attributt", @@ -2006,7 +2085,7 @@ "clusters": { "header": "Klynger", "help_cluster_dropdown": "Velg en klynge for å vise attributter og kommandoer.", - "introduction": "Klynger er byggesteinene for ZigBee-funksjonalitet. De skiller funksjonalitet i logiske enheter. Det finnes klient og server typer som består av attributter og kommandoer." + "introduction": "Klynger er byggesteinene for Zigbee-funksjonalitet. De skiller funksjonalitet i logiske enheter. Det finnes klient- og servertyper, og som består av attributter og kommandoer." }, "common": { "add_devices": "Legg til enheter", @@ -2093,6 +2172,7 @@ "new_zone": "Ny sone", "passive": "Passiv", "passive_note": "Passive soner er skjult i grensesnittet og brukes ikke som sted for enhetssporere. Dette er nyttig hvis du bare vil bruke dem til automasjoner.", + "radius": "", "required_error_msg": "Dette feltet er påkrevd", "update": "Oppdater" }, @@ -2138,7 +2218,7 @@ "true": "Sant" }, "node_management": { - "add_to_group": "Legg til i gruppe", + "add_to_group": "Legg til gruppe", "entities": "Entiteter fra denne noden", "entity_info": "Entitetsinformasjon", "exclude_entity": "Ekskluder denne entiteten fra Home Assistant", @@ -2155,7 +2235,7 @@ "pooling_intensity": "Intensitet for polling", "protection": "Beskyttelse", "remove_broadcast": "Fjern kringkasting", - "remove_from_group": "Fjern fra gruppe", + "remove_from_group": "Fjern fra gruppen", "set_protection": "Angi beskyttelse" }, "ozw_log": { @@ -2222,6 +2302,7 @@ "call_service": "Tilkall tjeneste", "column_description": "Beskrivelse", "column_example": "Eksempel", + "column_parameter": "", "data": "Tjenestedata (YAML, valgfritt)", "description": "Service utviklingsverktøyet lar deg tilkalle alle tilgjengelige tjenester i Home Assistant.", "fill_example_data": "Fyll ut eksempeldata", @@ -2297,6 +2378,7 @@ }, "picture-elements": { "call_service": "Tilkall tjeneste {name}", + "hold": "", "more_info": "Vis mer info: {name}", "navigate_to": "Naviger til {location}", "tap": "Trykk:", @@ -2391,6 +2473,7 @@ "manual": "Manuell", "manual_description": "Trenger du å legge til et tilpasset kort eller bare ønsker å skrive yaml manuelt?", "maximum": "Maksimalt", + "minimum": "", "name": "Navn", "no_theme": "Ingen tema", "refresh_interval": "Oppdateringsintervall", @@ -2404,7 +2487,8 @@ "tap_action": "Trykk handling", "theme": "Tema", "title": "Tittel", - "unit": "Betegnelse" + "unit": "Betegnelse", + "url": "" }, "glance": { "columns": "Kolonner", @@ -2442,7 +2526,8 @@ }, "markdown": { "content": "Innhold", - "description": "Markdown-kortet brukes til å gjengi Markdown." + "description": "Markdown-kortet brukes til å gjengi Markdown.", + "name": "" }, "media-control": { "description": "Mediekontroll kortet brukes til å vise mediespillerenheter på et grensesnitt med brukervennlige kontroller.", @@ -2471,7 +2556,8 @@ "sensor": { "description": "Sensorkortet gir deg en rask oversikt over sensortilstanden din med en valgfri graf for å visualisere endring over tid.", "graph_detail": "Detaljer for graf", - "graph_type": "Graf type" + "graph_type": "Graf type", + "name": "" }, "shopping-list": { "description": "På handlelistekortet kan du legge til, redigere, sjekke av og fjerne gjenstander fra handlelisten din.", @@ -2493,7 +2579,11 @@ } }, "cardpicker": { + "by_card": "Med kort", + "by_entity": "Etter enhet", "custom_card": "Tilpasset", + "domain": "Domene", + "entity": "Entitet", "no_description": "Ingen beskrivelse tilgjengelig." }, "edit_card": { @@ -2507,6 +2597,7 @@ "options": "Flere alternativer", "pick_card": "Hvilket kort vil du legge til?", "pick_card_view_title": "Hvilket kort vil du legge til i {name} visningen?", + "search_cards": "Søk på kort", "show_code_editor": "Vis koderedigering", "show_visual_editor": "Vis visuell redigering", "toggle_editor": "Bytt redigering", @@ -2774,6 +2865,7 @@ "upstairs": "Oppe" }, "unit": { + "minutes_abbr": "", "watching": "Ser på" } } @@ -2827,7 +2919,9 @@ }, "current_user": "Du er logget inn som {fullName}.", "dashboard": { - "description": "Velg et standard instrumentbord for denne enheten." + "description": "Velg et standard instrumentbord for denne enheten.", + "dropdown_label": "", + "header": "" }, "force_narrow": { "description": "Dette vil skjule sidepanelet som standard, tilsvarende opplevelsen på en mobil.", diff --git a/translations/frontend/nl.json b/translations/frontend/nl.json index 8e3ce0ba7010..8e5aaece5da8 100644 --- a/translations/frontend/nl.json +++ b/translations/frontend/nl.json @@ -1023,6 +1023,7 @@ "delete": "Verwijderen", "delete_confirm": "Weet je zeker dat je dit item wilt verwijderen?", "duplicate": "Dupliceren", + "header": "", "introduction": "Triggers starten de verwerking van een automatiseringsregel. Het is mogelijk om meerdere triggers voor dezelfde regel op te geven. Zodra een trigger start, valideert Home Assistant de eventuele voorwaarden en roept hij de actie aan.", "learn_more": "Meer informatie over triggers", "name": "Trigger", @@ -1052,6 +1053,7 @@ }, "homeassistant": { "event": "Gebeurtenis:", + "label": "", "shutdown": "Afsluiten", "start": "Opstarten" }, diff --git a/translations/frontend/pl.json b/translations/frontend/pl.json index 901952d6c027..cb96ebd1633a 100644 --- a/translations/frontend/pl.json +++ b/translations/frontend/pl.json @@ -505,6 +505,7 @@ "back": "Wstecz", "cancel": "Anuluj", "close": "Zamknij", + "continue": "Kontynuuj", "delete": "Usuń", "error_required": "To pole jest wymagane", "loading": "Ładowanie", @@ -562,6 +563,8 @@ "no_history_found": "Nie znaleziono historii." }, "media-browser": { + "audio_not_supported": "Twoja przeglądarka nie obsługuje elementu audio.", + "choose_player": "Wybierz odtwarzacz", "choose-source": "Wybierz źródło", "content-type": { "album": "Album", @@ -570,12 +573,16 @@ "playlist": "Lista odtwarzania", "server": "Serwer" }, + "media_not_supported": "Przeglądarka odtwarzacza mediów nie obsługuje tego typu mediów", + "media_player": "Odtwarzacz mediów", "media-player-browser": "Przeglądarka odtwarzacza mediów", "no_items": "Brak elementów", "pick": "Wybierz", "pick-media": "Wybierz media", "play": "Odtwarzaj", - "play-media": "Odtwarzaj media" + "play-media": "Odtwarzaj media", + "video_not_supported": "Twoja przeglądarka nie obsługuje elementu wideo.", + "web-browser": "Przeglądarka internetowa" }, "picture-upload": { "label": "Obraz", @@ -689,8 +696,10 @@ "crop": "Przytnij" }, "more_info_control": { + "controls": "Sterowanie", "dismiss": "Zamknij okno dialogowe", "edit": "Edytuj encję", + "history": "Historia", "person": { "create_zone": "Utwórz strefę z bieżącej lokalizacji" }, @@ -1543,6 +1552,7 @@ "reload_restart_confirm": "Uruchom ponownie Home Assistanta, aby dokończyć ponowne wczytywanie tej integracji", "rename": "Zmień nazwę", "restart_confirm": "Zrestartuj Home Assistanta, aby zakończyć usuwanie tej integracji", + "services": "{count} {count, plural,\n one {usługa}\n few {usługi}\n many {usług}\n other {usług}\n}", "settings_button": "Edytuj ustawienia dla {integration}", "system_options": "Opcje systemowe", "system_options_button": "Opcje systemowe dla {integration}", @@ -1746,7 +1756,7 @@ "node_query_stages": { "associations": "Odświeżanie grup skojarzeń i członkostwa", "cacheload": "Ładowanie informacji z pliku pamięci podręcznej OpenZWave. Węzły baterii pozostaną na tym etapie, dopóki węzeł się nie wybudzi.", - "complete": "Proces wywiadu jest zakończony", + "complete": "Proces odpytywania jest zakończony", "configuration": "Pobieranie wartości konfiguracyjnych z węzła", "dynamic": "Pobieranie często zmieniających się wartości z węzła", "instances": "Pobieranie szczegółowych informacji o instancjach lub kanałach obsługiwanych przez urządzenie", @@ -1762,8 +1772,21 @@ "versions": "Pobieranie informacji o wersjach oprogramowania i klas poleceń", "wakeup": "Konfigurowanie obsługi kolejek wybudzania i wiadomości" }, + "node": { + "button": "Szczegóły węzła", + "not_found": "Nie znaleziono węzła" + }, + "nodes_table": { + "failed": "Uszkodzony", + "id": "Identyfikator", + "manufacturer": "Producent", + "model": "Model", + "query_stage": "Etap odpytywania", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { "battery_note": "Jeśli węzeł jest zasilany bateryjnie, przed kontynuowaniem należy go wybudzić", + "button": "Odśwież węzeł", "complete": "Odświeżanie węzła zakończone", "description": "Poinformuje to OpenZWave o konieczności ponownego odpytania węzła i zaktualizowaniu jego klas poleceń, możliwości i wartości.", "node_status": "Stan węzła", @@ -1923,12 +1946,17 @@ "input_text": "Pomocnicy typu tekst", "introduction": "Niektóre fragmenty konfiguracji można przeładować bez ponownego uruchamiania. Poniższe przyciski pozwalają na ponowne wczytanie danej części konfiguracji YAML.", "min_max": "Encje komponentu min/max", + "mqtt": "Encje komponentu MQTT", "person": "Osoby", "ping": "Encje komponentu ping", + "reload": "Domenę {domain}", "rest": "Encje komponentu rest", + "rpi_gpio": "Encje komponentu Raspberry Pi GPIO", "scene": "Sceny", "script": "Skrypty", + "smtp": "Usługi powiadomień komponentu SMTP", "statistics": "Encje komponentu statystyka", + "telegram": "Usługi powiadomień komponentu Telegram", "template": "Szablony encji", "trend": "Encje komponentu trend", "universal": "Encje komponentu uniwersalny odtwarzacz mediów", @@ -2549,7 +2577,11 @@ } }, "cardpicker": { + "by_card": "Według karty", + "by_entity": "Według encji", "custom_card": "Niestandardowa", + "domain": "Domena", + "entity": "Encja", "no_description": "Brak dostępnego opisu." }, "edit_card": { @@ -2563,6 +2595,7 @@ "options": "Więcej opcji", "pick_card": "Wybierz kartę, którą chcesz dodać.", "pick_card_view_title": "Którą kartę chcesz dodać do widoku {name}?", + "search_cards": "Szukaj kart", "show_code_editor": "Edytor kodu", "show_visual_editor": "Edytor wizualny", "toggle_editor": "Przełącz edytor", diff --git a/translations/frontend/pt-BR.json b/translations/frontend/pt-BR.json index 078f61a7fc10..8326daffff3f 100644 --- a/translations/frontend/pt-BR.json +++ b/translations/frontend/pt-BR.json @@ -1039,6 +1039,7 @@ }, "homeassistant": { "event": "Evento:", + "label": "", "shutdown": "Desligar", "start": "Iniciar" }, @@ -1664,6 +1665,7 @@ "start_listening": "Começar a ouvir", "stop_listening": "Parar de ouvir", "subscribe_to": "Evento para se inscrever", + "title": "", "topic": "tópico" }, "ozw": { @@ -2040,6 +2042,7 @@ }, "zwave": { "button": "Configurar", + "caption": "", "common": { "index": "Índice", "instance": "Instância", diff --git a/translations/frontend/pt.json b/translations/frontend/pt.json index f34d128cfd8c..2d315f637e9a 100644 --- a/translations/frontend/pt.json +++ b/translations/frontend/pt.json @@ -419,7 +419,7 @@ "unlock": "Desbloquear" }, "media_player": { - "browse_media": "Pesquisar mídia", + "browse_media": "Pesquisar media", "media_next_track": "Próximo", "media_play": "Reproduzir", "media_play_pause": "Reproduzir/pausar", @@ -505,6 +505,7 @@ "back": "Retroceder", "cancel": "Cancelar", "close": "Fechar", + "continue": "Continuar", "delete": "Apagar", "error_required": "Obrigatório", "loading": "A carregar", @@ -562,6 +563,8 @@ "no_history_found": "Nenhum histórico de estado encontrado." }, "media-browser": { + "audio_not_supported": "O seu navegador não suporta o elemento de áudio.", + "choose_player": "Escolha o Leitor", "choose-source": "Escolha a fonte", "content-type": { "album": "Álbum", @@ -570,12 +573,15 @@ "playlist": "Lista de reprodução", "server": "Servidor" }, + "media_player": "Leitor multimédia", "media-player-browser": "Navegador do Media Player", "no_items": "Sem itens", "pick": "Escolher", - "pick-media": "Escolha a mídia", + "pick-media": "Escolha a média", "play": "Reproduzir", - "play-media": "Reproduzir Mídia" + "play-media": "Reproduzir Média", + "video_not_supported": "O seu navegador não suporta o elemento de vídeo.", + "web-browser": "Navegador web" }, "picture-upload": { "label": "Imagem", @@ -600,7 +606,7 @@ "second": "{count} {count, plural,\n one {segundo}\n other {segundos}\n}", "week": "{count} {count, plural,\n one {semana}\n other {semanas}\n}" }, - "future": "À {time}", + "future": "Há {time}", "just_now": "Agora mesmo", "never": "Nunca", "past": "{time} atrás" @@ -690,6 +696,7 @@ "more_info_control": { "dismiss": "Descartar diálogo", "edit": "Editar entidade", + "history": "Histórico", "person": { "create_zone": "Criar zona a partir da localização atual" }, @@ -851,7 +858,7 @@ }, "automation": { "caption": "Automação", - "description": "Criar e editar automações", + "description": "Gerir Automações", "editor": { "actions": { "add": "Adicionar ação", @@ -1145,7 +1152,8 @@ "manage_entities": "Gerir Entidades", "state_reporting_error": "Indisponível para {enable_disable} reportar estado.", "sync_entities": "Sincronizar Entidades", - "sync_entities_error": "Falha na sincronização das entidades:" + "sync_entities_error": "Falha na sincronização das entidades:", + "title": "" }, "connected": "Ligado", "connection_status": "Estado da ligação na cloud", @@ -1163,7 +1171,8 @@ "manage_entities": "Gerir Entidades", "security_devices": "Dispositivos de segurança", "sync_entities": "Sincronizar entidades com o Google", - "sync_entities_404_message": "Falha ao sincronizar suas entidades com o Google, peça ao Google 'Ok Google, sincronize os meus dispositivos' para sincronizar suas entidades." + "sync_entities_404_message": "Falha ao sincronizar suas entidades com o Google, peça ao Google 'Ok Google, sincronize os meus dispositivos' para sincronizar suas entidades.", + "title": "" }, "integrations": "Integrações", "integrations_introduction": "As integrações para o Home Assistant Cloud permitem-lhe ligar-se aos serviços na nuvem sem ter de expor publicamente o seu Home Assistant na Internet.", @@ -1192,7 +1201,8 @@ "no_hooks_yet": "Parece que você ainda não tem webhooks. Comece, configurando um", "no_hooks_yet_link_automation": "automação de webhook", "no_hooks_yet_link_integration": "integração baseada em webhook", - "no_hooks_yet2": " ou criando um " + "no_hooks_yet2": " ou criando um ", + "title": "" } }, "alexa": { @@ -1226,6 +1236,7 @@ }, "forgot_password": { "check_your_email": "Verifique o seu e-mail para obter instruções sobre como redefinir a sua palavra-passe.", + "email": "", "email_error_msg": "E-mail inválido", "instructions": "Introduza o seu endereço de e-mail e nós lhe enviaremos um link para redefinir sua password.", "send_reset_email": "Enviar e-mail de redefinição", @@ -1245,6 +1256,7 @@ "alert_email_confirm_necessary": "É necessário confirmar o seu e-mail antes de fazer login.", "alert_password_change_required": "É necessário alterar a sua password antes de fazer login.", "dismiss": "Fechar", + "email": "", "email_error_msg": "E-mail inválido", "forgot_password": "Esqueceu-se da palavra-passe?", "introduction": "O Home Assistant Cloud fornece uma conexão remota segura à sua instância enquanto estiver fora de casa. Também permite que você se conecte com serviços que apenas utilizam a nuvem: Amazon Alexa e Google Assistant.", @@ -1357,7 +1369,7 @@ "caption": "Dispositivos", "confirm_delete": "Tem a certeza que quer apagar este dispositivo?", "confirm_rename_entity_ids": "Deseja também renomear os id's de entidade de suas entidades?", - "confirm_rename_entity_ids_warning": "Isso não mudará nenhuma configuração (como automações, scripts, cenas, Lovelace) que está usando essas entidades, você mesmo terá que atualizá-las.", + "confirm_rename_entity_ids_warning": "Tal não altera nenhuma configuração (como automações, scripts, cenas, Lovelace) que esteja a usar essas entidades, terá que atualizá-las por si.", "data_table": { "area": "Área", "battery": "Bateria", @@ -1482,6 +1494,7 @@ "description": "Ver informações sobre a instalação do Home Assistant", "developed_by": "Desenvolvido por um punhado de pessoas incríveis.", "documentation": "Documentação", + "frontend": "", "frontend_version": "Versão frontend: {version} - {type}", "home_assistant_logo": "Logotipo do Home Assistant", "icons_by": "Ícones por", @@ -1720,10 +1733,22 @@ }, "network": { "header": "Gestão de Rede", - "introduction": "Gerenciar funções em toda a rede.", + "introduction": "Gerir funções de rede.", "node_count": "{count} nós" }, + "node": { + "button": "Detalhes do nó", + "not_found": "Nó não encontrado" + }, + "nodes_table": { + "failed": "Falhou", + "id": "ID", + "manufacturer": "Fabricante", + "model": "Modelo", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { + "button": "Atualizar nó", "node_status": "Estado do Nó", "refreshing_description": "A atualizar as informações do nó ...", "start_refresh_button": "Iniciar atualização", @@ -1733,7 +1758,7 @@ }, "select_instance": { "header": "Selecione uma instância OpenZWave", - "introduction": "Você tem mais de uma instância OpenZWave em execução. Qual instância você gostaria de gerenciar?" + "introduction": "Tem mais do que uma instância Openzwave em execução. Que instância deseja gerir?" }, "services": { "add_node": "Adicionar nó", @@ -1880,9 +1905,11 @@ "input_text": "Recarregar input texts", "introduction": "Algumas partes do Home Assistant podem ser recarregadas sem a necessidade de reiniciar. Ao carregar em Recarregar a configuração irá descartar a configuração atual e carregar a nova.", "min_max": "Recarregar entidades Mín. / Máx.", + "mqtt": "Recarregar entidades mqtt", "person": "Recarregar pessoas", "ping": "Recarregar entidades de sensor binárias de ping", - "rest": "Recarregar entidades REST", + "reload": "Recarregar {domain}", + "rest": "Recarregar as restantes entidades e notificar serviços", "scene": "Recarregar cenas", "script": "Recarregar scripts", "statistics": "Recarregar entidades estatísticas", @@ -2011,7 +2038,7 @@ "clusters": { "header": "Clusters", "help_cluster_dropdown": "Selecione um cluster para visualizar atributos e comandos.", - "introduction": "Clusters são os blocos de construção para a funcionalidade Zigbee. Eles separam a funcionalidade em unidades lógicas. Existem tipos de cliente e servidor e que são compostos de atributos e comandos." + "introduction": "Os Clusters são os blocos de construção da funcionalidade Zigbee. Eles separam a funcionalidade em unidades lógicas. Existem do tipo cliente e servidor e são compostos de atributos e comandos." }, "common": { "add_devices": "Adicionar dispositivos", @@ -2505,7 +2532,11 @@ } }, "cardpicker": { + "by_card": "Pelo Cartão", + "by_entity": "Pela Entidade", "custom_card": "Personalizado", + "domain": "Domínio", + "entity": "Entidade", "no_description": "Não há descrição disponível." }, "edit_card": { @@ -2519,6 +2550,7 @@ "options": "Mais opções", "pick_card": "Que cartão gostaria de adicionar?", "pick_card_view_title": "Que cartão você gostaria de adicionar à sua vista {name}?", + "search_cards": "Procurar cartões", "show_code_editor": "Mostrar Editor de Código", "show_visual_editor": "Mostrar Editor Visual", "toggle_editor": "Alternar Editor", @@ -2606,7 +2638,7 @@ }, "menu": { "close": "Fechar", - "configure_ui": "Configurar UI", + "configure_ui": "Configurar Painel", "exit_edit_mode": "Sair do modo de edição do IU", "help": "Ajuda", "refresh": "Atualizar", diff --git a/translations/frontend/ru.json b/translations/frontend/ru.json index b1acc8ea5f13..8bc907ffb4e8 100644 --- a/translations/frontend/ru.json +++ b/translations/frontend/ru.json @@ -505,6 +505,7 @@ "back": "Назад", "cancel": "Отменить", "close": "Закрыть", + "continue": "Продолжить", "delete": "Удалить", "error_required": "Обязательное поле", "loading": "Загрузка", @@ -562,6 +563,7 @@ "no_history_found": "История не найдена." }, "media-browser": { + "choose_player": "Выберите медиаплеер", "choose-source": "Выбрать источник", "content-type": { "album": "Альбом", @@ -570,12 +572,14 @@ "playlist": "Плейлист", "server": "Сервер" }, + "media_player": "Медиаплеер", "media-player-browser": "Браузер медиаплеера", "no_items": "Нет элементов", "pick": "Выбрать", "pick-media": "Выбрать Медиа", "play": "Воспроизведение", - "play-media": "Воспроизведение Медиа" + "play-media": "Воспроизведение Медиа", + "web-browser": "Веб-браузер" }, "picture-upload": { "label": "Изображение", @@ -617,7 +621,7 @@ "update": "Обновить" }, "domain_toggler": { - "reset_entities": "Сбросить объекты", + "reset_entities": "Сбросить настройки доступа объектов", "title": "Переключить домены" }, "entity_registry": { @@ -689,8 +693,10 @@ "crop": "Обрезать" }, "more_info_control": { + "controls": "Управление", "dismiss": "Закрыть диалог", "edit": "Изменить объект", + "history": "История", "person": { "create_zone": "Создать зону из текущего местоположения" }, @@ -1201,15 +1207,19 @@ }, "alexa": { "banner": "Редактирование списка доступных объектов через пользовательский интерфейс отключено, так как Вы уже настроили фильтры в файле configuration.yaml.", + "dont_expose_entity": "Закрыть доступ", "expose": "Предоставить доступ", - "expose_entity": "Предоставить доступ к объекту", - "exposed_entities": "Объекты, к которым предоставлен доступ", + "expose_entity": "Открыть доступ", + "exposed": "Всего: {selected}", + "exposed_entities": "Объекты, к которым открыт доступ", + "follow_domain": "По домену", "manage_domains": "Управление доменами", - "not_exposed_entities": "Объекты, к которым не предоставлен доступ", + "not_exposed": "Всего: {selected}", + "not_exposed_entities": "Объекты, к которым закрыт доступ", "title": "Alexa" }, "caption": "Home Assistant Cloud", - "description_features": "Управление сервером вдали от дома, интеграция с Alexa и Google Assistant", + "description_features": "Удалённый доступ к серверу, интеграция с Alexa и Google Assistant", "description_login": "{email}", "description_not_login": "Вход не выполнен", "dialog_certificate": { @@ -1242,9 +1252,15 @@ "google": { "banner": "Редактирование списка доступных объектов через пользовательский интерфейс отключено, так как Вы уже настроили фильтры в файле configuration.yaml.", "disable_2FA": "Отключить двухфакторную аутентификацию", + "dont_expose_entity": "Закрыть доступ", "expose": "Предоставить доступ", - "exposed_entities": "Объекты, к которым предоставлен доступ", - "not_exposed_entities": "Объекты, к которым не предоставлен доступ", + "expose_entity": "Открыть доступ", + "exposed": "Всего: {selected}", + "exposed_entities": "Объекты, к которым открыт доступ", + "follow_domain": "По домену", + "manage_domains": "Управление доменами", + "not_exposed": "Всего: {selected}", + "not_exposed_entities": "Объекты, к которым закрыт доступ", "sync_to_google": "Синхронизация изменений с Google.", "title": "Google Assistant" }, @@ -1255,7 +1271,7 @@ "email": "Адрес электронной почты", "email_error_msg": "Неверный адрес электронной почты.", "forgot_password": "забыли пароль?", - "introduction": "Home Assistant Cloud обеспечивает безопасный доступ к Вашему серверу, даже если Вы находитесь вдали от дома. Также это даёт возможность подключения к функциям облачных сервисов Amazon Alexa и Google Assistant.", + "introduction": "Home Assistant Cloud обеспечивает безопасный доступ к Вашему серверу, даже если Вы находитесь вдали от дома. Также это даёт возможность простого подключения к функциям облачных сервисов Amazon Alexa и Google Assistant.", "introduction2": "Услуга предоставляется нашим партнером ", "introduction2a": ", компанией от основателей Home Assistant и Hass.io.", "introduction3": "Home Assistant Cloud предлагает одноразовый бесплатный пробный период продолжительностью один месяц. Для активации пробного периода платёжная информация не требуется.", @@ -1402,7 +1418,7 @@ }, "scripts": "Сценарии", "unknown_error": "Неизвестная ошибка.", - "unnamed_device": "Безымянное устройство", + "unnamed_device": "Устройство без названия", "update": "Обновить" }, "entities": { @@ -1533,6 +1549,7 @@ "reload_restart_confirm": "Перезапустите Home Assistant, чтобы завершить перезагрузку этой интеграции", "rename": "Переименовать", "restart_confirm": "Перезапустите Home Assistant, чтобы завершить удаление этой интеграции", + "services": "{count} {count, plural,\n one {служба}\n other {служб}\n}", "settings_button": "Настройки интеграции {integration}", "system_options": "Настройки интеграции", "system_options_button": "Системные параметры интеграции {integration}", @@ -1579,7 +1596,7 @@ "none_found_detail": "Измените критерии поиска", "note_about_integrations": "Пока что не все интеграции могут быть настроены через пользовательский интерфейс.", "note_about_website_reference": "Все доступные интеграции Вы можете найти на ", - "rename_dialog": "Изменение названия интеграции", + "rename_dialog": "Название интеграции", "rename_input_label": "Название", "search": "Поиск интеграций" }, @@ -1752,8 +1769,21 @@ "versions": "Получение информации о версиях прошивки и классов команд", "wakeup": "Настройка поддержки очередей пробуждения и сообщений" }, + "node": { + "button": "Подробности об узле", + "not_found": "Узел не найден" + }, + "nodes_table": { + "failed": "Сбой", + "id": "ID", + "manufacturer": "Производитель", + "model": "Модель", + "query_stage": "Стадия запроса", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { "battery_note": "Если узел работает от батареи, обязательно разбудите его, прежде чем продолжить", + "button": "Обновить узел", "complete": "Обновление узла завершено", "description": "Повторный опрос узла и обновление классов команд, возможностей и значений узла.", "node_status": "Статус узла", @@ -1902,7 +1932,7 @@ "filter": "Перезагрузить объекты интеграции \"Filter\"", "generic": "Перезагрузить объекты интеграции \"Generic IP Camera\"", "generic_thermostat": "Перезагрузить объекты интеграции \"Generic Thermostat\"", - "group": "Перезагрузить группы", + "group": "Перезагрузить группы, объекты групп и службы уведомлений", "heading": "Перезагрузка конфигурации YAML", "history_stats": "Перезагрузить объекты интеграции \"History Stats\"", "homekit": "Перезагрузить HomeKit", @@ -1913,12 +1943,17 @@ "input_text": "Перезагрузить вспомогательные элементы ввода текста", "introduction": "Некоторые компоненты Home Assistant можно перезагрузить без необходимости перезапуска всей системы. Перезагрузка выгружает текущую конфигурацию YAML и загружает новую.", "min_max": "Перезагрузить объекты интеграции \"Min/Max\"", + "mqtt": "Перезагрузить объекты интеграции \"MQTT\"", "person": "Перезагрузить персоны", "ping": "Перезагрузить объекты интеграции \"Ping (ICMP)\"", - "rest": "Перезагрузить объекты интеграции \"REST\"", + "reload": "Перезагрузить {domain}", + "rest": "Перезагрузить объекты и службы уведомлений интеграции \"REST\"", + "rpi_gpio": "Перезагрузить объекты интеграции \"Raspberry Pi GPIO\"", "scene": "Перезагрузить сцены", "script": "Перезагрузить сценарии", + "smtp": "Перезагрузить службы уведомлений SMTP", "statistics": "Перезагрузить объекты интеграции \"Statistics\"", + "telegram": "Перезагрузить службы уведомлений Telegram", "template": "Перезагрузить объекты шаблонов", "trend": "Перезагрузить объекты интеграции \"Trend\"", "universal": "Перезагрузить объекты интеграции \"Universal Media Player\"", @@ -2007,7 +2042,7 @@ "system": "Системный" } }, - "users_privileges_note": "Группа пользователей находится в стадии разработки. В дальнейшем пользователи не смогут администрировать сервер через пользовательский интерфейс. Мы все еще проверяем все конечные точки API управления, чтобы убедиться, что они правильно ограничивают доступ." + "users_privileges_note": "Функционал пользователей всё ещё в стадии разработки. В дальнейшем пользователи не смогут администрировать сервер через пользовательский интерфейс. Мы все еще проверяем все конечные точки API управления, чтобы убедиться, что они правильно ограничивают доступ." }, "zha": { "add_device_page": { @@ -2539,7 +2574,11 @@ } }, "cardpicker": { + "by_card": "Карточки", + "by_entity": "Объекты", "custom_card": "Custom", + "domain": "Домен", + "entity": "Объект", "no_description": "Описание недоступно." }, "edit_card": { @@ -2553,6 +2592,7 @@ "options": "Больше параметров", "pick_card": "Какую карточку Вы хотели бы добавить?", "pick_card_view_title": "Какую карточку Вы хотели бы добавить на вкладку {name}?", + "search_cards": "Поиск карточек", "show_code_editor": "Текстовый редактор", "show_visual_editor": "Форма ввода", "toggle_editor": "Переключить редактор", @@ -2838,7 +2878,7 @@ }, "integration": { "finish": "Готово", - "intro": "Устройства и сервисы представлены в Home Assistant как интеграции. Вы можете добавить их сейчас или сделать это позже в разделе настроек.", + "intro": "Устройства и службы представлены в Home Assistant как интеграции. Вы можете добавить их сейчас или сделать это позже в разделе настроек.", "more_integrations": "Ещё" }, "intro": "Готовы ли Вы разбудить свой дом, вернуть свою конфиденциальность и присоединиться к всемирному сообществу?", diff --git a/translations/frontend/sk.json b/translations/frontend/sk.json index 6e865645c754..9e3bacaab7c5 100644 --- a/translations/frontend/sk.json +++ b/translations/frontend/sk.json @@ -484,6 +484,7 @@ "back": "Späť", "cancel": "Zrušiť", "close": "Zavrieť", + "continue": "Pokračovať", "delete": "Odstrániť", "loading": "Načítava sa", "next": "Ďalej", @@ -533,6 +534,11 @@ "loading_history": "Načítavam históriu stavov", "no_history_found": "Nenašla sa žiadna história stavov" }, + "media-browser": { + "choose_player": "Vyberte prehrávač", + "media_player": "Prehrávač médií", + "web-browser": "Webový prehliadač" + }, "related-items": { "area": "Oblasť", "automation": "Súčasťou nasledujúcich automatizácií", @@ -635,6 +641,7 @@ "more_info_control": { "dismiss": "Zrušiť dialógové okno", "edit": "Upraviť entitu", + "history": "História", "person": { "create_zone": "Vytvoriť zónu z aktuálnej polohy" }, @@ -1534,6 +1541,11 @@ "title": "MQTT", "topic": "téma" }, + "ozw": { + "nodes_table": { + "id": "ID" + } + }, "person": { "add_person": "Pridať osobu", "caption": "Osoby", @@ -1683,6 +1695,8 @@ "create": "Vytvoriť", "name": "Meno", "password": "Heslo", + "password_confirm": "Potvrdiť heslo", + "password_not_match": "Heslá sa nezhodujú", "username": "Užívateľské meno" }, "caption": "Používatelia", @@ -1699,7 +1713,9 @@ "group": "Skupina", "id": "ID", "name": "Názov", + "new_password": "Nové heslo", "owner": "Vlastník", + "password_changed": "Heslo je zmenené!", "system_generated": "Systémom vytvorený", "system_generated_users_not_editable": "Nie je možné aktualizovať používateľov generovaných systémom.", "system_generated_users_not_removable": "Nie je možné odstrániť používateľov generovaných systémom.", @@ -1872,6 +1888,10 @@ "set_wakeup": "Nastaviť interval prebudenia", "true": "True" }, + "node_management": { + "add_to_group": "Pridať do skupiny", + "remove_from_group": "Odstrániť zo skupiny" + }, "ozw_log": { "introduction": "Zobraziť denník. 0 je minimum (načíta celý protokol) a 1000 je maximum. Načítanie zobrazí statický protokol a posledný riadok sa automaticky aktualizuje s určeným počtom riadkov protokolu." }, @@ -2176,6 +2196,7 @@ "options": "Viac možností", "pick_card": "Ktorú kartu chcete pridať?", "pick_card_view_title": "Ktorú kartu chcete pridať do svojho zobrazenia {name} ?", + "search_cards": "Vyhľadať karty", "show_code_editor": "Zobraziť editor kódu", "show_visual_editor": "Zobraziť vizuálny editor", "toggle_editor": "Prepnúť editor" diff --git a/translations/frontend/sr-Latn.json b/translations/frontend/sr-Latn.json index 7f497faa5b40..11f7af24b54d 100644 --- a/translations/frontend/sr-Latn.json +++ b/translations/frontend/sr-Latn.json @@ -8,7 +8,85 @@ "shopping_list": "Lista za kupovinu", "states": "Pregled" }, + "state_badge": { + "alarm_control_panel": { + "armed_away": "", + "armed_custom_bypass": "", + "armed_night": "" + }, + "device_tracker": { + "home": "", + "not_home": "" + } + }, "state": { + "automation": { + "off": "", + "on": "" + }, + "binary_sensor": { + "default": { + "off": "", + "on": "" + }, + "motion": { + "on": "" + }, + "occupancy": { + "off": "" + }, + "opening": { + "on": "" + }, + "presence": { + "off": "", + "on": "" + }, + "vibration": { + "off": "", + "on": "" + } + }, + "calendar": { + "off": "", + "on": "" + }, + "fan": { + "off": "", + "on": "" + }, + "group": { + "home": "", + "not_home": "", + "off": "", + "on": "", + "problem": "" + }, + "input_boolean": { + "on": "" + }, + "light": { + "off": "", + "on": "" + }, + "media_player": { + "off": "", + "on": "" + }, + "remote": { + "on": "" + }, + "script": { + "off": "", + "on": "" + }, + "sensor": { + "off": "", + "on": "" + }, + "switch": { + "on": "" + }, "weather": { "clear-night": "Vedra noć", "cloudy": "Oblačno", @@ -73,6 +151,13 @@ "config": { "automation": { "editor": { + "conditions": { + "type": { + "zone": { + "entity": "" + } + } + }, "triggers": { "type": { "mqtt": { @@ -107,6 +192,45 @@ "set_config_parameter": "Подесите параметар Цонфиг" } } + }, + "page-authorize": { + "form": { + "providers": { + "command_line": { + "abort": { + "login_expired": "" + }, + "error": { + "invalid_auth": "", + "invalid_code": "" + }, + "step": { + "init": { + "data": { + "password": "", + "username": "" + } + }, + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + }, + "legacy_api_password": { + "step": { + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + } + } + } } } } diff --git a/translations/frontend/sr.json b/translations/frontend/sr.json index 2a32b3aca6ce..b5debac397a0 100644 --- a/translations/frontend/sr.json +++ b/translations/frontend/sr.json @@ -11,21 +11,93 @@ }, "state_badge": { "alarm_control_panel": { + "armed_away": "", + "armed_custom_bypass": "", + "armed_night": "", "arming": "Aktiviranje", "disarming": "Deaktiviraj" }, "default": { "entity_not_found": "Вредност није пронађена", "error": "Грешка" + }, + "device_tracker": { + "home": "", + "not_home": "" } }, "state": { + "automation": { + "off": "", + "on": "" + }, + "binary_sensor": { + "default": { + "off": "", + "on": "" + }, + "motion": { + "on": "" + }, + "occupancy": { + "off": "" + }, + "opening": { + "on": "" + }, + "presence": { + "off": "", + "on": "" + }, + "vibration": { + "off": "", + "on": "" + } + }, + "calendar": { + "off": "", + "on": "" + }, + "fan": { + "off": "", + "on": "" + }, + "group": { + "home": "", + "not_home": "", + "off": "", + "on": "", + "problem": "" + }, + "input_boolean": { + "on": "" + }, + "light": { + "off": "", + "on": "" + }, + "media_player": { + "off": "", + "on": "" + }, + "remote": { + "on": "" + }, + "script": { + "off": "", + "on": "" + }, + "sensor": { + "off": "", + "on": "" + }, "sun": { "above_horizon": "Iznad horizonta", "below_horizon": "Ispod horizonta" }, "switch": { - "off": "Isključen" + "off": "Isključen", + "on": "" }, "timer": { "active": "укључен", @@ -56,6 +128,13 @@ "config": { "automation": { "editor": { + "conditions": { + "type": { + "zone": { + "entity": "" + } + } + }, "triggers": { "learn_more": "Сазнајте више о окидачима", "type": { @@ -113,6 +192,45 @@ } } }, + "page-authorize": { + "form": { + "providers": { + "command_line": { + "abort": { + "login_expired": "" + }, + "error": { + "invalid_auth": "", + "invalid_code": "" + }, + "step": { + "init": { + "data": { + "password": "", + "username": "" + } + }, + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + }, + "legacy_api_password": { + "step": { + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + } + } + } + }, "page-onboarding": { "integration": { "finish": "Крај" diff --git a/translations/frontend/sv.json b/translations/frontend/sv.json index f684b5109831..cf1615ecc187 100644 --- a/translations/frontend/sv.json +++ b/translations/frontend/sv.json @@ -1145,7 +1145,8 @@ "manage_entities": "Hantera Entiteter", "state_reporting_error": "Det går inte att {enable_disable} rapportera tillståndet.", "sync_entities": "Synkronisera Entiteter", - "sync_entities_error": "Det gick inte att synkronisera entiteter:" + "sync_entities_error": "Det gick inte att synkronisera entiteter:", + "title": "" }, "connected": "Ansluten", "connection_status": "Status för molnanslutning", @@ -1193,14 +1194,16 @@ "no_hooks_yet": "Det verkar som du inte har några webhooks ännu. Kom igång genom att konfigurera en ", "no_hooks_yet_link_automation": "webhook automation", "no_hooks_yet_link_integration": "webhook-baserad integration", - "no_hooks_yet2": " eller genom att skapa en " + "no_hooks_yet2": " eller genom att skapa en ", + "title": "" } }, "alexa": { "banner": "Redigering av vilka entiteter som visas via det här användargränssnittet är inaktiverat eftersom du har konfigurerat entitetsfilter i configuration.yaml.", "expose": "Exponera för Alexa", "exposed_entities": "Exponerade entiteter", - "not_exposed_entities": "Ej exponerade entiteter" + "not_exposed_entities": "Ej exponerade entiteter", + "title": "" }, "caption": "Home Assistant Cloud", "description_features": "Styra även när du inte är hemma, integrera med Alexa och Google Assistant.", @@ -2375,6 +2378,8 @@ "image": "Bildsökväg", "manual": "Manuell", "manual_description": "Behöver du lägga till ett anpassat kort eller vill du bara skriva yaml manuellt?", + "maximum": "", + "minimum": "", "name": "Namn", "no_theme": "Inget tema", "refresh_interval": "Uppdateringsintervall", diff --git a/translations/frontend/ta.json b/translations/frontend/ta.json index 7445b2d66a36..9043c84c3048 100644 --- a/translations/frontend/ta.json +++ b/translations/frontend/ta.json @@ -35,7 +35,8 @@ "unknown": "தெரியாத" }, "device_tracker": { - "home": "முகப்பு" + "home": "முகப்பு", + "not_home": "" }, "person": { "home": "முகப்பு" @@ -239,6 +240,13 @@ "config": { "automation": { "editor": { + "conditions": { + "type": { + "zone": { + "entity": "" + } + } + }, "triggers": { "type": { "mqtt": { @@ -257,6 +265,45 @@ "set_config_parameter": "கட்டமைப்பு அளவுருவை அமைக்கவும்" } } + }, + "page-authorize": { + "form": { + "providers": { + "command_line": { + "abort": { + "login_expired": "" + }, + "error": { + "invalid_auth": "", + "invalid_code": "" + }, + "step": { + "init": { + "data": { + "password": "", + "username": "" + } + }, + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + }, + "legacy_api_password": { + "step": { + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + } + } + } } } } diff --git a/translations/frontend/te.json b/translations/frontend/te.json index 1bddd1e80bc7..50cf3c096941 100644 --- a/translations/frontend/te.json +++ b/translations/frontend/te.json @@ -37,7 +37,8 @@ "unknown": "తెలియదు" }, "device_tracker": { - "home": "ఇంట" + "home": "ఇంట", + "not_home": "" }, "person": { "home": "ఇంట" @@ -410,6 +411,9 @@ "time": { "after": "తరువాత", "before": "ముందు" + }, + "zone": { + "entity": "" } } }, @@ -531,7 +535,8 @@ "login_expired": "సెషన్ గడువు ముగిసింది, మళ్ళీ లాగిన్ అవ్వండి." }, "error": { - "invalid_auth": "తప్పు యూజర్ పేరు లేదా తప్పు పాస్ వర్డ్" + "invalid_auth": "తప్పు యూజర్ పేరు లేదా తప్పు పాస్ వర్డ్", + "invalid_code": "" }, "step": { "init": { @@ -539,6 +544,12 @@ "password": "పాస్వర్డ్", "username": "యూజర్ పేరు" } + }, + "mfa": { + "data": { + "code": "" + }, + "description": "" } } }, @@ -558,6 +569,16 @@ } } }, + "legacy_api_password": { + "step": { + "mfa": { + "data": { + "code": "" + }, + "description": "" + } + } + }, "trusted_networks": { "abort": { "not_whitelisted": "మీ కంప్యూటర్ అనుమతి జాబితాలో లేదు." diff --git a/translations/frontend/tr.json b/translations/frontend/tr.json index 13376adc8027..f0e477f212b4 100644 --- a/translations/frontend/tr.json +++ b/translations/frontend/tr.json @@ -2434,7 +2434,8 @@ "mfa": { "data": { "code": "İki adımlı kimlik doğrulama kodu" - } + }, + "description": "" } } }, @@ -2479,7 +2480,8 @@ "mfa": { "data": { "code": "İki adımlı kimlik doğrulama kodu" - } + }, + "description": "" } } }, diff --git a/translations/frontend/ur.json b/translations/frontend/ur.json index c130104798c5..2f7a214405aa 100644 --- a/translations/frontend/ur.json +++ b/translations/frontend/ur.json @@ -1,4 +1,49 @@ { + "state_badge": { + "device_tracker": { + "home": "" + } + }, + "state": { + "automation": { + "off": "" + }, + "binary_sensor": { + "default": { + "on": "" + }, + "presence": { + "on": "" + } + }, + "calendar": { + "on": "" + }, + "group": { + "home": "", + "off": "", + "on": "" + }, + "input_boolean": { + "on": "" + }, + "light": { + "off": "", + "on": "" + }, + "media_player": { + "off": "" + }, + "script": { + "off": "" + }, + "sensor": { + "off": "" + }, + "switch": { + "on": "" + } + }, "ui": { "card": { "fan": { @@ -39,6 +84,13 @@ "label": "آلہ" } } + }, + "triggers": { + "type": { + "mqtt": { + "label": "" + } + } } } }, diff --git a/translations/frontend/zh-Hans.json b/translations/frontend/zh-Hans.json index a318db04426d..525efe5654b5 100644 --- a/translations/frontend/zh-Hans.json +++ b/translations/frontend/zh-Hans.json @@ -505,6 +505,7 @@ "back": "返回", "cancel": "取消", "close": "关闭", + "continue": "继续", "delete": "删除", "error_required": "必填", "loading": "加载中", @@ -562,6 +563,8 @@ "no_history_found": "没有找到历史状态。" }, "media-browser": { + "audio_not_supported": "您的浏览器不支持音频元素。", + "choose_player": "选择播放器", "choose-source": "选择媒体源", "content-type": { "album": "专辑", @@ -570,12 +573,16 @@ "playlist": "播放列表", "server": "服务器" }, + "media_not_supported": "浏览器媒体播放器不支持此类型的媒体", + "media_player": "媒体播放器", "media-player-browser": "媒体播放浏览器", "no_items": "没有项目", "pick": "选定", "pick-media": "选定媒体", "play": "播放", - "play-media": "播放媒体" + "play-media": "播放媒体", + "video_not_supported": "您的浏览器不支持视频元素。", + "web-browser": "网页浏览器" }, "picture-upload": { "label": "图片", @@ -689,8 +696,10 @@ "crop": "剪裁" }, "more_info_control": { + "controls": "控制项", "dismiss": "关闭对话框", "edit": "编辑实体", + "history": "历史", "person": { "create_zone": "从当前位置创建地点" }, @@ -1762,8 +1771,21 @@ "versions": "正在获取固件和命令类版本信息", "wakeup": "正在设置对唤醒队列和消息的支持" }, + "node": { + "button": "节点详细信息", + "not_found": "节点未找到" + }, + "nodes_table": { + "failed": "故障", + "id": "ID", + "manufacturer": "制造商", + "model": "型号", + "query_stage": "查询阶段", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { "battery_note": "如果节点由电池供电,请确保在继续操作之前将其唤醒", + "button": "刷新节点", "complete": "节点刷新完成", "description": "这将通知 OpenZWave 重新访问节点并更新节点的命令类、功能和值。", "node_status": "节点状态", @@ -1912,7 +1934,7 @@ "filter": "重载 filter 实体", "generic": "重载通用 IP 摄像机实体", "generic_thermostat": "重载通用恒温器实体", - "group": "重载分组", + "group": "重载分组、分组实体及通知服务", "heading": "配置重载", "history_stats": "重载历史记录统计实体", "homekit": "重载 HomeKit", @@ -1923,12 +1945,17 @@ "input_text": "重载文字输入", "introduction": "Home Assistant 中的部分配置可以直接重载,而无需重启服务。点击重载按钮将重新载入新的配置。", "min_max": "重载最小值/最大值实体", + "mqtt": "重载 mqtt 实体", "person": "重载人员", "ping": "重载 ping 二元传感器实体", - "rest": "重载 REST 实体", + "reload": "重载{domain}", + "rest": "重载 REST 实体及通知服务", + "rpi_gpio": "重载树莓派 GPIO 实体", "scene": "重载场景", "script": "重载脚本", + "smtp": "重载 smtp 通知服务", "statistics": "重载 statistics 实体", + "telegram": "重载 telegram 通知服务", "template": "重载模板实体", "trend": "重载 trend 实体", "universal": "重载通用媒体播放器实体", @@ -2549,7 +2576,11 @@ } }, "cardpicker": { + "by_card": "按卡片", + "by_entity": "按实体", "custom_card": "自定义", + "domain": "域", + "entity": "实体", "no_description": "没有描述。" }, "edit_card": { @@ -2563,6 +2594,7 @@ "options": "更多选项", "pick_card": "请选择要添加的卡片。", "pick_card_view_title": "您想将哪张卡片添加到 {name} 视图?", + "search_cards": "搜索卡片", "show_code_editor": "显示代码编辑器", "show_visual_editor": "显示可视化编辑器", "toggle_editor": "切换编辑器", diff --git a/translations/frontend/zh-Hant.json b/translations/frontend/zh-Hant.json index 78b42e133e1a..30f780c63914 100644 --- a/translations/frontend/zh-Hant.json +++ b/translations/frontend/zh-Hant.json @@ -19,6 +19,7 @@ "logbook": "日誌", "mailbox": "郵箱", "map": "地圖", + "media_browser": "媒體瀏覽器", "profile": "個人設定", "shopping_list": "購物清單", "states": "總覽" @@ -505,6 +506,7 @@ "back": "上一步", "cancel": "取消", "close": "關閉", + "continue": "繼續", "delete": "刪除", "error_required": "必填", "loading": "讀取中", @@ -562,6 +564,8 @@ "no_history_found": "找不到狀態歷史。" }, "media-browser": { + "audio_not_supported": "瀏覽器不支援音效元件。", + "choose_player": "選擇播放器", "choose-source": "選擇來源", "content-type": { "album": "專輯", @@ -570,12 +574,17 @@ "playlist": "播放列表", "server": "伺服器" }, + "media_browsing_error": "媒體瀏覽錯誤", + "media_not_supported": "瀏覽器媒體播放器不支援此類型媒體", + "media_player": "媒體播放器", "media-player-browser": "媒體播放器瀏覽器", "no_items": "沒有項目", "pick": "選擇", "pick-media": "選擇媒體", "play": "播放", - "play-media": "播放媒體" + "play-media": "播放媒體", + "video_not_supported": "瀏覽器不支援影片元件。", + "web-browser": "網頁瀏覽器" }, "picture-upload": { "label": "照片", @@ -689,8 +698,10 @@ "crop": "裁切" }, "more_info_control": { + "controls": "控制", "dismiss": "忽略對話", "edit": "編輯實體", + "history": "歷史", "person": { "create_zone": "使用目前位置新增區域" }, @@ -1543,6 +1554,7 @@ "reload_restart_confirm": "重啟 Home Assistant 以為重整合重新載入", "rename": "重新命名", "restart_confirm": "重啟 Home Assistant 以完成此整合移動", + "services": "{count} {count, plural,\n one {項服務}\n other {項服務}\n}", "settings_button": "編輯 {integration} 設定", "system_options": "系統選項", "system_options_button": "{integration} 系統選項", @@ -1762,8 +1774,21 @@ "versions": "獲得韌體與命令 Class 版本資訊", "wakeup": "設定喚醒序列與訊息之支援" }, + "node": { + "button": "節點詳細資訊", + "not_found": "找不到節點" + }, + "nodes_table": { + "failed": "失敗", + "id": "ID", + "manufacturer": "廠牌", + "model": "型號", + "query_stage": "查詢階段", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { "battery_note": "假如節點為電池供電、請確定先行喚醒以繼續", + "button": "更新節點", "complete": "節點更新完成", "description": "將會通知 OpenZWave 重新探訪節點並更新節點命令 Class、相容性與數值。", "node_status": "節點狀態", @@ -1912,7 +1937,7 @@ "filter": "重新載入過濾器實體", "generic": "重新載入通用 IP 攝影機實體", "generic_thermostat": "重新載入通用溫控器實體", - "group": "重新載入群組", + "group": "重新載入群組、群組實體及通知服務", "heading": "YAML 設定新載入中", "history_stats": "重新載入歷史狀態實體", "homekit": "重新載入 Homekit", @@ -1923,12 +1948,17 @@ "input_text": "重新載入輸入文字", "introduction": "Home Assistant 中部分設定無須重啟即可重新載入生效。點選重新載入按鈕,即可解除目前 YAML 設定,並重新載入最新設定。", "min_max": "重新載入最低/最高實體", + "mqtt": "重新載入 MQTT 實體", "person": "重新載入人員", "ping": "重新載入 Pung 二進位傳感器實體", - "rest": "重新載入剩餘實體", + "reload": "重新載入{domain}", + "rest": "重新載入剩餘實體及通知服務", + "rpi_gpio": "重新載入 Raspberry Pi GPIO 實體", "scene": "重新載入場景", "script": "重新載入腳本", + "smtp": "重新載入 SMTP 通知服務", "statistics": "重新載入統計資訊實體", + "telegram": "重新載入 Telegram 通知服務", "template": "重新載入範例實體", "trend": "重新載入趨勢實體", "universal": "重新載入通用媒體播放器實體", @@ -2017,7 +2047,7 @@ "system": "系統" } }, - "users_privileges_note": "使用者群組功能進行中。將無法透過 UI 進行使用者管理,仍在檢視所有管理 API Endpoint 以確保能夠正確符合管理員存取需求。" + "users_privileges_note": "使用者群組功能仍在開發中。將無法透過 UI 進行使用者管理,仍在檢視所有管理 API Endpoint 以確保能夠正確符合管理員存取需求。" }, "zha": { "add_device_page": { @@ -2549,7 +2579,11 @@ } }, "cardpicker": { + "by_card": "以面板", + "by_entity": "以實體", "custom_card": "自訂面板", + "domain": "區域", + "entity": "實體", "no_description": "無描述可使用。" }, "edit_card": { @@ -2563,6 +2597,7 @@ "options": "更多選項", "pick_card": "選擇所要新增的面板?", "pick_card_view_title": "要加入 {name} 視圖的面板?", + "search_cards": "搜尋面板", "show_code_editor": "顯示編碼編輯器", "show_visual_editor": "顯示視覺編輯器", "toggle_editor": "切換編輯器", diff --git a/yarn.lock b/yarn.lock index 8b8451ef9c8f..4a0450044e52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2710,6 +2710,11 @@ dependencies: "@types/node" "*" +"@types/sortablejs@^1.10.6": + version "1.10.6" + resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.10.6.tgz#98725ae08f1dfe28b8da0fdf302c417f5ff043c0" + integrity sha512-QRz8Z+uw2Y4Gwrtxw8hD782zzuxxugdcq8X/FkPsXUa1kfslhGzy13+4HugO9FXNo+jlWVcE6DYmmegniIQ30A== + "@types/tern@*": version "0.23.3" resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460"