diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..f03f0dcbd0 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,60 @@ +name: "CodeQL" + +on: + push: + branches: [dev, master] + pull_request: + # The branches below must be a subset of the branches above + branches: [dev] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['javascript'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/gallery/src/components/demo-more-info.js b/gallery/src/components/demo-more-info.js index 9def4e6095..9e4a34023f 100644 --- a/gallery/src/components/demo-more-info.js +++ b/gallery/src/components/demo-more-info.js @@ -2,8 +2,8 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; /* eslint-plugin-disable lit */ import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../../../src/components/ha-card"; -import "../../../src/dialogs/more-info/more-info-content"; import "../../../src/state-summary/state-card-content"; +import "./more-info-content"; class DemoMoreInfo extends PolymerElement { static get template() { diff --git a/gallery/src/components/more-info-content.ts b/gallery/src/components/more-info-content.ts new file mode 100644 index 0000000000..6d7271155e --- /dev/null +++ b/gallery/src/components/more-info-content.ts @@ -0,0 +1,73 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { property, PropertyValues, UpdatingElement } from "lit-element"; +import dynamicContentUpdater from "../../../src/common/dom/dynamic_content_updater"; +import { stateMoreInfoType } from "../../../src/common/entity/state_more_info_type"; +import "../../../src/dialogs/more-info/controls/more-info-alarm_control_panel"; +import "../../../src/dialogs/more-info/controls/more-info-automation"; +import "../../../src/dialogs/more-info/controls/more-info-camera"; +import "../../../src/dialogs/more-info/controls/more-info-climate"; +import "../../../src/dialogs/more-info/controls/more-info-configurator"; +import "../../../src/dialogs/more-info/controls/more-info-counter"; +import "../../../src/dialogs/more-info/controls/more-info-cover"; +import "../../../src/dialogs/more-info/controls/more-info-default"; +import "../../../src/dialogs/more-info/controls/more-info-fan"; +import "../../../src/dialogs/more-info/controls/more-info-group"; +import "../../../src/dialogs/more-info/controls/more-info-humidifier"; +import "../../../src/dialogs/more-info/controls/more-info-input_datetime"; +import "../../../src/dialogs/more-info/controls/more-info-light"; +import "../../../src/dialogs/more-info/controls/more-info-lock"; +import "../../../src/dialogs/more-info/controls/more-info-media_player"; +import "../../../src/dialogs/more-info/controls/more-info-person"; +import "../../../src/dialogs/more-info/controls/more-info-script"; +import "../../../src/dialogs/more-info/controls/more-info-sun"; +import "../../../src/dialogs/more-info/controls/more-info-timer"; +import "../../../src/dialogs/more-info/controls/more-info-vacuum"; +import "../../../src/dialogs/more-info/controls/more-info-water_heater"; +import "../../../src/dialogs/more-info/controls/more-info-weather"; +import { HomeAssistant } from "../../../src/types"; + +class MoreInfoContent extends UpdatingElement { + @property({ attribute: false }) public hass?: HomeAssistant; + + @property() public stateObj?: HassEntity; + + private _detachedChild?: ChildNode; + + protected firstUpdated(): void { + this.style.position = "relative"; + this.style.display = "block"; + } + + // This is not a lit element, but an updating element, so we implement update + protected update(changedProps: PropertyValues): void { + super.update(changedProps); + const stateObj = this.stateObj; + const hass = this.hass; + + if (!stateObj || !hass) { + if (this.lastChild) { + this._detachedChild = this.lastChild; + // Detach child to prevent it from doing work. + this.removeChild(this.lastChild); + } + return; + } + + if (this._detachedChild) { + this.appendChild(this._detachedChild); + this._detachedChild = undefined; + } + + const moreInfoType = + stateObj.attributes && "custom_ui_more_info" in stateObj.attributes + ? stateObj.attributes.custom_ui_more_info + : "more-info-" + stateMoreInfoType(stateObj); + + dynamicContentUpdater(this, moreInfoType.toUpperCase(), { + hass, + stateObj, + }); + } +} + +customElements.define("more-info-content", MoreInfoContent); diff --git a/gallery/src/demos/demo-more-info-light.ts b/gallery/src/demos/demo-more-info-light.ts index 19677825d5..8c5ac611e9 100644 --- a/gallery/src/demos/demo-more-info-light.ts +++ b/gallery/src/demos/demo-more-info-light.ts @@ -3,10 +3,10 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../../../src/components/ha-card"; import { SUPPORT_BRIGHTNESS } from "../../../src/data/light"; -import "../../../src/dialogs/more-info/more-info-content"; import { getEntity } from "../../../src/fake_data/entity"; import { provideHass } from "../../../src/fake_data/provide_hass"; import "../components/demo-more-infos"; +import "../components/more-info-content"; const ENTITIES = [ getEntity("light", "bed_light", "on", { diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts index c02d8c1b12..baf2fef515 100644 --- a/hassio/src/addon-store/hassio-addon-store.ts +++ b/hassio/src/addon-store/hassio-addon-store.ts @@ -1,12 +1,13 @@ import "@material/mwc-icon-button/mwc-icon-button"; +import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { mdiDotsVertical } from "@mdi/js"; import { css, CSSResult, + internalProperty, LitElement, property, - internalProperty, PropertyValues, } from "lit-element"; import { html, TemplateResult } from "lit-html"; @@ -19,13 +20,13 @@ import { HassioAddonRepository, reloadHassioAddons, } from "../../../src/data/hassio/addon"; -import "../../../src/layouts/hass-tabs-subpage"; +import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import "../../../src/layouts/hass-loading-screen"; +import "../../../src/layouts/hass-tabs-subpage"; import { HomeAssistant, Route } from "../../../src/types"; import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; import { supervisorTabs } from "../hassio-tabs"; import "./hassio-addon-repository"; -import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { if (a.slug === "local") { @@ -179,7 +180,7 @@ class HassioAddonStore extends LitElement { this._repos.sort(sortRepos); this._addons = addonsInfo.addons; } catch (err) { - alert("Failed to fetch add-on info"); + alert(extractApiErrorMessage(err)); } } diff --git a/hassio/src/addon-view/config/hassio-addon-audio.ts b/hassio/src/addon-view/config/hassio-addon-audio.ts index a5de831ba2..71ef1170d5 100644 --- a/hassio/src/addon-view/config/hassio-addon-audio.ts +++ b/hassio/src/addon-view/config/hassio-addon-audio.ts @@ -176,7 +176,7 @@ class HassioAddonAudio extends LitElement { } private async _saveSettings(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; this._error = undefined; diff --git a/hassio/src/addon-view/config/hassio-addon-config.ts b/hassio/src/addon-view/config/hassio-addon-config.ts index 4fa442e3a3..82755a9840 100644 --- a/hassio/src/addon-view/config/hassio-addon-config.ts +++ b/hassio/src/addon-view/config/hassio-addon-config.ts @@ -5,14 +5,15 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, query, TemplateResult, } from "lit-element"; import { fireEvent } from "../../../../src/common/dom/fire_event"; +import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/ha-card"; import "../../../../src/components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor"; @@ -21,8 +22,7 @@ import { HassioAddonSetOptionParams, setHassioAddonOption, } from "../../../../src/data/hassio/addon"; -import "../../../../src/components/buttons/ha-progress-button"; - +import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../src/resources/styles"; import type { HomeAssistant } from "../../../../src/types"; @@ -113,9 +113,9 @@ class HassioAddonConfig extends LitElement { }; fireEvent(this, "hass-api-called", eventdata); } catch (err) { - this._error = `Failed to reset addon configuration, ${ - err.body?.message || err - }`; + this._error = `Failed to reset addon configuration, ${extractApiErrorMessage( + err + )}`; } button.progress = false; } @@ -147,9 +147,9 @@ class HassioAddonConfig extends LitElement { await suggestAddonRestart(this, this.hass, this.addon); } } catch (err) { - this._error = `Failed to save addon configuration, ${ - err.body?.message || err - }`; + this._error = `Failed to save addon configuration, ${extractApiErrorMessage( + err + )}`; } button.progress = false; } diff --git a/hassio/src/addon-view/config/hassio-addon-network.ts b/hassio/src/addon-view/config/hassio-addon-network.ts index 07ce08f5a4..99f871be99 100644 --- a/hassio/src/addon-view/config/hassio-addon-network.ts +++ b/hassio/src/addon-view/config/hassio-addon-network.ts @@ -4,24 +4,25 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; import { fireEvent } from "../../../../src/common/dom/fire_event"; +import "../../../../src/components/buttons/ha-progress-button"; import "../../../../src/components/ha-card"; import { HassioAddonDetails, HassioAddonSetOptionParams, setHassioAddonOption, } from "../../../../src/data/hassio/addon"; +import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { haStyle } from "../../../../src/resources/styles"; import { HomeAssistant } from "../../../../src/types"; import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; import { hassioStyle } from "../../resources/hassio-style"; -import "../../../../src/components/buttons/ha-progress-button"; interface NetworkItem { description: string; @@ -87,8 +88,8 @@ class HassioAddonNetwork extends LitElement {
- Reset to defaults > + Reset to defaults + Save @@ -130,7 +131,7 @@ class HassioAddonNetwork extends LitElement { } private async _resetTapped(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; const data: HassioAddonSetOptionParams = { @@ -149,16 +150,16 @@ class HassioAddonNetwork extends LitElement { await suggestAddonRestart(this, this.hass, this.addon); } } catch (err) { - this._error = `Failed to set addon network configuration, ${ - err.body?.message || err - }`; + this._error = `Failed to set addon network configuration, ${extractApiErrorMessage( + err + )}`; } button.progress = false; } private async _saveTapped(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; this._error = undefined; @@ -183,9 +184,9 @@ class HassioAddonNetwork extends LitElement { await suggestAddonRestart(this, this.hass, this.addon); } } catch (err) { - this._error = `Failed to set addon network configuration, ${ - err.body?.message || err - }`; + this._error = `Failed to set addon network configuration, ${extractApiErrorMessage( + err + )}`; } button.progress = false; } diff --git a/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts b/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts index 7e5b519886..3d604e03bb 100644 --- a/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts +++ b/hassio/src/addon-view/documentation/hassio-addon-documentation-tab.ts @@ -3,18 +3,19 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; +import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-markdown"; import { fetchHassioAddonDocumentation, HassioAddonDetails, } from "../../../../src/data/hassio/addon"; +import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import "../../../../src/layouts/hass-loading-screen"; -import "../../../../src/components/ha-circular-progress"; import { haStyle } from "../../../../src/resources/styles"; import { HomeAssistant } from "../../../../src/types"; import { hassioStyle } from "../../resources/hassio-style"; @@ -80,9 +81,9 @@ class HassioAddonDocumentationDashboard extends LitElement { this.addon!.slug ); } catch (err) { - this._error = `Failed to get addon documentation, ${ - err.body?.message || err - }`; + this._error = `Failed to get addon documentation, ${extractApiErrorMessage( + err + )}`; } } } diff --git a/hassio/src/addon-view/info/hassio-addon-info.ts b/hassio/src/addon-view/info/hassio-addon-info.ts index a17c7cc86c..1de072ee99 100644 --- a/hassio/src/addon-view/info/hassio-addon-info.ts +++ b/hassio/src/addon-view/info/hassio-addon-info.ts @@ -768,7 +768,7 @@ class HassioAddonInfo extends LitElement { } private async _installClicked(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; try { @@ -833,7 +833,7 @@ class HassioAddonInfo extends LitElement { } private async _uninstallClicked(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; const confirmed = await showConfirmationDialog(this, { diff --git a/hassio/src/addon-view/log/hassio-addon-logs.ts b/hassio/src/addon-view/log/hassio-addon-logs.ts index e9f6703c2c..314dc29c09 100644 --- a/hassio/src/addon-view/log/hassio-addon-logs.ts +++ b/hassio/src/addon-view/log/hassio-addon-logs.ts @@ -4,9 +4,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import "../../../../src/components/ha-card"; @@ -14,6 +14,7 @@ import { fetchHassioAddonLogs, HassioAddonDetails, } from "../../../../src/data/hassio/addon"; +import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { haStyle } from "../../../../src/resources/styles"; import { HomeAssistant } from "../../../../src/types"; import "../../components/hassio-ansi-to-html"; @@ -75,7 +76,7 @@ class HassioAddonLogs extends LitElement { try { this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug); } catch (err) { - this._error = `Failed to get addon logs, ${err.body?.message || err}`; + this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`; } } diff --git a/hassio/src/dashboard/hassio-update.ts b/hassio/src/dashboard/hassio-update.ts index 4af34bb58b..a95eafbd76 100644 --- a/hassio/src/dashboard/hassio-update.ts +++ b/hassio/src/dashboard/hassio-update.ts @@ -5,27 +5,31 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-card"; import "../../../src/components/ha-svg-icon"; +import { + extractApiErrorMessage, + HassioResponse, + ignoredStatusCodes, +} from "../../../src/data/hassio/common"; import { HassioHassOSInfo } from "../../../src/data/hassio/host"; import { HassioHomeAssistantInfo, HassioSupervisorInfo, } from "../../../src/data/hassio/supervisor"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../src/dialogs/generic/show-dialog-box"; import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant } from "../../../src/types"; import { hassioStyle } from "../resources/hassio-style"; -import { - showConfirmationDialog, - showAlertDialog, -} from "../../../src/dialogs/generic/show-dialog-box"; -import { HassioResponse } from "../../../src/data/hassio/common"; @customElement("hassio-update") export class HassioUpdate extends LitElement { @@ -145,11 +149,11 @@ export class HassioUpdate extends LitElement { } private async _confirmUpdate(ev): Promise { - const item = ev.target; + const item = ev.currentTarget; item.progress = true; const confirmed = await showConfirmationDialog(this, { title: `Update ${item.name}`, - text: `Are you sure you want to upgrade ${item.name} to version ${item.version}?`, + text: `Are you sure you want to update ${item.name} to version ${item.version}?`, confirmText: "update", dismissText: "cancel", }); @@ -161,16 +165,12 @@ 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 && !ignoredStatusCodes.has(err.status_code)) { showAlertDialog(this, { title: "Update failed", - text: - typeof err === "object" - ? typeof err.body === "object" - ? err.body.message - : err.body || "Unkown error" - : err, + text: extractApiErrorMessage(err), }); } } diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts index f49d1c1a86..1ab958891b 100644 --- a/hassio/src/dialogs/network/dialog-hassio-network.ts +++ b/hassio/src/dialogs/network/dialog-hassio-network.ts @@ -1,43 +1,42 @@ import "@material/mwc-button/mwc-button"; import "@material/mwc-icon-button"; -import "@material/mwc-tab-bar"; import "@material/mwc-tab"; -import { PaperInputElement } from "@polymer/paper-input/paper-input"; +import "@material/mwc-tab-bar"; import { mdiClose } from "@mdi/js"; +import { PaperInputElement } from "@polymer/paper-input/paper-input"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import { cache } from "lit-html/directives/cache"; - -import { - updateNetworkInterface, - NetworkInterface, -} from "../../../../src/data/hassio/network"; import { fireEvent } from "../../../../src/common/dom/fire_event"; -import { HassioNetworkDialogParams } from "./show-dialog-network"; -import { haStyleDialog } from "../../../../src/resources/styles"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../../src/dialogs/generic/show-dialog-box"; -import type { HomeAssistant } from "../../../../src/types"; -import type { HaRadio } from "../../../../src/components/ha-radio"; -import { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; - import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-radio"; +import type { HaRadio } from "../../../../src/components/ha-radio"; import "../../../../src/components/ha-related-items"; import "../../../../src/components/ha-svg-icon"; +import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; +import { + NetworkInterface, + updateNetworkInterface, +} from "../../../../src/data/hassio/network"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../../src/dialogs/generic/show-dialog-box"; +import { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; +import { haStyleDialog } from "../../../../src/resources/styles"; +import type { HomeAssistant } from "../../../../src/types"; +import { HassioNetworkDialogParams } from "./show-dialog-network"; @customElement("dialog-hassio-network") export class DialogHassioNetwork extends LitElement implements HassDialog { @@ -201,8 +200,7 @@ export class DialogHassioNetwork extends LitElement implements HassDialog { } catch (err) { showAlertDialog(this, { title: "Failed to change network settings", - text: - typeof err === "object" ? err.body.message || "Unkown error" : err, + text: extractApiErrorMessage(err), }); this._prosessing = false; return; diff --git a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts index 0f9f42f860..150fc2b181 100644 --- a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts +++ b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts @@ -5,25 +5,26 @@ import "@polymer/paper-input/paper-input"; import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item-body"; -import "../../../../src/components/ha-circular-progress"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, query, TemplateResult, } from "lit-element"; import memoizeOne from "memoize-one"; +import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-svg-icon"; import { fetchHassioAddonsInfo, HassioAddonRepository, } from "../../../../src/data/hassio/addon"; +import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { setSupervisorOption } from "../../../../src/data/hassio/supervisor"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import type { HomeAssistant } from "../../../../src/types"; @@ -190,7 +191,7 @@ class HassioRepositoriesDialog extends LitElement { input.value = ""; } catch (err) { - this._error = err.message; + this._error = extractApiErrorMessage(err); } this._prosessing = false; } @@ -222,7 +223,7 @@ class HassioRepositoriesDialog extends LitElement { await this._dialogParams!.loadData(); } catch (err) { - this._error = err.message; + this._error = extractApiErrorMessage(err); } } } diff --git a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts index b54e18edd4..eb70aa51c7 100755 --- a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts +++ b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts @@ -15,6 +15,7 @@ import { import { createCloseHeading } from "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-svg-icon"; import { getSignedPath } from "../../../../src/data/auth"; +import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { fetchHassioSnapshotInfo, HassioSnapshotDetail, @@ -379,7 +380,7 @@ class HassioSnapshotDialog extends LitElement { `/api/hassio/snapshots/${this._snapshot!.slug}/download` ); } catch (err) { - alert(`Error: ${err.message}`); + alert(`Error: ${extractApiErrorMessage(err)}`); return; } diff --git a/hassio/src/dialogs/suggestAddonRestart.ts b/hassio/src/dialogs/suggestAddonRestart.ts index 2af4f31fa4..de4343b379 100644 --- a/hassio/src/dialogs/suggestAddonRestart.ts +++ b/hassio/src/dialogs/suggestAddonRestart.ts @@ -3,6 +3,7 @@ import { HassioAddonDetails, restartHassioAddon, } from "../../../src/data/hassio/addon"; +import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { showAlertDialog, showConfirmationDialog, @@ -26,7 +27,7 @@ export const suggestAddonRestart = async ( } catch (err) { showAlertDialog(element, { title: "Failed to restart", - text: err.body.message, + text: extractApiErrorMessage(err), }); } } diff --git a/hassio/src/snapshots/hassio-snapshots.ts b/hassio/src/snapshots/hassio-snapshots.ts index 90f338ba7b..18f2b1d0ed 100644 --- a/hassio/src/snapshots/hassio-snapshots.ts +++ b/hassio/src/snapshots/hassio-snapshots.ts @@ -1,15 +1,13 @@ import "@material/mwc-button"; import "@material/mwc-icon-button"; +import { mdiPackageVariant, mdiPackageVariantClosed, mdiReload } from "@mdi/js"; import "@polymer/paper-checkbox/paper-checkbox"; +import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox"; import "@polymer/paper-input/paper-input"; +import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import "@polymer/paper-radio-button/paper-radio-button"; import "@polymer/paper-radio-group/paper-radio-group"; - -import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox"; -import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import type { PaperRadioGroupElement } from "@polymer/paper-radio-group/paper-radio-group"; -import { mdiPackageVariant, mdiPackageVariantClosed, mdiReload } from "@mdi/js"; -import { fireEvent } from "../../../src/common/dom/fire_event"; import { css, CSSResultArray, @@ -21,7 +19,11 @@ import { PropertyValues, TemplateResult, } from "lit-element"; - +import { fireEvent } from "../../../src/common/dom/fire_event"; +import "../../../src/components/buttons/ha-progress-button"; +import "../../../src/components/ha-card"; +import "../../../src/components/ha-svg-icon"; +import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { createHassioFullSnapshot, createHassioPartialSnapshot, @@ -31,19 +33,15 @@ import { HassioSnapshot, reloadHassioSnapshots, } from "../../../src/data/hassio/snapshot"; -import "../../../src/components/buttons/ha-progress-button"; -import { hassioStyle } from "../resources/hassio-style"; import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor"; +import "../../../src/layouts/hass-tabs-subpage"; +import { PolymerChangedEvent } from "../../../src/polymer-types"; import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant, Route } from "../../../src/types"; -import { PolymerChangedEvent } from "../../../src/polymer-types"; +import "../components/hassio-card-content"; import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot"; import { supervisorTabs } from "../hassio-tabs"; - -import "../../../src/components/ha-card"; -import "../../../src/components/ha-svg-icon"; -import "../../../src/layouts/hass-tabs-subpage"; -import "../components/hassio-card-content"; +import { hassioStyle } from "../resources/hassio-style"; interface CheckboxItem { slug: string; @@ -81,6 +79,7 @@ class HassioSnapshots extends LitElement { }, { slug: "ssl", name: "SSL", checked: true }, { slug: "share", name: "Share", checked: true }, + { slug: "media", name: "Media", checked: true }, { slug: "addons/local", name: "Local add-ons", checked: true }, ]; @@ -243,7 +242,7 @@ class HassioSnapshots extends LitElement { protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); - this._updateSnapshots(); + this.refreshData(); } protected updated(changedProps: PropertyValues) { @@ -292,12 +291,12 @@ class HassioSnapshots extends LitElement { this._snapshots = await fetchHassioSnapshots(this.hass); this._snapshots.sort((a, b) => (a.date < b.date ? 1 : -1)); } catch (err) { - this._error = err.message; + this._error = extractApiErrorMessage(err); } } private async _createSnapshot(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; this._error = ""; @@ -345,7 +344,7 @@ class HassioSnapshots extends LitElement { this._updateSnapshots(); fireEvent(this, "hass-api-called", { success: true, response: null }); } catch (err) { - this._error = err.message; + this._error = extractApiErrorMessage(err); } button.progress = false; } diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts index dfe7224b4a..94400402ae 100644 --- a/hassio/src/system/hassio-host-info.ts +++ b/hassio/src/system/hassio-host-info.ts @@ -14,11 +14,14 @@ import { TemplateResult, } from "lit-element"; import memoizeOne from "memoize-one"; -import { atLeastVersion } from "../../../src/common/config/version"; import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-card"; import "../../../src/components/ha-settings-row"; +import { + extractApiErrorMessage, + ignoredStatusCodes, +} from "../../../src/data/hassio/common"; import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware"; import { changeHostOptions, @@ -81,8 +84,7 @@ class HassioHostInfo extends LitElement { ` : ""} - ${this.hostInfo.features.includes("network") && - atLeastVersion(this.hass.config.version, 0, 115) + ${this.hostInfo.features.includes("network") ? html` IP address @@ -220,14 +222,13 @@ class HassioHostInfo extends LitElement { } catch (err) { showAlertDialog(this, { title: "Failed to get Hardware list", - text: - typeof err === "object" ? err.body?.message || "Unkown error" : err, + text: extractApiErrorMessage(err), }); } } private async _hostReboot(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; const confirmed = await showConfirmationDialog(this, { @@ -245,17 +246,19 @@ class HassioHostInfo extends LitElement { try { await rebootHost(this.hass); } catch (err) { - showAlertDialog(this, { - title: "Failed to reboot", - text: - typeof err === "object" ? err.body?.message || "Unkown error" : err, - }); + // Ignore connection errors, these are all expected + if (err.status_code && !ignoredStatusCodes.has(err.status_code)) { + showAlertDialog(this, { + title: "Failed to reboot", + text: extractApiErrorMessage(err), + }); + } } button.progress = false; } private async _hostShutdown(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; const confirmed = await showConfirmationDialog(this, { @@ -273,17 +276,19 @@ class HassioHostInfo extends LitElement { try { await shutdownHost(this.hass); } catch (err) { - showAlertDialog(this, { - title: "Failed to shutdown", - text: - typeof err === "object" ? err.body?.message || "Unkown error" : err, - }); + // Ignore connection errors, these are all expected + if (err.status_code && !ignoredStatusCodes.has(err.status_code)) { + showAlertDialog(this, { + title: "Failed to shutdown", + text: extractApiErrorMessage(err), + }); + } } button.progress = false; } private async _osUpdate(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; const confirmed = await showConfirmationDialog(this, { @@ -303,8 +308,7 @@ class HassioHostInfo extends LitElement { } catch (err) { showAlertDialog(this, { title: "Failed to update", - text: - typeof err === "object" ? err.body?.message || "Unkown error" : err, + text: extractApiErrorMessage(err), }); } button.progress = false; @@ -333,8 +337,7 @@ class HassioHostInfo extends LitElement { } catch (err) { showAlertDialog(this, { title: "Setting hostname failed", - text: - typeof err === "object" ? err.body?.message || "Unkown error" : err, + text: extractApiErrorMessage(err), }); } } @@ -347,8 +350,7 @@ class HassioHostInfo extends LitElement { } catch (err) { showAlertDialog(this, { title: "Failed to import from USB", - text: - typeof err === "object" ? err.body?.message || "Unkown error" : err, + text: extractApiErrorMessage(err), }); } } diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts index 6d0bf96efe..fcec51d607 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, @@ -26,6 +27,7 @@ import { import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant } from "../../../src/types"; import { hassioStyle } from "../resources/hassio-style"; +import { extractApiErrorMessage } from "../../../src/data/hassio/common"; @customElement("hassio-supervisor-info") class HassioSupervisorInfo extends LitElement { @@ -143,7 +145,7 @@ class HassioSupervisorInfo extends LitElement { } private async _toggleBeta(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; if (this.supervisorInfo.channel === "stable") { @@ -175,43 +177,43 @@ 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", - text: - typeof err === "object" ? err.body?.message || "Unkown error" : err, + text: extractApiErrorMessage(err), }); } button.progress = false; } private async _supervisorReload(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; try { await reloadSupervisor(this.hass); + this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass); } catch (err) { showAlertDialog(this, { title: "Failed to reload the supervisor", - text: - typeof err === "object" ? err.body?.message || "Unkown error" : err, + text: extractApiErrorMessage(err), }); } button.progress = false; } private async _supervisorUpdate(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; const confirmed = await showConfirmationDialog(this, { title: "Update supervisor", - text: `Are you sure you want to upgrade supervisor to version ${this.supervisorInfo.version_latest}?`, + text: `Are you sure you want to update supervisor to version ${this.supervisorInfo.version_latest}?`, confirmText: "update", dismissText: "cancel", }); @@ -226,8 +228,7 @@ class HassioSupervisorInfo extends LitElement { } catch (err) { showAlertDialog(this, { title: "Failed to update the supervisor", - text: - typeof err === "object" ? err.body.message || "Unkown error" : err, + text: extractApiErrorMessage(err), }); } button.progress = false; @@ -257,8 +258,7 @@ class HassioSupervisorInfo extends LitElement { } catch (err) { showAlertDialog(this, { title: "Failed to set supervisor option", - text: - typeof err === "object" ? err.body.message || "Unkown error" : err, + text: extractApiErrorMessage(err), }); } } diff --git a/hassio/src/system/hassio-supervisor-log.ts b/hassio/src/system/hassio-supervisor-log.ts index 0522e5c767..88528eb431 100644 --- a/hassio/src/system/hassio-supervisor-log.ts +++ b/hassio/src/system/hassio-supervisor-log.ts @@ -12,16 +12,15 @@ import { property, TemplateResult, } from "lit-element"; - -import { fetchHassioLogs } from "../../../src/data/hassio/supervisor"; -import { hassioStyle } from "../resources/hassio-style"; -import { haStyle } from "../../../src/resources/styles"; -import { HomeAssistant } from "../../../src/types"; - import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/ha-card"; +import { extractApiErrorMessage } from "../../../src/data/hassio/common"; +import { fetchHassioLogs } from "../../../src/data/hassio/supervisor"; import "../../../src/layouts/hass-loading-screen"; +import { haStyle } from "../../../src/resources/styles"; +import { HomeAssistant } from "../../../src/types"; import "../components/hassio-ansi-to-html"; +import { hassioStyle } from "../resources/hassio-style"; interface LogProvider { key: string; @@ -120,7 +119,7 @@ class HassioSupervisorLog extends LitElement { } private async _refresh(ev: CustomEvent): Promise { - const button = ev.target as any; + const button = ev.currentTarget as any; button.progress = true; await this._loadData(); button.progress = false; @@ -135,9 +134,9 @@ class HassioSupervisorLog extends LitElement { this._selectedLogProvider ); } catch (err) { - this._error = `Failed to get supervisor logs, ${ - typeof err === "object" ? err.body?.message || "Unkown error" : err - }`; + this._error = `Failed to get supervisor logs, ${extractApiErrorMessage( + err + )}`; } } diff --git a/package.json b/package.json index 1410d6bf3b..1db467294c 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 8ca7a0f6d6..b7b90326ef 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20200901.0", + version="20200915.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/config/components_with_service.ts b/src/common/config/components_with_service.ts new file mode 100644 index 0000000000..9775b8c6f6 --- /dev/null +++ b/src/common/config/components_with_service.ts @@ -0,0 +1,9 @@ +import { HomeAssistant } from "../../types"; + +/** Return an array of domains with the service. */ +export const componentsWithService = ( + hass: HomeAssistant, + service: string +): Array => + hass && + Object.keys(hass.services).filter((key) => service in hass.services[key]); diff --git a/src/common/const.ts b/src/common/const.ts index dc2bf20595..640c4fb13a 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -44,7 +44,6 @@ export const DOMAINS_WITH_MORE_INFO = [ "script", "sun", "timer", - "updater", "vacuum", "water_heater", "weather", diff --git a/src/common/decorators/local-storage.ts b/src/common/decorators/local-storage.ts index 99cdebdf3d..d4034ae25f 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/common/dom/apply_themes_on_element.ts b/src/common/dom/apply_themes_on_element.ts index f994f0d99d..902a1a196a 100644 --- a/src/common/dom/apply_themes_on_element.ts +++ b/src/common/dom/apply_themes_on_element.ts @@ -1,14 +1,14 @@ -import { derivedStyles, darkStyles } from "../../resources/styles"; +import { darkStyles, derivedStyles } from "../../resources/styles"; import { HomeAssistant, Theme } from "../../types"; import { hex2rgb, + lab2hex, + lab2rgb, rgb2hex, rgb2lab, - lab2rgb, - lab2hex, } from "../color/convert-color"; +import { labBrighten, labDarken } from "../color/lab"; import { rgbContrast } from "../color/rgb"; -import { labDarken, labBrighten } from "../color/lab"; interface ProcessedTheme { keys: { [key: string]: "" }; @@ -105,7 +105,7 @@ const processTheme = ( const keys = {}; for (const key of Object.keys(combinedTheme)) { const prefixedKey = `--${key}`; - const value = combinedTheme[key]!; + const value = String(combinedTheme[key]); styles[prefixedKey] = value; keys[prefixedKey] = ""; diff --git a/src/common/entity/binary_sensor_icon.ts b/src/common/entity/binary_sensor_icon.ts index c1eb694afe..5442e432ff 100644 --- a/src/common/entity/binary_sensor_icon.ts +++ b/src/common/entity/binary_sensor_icon.ts @@ -3,49 +3,51 @@ import { HassEntity } from "home-assistant-js-websocket"; /** Return an icon representing a binary sensor state. */ export const binarySensorIcon = (state: HassEntity) => { - const activated = state.state && state.state === "off"; + const is_off = state.state && state.state === "off"; switch (state.attributes.device_class) { case "battery": - return activated ? "hass:battery" : "hass:battery-outline"; + return is_off ? "hass:battery" : "hass:battery-outline"; + case "battery_charging": + return is_off ? "hass:battery" : "hass:battery-charging"; case "cold": - return activated ? "hass:thermometer" : "hass:snowflake"; + return is_off ? "hass:thermometer" : "hass:snowflake"; case "connectivity": - return activated ? "hass:server-network-off" : "hass:server-network"; + return is_off ? "hass:server-network-off" : "hass:server-network"; case "door": - return activated ? "hass:door-closed" : "hass:door-open"; + return is_off ? "hass:door-closed" : "hass:door-open"; case "garage_door": - return activated ? "hass:garage" : "hass:garage-open"; + return is_off ? "hass:garage" : "hass:garage-open"; case "gas": case "power": case "problem": case "safety": case "smoke": - return activated ? "hass:shield-check" : "hass:alert"; + return is_off ? "hass:shield-check" : "hass:alert"; case "heat": - return activated ? "hass:thermometer" : "hass:fire"; + return is_off ? "hass:thermometer" : "hass:fire"; case "light": - return activated ? "hass:brightness-5" : "hass:brightness-7"; + return is_off ? "hass:brightness-5" : "hass:brightness-7"; case "lock": - return activated ? "hass:lock" : "hass:lock-open"; + return is_off ? "hass:lock" : "hass:lock-open"; case "moisture": - return activated ? "hass:water-off" : "hass:water"; + return is_off ? "hass:water-off" : "hass:water"; case "motion": - return activated ? "hass:walk" : "hass:run"; + return is_off ? "hass:walk" : "hass:run"; case "occupancy": - return activated ? "hass:home-outline" : "hass:home"; + return is_off ? "hass:home-outline" : "hass:home"; case "opening": - return activated ? "hass:square" : "hass:square-outline"; + return is_off ? "hass:square" : "hass:square-outline"; case "plug": - return activated ? "hass:power-plug-off" : "hass:power-plug"; + return is_off ? "hass:power-plug-off" : "hass:power-plug"; case "presence": - return activated ? "hass:home-outline" : "hass:home"; + return is_off ? "hass:home-outline" : "hass:home"; case "sound": - return activated ? "hass:music-note-off" : "hass:music-note"; + return is_off ? "hass:music-note-off" : "hass:music-note"; case "vibration": - return activated ? "hass:crop-portrait" : "hass:vibrate"; + return is_off ? "hass:crop-portrait" : "hass:vibrate"; case "window": - return activated ? "hass:window-closed" : "hass:window-open"; + return is_off ? "hass:window-closed" : "hass:window-open"; default: - return activated ? "hass:radiobox-blank" : "hass:checkbox-marked-circle"; + return is_off ? "hass:radiobox-blank" : "hass:checkbox-marked-circle"; } }; diff --git a/src/common/entity/state_card_type.ts b/src/common/entity/state_card_type.ts index a24994d5b3..2013ee2058 100644 --- a/src/common/entity/state_card_type.ts +++ b/src/common/entity/state_card_type.ts @@ -3,9 +3,10 @@ import { HomeAssistant } from "../../types"; import { DOMAINS_WITH_CARD } from "../const"; import { canToggleState } from "./can_toggle_state"; import { computeStateDomain } from "./compute_state_domain"; +import { UNAVAILABLE } from "../../data/entity"; export const stateCardType = (hass: HomeAssistant, stateObj: HassEntity) => { - if (stateObj.state === "unavailable") { + if (stateObj.state === UNAVAILABLE) { return "display"; } diff --git a/src/common/entity/timer_time_remaining.ts b/src/common/entity/timer_time_remaining.ts index cd7c90ef8f..5b2f54654d 100644 --- a/src/common/entity/timer_time_remaining.ts +++ b/src/common/entity/timer_time_remaining.ts @@ -1,7 +1,12 @@ import { HassEntity } from "home-assistant-js-websocket"; import durationToSeconds from "../datetime/duration_to_seconds"; -export const timerTimeRemaining = (stateObj: HassEntity) => { +export const timerTimeRemaining = ( + stateObj: HassEntity +): undefined | number => { + if (!stateObj.attributes.remaining) { + return undefined; + } let timeRemaining = durationToSeconds(stateObj.attributes.remaining); if (stateObj.state === "active") { diff --git a/src/common/util/throttle.ts b/src/common/util/throttle.ts new file mode 100644 index 0000000000..1cd98ea188 --- /dev/null +++ b/src/common/util/throttle.ts @@ -0,0 +1,50 @@ +// From: underscore.js https://github.com/jashkenas/underscore/blob/master/underscore.js + +// Returns a function, that, when invoked, will only be triggered at most once +// during a given window of time. Normally, the throttled function will run +// as much as it can, without ever going more than once per `wait` duration; +// but if you'd like to disable the execution on the leading edge, pass +// `false for leading`. To disable execution on the trailing edge, ditto. +export const throttle = ( + func: T, + wait: number, + leading = true, + trailing = true +): T => { + let timeout: number | undefined; + let previous = 0; + let context: any; + let args: any; + const later = () => { + previous = leading === false ? 0 : Date.now(); + timeout = undefined; + func.apply(context, args); + if (!timeout) { + context = null; + args = null; + } + }; + // @ts-ignore + return function (...argmnts) { + // @ts-ignore + // @typescript-eslint/no-this-alias + context = this; + args = argmnts; + + const now = Date.now(); + if (!previous && leading === false) { + previous = now; + } + const remaining = wait - (now - previous); + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + previous = now; + func.apply(context, args); + } else if (!timeout && trailing !== false) { + timeout = window.setTimeout(later, remaining); + } + }; +}; 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 0000000000..ee2958323b --- /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 300ae57a79..3cafa0a88b 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"; @@ -7,6 +6,7 @@ import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResult, + customElement, html, LitElement, property, @@ -20,6 +20,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; @@ -51,7 +52,8 @@ const rowRenderer = ( root.querySelector("[secondary]")!.textContent = model.item.entity_id; }; -class HaEntityPicker extends LitElement { +@customElement("ha-entity-picker") +export class HaEntityPicker extends LitElement { @property({ type: Boolean }) public autofocus = false; @property({ type: Boolean }) public disabled?: boolean; @@ -95,6 +97,8 @@ class HaEntityPicker extends LitElement { @query("vaadin-combo-box-light") private _comboBox!: HTMLElement; + private _initedStates = false; + private _getStates = memoizeOne( ( _opened: boolean, @@ -148,11 +152,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 +173,7 @@ class HaEntityPicker extends LitElement { this.includeDeviceClasses ); (this._comboBox as any).items = states; + this._initedStates = true; } } @@ -169,7 +181,6 @@ class HaEntityPicker extends LitElement { if (!this.hass) { return html``; } - return html` @@ -108,7 +110,7 @@ export class HaStateLabelBadge extends LitElement { return null; case "sensor": default: - return state.state === "unknown" + return state.state === UNKNOWN ? "-" : state.attributes.unit_of_measurement ? state.state @@ -121,7 +123,7 @@ export class HaStateLabelBadge extends LitElement { } private _computeIcon(domain: string, state: HassEntity) { - if (state.state === "unavailable") { + if (state.state === UNAVAILABLE) { return null; } switch (domain) { @@ -166,7 +168,7 @@ export class HaStateLabelBadge extends LitElement { private _computeLabel(domain, state, _timerTimeRemaining) { if ( - state.state === "unavailable" || + state.state === UNAVAILABLE || ["device_tracker", "alarm_control_panel", "person"].includes(domain) ) { // Localize the state with a special state_badge namespace, which has variations of diff --git a/src/components/ha-button-menu.ts b/src/components/ha-button-menu.ts index e325925d57..7adc266a56 100644 --- a/src/components/ha-button-menu.ts +++ b/src/components/ha-button-menu.ts @@ -1,17 +1,16 @@ -import { - customElement, - html, - TemplateResult, - LitElement, - CSSResult, - css, - query, - property, -} from "lit-element"; import "@material/mwc-button"; import "@material/mwc-menu"; -import type { Menu, Corner } from "@material/mwc-menu"; - +import type { Corner, Menu } from "@material/mwc-menu"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + query, + TemplateResult, +} from "lit-element"; import "./ha-icon-button"; @customElement("ha-button-menu") @@ -22,6 +21,8 @@ export class HaButtonMenu extends LitElement { @property({ type: Boolean }) public activatable = false; + @property({ type: Boolean }) public disabled = false; + @query("mwc-menu") private _menu?: Menu; public get items() { @@ -48,6 +49,9 @@ export class HaButtonMenu extends LitElement { } private _handleClick(): void { + if (this.disabled) { + return; + } this._menu!.anchor = this; this._menu!.show(); } diff --git a/src/components/ha-camera-stream.ts b/src/components/ha-camera-stream.ts index e4eda4b3d8..eb9ce99718 100644 --- a/src/components/ha-camera-stream.ts +++ b/src/components/ha-camera-stream.ts @@ -3,56 +3,46 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; import { fireEvent } from "../common/dom/fire_event"; import { computeStateName } from "../common/entity/compute_state_name"; import { supportsFeature } from "../common/entity/supports-feature"; -import { nextRender } from "../common/util/render-status"; -import { getExternalConfig } from "../external_app/external_config"; import { CAMERA_SUPPORT_STREAM, computeMJPEGStreamUrl, fetchStreamUrl, } from "../data/camera"; import { CameraEntity, HomeAssistant } from "../types"; - -type HLSModule = typeof import("hls.js"); +import "./ha-hls-player"; @customElement("ha-camera-stream") class HaCameraStream extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property() public stateObj?: CameraEntity; + @property({ attribute: false }) public stateObj?: CameraEntity; - @property({ type: Boolean }) public showControls = false; + @property({ type: Boolean, attribute: "controls" }) + public controls = false; - @internalProperty() private _attached = false; + @property({ type: Boolean, attribute: "muted" }) + public muted = false; + + @property({ type: Boolean, attribute: "allow-exoplayer" }) + public allowExoPlayer = false; // We keep track if we should force MJPEG with a string // that way it automatically resets if we change entity. - @internalProperty() private _forceMJPEG: string | undefined = undefined; + @internalProperty() private _forceMJPEG?: string; - private _hlsPolyfillInstance?: Hls; - - private _useExoPlayer = false; - - public connectedCallback() { - super.connectedCallback(); - this._attached = true; - } - - public disconnectedCallback() { - super.disconnectedCallback(); - this._attached = false; - } + @internalProperty() private _url?: string; protected render(): TemplateResult { - if (!this.stateObj || !this._attached) { + if (!this.stateObj) { return html``; } @@ -69,51 +59,26 @@ class HaCameraStream extends LitElement { )} camera.`} /> ` - : html` - - `} + .allowExoPlayer=${this.allowExoPlayer} + .muted=${this.muted} + .controls=${this.controls} + .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._shouldRenderMJPEG) { + this._forceMJPEG = undefined; + this._getStreamUrl(); } } @@ -125,136 +90,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-code-editor.ts b/src/components/ha-code-editor.ts index 452e75dffd..dbbc9b616f 100644 --- a/src/components/ha-code-editor.ts +++ b/src/components/ha-code-editor.ts @@ -97,6 +97,7 @@ export class HaCodeEditor extends UpdatingElement { .CodeMirror { height: var(--code-mirror-height, auto); direction: var(--code-mirror-direction, ltr); + font-family: var(--code-font-family, monospace); } .CodeMirror-scroll { max-height: var(--code-mirror-max-height, --code-mirror-height); diff --git a/src/components/ha-color-picker.js b/src/components/ha-color-picker.js index 49fce6c7f5..a50601c0ea 100644 --- a/src/components/ha-color-picker.js +++ b/src/components/ha-color-picker.js @@ -176,6 +176,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) { this.drawColorWheel(); this.drawMarker(); + if (this.desiredHsColor) { + this.setMarkerOnColor(this.desiredHsColor); + this.applyColorToCanvas(this.desiredHsColor); + } + this.interactionLayer.addEventListener("mousedown", (ev) => this.onMouseDown(ev) ); @@ -319,6 +324,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) { // set marker position to the given color setMarkerOnColor(hs) { + if (!this.marker || !this.tooltip) { + return; + } const dist = hs.s * this.radius; const theta = ((hs.h - 180) / 180) * Math.PI; const markerdX = -dist * Math.cos(theta); @@ -330,6 +338,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) { // apply given color to interface elements applyColorToCanvas(hs) { + if (!this.interactionLayer) { + return; + } // we're not really converting hs to hsl here, but we keep it cheap // setting the color on the interactionLayer, the svg elements can inherit this.interactionLayer.style.color = `hsl(${hs.h}, 100%, ${ diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index 04dcbb1155..8c34add0e3 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 946c1979de..0677e96673 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 0000000000..b613a5ad05 --- /dev/null +++ b/src/components/ha-hls-player.ts @@ -0,0 +1,217 @@ +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 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; + + @property({ type: Boolean, attribute: "allow-exoplayer" }) + public allowExoPlayer = 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 { + return false; + } + + 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: { + url: new URL(url, window.location.href).toString(), + muted: this.muted, + }, + }); + } + + 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-settings-row.ts b/src/components/ha-settings-row.ts index 0d53c58d29..4fdae88f3e 100644 --- a/src/components/ha-settings-row.ts +++ b/src/components/ha-settings-row.ts @@ -1,3 +1,4 @@ +import "@polymer/paper-item/paper-item-body"; import { css, CSSResult, @@ -7,7 +8,6 @@ import { property, SVGTemplateResult, } from "lit-element"; -import "@polymer/paper-item/paper-item-body"; @customElement("ha-settings-row") export class HaSettingsRow extends LitElement { @@ -49,6 +49,9 @@ export class HaSettingsRow extends LitElement { border-top: 1px solid var(--divider-color); padding-bottom: 8px; } + ::slotted(ha-switch) { + padding: 16px 0; + } `; } } diff --git a/src/components/ha-sidebar-sort-styles.ts b/src/components/ha-sidebar-sort-styles.ts deleted file mode 100644 index b87b3c92ba..0000000000 --- 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 575cb4cc2f..3266046064 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"; @@ -43,6 +42,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,24 +159,22 @@ const computePanels = memoizeOne( let Sortable; -let sortStyles: TemplateResult; - @customElement("ha-sidebar") class HaSidebar extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public narrow!: boolean; + @property({ type: Boolean, reflect: true }) public narrow!: boolean; @property({ type: Boolean }) public alwaysExpand = false; @property({ type: Boolean, reflect: true }) public expanded = false; + @property({ type: Boolean }) public editMode = false; + @internalProperty() private _externalConfig?: ExternalConfig; @internalProperty() private _notifications?: PersistentNotification[]; - @internalProperty() private _editMode = false; - // property used only in css // @ts-ignore @property({ type: Boolean, reflect: true }) public rtl = false; @@ -190,11 +188,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,8 +225,14 @@ class HaSidebar extends LitElement { } return html` - ${this._editMode ? sortStyles : ""} - - ${this._editMode + ${this.editMode ? html`
${guard([this._hiddenPanels, this._renderEmptySortable], () => this._renderEmptySortable @@ -270,31 +274,33 @@ class HaSidebar extends LitElement {
` : this._renderPanels(beforeSpacer)}
- ${this._editMode && this._hiddenPanels.length + ${this.editMode && this._hiddenPanels.length ? html` ${this._hiddenPanels.map((url) => { const panel = this.hass.panels[url]; + if (!panel) { + return ""; + } return html` ${panel.url_path === "lovelace" + >${panel.url_path === this.hass.defaultPanel ? hass.localize("panel.states") : hass.localize(`panel.${panel.title}`) || panel.title} - + + + `; })}
@@ -374,7 +380,11 @@ class HaSidebar extends LitElement { @mouseleave=${this._itemMouseLeave} > - + ${hass.user ? hass.user.name : ""} @@ -393,8 +403,10 @@ class HaSidebar extends LitElement { changedProps.has("alwaysExpand") || changedProps.has("_externalConfig") || changedProps.has("_notifications") || - changedProps.has("_editMode") || - changedProps.has("_renderEmptySortable") + changedProps.has("editMode") || + changedProps.has("_renderEmptySortable") || + changedProps.has("_hiddenPanels") || + (changedProps.has("_panelOrder") && !this.editMode) ) { return true; } @@ -435,6 +447,13 @@ class HaSidebar extends LitElement { if (changedProps.has("alwaysExpand")) { this.expanded = this.alwaysExpand; } + if (changedProps.has("editMode")) { + if (this.editMode) { + this._activateEditMode(); + } else { + this._deactivateEditMode(); + } + } if (!changedProps.has("hass")) { return; } @@ -460,24 +479,29 @@ class HaSidebar extends LitElement { return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement; } - private async _handleAction(ev: CustomEvent) { + private _handleAction(ev: CustomEvent) { if (ev.detail.action !== "hold") { return; } + fireEvent(this, "hass-edit-sidebar", { editMode: true }); + } + + private async _activateEditMode() { if (!Sortable) { const [sortableImport, sortStylesImport] = await Promise.all([ import("sortablejs/modular/sortable.core.esm"), - import("./ha-sidebar-sort-styles"), + import("../resources/ha-sortable-style"), ]); - sortStyles = sortStylesImport.sortStyles; + const style = document.createElement("style"); + style.innerHTML = sortStylesImport.sortableStyles.cssText; + this.shadowRoot!.appendChild(style); Sortable = sortableImport.Sortable; Sortable.mount(sortableImport.OnSpill); Sortable.mount(sortableImport.AutoScroll()); } - this._editMode = true; await this.updateComplete; @@ -489,21 +513,25 @@ class HaSidebar extends LitElement { animation: 150, fallbackClass: "sortable-fallback", dataIdAttr: "data-panel", + handle: "paper-icon-item", onSort: async () => { this._panelOrder = this._sortable.toArray(); }, }); } - private _closeEditMode() { + private _deactivateEditMode() { this._sortable?.destroy(); this._sortable = undefined; - this._editMode = false; + } + + private _closeEditMode() { + fireEvent(this, "hass-edit-sidebar", { editMode: false }); } private async _hidePanel(ev: Event) { ev.preventDefault(); - const panel = (ev.target as any).panel; + const panel = (ev.currentTarget as any).panel; if (this._hiddenPanels.includes(panel)) { return; } @@ -516,7 +544,7 @@ class HaSidebar extends LitElement { private async _unhidePanel(ev: Event) { ev.preventDefault(); - const index = this._hiddenPanels.indexOf((ev.target as any).panel); + const index = this._hiddenPanels.indexOf((ev.currentTarget as any).panel); if (index < 0) { return; } @@ -628,11 +656,13 @@ class HaSidebar extends LitElement { return panels.map((panel) => this._renderPanel( panel.url_path, - panel.url_path === "lovelace" - ? this.hass.localize("panel.states") + panel.url_path === this.hass.defaultPanel + ? panel.title || this.hass.localize("panel.states") : this.hass.localize(`panel.${panel.title}`) || panel.title, - panel.url_path === "lovelace" ? undefined : panel.icon, - panel.url_path === "lovelace" ? mdiViewDashboard : undefined + panel.icon, + panel.url_path === this.hass.defaultPanel && !panel.icon + ? mdiViewDashboard + : undefined ) ); } @@ -646,8 +676,8 @@ class HaSidebar extends LitElement { return html` ` : html``} ${title} - ${this._editMode - ? html`` - : ""} + ${this.editMode + ? html` + + ` + : ""} `; } - 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; - } + 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%; - } + .title { + width: 100%; + display: none; + } + :host([narrow]) .title { + padding: 0 16px; + } + :host([expanded]) .title { + display: initial; + } + .title mwc-button { + width: 100%; + } - paper-listbox::-webkit-scrollbar { - width: 0.4rem; - height: 0.4rem; - } + #sortable, + .hidden-panel { + display: none; + } - 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-x: hidden; + background: none; + margin-left: env(safe-area-inset-left); + } - 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); + } - :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; + } - 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; + } - 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); + } - 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::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; + } - .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; + } - 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 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); + } - 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; + } - 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; + } - .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; + } - .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; + } - .spacer { - flex: 1; - pointer-events: none; - } + .subheader { + color: var(--sidebar-text-color); + font-weight: 500; + font-size: 14px; + padding: 16px; + white-space: nowrap; + } - .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 { - 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); + } - .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; + } - .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); - } - `; + :host([rtl]) .menu mwc-icon-button { + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + } + `, + ]; } } diff --git a/src/components/map/ha-location-editor.ts b/src/components/map/ha-location-editor.ts index 9bfcf5e50e..3b42fd38cc 100644 --- a/src/components/map/ha-location-editor.ts +++ b/src/components/map/ha-location-editor.ts @@ -279,6 +279,7 @@ class LocationEditor extends LitElement { } #map { height: 100%; + background: inherit; } .leaflet-edit-move { border-radius: 50%; diff --git a/src/components/media-player/dialog-media-player-browse.ts b/src/components/media-player/dialog-media-player-browse.ts index 5f4d14649a..311536844f 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 c485a96453..1f6af63c66 100644 --- a/src/components/media-player/ha-media-player-browse.ts +++ b/src/components/media-player/ha-media-player-browse.ts @@ -2,7 +2,7 @@ import "@material/mwc-button/mwc-button"; import "@material/mwc-fab/mwc-fab"; import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list-item"; -import { mdiArrowLeft, mdiClose, mdiFolder, mdiPlay, mdiPlus } from "@mdi/js"; +import { mdiArrowLeft, mdiClose, mdiPlay, mdiPlus } from "@mdi/js"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; import { @@ -18,15 +18,24 @@ import { } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; import { ifDefined } from "lit-html/directives/if-defined"; -import memoizeOne from "memoize-one"; +import { styleMap } from "lit-html/directives/style-map"; import { fireEvent } from "../../common/dom/fire_event"; import { computeRTLDirection } from "../../common/util/compute_rtl"; import { debounce } from "../../common/util/debounce"; -import { browseMediaPlayer, MediaPickedEvent } from "../../data/media-player"; +import { + browseLocalMediaPlayer, + browseMediaPlayer, + BROWSER_PLAYER, + MediaClassBrowserSettings, + 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"; +import { documentationUrl } from "../../util/documentation-url"; import "../entity/ha-entity-picker"; import "../ha-button-menu"; import "../ha-card"; @@ -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; @@ -88,203 +95,238 @@ export class HaMediaPlayerBrowse extends LitElement { } protected render(): TemplateResult { - if (!this._mediaPlayerItems.length) { - return html``; - } - 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); - - const showImages = mostRecentItem.children?.some( - (child) => child.thumbnail && child.thumbnail !== mostRecentItem.thumbnail - ); - - const mediaType = this.hass.localize( - `ui.components.media-browser.content-type.${mostRecentItem.media_content_type}` + const subtitle = this.hass.localize( + `ui.components.media-browser.class.${currentItem.media_class}` ); + const mediaClass = MediaClassBrowserSettings[currentItem.media_class]; + const childrenMediaClass = + MediaClassBrowserSettings[currentItem.children_media_class]; return html`
-
- ${mostRecentItem.thumbnail - ? html` -
- ${this._narrow && mostRecentItem?.can_play - ? html` - - - ${this.hass.localize( - `ui.components.media-browser.${this.action}` - )} - - ` - : ""} -
- ` - : html``} -
- ${this.hideTitle && (this._narrow || !mostRecentItem.thumbnail) - ? "" - : html` - ${mostRecentItem.children?.length - ? hasExpandableChildren - ? html` -
- ${mostRecentItem.children?.length + : html``} +
+ + ${currentItem.can_play && + (!currentItem.thumbnail || !this._narrow) + ? html` + + + ${this.hass.localize( + `ui.components.media-browser.${this.action}` + )} + + ` + : ""} +
+
+ ${this.dialog + ? html` + + + + ` + : ""} +
+
+ ${this._error + ? html` +
+ ${this._renderError(this._error)} +
+ ` + : currentItem.children?.length + ? childrenMediaClass.layout === "grid" + ? html` +
+ ${currentItem.children.map( + (child) => html` +
+
+ + ${!child.thumbnail + ? html` + + ` + : ""} + + ${child.can_play + ? html` + + + + ` + : ""} +
+
${child.title}
+
+ ${this.hass.localize( + `ui.components.media-browser.content-type.${child.media_content_type}` + )} +
+
+ ` + )}
` : html` - ${mostRecentItem.children.map( + ${currentItem.children.map( (child) => html`
- ${child.title} + ${child.title}
  • ` )}
    ` - : this.hass.localize("ui.components.media-browser.no_items")} + : html` +
    + ${this.hass.localize("ui.components.media-browser.no_items")}
    + + ${currentItem.media_content_id.startsWith( + "media-source://media_source/local_source" + ) + ? html`${this.hass.localize( + "ui.components.media-browser.learn_adding_local_media", + "documentation", + html`${this.hass.localize( + "ui.components.media-browser.documentation" + )}` + )} +
    + ${this.hass.localize( + "ui.components.media-browser.local_media_files" + )}.` + : ""} +
    + `} `; } @@ -338,11 +409,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 +435,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,12 +482,15 @@ 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_PLAYER + ? await browseMediaPlayer( + this.hass, + this.entityId, + mediaContentId, + mediaContentType + ) + : await browseLocalMediaPlayer(this.hass, mediaContentId); return itemData; } @@ -416,14 +518,44 @@ export class HaMediaPlayerBrowse extends LitElement { this._resizeObserver.observe(this); } - private _hasExpandableChildren = memoizeOne((children) => - children.find((item: MediaPlayerItem) => item.can_expand) - ); - private _closeDialogAction(): void { fireEvent(this, "close-dialog"); } + private _renderError(err: { message: string; code: string }) { + if (err.message === "Media directory does not exist.") { + return html` +

    + ${this.hass.localize( + "ui.components.media-browser.no_local_media_found" + )} +

    +

    + ${this.hass.localize("ui.components.media-browser.no_media_folder")} +
    + ${this.hass.localize( + "ui.components.media-browser.setup_local_help", + "documentation", + html`${this.hass.localize( + "ui.components.media-browser.documentation" + )}` + )} +
    + ${this.hass.localize("ui.components.media-browser.local_media_files")} +

    + `; + } + return html`${err.message}`; + } + static get styles(): CSSResultArray { return [ haStyle, @@ -436,26 +568,26 @@ export class HaMediaPlayerBrowse extends LitElement { flex-direction: column; } + .container { + padding: 16px; + } + .header { - display: flex; + display: block; 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; } + .header-wrapper { + display: flex; + } + .header-content { display: flex; flex-wrap: wrap; @@ -483,12 +615,7 @@ export class HaMediaPlayerBrowse extends LitElement { .header-info mwc-button { display: block; - } - - .breadcrumb-overflow { - display: flex; - flex-grow: 1; - justify-content: space-between; + --mdc-theme-primary: var(--primary-color); } .breadcrumb { @@ -532,6 +659,7 @@ export class HaMediaPlayerBrowse extends LitElement { mwc-list { --mdc-list-vertical-padding: 0; + --mdc-list-item-graphic-margin: 0; --mdc-theme-text-icon-on-background: var(--secondary-text-color); margin-top: 10px; } @@ -548,14 +676,18 @@ export class HaMediaPlayerBrowse extends LitElement { display: grid; grid-template-columns: repeat( auto-fit, - minmax(var(--media-browse-item-size, 175px), 0.33fr) + minmax(var(--media-browse-item-size, 175px), 0.1fr) ); grid-gap: 16px; margin: 8px 0px; + padding: 0px 24px; } - :host(:not([narrow])) .children { - padding: 0px 24px; + :host([dialog]) .children { + grid-template-columns: repeat( + auto-fit, + minmax(var(--media-browse-item-size, 175px), 0.33fr) + ); } .child { @@ -569,7 +701,7 @@ export class HaMediaPlayerBrowse extends LitElement { width: 100%; } - ha-card { + .children ha-card { width: 100%; padding-bottom: 100%; position: relative; @@ -577,6 +709,11 @@ export class HaMediaPlayerBrowse extends LitElement { background-size: cover; background-repeat: no-repeat; background-position: center; + transition: padding-bottom 0.1s ease-out; + } + + .portrait.children ha-card { + padding-bottom: 150%; } .child .folder, @@ -592,24 +729,43 @@ export class HaMediaPlayerBrowse extends LitElement { } .child .play { + transition: color 0.5s; + border-radius: 50%; + bottom: calc(50% - 35px); + right: calc(50% - 35px); + opacity: 0; + transition: opacity 0.1s ease-out; + } + + .child .play:not(.can_expand) { + --mdc-icon-button-size: 70px; + --mdc-icon-size: 48px; + } + + .ha-card-parent:hover .play:not(.can_expand) { + opacity: 1; + color: var(--primary-color); + } + + .child .play.can_expand { + opacity: 1; + background-color: rgba(var(--rgb-card-background-color), 0.5); bottom: 4px; right: 4px; - transition: all 0.5s; - background-color: rgba(var(--rgb-card-background-color), 0.5); - border-radius: 50%; } .child .play:hover { color: var(--primary-color); } - ha-card:hover { + .ha-card-parent:hover ha-card { opacity: 0.5; } .child .title { font-size: 16px; padding-top: 8px; + padding-left: 2px; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; @@ -619,6 +775,7 @@ export class HaMediaPlayerBrowse extends LitElement { .child .type { font-size: 12px; color: var(--secondary-text-color); + padding-left: 2px; } mwc-list-item .graphic { @@ -643,6 +800,14 @@ export class HaMediaPlayerBrowse extends LitElement { background-color: transparent; } + mwc-list-item .title { + margin-left: 16px; + } + mwc-list-item[dir="rtl"] .title { + margin-right: 16px; + margin-left: 0; + } + /* ============= Narrow ============= */ :host([narrow]) { @@ -657,6 +822,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 +865,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 +884,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 0000000000..eead0784c2 --- /dev/null +++ b/src/components/user/ha-person-badge.ts @@ -0,0 +1,74 @@ +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` + :host { + display: contents; + } + .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 3e418ff596..cb978dd004 100644 --- a/src/components/user/ha-user-badge.ts +++ b/src/components/user/ha-user-badge.ts @@ -3,17 +3,20 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, TemplateResult, } from "lit-element"; -import { toggleAttribute } from "../../common/dom/toggle_attribute"; +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 { User } from "../../data/user"; -import { CurrentUser } from "../../types"; +import { CurrentUser, HomeAssistant } from "../../types"; -const computeInitials = (name: string) => { +export const computeInitials = (name: string) => { if (!name) { - return "user"; + return "?"; } return ( name @@ -28,27 +31,89 @@ const computeInitials = (name: string) => { }; @customElement("ha-user-badge") -class StateBadge extends LitElement { - @property() public user?: User | CurrentUser; +class UserBadge extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; - protected render(): TemplateResult { - const user = this.user; - const initials = user ? computeInitials(user.name) : "?"; - return html` ${initials} `; - } + @property({ attribute: false }) public user?: User | CurrentUser; + + @internalProperty() private _personPicture?: string; + + private _personEntityId?: string; protected updated(changedProps) { super.updated(changedProps); - toggleAttribute( - this, - "long", - (this.user ? computeInitials(this.user.name) : "?").length > 2 - ); + if (changedProps.has("user")) { + this._getPersonPicture(); + return; + } + const oldHass = changedProps.get("hass"); + if ( + this._personEntityId && + oldHass && + this.hass.states[this._personEntityId] !== + oldHass.states[this._personEntityId] + ) { + const state = this.hass.states[this._personEntityId]; + if (state) { + this._personPicture = state.attributes.entity_picture; + } else { + this._getPersonPicture(); + } + } else if (!this._personEntityId && oldHass) { + this._getPersonPicture(); + } + } + + protected render(): TemplateResult { + if (!this.hass || !this.user) { + return html``; + } + const picture = this._personPicture; + + if (picture) { + return html`
    `; + } + const initials = computeInitials(this.user.name); + return html`
    + ${initials} +
    `; + } + + private _getPersonPicture() { + this._personEntityId = undefined; + this._personPicture = undefined; + if (!this.hass || !this.user) { + return; + } + for (const entity of Object.values(this.hass.states)) { + if ( + entity.attributes.user_id === this.user.id && + computeStateDomain(entity) === "person" + ) { + this._personEntityId = entity.entity_id; + this._personPicture = entity.attributes.entity_picture; + break; + } + } } static get styles(): CSSResult { return css` :host { + display: contents; + } + .picture { + width: 40px; + height: 40px; + background-size: cover; + border-radius: 50%; + } + .initials { display: inline-block; box-sizing: border-box; width: 40px; @@ -60,8 +125,7 @@ class StateBadge extends LitElement { color: var(--text-light-primary-color, var(--primary-text-color)); overflow: hidden; } - - :host([long]) { + .initials.long { font-size: 80%; } `; @@ -70,6 +134,6 @@ class StateBadge extends LitElement { declare global { interface HTMLElementTagNameMap { - "ha-user-badge": StateBadge; + "ha-user-badge": UserBadge; } } diff --git a/src/components/user/ha-user-picker.ts b/src/components/user/ha-user-picker.ts index d9a7fda1ec..ecd99f188f 100644 --- a/src/components/user/ha-user-picker.ts +++ b/src/components/user/ha-user-picker.ts @@ -53,7 +53,11 @@ class HaUserPicker extends LitElement { ${this._sortedUsers(this.users).map( (user) => html` - + ${user.name} ` diff --git a/src/data/automation.ts b/src/data/automation.ts index 97395806db..7ef285e1e7 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; diff --git a/src/data/config_flow.ts b/src/data/config_flow.ts index a06f8f42ec..94cce83ba8 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 b3681d72a8..ad315096a3 100644 --- a/src/data/hassio/common.ts +++ b/src/data/hassio/common.ts @@ -9,7 +9,9 @@ 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; }; + +export const ignoredStatusCodes = new Set([502, 503, 504]); diff --git a/src/data/logbook.ts b/src/data/logbook.ts index c1ff44b6fe..00fd8e937d 100644 --- a/src/data/logbook.ts +++ b/src/data/logbook.ts @@ -23,7 +23,8 @@ export const getLogbookData = ( hass: HomeAssistant, startDate: string, endDate: string, - entityId?: string + entityId?: string, + entity_matches_only?: boolean ) => { const ALL_ENTITIES = "*"; @@ -51,7 +52,8 @@ export const getLogbookData = ( hass, startDate, endDate, - entityId !== ALL_ENTITIES ? entityId : undefined + entityId !== ALL_ENTITIES ? entityId : undefined, + entity_matches_only ).then((entries) => entries.reverse()); return DATA_CACHE[cacheKey][entityId]; }; @@ -60,11 +62,13 @@ const getLogbookDataFromServer = async ( hass: HomeAssistant, startDate: string, endDate: string, - entityId?: string + entityId?: string, + entity_matches_only?: boolean ) => { const url = `logbook/${startDate}?end_time=${endDate}${ entityId ? `&entity=${entityId}` : "" - }`; + }${entity_matches_only ? `&entity_matches_only` : ""}`; + return hass.callApi("GET", url); }; diff --git a/src/data/media-player.ts b/src/data/media-player.ts index 0a3d289259..393c06cb71 100644 --- a/src/data/media-player.ts +++ b/src/data/media-player.ts @@ -1,3 +1,21 @@ +import { + mdiAccountMusic, + mdiAccountMusicOutline, + mdiAlbum, + mdiApplication, + mdiDramaMasks, + mdiFileMusic, + mdiFolder, + mdiGamepadVariant, + mdiImage, + mdiMovie, + mdiMusic, + mdiPlaylistMusic, + mdiPodcast, + mdiTelevisionClassic, + mdiVideo, + mdiWeb, +} from "@mdi/js"; import type { HassEntity } from "home-assistant-js-websocket"; import type { HomeAssistant } from "../types"; @@ -20,9 +38,70 @@ export const CONTRAST_RATIO = 4.5; export type MediaPlayerBrowseAction = "pick" | "play"; +export const BROWSER_PLAYER = "browser"; + +export type MediaClassBrowserSetting = { + icon: string; + thumbnail_ratio?: string; + layout?: string; + show_list_images?: boolean; +}; + +export const MediaClassBrowserSettings: { + [type: string]: MediaClassBrowserSetting; +} = { + album: { icon: mdiAlbum, layout: "grid" }, + app: { icon: mdiApplication, layout: "grid" }, + artist: { icon: mdiAccountMusic, layout: "grid", show_list_images: true }, + channel: { + icon: mdiTelevisionClassic, + thumbnail_ratio: "portrait", + layout: "grid", + }, + composer: { + icon: mdiAccountMusicOutline, + layout: "grid", + show_list_images: true, + }, + contributing_artist: { + icon: mdiAccountMusic, + layout: "grid", + show_list_images: true, + }, + directory: { icon: mdiFolder, layout: "grid", show_list_images: true }, + episode: { + icon: mdiTelevisionClassic, + layout: "grid", + thumbnail_ratio: "portrait", + }, + game: { + icon: mdiGamepadVariant, + layout: "grid", + thumbnail_ratio: "portrait", + }, + genre: { icon: mdiDramaMasks, layout: "grid", show_list_images: true }, + image: { icon: mdiImage, layout: "grid" }, + movie: { icon: mdiMovie, thumbnail_ratio: "portrait", layout: "grid" }, + music: { icon: mdiMusic }, + playlist: { icon: mdiPlaylistMusic, layout: "grid", show_list_images: true }, + podcast: { icon: mdiPodcast, layout: "grid" }, + season: { + icon: mdiTelevisionClassic, + layout: "grid", + thumbnail_ratio: "portrait", + }, + track: { icon: mdiFileMusic }, + tv_show: { + icon: mdiTelevisionClassic, + layout: "grid", + thumbnail_ratio: "portrait", + }, + url: { icon: mdiWeb }, + video: { icon: mdiVideo, layout: "grid" }, +}; + export interface MediaPickedEvent { - media_content_id: string; - media_content_type: string; + item: MediaPlayerItem; } export interface MediaPlayerThumbnail { @@ -39,6 +118,8 @@ export interface MediaPlayerItem { title: string; media_content_type: string; media_content_id: string; + media_class: string; + children_media_class: string; can_play: boolean; can_expand: boolean; thumbnail?: string; @@ -58,6 +139,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 b24692c01f..80ad9882b7 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/refresh_token.ts b/src/data/refresh_token.ts new file mode 100644 index 0000000000..bc2752bdf0 --- /dev/null +++ b/src/data/refresh_token.ts @@ -0,0 +1,17 @@ +declare global { + interface HASSDomEvents { + "hass-refresh-tokens": undefined; + } +} + +export interface RefreshToken { + client_icon?: string; + client_id: string; + client_name?: string; + created_at: string; + id: string; + is_current: boolean; + last_used_at?: string; + last_used_ip?: string; + type: "normal" | "long_lived_access_token"; +} diff --git a/src/data/script.ts b/src/data/script.ts index 7a8e83d717..240472f743 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/data/weather.ts b/src/data/weather.ts index 369a64ec34..fa6f6354b0 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -200,7 +200,7 @@ export const weatherSVGStyles = css` fill: var(--weather-icon-sun-color, #fdd93c); } .moon { - fill: var(--weather-icon-moon-color, #fdf9cc); + fill: var(--weather-icon-moon-color, #fcf497); } .cloud-back { fill: var(--weather-icon-cloud-back-color, #d4d4d4); diff --git a/src/data/ws-templates.ts b/src/data/ws-templates.ts index aec9903803..6a331e1b11 100644 --- a/src/data/ws-templates.ts +++ b/src/data/ws-templates.ts @@ -1,20 +1,27 @@ import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket"; -interface RenderTemplateResult { +export interface RenderTemplateResult { result: string; + listeners: TemplateListeners; +} + +interface TemplateListeners { + all: boolean; + domains: string[]; + entities: string[]; } export const subscribeRenderTemplate = ( conn: Connection, - onChange: (result: string) => void, + onChange: (result: RenderTemplateResult) => void, params: { template: string; entity_ids?: string | string[]; variables?: object; } ): Promise => { - return conn.subscribeMessage( - (msg: RenderTemplateResult) => onChange(msg.result), - { type: "render_template", ...params } - ); + return conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), { + type: "render_template", + ...params, + }); }; diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index 1ac015ecde..92bbb21597 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -97,8 +97,13 @@ export const showConfigFlowDialog = ( }, renderExternalStepHeader(hass, step) { - return hass.localize( - `component.${step.handler}.config.step.${step.step_id}.title` + return ( + hass.localize( + `component.${step.handler}.config.step.${step.step_id}.title` + ) || + hass.localize( + "ui.panel.config.integrations.config_flow.external_step.open_site" + ) ); }, diff --git a/src/dialogs/config-flow/step-flow-pick-handler.ts b/src/dialogs/config-flow/step-flow-pick-handler.ts index c875a58218..cbfcf3b8d5 100644 --- a/src/dialogs/config-flow/step-flow-pick-handler.ts +++ b/src/dialogs/config-flow/step-flow-pick-handler.ts @@ -20,6 +20,7 @@ import { LocalizeFunc } from "../../common/translations/localize"; import "../../components/ha-icon-next"; import { domainToName } from "../../data/integration"; import { HomeAssistant } from "../../types"; +import { documentationUrl } from "../../util/documentation-url"; import { FlowConfig } from "./show-dialog-data-entry-flow"; import { configFlowContentStyles } from "./styles"; @@ -122,7 +123,7 @@ class StepFlowPickHandler extends LitElement { ${this.hass.localize( "ui.panel.config.integrations.note_about_website_reference" )}${this.hass.localize( diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts index b8fcea9a70..5df7cb4f1e 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 { @@ -57,7 +57,8 @@ class DialogBox extends LitElement { open ?scrimClickAction=${this._params.prompt} ?escapeKeyAction=${this._params.prompt} - @closed=${this._dismiss} + @closed=${this._dialogClosed} + defaultAction="ignore" .heading=${this._params.title ? this._params.title : this._params.confirmation && @@ -78,10 +79,10 @@ class DialogBox extends LitElement { ${this._params.prompt ? html` `} - + ${this._params.confirmText ? this._params.confirmText : this.hass.localize("ui.dialogs.generic.ok")} @@ -114,8 +119,8 @@ class DialogBox extends LitElement { } private _dismiss(): void { - if (this._params!.cancel) { - this._params!.cancel(); + if (this._params?.cancel) { + this._params.cancel(); } this._close(); } @@ -133,7 +138,17 @@ class DialogBox extends LitElement { this._close(); } + private _dialogClosed(ev) { + if (ev.detail.action === "ignore") { + return; + } + this.closeDialog(); + } + private _close(): void { + if (!this._params) { + return; + } this._params = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } diff --git a/src/dialogs/more-info/controls/more-info-automation.ts b/src/dialogs/more-info/controls/more-info-automation.ts index 8a25e93fe4..6a1a789b99 100644 --- a/src/dialogs/more-info/controls/more-info-automation.ts +++ b/src/dialogs/more-info/controls/more-info-automation.ts @@ -12,12 +12,13 @@ import { import "../../../components/ha-relative-time"; import { triggerAutomation } from "../../../data/automation"; import { HomeAssistant } from "../../../types"; +import { UNAVAILABLE_STATES } from "../../../data/entity"; @customElement("more-info-automation") class MoreInfoAutomation extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public stateObj?: HassEntity; + @property({ attribute: false }) public stateObj?: HassEntity; protected render(): TemplateResult { if (!this.hass || !this.stateObj) { @@ -34,7 +35,10 @@ class MoreInfoAutomation extends LitElement {
    - + ${this.hass.localize("ui.card.automation.trigger")}
    @@ -52,7 +56,7 @@ class MoreInfoAutomation extends LitElement { justify-content: space-between; } .actions { - margin: 36px 0 8px 0; + margin: 8px 0; text-align: right; } `; diff --git a/src/dialogs/more-info/controls/more-info-camera.ts b/src/dialogs/more-info/controls/more-info-camera.ts index d990cf25a0..7fa55778b2 100644 --- a/src/dialogs/more-info/controls/more-info-camera.ts +++ b/src/dialogs/more-info/controls/more-info-camera.ts @@ -4,9 +4,9 @@ import { css, CSSResult, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; @@ -47,8 +47,9 @@ class MoreInfoCamera extends LitElement { return html` ${this._cameraPrefs ? html` diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts index 6b0bba5572..7ce5896ad6 100644 --- a/src/dialogs/more-info/controls/more-info-light.ts +++ b/src/dialogs/more-info/controls/more-info-light.ts @@ -61,20 +61,20 @@ class MoreInfoLight extends LitElement { "is-on": this.stateObj.state === "on", })}" > + ${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS) + ? html` + + ` + : ""} ${this.stateObj.state === "on" ? html` - ${supportsFeature(this.stateObj!, SUPPORT_BRIGHTNESS) - ? html` - - ` - : ""} ${supportsFeature(this.stateObj, SUPPORT_COLOR_TEMP) ? html` ${this.stateObj.attributes.effect_list.map( (effect: string) => html` - ${effect} ` @@ -170,7 +170,7 @@ class MoreInfoLight extends LitElement { } private _effectChanged(ev: CustomEvent) { - const newVal = ev.detail.value; + const newVal = ev.detail.item.itemName; if (!newVal || this.stateObj!.attributes.effect === newVal) { return; 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 0e7a445c0b..2a54b7053c 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` @@ -188,14 +188,17 @@ class MoreInfoMediaPlayer extends LitElement {
    - +
    ` @@ -409,8 +412,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/controls/more-info-person.ts b/src/dialogs/more-info/controls/more-info-person.ts index ef8b3f803b..84fa6a733b 100644 --- a/src/dialogs/more-info/controls/more-info-person.ts +++ b/src/dialogs/more-info/controls/more-info-person.ts @@ -32,7 +32,7 @@ class MoreInfoPerson extends LitElement { return html` ${this.stateObj.attributes.latitude && this.stateObj.attributes.longitude ? html` @@ -78,6 +78,9 @@ class MoreInfoPerson extends LitElement { margin: 36px 0 8px 0; text-align: right; } + ha-map { + margin-top: 16px; + } `; } } diff --git a/src/dialogs/more-info/controls/more-info-timer.ts b/src/dialogs/more-info/controls/more-info-timer.ts index 809d697c70..c06f549896 100644 --- a/src/dialogs/more-info/controls/more-info-timer.ts +++ b/src/dialogs/more-info/controls/more-info-timer.ts @@ -26,15 +26,12 @@ class MoreInfoTimer extends LitElement { return html`
    ${this.stateObj.state === "idle" || this.stateObj.state === "paused" ? html` - + ${this.hass!.localize("ui.card.timer.actions.start")} ` @@ -42,7 +39,7 @@ class MoreInfoTimer extends LitElement { ${this.stateObj.state === "active" ? html` ${this.hass!.localize("ui.card.timer.actions.pause")} @@ -52,13 +49,13 @@ class MoreInfoTimer extends LitElement { ${this.stateObj.state === "active" || this.stateObj.state === "paused" ? html` ${this.hass!.localize("ui.card.timer.actions.cancel")} ${this.hass!.localize("ui.card.timer.actions.finish")} diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index b1953f678d..4d787863d8 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -1,38 +1,70 @@ import "@material/mwc-button"; import "@material/mwc-icon-button"; -import "../../components/ha-header-bar"; -import "../../components/ha-dialog"; -import "../../components/ha-svg-icon"; -import { isComponentLoaded } from "../../common/config/is_component_loaded"; -import { DOMAINS_MORE_INFO_NO_HISTORY } from "../../common/const"; -import { computeStateName } from "../../common/entity/compute_state_name"; -import { navigate } from "../../common/navigate"; -import { fireEvent } from "../../common/dom/fire_event"; -import "../../components/state-history-charts"; -import { removeEntityRegistryEntry } from "../../data/entity_registry"; -import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor"; -import "../../state-summary/state-card-content"; -import { showConfirmationDialog } from "../generic/show-dialog-box"; -import "./more-info-content"; +import "@material/mwc-tab"; +import "@material/mwc-tab-bar"; +import { mdiClose, mdiCog, mdiPencil } from "@mdi/js"; import { + css, customElement, + html, + internalProperty, LitElement, property, - internalProperty, - css, - html, } from "lit-element"; -import { haStyleDialog } from "../../resources/styles"; -import { HomeAssistant } from "../../types"; -import { getRecentWithCache } from "../../data/cached-history"; +import { cache } from "lit-html/directives/cache"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; +import { + DOMAINS_MORE_INFO_NO_HISTORY, + DOMAINS_WITH_MORE_INFO, +} from "../../common/const"; +import { dynamicElement } from "../../common/dom/dynamic-element-directive"; +import { fireEvent } from "../../common/dom/fire_event"; import { computeDomain } from "../../common/entity/compute_domain"; -import { mdiClose, mdiCog, mdiPencil } from "@mdi/js"; -import { HistoryResult } from "../../data/history"; +import { computeStateName } from "../../common/entity/compute_state_name"; +import { stateMoreInfoType } from "../../common/entity/state_more_info_type"; +import { navigate } from "../../common/navigate"; +import "../../components/ha-dialog"; +import "../../components/ha-header-bar"; +import "../../components/ha-svg-icon"; +import { removeEntityRegistryEntry } from "../../data/entity_registry"; +import { showEntityEditorDialog } from "../../panels/config/entities/show-dialog-entity-editor"; +import { haStyleDialog } from "../../resources/styles"; +import "../../state-summary/state-card-content"; +import { HomeAssistant } from "../../types"; +import { showConfirmationDialog } from "../generic/show-dialog-box"; +import "./ha-more-info-history"; +import "./ha-more-info-logbook"; const DOMAINS_NO_INFO = ["camera", "configurator"]; const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"]; const EDITABLE_DOMAINS = ["script"]; +const MORE_INFO_CONTROL_IMPORT = { + alarm_control_panel: () => import("./controls/more-info-alarm_control_panel"), + automation: () => import("./controls/more-info-automation"), + camera: () => import("./controls/more-info-camera"), + climate: () => import("./controls/more-info-climate"), + configurator: () => import("./controls/more-info-configurator"), + counter: () => import("./controls/more-info-counter"), + cover: () => import("./controls/more-info-cover"), + fan: () => import("./controls/more-info-fan"), + group: () => import("./controls/more-info-group"), + humidifier: () => import("./controls/more-info-humidifier"), + input_datetime: () => import("./controls/more-info-input_datetime"), + light: () => import("./controls/more-info-light"), + lock: () => import("./controls/more-info-lock"), + media_player: () => import("./controls/more-info-media_player"), + person: () => import("./controls/more-info-person"), + script: () => import("./controls/more-info-script"), + sun: () => import("./controls/more-info-sun"), + timer: () => import("./controls/more-info-timer"), + vacuum: () => import("./controls/more-info-vacuum"), + water_heater: () => import("./controls/more-info-water_heater"), + weather: () => import("./controls/more-info-weather"), + hidden: () => {}, + default: () => import("./controls/more-info-default"), +}; + export interface MoreInfoDialogParams { entityId: string | null; } @@ -43,11 +75,11 @@ export class MoreInfoDialog extends LitElement { @property({ type: Boolean, reflect: true }) public large = false; - @internalProperty() private _stateHistory?: HistoryResult; - @internalProperty() private _entityId?: string | null; - private _historyRefreshInterval?: number; + @internalProperty() private _moreInfoType?: string; + + @internalProperty() private _currTabIndex = 0; public showDialog(params: MoreInfoDialogParams) { this._entityId = params.entityId; @@ -55,24 +87,31 @@ export class MoreInfoDialog extends LitElement { this.closeDialog(); } this.large = false; - this._stateHistory = undefined; - if (this._computeShowHistoryComponent(this._entityId)) { - this._getStateHistory(); - clearInterval(this._historyRefreshInterval); - this._historyRefreshInterval = window.setInterval(() => { - this._getStateHistory(); - }, 60 * 1000); - } } public closeDialog() { this._entityId = undefined; - this._stateHistory = undefined; - clearInterval(this._historyRefreshInterval); - this._historyRefreshInterval = undefined; + this._currTabIndex = 0; fireEvent(this, "dialog-closed", { dialog: this.localName }); } + protected updated(changedProperties) { + if (!this.hass || !this._entityId || !changedProperties.has("_entityId")) { + return; + } + const stateObj = this.hass.states[this._entityId]; + if (!stateObj) { + return; + } + if (stateObj.attributes && "custom_ui_more_info" in stateObj.attributes) { + this._moreInfoType = stateObj.attributes.custom_ui_more_info; + } else { + const type = stateMoreInfoType(stateObj); + this._moreInfoType = `more-info-${type}`; + MORE_INFO_CONTROL_IMPORT[type](); + } + } + protected render() { if (!this._entityId) { return html``; @@ -93,85 +132,135 @@ export class MoreInfoDialog extends LitElement { hideActions data-domain=${domain} > - - - - -
    - ${computeStateName(stateObj)} -
    - ${this.hass.user!.is_admin - ? html` - - ` - : ""} - ${this.hass.user!.is_admin && - ((EDITABLE_DOMAINS_WITH_ID.includes(domain) && - stateObj.attributes.id) || - EDITABLE_DOMAINS.includes(domain)) - ? html` - - ` - : ""} -
    -
    - ${DOMAINS_NO_INFO.includes(domain) - ? "" - : html` - - `} - ${this._computeShowHistoryComponent(entityId) +
    + + + + +
    + ${computeStateName(stateObj)} +
    + ${this.hass.user!.is_admin + ? html` + + + + ` + : ""} + ${this.hass.user!.is_admin && + ((EDITABLE_DOMAINS_WITH_ID.includes(domain) && + stateObj.attributes.id) || + EDITABLE_DOMAINS.includes(domain)) + ? html` + + + + ` + : ""} +
    + ${DOMAINS_WITH_MORE_INFO.includes(domain) && + this._computeShowHistoryComponent(entityId) ? 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" - )} - ` - : ""} +
    +
    + ${cache( + this._currTabIndex === 0 + ? html` + ${DOMAINS_NO_INFO.includes(domain) + ? "" + : html` + + `} + ${DOMAINS_WITH_MORE_INFO.includes(domain) || + !this._computeShowHistoryComponent(entityId) + ? "" + : html` + `} + ${this._moreInfoType + ? dynamicElement(this._moreInfoType, { + hass: this.hass, + stateObj, + }) + : ""} + ${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` + + + ` + )}
    `; @@ -181,26 +270,10 @@ export class MoreInfoDialog extends LitElement { this.large = !this.large; } - private async _getStateHistory(): Promise { - if (!this._entityId) { - return; - } - this._stateHistory = await getRecentWithCache( - this.hass!, - this._entityId, - { - refresh: 60, - cacheKey: `more_info.${this._entityId}`, - hoursToShow: 24, - }, - this.hass!.localize, - this.hass!.language - ); - } - private _computeShowHistoryComponent(entityId) { return ( - isComponentLoaded(this.hass, "history") && + (isComponentLoaded(this.hass, "history") || + isComponentLoaded(this.hass, "logbook")) && !DOMAINS_MORE_INFO_NO_HISTORY.includes(computeDomain(entityId)) ); } @@ -243,6 +316,15 @@ export class MoreInfoDialog extends LitElement { this.closeDialog(); } + private _handleTabChanged(ev: CustomEvent): void { + const newTab = ev.detail.index; + if (newTab === this._currTabIndex) { + return; + } + + this._currTabIndex = ev.detail.index; + } + static get styles() { return [ haStyleDialog, @@ -256,8 +338,7 @@ export class MoreInfoDialog extends LitElement { --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)); + display: block; } @media all and (max-width: 450px), all and (max-height: 500px) { @@ -268,6 +349,11 @@ export class MoreInfoDialog extends LitElement { } } + .heading { + border-bottom: 1px solid + var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); + } + @media all and (min-width: 451px) and (min-height: 501px) { ha-dialog { --mdc-dialog-max-width: 90vw; @@ -307,7 +393,8 @@ export class MoreInfoDialog extends LitElement { } state-card-content, - state-history-charts { + ha-more-info-history, + ha-more-info-logbook:not(:last-child) { display: block; margin-bottom: 16px; } @@ -315,3 +402,9 @@ export class MoreInfoDialog extends LitElement { ]; } } + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-dialog": MoreInfoDialog; + } +} diff --git a/src/dialogs/more-info/ha-more-info-history.ts b/src/dialogs/more-info/ha-more-info-history.ts new file mode 100644 index 0000000000..93970b7b93 --- /dev/null +++ b/src/dialogs/more-info/ha-more-info-history.ts @@ -0,0 +1,95 @@ +import { + customElement, + html, + internalProperty, + LitElement, + property, + PropertyValues, + TemplateResult, +} from "lit-element"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; +import { throttle } from "../../common/util/throttle"; +import "../../components/state-history-charts"; +import { getRecentWithCache } from "../../data/cached-history"; +import { HistoryResult } from "../../data/history"; +import { HomeAssistant } from "../../types"; + +@customElement("ha-more-info-history") +export class MoreInfoHistory extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public entityId!: string; + + @internalProperty() private _stateHistory?: HistoryResult; + + private _throttleGetStateHistory = throttle(() => { + this._getStateHistory(); + }, 10000); + + protected render(): TemplateResult { + if (!this.entityId) { + return html``; + } + + return html`${isComponentLoaded(this.hass, "history") + ? html`` + : ""} `; + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + + if (changedProps.has("entityId")) { + this._stateHistory = undefined; + + if (!this.entityId) { + return; + } + + this._throttleGetStateHistory(); + return; + } + + if (!this.entityId || !changedProps.has("hass")) { + return; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + + if ( + oldHass && + this.hass.states[this.entityId] !== oldHass?.states[this.entityId] + ) { + // wait for commit of data (we only account for the default setting of 1 sec) + setTimeout(this._throttleGetStateHistory, 1000); + } + } + + private async _getStateHistory(): Promise { + if (!isComponentLoaded(this.hass, "history")) { + return; + } + this._stateHistory = await getRecentWithCache( + this.hass!, + this.entityId, + { + refresh: 60, + cacheKey: `more_info.${this.entityId}`, + hoursToShow: 24, + }, + this.hass!.localize, + this.hass!.language + ); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-history": MoreInfoHistory; + } +} diff --git a/src/dialogs/more-info/ha-more-info-logbook.ts b/src/dialogs/more-info/ha-more-info-logbook.ts new file mode 100644 index 0000000000..6c559e7dae --- /dev/null +++ b/src/dialogs/more-info/ha-more-info-logbook.ts @@ -0,0 +1,169 @@ +import { + css, + customElement, + html, + internalProperty, + LitElement, + property, + PropertyValues, + TemplateResult, +} from "lit-element"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; +import { computeStateDomain } from "../../common/entity/compute_state_domain"; +import { throttle } from "../../common/util/throttle"; +import "../../components/ha-circular-progress"; +import "../../components/state-history-charts"; +import { getLogbookData, LogbookEntry } from "../../data/logbook"; +import "../../panels/logbook/ha-logbook"; +import { haStyle, haStyleScrollbar } from "../../resources/styles"; +import { HomeAssistant } from "../../types"; + +@customElement("ha-more-info-logbook") +export class MoreInfoLogbook extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public entityId!: string; + + @internalProperty() private _logbookEntries?: LogbookEntry[]; + + @internalProperty() private _persons = {}; + + private _lastLogbookDate?: Date; + + private _throttleGetLogbookEntries = throttle(() => { + this._getLogBookData(); + }, 10000); + + protected render(): TemplateResult { + if (!this.entityId) { + return html``; + } + const stateObj = this.hass.states[this.entityId]; + + if (!stateObj) { + return html``; + } + + return html` + ${isComponentLoaded(this.hass, "logbook") + ? !this._logbookEntries + ? html` + + ` + : this._logbookEntries.length + ? html` + + ` + : html`
    + ${this.hass.localize("ui.components.logbook.entries_not_found")} +
    ` + : ""} + `; + } + + protected firstUpdated(): void { + this._fetchPersonNames(); + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + + if (changedProps.has("entityId")) { + this._lastLogbookDate = undefined; + this._logbookEntries = undefined; + + if (!this.entityId) { + return; + } + + this._throttleGetLogbookEntries(); + return; + } + + if (!this.entityId || !changedProps.has("hass")) { + return; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + + if ( + oldHass && + this.hass.states[this.entityId] !== oldHass?.states[this.entityId] + ) { + // wait for commit of data (we only account for the default setting of 1 sec) + setTimeout(this._throttleGetLogbookEntries, 1000); + } + } + + private async _getLogBookData() { + if (!isComponentLoaded(this.hass, "logbook")) { + return; + } + const lastDate = + this._lastLogbookDate || + new Date(new Date().getTime() - 24 * 60 * 60 * 1000); + const now = new Date(); + const newEntries = await getLogbookData( + this.hass, + lastDate.toISOString(), + now.toISOString(), + this.entityId, + true + ); + this._logbookEntries = this._logbookEntries + ? [...newEntries, ...this._logbookEntries] + : newEntries; + this._lastLogbookDate = now; + } + + private _fetchPersonNames() { + Object.values(this.hass.states).forEach((entity) => { + if ( + entity.attributes.user_id && + computeStateDomain(entity) === "person" + ) { + this._persons[entity.attributes.user_id] = + entity.attributes.friendly_name; + } + }); + } + + static get styles() { + return [ + haStyle, + haStyleScrollbar, + css` + .no-entries { + text-align: center; + padding: 16px; + color: var(--secondary-text-color); + } + ha-logbook { + max-height: 250px; + overflow: auto; + } + ha-circular-progress { + display: flex; + justify-content: center; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-logbook": MoreInfoLogbook; + } +} diff --git a/src/dialogs/more-info/more-info-content.ts b/src/dialogs/more-info/more-info-content.ts deleted file mode 100644 index 369d9e8d4f..0000000000 --- a/src/dialogs/more-info/more-info-content.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { HassEntity } from "home-assistant-js-websocket"; -import { property, PropertyValues, UpdatingElement } from "lit-element"; -import dynamicContentUpdater from "../../common/dom/dynamic_content_updater"; -import { stateMoreInfoType } from "../../common/entity/state_more_info_type"; -import { HomeAssistant } from "../../types"; -import "./controls/more-info-alarm_control_panel"; -import "./controls/more-info-automation"; -import "./controls/more-info-camera"; -import "./controls/more-info-climate"; -import "./controls/more-info-configurator"; -import "./controls/more-info-counter"; -import "./controls/more-info-cover"; -import "./controls/more-info-default"; -import "./controls/more-info-fan"; -import "./controls/more-info-group"; -import "./controls/more-info-humidifier"; -import "./controls/more-info-input_datetime"; -import "./controls/more-info-light"; -import "./controls/more-info-lock"; -import "./controls/more-info-media_player"; -import "./controls/more-info-person"; -import "./controls/more-info-script"; -import "./controls/more-info-sun"; -import "./controls/more-info-timer"; -import "./controls/more-info-vacuum"; -import "./controls/more-info-water_heater"; -import "./controls/more-info-weather"; - -class MoreInfoContent extends UpdatingElement { - @property({ attribute: false }) public hass?: HomeAssistant; - - @property() public stateObj?: HassEntity; - - private _detachedChild?: ChildNode; - - protected firstUpdated(): void { - this.style.position = "relative"; - this.style.display = "block"; - } - - // This is not a lit element, but an updating element, so we implement update - protected update(changedProps: PropertyValues): void { - super.update(changedProps); - const stateObj = this.stateObj; - const hass = this.hass; - - if (!stateObj || !hass) { - if (this.lastChild) { - this._detachedChild = this.lastChild; - // Detach child to prevent it from doing work. - this.removeChild(this.lastChild); - } - return; - } - - if (this._detachedChild) { - this.appendChild(this._detachedChild); - this._detachedChild = undefined; - } - - const moreInfoType = - stateObj.attributes && "custom_ui_more_info" in stateObj.attributes - ? stateObj.attributes.custom_ui_more_info - : "more-info-" + stateMoreInfoType(stateObj); - - dynamicContentUpdater(this, moreInfoType.toUpperCase(), { - hass, - stateObj, - }); - } -} - -customElements.define("more-info-content", MoreInfoContent); diff --git a/src/entrypoints/app.ts b/src/entrypoints/app.ts index bb4b54b6e7..a4dd8580d4 100644 --- a/src/entrypoints/app.ts +++ b/src/entrypoints/app.ts @@ -7,5 +7,3 @@ import "../util/legacy-support"; setPassiveTouchGestures(true); (window as any).frontendVersion = __VERSION__; - -import("../resources/html-import/polyfill"); diff --git a/src/html/index.html.template b/src/html/index.html.template index 5ec5dd1dc1..f5d8be97d1 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; @@ -100,9 +100,5 @@ {% endfor -%} } - - {% for extra_url in extra_urls -%} - - {% endfor -%} diff --git a/src/html/onboarding.html.template b/src/html/onboarding.html.template index cf95d09362..f4bc333ede 100644 --- a/src/html/onboarding.html.template +++ b/src/html/onboarding.html.template @@ -5,6 +5,20 @@ <%= renderTemplate('_header') %> diff --git a/src/layouts/hass-error-screen.ts b/src/layouts/hass-error-screen.ts index eab453b29e..8776184498 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/hass-tabs-subpage.ts b/src/layouts/hass-tabs-subpage.ts index 2f35492c5c..c110a02836 100644 --- a/src/layouts/hass-tabs-subpage.ts +++ b/src/layouts/hass-tabs-subpage.ts @@ -3,26 +3,26 @@ import { css, CSSResult, customElement, + eventOptions, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, - eventOptions, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../common/config/is_component_loaded"; -import { navigate } from "../common/navigate"; -import "../components/ha-menu-button"; -import "../components/ha-icon-button-arrow-prev"; -import { HomeAssistant, Route } from "../types"; -import "../components/ha-svg-icon"; -import "../components/ha-icon"; -import "../components/ha-tab"; import { restoreScroll } from "../common/decorators/restore-scroll"; +import { navigate } from "../common/navigate"; import { computeRTL } from "../common/util/compute_rtl"; +import "../components/ha-icon"; +import "../components/ha-icon-button-arrow-prev"; +import "../components/ha-menu-button"; +import "../components/ha-svg-icon"; +import "../components/ha-tab"; +import { HomeAssistant, Route } from "../types"; export interface PageNavigation { path: string; @@ -132,7 +132,7 @@ class HassTabsSubpage extends LitElement { this.hass.language, this.narrow ); - + const showTabs = tabs.length > 1 || !this.narrow; return html`
    ${this.mainPage @@ -152,7 +152,7 @@ class HassTabsSubpage extends LitElement { ${this.narrow ? html`
    ` : ""} - ${tabs.length > 1 || !this.narrow + ${showTabs ? html`
    ${tabs} @@ -163,10 +163,15 @@ class HassTabsSubpage extends LitElement {
    -
    +
    -
    +
    + +
    `; } @@ -274,12 +279,13 @@ class HassTabsSubpage extends LitElement { margin-left: env(safe-area-inset-left); margin-right: env(safe-area-inset-right); height: calc(100% - 65px); + height: calc(100% - 65px - env(safe-area-inset-bottom)); overflow-y: auto; overflow: auto; -webkit-overflow-scrolling: touch; } - :host([narrow]) .content { + :host([narrow]) .content.tabs { height: calc(100% - 128px); height: calc(100% - 128px - env(safe-area-inset-bottom)); } @@ -290,7 +296,7 @@ class HassTabsSubpage extends LitElement { bottom: calc(16px + env(safe-area-inset-bottom)); z-index: 1; } - :host([narrow]) #fab { + :host([narrow]) #fab.tabs { bottom: calc(84px + env(safe-area-inset-bottom)); } #fab[is-wide] { diff --git a/src/layouts/home-assistant-main.ts b/src/layouts/home-assistant-main.ts index 31609dbdc5..a144b837a1 100644 --- a/src/layouts/home-assistant-main.ts +++ b/src/layouts/home-assistant-main.ts @@ -7,12 +7,13 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, PropertyValues, TemplateResult, } from "lit-element"; -import { fireEvent } from "../common/dom/fire_event"; +import { fireEvent, HASSDomEvent } from "../common/dom/fire_event"; import { listenMediaQuery } from "../common/dom/media_query"; import { toggleAttribute } from "../common/dom/toggle_attribute"; import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer"; @@ -25,8 +26,16 @@ declare global { // for fire event interface HASSDomEvents { "hass-toggle-menu": undefined; + "hass-edit-sidebar": EditSideBarEvent; "hass-show-notifications": undefined; } + interface HTMLElementEventMap { + "hass-edit-sidebar": HASSDomEvent; + } +} + +interface EditSideBarEvent { + editMode: boolean; } @customElement("home-assistant-main") @@ -35,7 +44,9 @@ class HomeAssistantMain extends LitElement { @property() public route?: Route; - @property({ type: Boolean }) private narrow?: boolean; + @property({ type: Boolean }) public narrow?: boolean; + + @internalProperty() private _sidebarEditMode = false; protected render(): TemplateResult { const hass = this.hass; @@ -47,7 +58,9 @@ class HomeAssistantMain extends LitElement { const sidebarNarrow = this._sidebarNarrow; const disableSwipe = - !sidebarNarrow || NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1; + this._sidebarEditMode || + !sidebarNarrow || + NON_SWIPABLE_PANELS.indexOf(hass.panelUrl) !== -1; // Style block in render because of the mixin that is not supported return html` @@ -75,6 +88,7 @@ class HomeAssistantMain extends LitElement { @@ -92,7 +106,28 @@ class HomeAssistantMain extends LitElement { protected firstUpdated() { import(/* webpackChunkName: "ha-sidebar" */ "../components/ha-sidebar"); + this.addEventListener( + "hass-edit-sidebar", + (ev: HASSDomEvent) => { + this._sidebarEditMode = ev.detail.editMode; + + if (this._sidebarEditMode) { + if (this._sidebarNarrow) { + this.drawer.open(); + } else { + fireEvent(this, "hass-dock-sidebar", { + dock: "docked", + }); + setTimeout(() => this.appLayout.resetLayout()); + } + } + } + ); + this.addEventListener("hass-toggle-menu", () => { + if (this._sidebarEditMode) { + return; + } if (this._sidebarNarrow) { if (this.drawer.opened) { this.drawer.close(); diff --git a/src/layouts/partial-panel-resolver.ts b/src/layouts/partial-panel-resolver.ts index 280a32eb16..ad04160caa 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/onboarding/onboarding-create-user.ts b/src/onboarding/onboarding-create-user.ts index d12fcd97ce..bb3f27ba2e 100644 --- a/src/onboarding/onboarding-create-user.ts +++ b/src/onboarding/onboarding-create-user.ts @@ -6,9 +6,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; 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 4a40b0ee51..f9f82bbd5b 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,12 +168,12 @@ export default class HaAutomationActionRow extends LitElement { "ui.panel.config.automation.editor.edit_yaml" )} - + ${this.hass.localize( "ui.panel.config.automation.editor.actions.duplicate" )} - + ${this.hass.localize( "ui.panel.config.automation.editor.actions.delete" )} @@ -261,6 +263,7 @@ export default class HaAutomationActionRow extends LitElement { this._switchYamlMode(); break; case 1: + fireEvent(this, "duplicate"); break; case 2: this._onDelete(); @@ -333,7 +336,6 @@ export default class HaAutomationActionRow extends LitElement { --mdc-theme-text-primary-on-background: var(--disabled-text-color); } .warning { - color: var(--warning-color); margin-bottom: 8px; } .warning ul { diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts index d6e815ea63..77db982c3b 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 0bca89bf91..fa1f52f349 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 3fd2cd95cb..13adc9aa74 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 { html } from "lit-html"; -import { - RepeatAction, - Action, - CountRepeat, - WhileRepeat, - UntilRepeat, -} from "../../../../../data/script"; -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 { CSSResult, customElement, LitElement, property } from "lit-element"; +import { html } from "lit-html"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import "../ha-automation-action"; -import { Condition } from "../../../../lovelace/common/validate-condition"; +import { + Action, + CountRepeat, + RepeatAction, + UntilRepeat, + WhileRepeat, +} from "../../../../../data/script"; import { haStyle } from "../../../../../resources/styles"; +import { HomeAssistant } from "../../../../../types"; +import { Condition } from "../../../../lovelace/common/validate-condition"; +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 5ef46a6875..c7e564bcb8 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 0000000000..2d99185f5a --- /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 e585d52032..2cf6a86299 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 1afc44967c..89bbfcab54 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,25 @@ -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"; +import { haStyle } from "../../../../resources/styles"; export interface ConditionElement extends LitElement { condition: Condition; @@ -81,12 +82,12 @@ export default class HaAutomationConditionRow extends LitElement { "ui.panel.config.automation.editor.edit_yaml" )}
    - + ${this.hass.localize( "ui.panel.config.automation.editor.actions.duplicate" )} - + ${this.hass.localize( "ui.panel.config.automation.editor.actions.delete" )} @@ -109,6 +110,7 @@ export default class HaAutomationConditionRow extends LitElement { this._switchYamlMode(); break; case 1: + fireEvent(this, "duplicate"); break; case 2: this._onDelete(); @@ -133,20 +135,23 @@ export default class HaAutomationConditionRow extends LitElement { this._yamlMode = !this._yamlMode; } - static get styles(): CSSResult { - return css` - .card-menu { - float: right; - z-index: 3; - --mdc-theme-text-primary-on-background: var(--primary-text-color); - } - .rtl .card-menu { - float: left; - } - mwc-list-item[disabled] { - --mdc-theme-text-primary-on-background: var(--disabled-text-color); - } - `; + static get styles(): CSSResult[] { + return [ + haStyle, + css` + .card-menu { + float: right; + z-index: 3; + --mdc-theme-text-primary-on-background: var(--primary-text-color); + } + .rtl .card-menu { + float: left; + } + mwc-list-item[disabled] { + --mdc-theme-text-primary-on-background: var(--disabled-text-color); + } + `, + ]; } } diff --git a/src/panels/config/automation/condition/ha-automation-condition.ts b/src/panels/config/automation/condition/ha-automation-condition.ts index 05367bd6a2..b4529c4650 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 656fc999e6..71259efe5d 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 7f790194fc..baac37cf35 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,38 +16,130 @@ import { handleChangeEvent, } from "../ha-automation-condition-row"; +const includeDomains = ["input_datetime"]; + @customElement("ha-automation-condition-time") export class HaTimeCondition extends LitElement implements ConditionElement { @property({ attribute: false }) public hass!: HomeAssistant; @property() public condition!: TimeCondition; + @internalProperty() private _inputModeBefore?: boolean; + + @internalProperty() private _inputModeAfter?: boolean; + public static get defaultConfig() { return {}; } 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); } diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 255edefff7..5a39e7eac8 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"; @@ -35,6 +39,7 @@ import "../../../layouts/ha-app-layout"; import "../../../layouts/hass-tabs-subpage"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import "./action/ha-automation-action"; @@ -42,9 +47,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 +55,7 @@ declare global { // for fire event interface HASSDomEvents { "ui-mode-not-available": Error; + duplicate: undefined; } } @@ -92,14 +95,25 @@ export class HaAutomationEditor extends LitElement { ${!this.automationId ? "" : html` - + + + + > + + `} ${this._config ? html` @@ -146,7 +160,10 @@ export class HaAutomationEditor extends LitElement { "ui.panel.config.automation.editor.modes.description", "documentation_link", html`
    ${this.hass.localize( @@ -234,7 +251,10 @@ export class HaAutomationEditor extends LitElement { )}

    @@ -263,7 +283,10 @@ export class HaAutomationEditor extends LitElement { )}

    @@ -292,7 +315,10 @@ export class HaAutomationEditor extends LitElement { )}

    @@ -473,6 +499,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/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index e56b62fe2b..d95e80f079 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -25,6 +25,7 @@ import { showAutomationEditor, triggerAutomation, } from "../../../data/automation"; +import { UNAVAILABLE_STATES } from "../../../data/entity"; import "../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; @@ -35,9 +36,9 @@ import { showThingtalkDialog } from "./show-dialog-thingtalk"; class HaAutomationPicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public isWide!: boolean; + @property({ type: Boolean }) public isWide!: boolean; - @property() public narrow!: boolean; + @property({ type: Boolean }) public narrow!: boolean; @property() public route!: Route; @@ -58,7 +59,7 @@ class HaAutomationPicker extends LitElement { toggle: { title: "", type: "icon", - template: (_toggle, automation) => + template: (_toggle, automation: any) => html` html` + template: (_info, automation: any) => html` this._execute(ev)} + .disabled=${UNAVAILABLE_STATES.includes(automation.state)} > ${this.hass.localize("ui.card.automation.trigger")} 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 a50373bbc8..aa80ff1a96 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,12 +113,12 @@ export default class HaAutomationTriggerRow extends LitElement { "ui.panel.config.automation.editor.edit_yaml" )} - + ${this.hass.localize( "ui.panel.config.automation.editor.actions.duplicate" )} - + ${this.hass.localize( "ui.panel.config.automation.editor.actions.delete" )} @@ -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 a074cbb881..953ec67037 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 068dbd6856..0c54205b2b 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 7ea3043660..1d9e6fa677 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/cloud/alexa/cloud-alexa.ts b/src/panels/config/cloud/alexa/cloud-alexa.ts index 5614318886..9fb80369d9 100644 --- a/src/panels/config/cloud/alexa/cloud-alexa.ts +++ b/src/panels/config/cloud/alexa/cloud-alexa.ts @@ -103,7 +103,9 @@ class CloudAlexa extends LitElement { this._entities.forEach((entity) => { const stateObj = this.hass.states[entity.entity_id]; - const config = this._entityConfigs[entity.entity_id] || {}; + const config = this._entityConfigs[entity.entity_id] || { + should_expose: null, + }; const isExposed = emptyFilter ? this._configIsExposed(entity.entity_id, config) : filterFunc(entity.entity_id); @@ -122,6 +124,26 @@ class CloudAlexa extends LitElement { ? exposedCards : notExposedCards; + const iconButton = html` + + `; + target.push(html`
    @@ -137,67 +159,50 @@ class CloudAlexa extends LitElement { .map((ifc) => ifc.replace(/(Alexa.|Controller)/g, "")) .join(", ")} - - - - - - ${this.hass!.localize( - "ui.panel.config.cloud.alexa.expose_entity" - )} - - - - ${this.hass!.localize( - "ui.panel.config.cloud.alexa.dont_expose_entity" - )} - - - - ${this.hass!.localize( - "ui.panel.config.cloud.alexa.follow_domain" - )} - - - + ${!emptyFilter + ? html`${iconButton}` + : html` + ${iconButton} + + ${this.hass!.localize( + "ui.panel.config.cloud.google.expose_entity" + )} + + + + ${this.hass!.localize( + "ui.panel.config.cloud.google.dont_expose_entity" + )} + + + + ${this.hass!.localize( + "ui.panel.config.cloud.google.follow_domain" + )} + + + `}
    @@ -319,9 +324,7 @@ class CloudAlexa extends LitElement { } private _configIsExposed(entityId: string, config: AlexaEntityConfig) { - return config.should_expose === null - ? this._configIsDomainExposed(entityId) - : config.should_expose; + return config.should_expose ?? this._configIsDomainExposed(entityId); } private async _exposeChanged(ev: CustomEvent) { diff --git a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts index b590848f99..1561ed7420 100644 --- a/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts +++ b/src/panels/config/cloud/dialog-manage-cloudhook/dialog-manage-cloudhook.ts @@ -14,6 +14,7 @@ import type { HaPaperDialog } from "../../../../components/dialog/ha-paper-dialo import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../resources/styles"; import { HomeAssistant } from "../../../../types"; +import { documentationUrl } from "../../../../util/documentation-url"; import { WebhookDialogParams } from "./show-dialog-manage-cloudhook"; const inputLabel = "Public URL – Click to copy to clipboard"; @@ -37,8 +38,11 @@ export class DialogManageCloudhook extends LitElement { const { webhook, cloudhook } = this._params; const docsUrl = webhook.domain === "automation" - ? "https://www.home-assistant.io/docs/automation/trigger/#webhook-trigger" - : `https://www.home-assistant.io/integrations/${webhook.domain}/`; + ? documentationUrl( + this.hass!, + "/docs/automation/trigger/#webhook-trigger" + ) + : documentationUrl(this.hass!, `/integrations/${webhook.domain}/`); return html`

    diff --git a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts index c29e22eaa0..40067a0eaf 100644 --- a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts +++ b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts @@ -109,7 +109,9 @@ class CloudGoogleAssistant extends LitElement { this._entities.forEach((entity) => { const stateObj = this.hass.states[entity.entity_id]; - const config = this._entityConfigs[entity.entity_id] || {}; + const config = this._entityConfigs[entity.entity_id] || { + should_expose: null, + }; const isExposed = emptyFilter ? this._configIsExposed(entity.entity_id, config) : filterFunc(entity.entity_id); @@ -128,6 +130,26 @@ class CloudGoogleAssistant extends LitElement { ? exposedCards : notExposedCards; + const iconButton = html` + + `; + target.push(html`
    @@ -142,67 +164,50 @@ class CloudGoogleAssistant extends LitElement { .map((trait) => trait.substr(trait.lastIndexOf(".") + 1)) .join(", ")} - - - - - - ${this.hass!.localize( - "ui.panel.config.cloud.google.expose_entity" - )} - - - - ${this.hass!.localize( - "ui.panel.config.cloud.google.dont_expose_entity" - )} - - - - ${this.hass!.localize( - "ui.panel.config.cloud.google.follow_domain" - )} - - - + ${!emptyFilter + ? html`${iconButton}` + : html` + ${iconButton} + + ${this.hass!.localize( + "ui.panel.config.cloud.google.expose_entity" + )} + + + + ${this.hass!.localize( + "ui.panel.config.cloud.google.dont_expose_entity" + )} + + + + ${this.hass!.localize( + "ui.panel.config.cloud.google.follow_domain" + )} + + + `}
    ${entity.might_2fa ? html` @@ -324,9 +329,7 @@ class CloudGoogleAssistant extends LitElement { } private _configIsExposed(entityId: string, config: GoogleEntityConfig) { - return config.should_expose === null - ? this._configIsDomainExposed(entityId) - : config.should_expose; + return config.should_expose ?? this._configIsDomainExposed(entityId); } private async _fetchData() { diff --git a/src/panels/config/cloud/register/cloud-register.js b/src/panels/config/cloud/register/cloud-register.js index d073e13c29..345b15c539 100644 --- a/src/panels/config/cloud/register/cloud-register.js +++ b/src/panels/config/cloud/register/cloud-register.js @@ -8,6 +8,7 @@ import "../../../../layouts/hass-subpage"; import { EventsMixin } from "../../../../mixins/events-mixin"; import LocalizeMixin from "../../../../mixins/localize-mixin"; import "../../../../styles/polymer-ha-style"; +import { documentationUrl } from "../../../../util/documentation-url"; import "../../ha-config-section"; /* @@ -70,8 +71,8 @@ class CloudRegister extends LocalizeMixin(EventsMixin(PolymerElement)) {

    [[localize('ui.panel.config.cloud.register.information4')]]

    @@ -137,6 +138,14 @@ class CloudRegister extends LocalizeMixin(EventsMixin(PolymerElement)) { } } + _computeDocumentationUrlTos(hass) { + return documentationUrl(hass, "/tos/"); + } + + _computeDocumentationUrlPrivacy(hass) { + return documentationUrl(hass, "/privacy/"); + } + _handleRegister() { let invalid = false; diff --git a/src/panels/config/customize/ha-form-customize.js b/src/panels/config/customize/ha-form-customize.js index 4c85d7b27f..2ad523125d 100644 --- a/src/panels/config/customize/ha-form-customize.js +++ b/src/panels/config/customize/ha-form-customize.js @@ -6,10 +6,11 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import LocalizeMixin from "../../../mixins/localize-mixin"; -import hassAttributeUtil from "../../../util/hass-attributes-util"; -import "./ha-form-customize-attributes"; -import "../ha-form-style"; import "../../../styles/polymer-ha-style"; +import { documentationUrl } from "../../../util/documentation-url"; +import hassAttributeUtil from "../../../util/hass-attributes-util"; +import "../ha-form-style"; +import "./ha-form-customize-attributes"; class HaFormCustomize extends LocalizeMixin(PolymerElement) { static get template() { @@ -30,7 +31,7 @@ class HaFormCustomize extends LocalizeMixin(PolymerElement) {
    [[localize('ui.panel.config.customize.warning.include_sentence')]] [[localize('ui.panel.config.customize.warning.include_link')]] + ${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 2197fe689b..ce639c75b8 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 0287b084cd..22c2ea190a 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/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index ad7c3d9e3d..366c852fa9 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -58,6 +58,8 @@ import { loadEntityEditorDialog, showEntityEditorDialog, } from "./show-dialog-entity-editor"; +import { haStyle } from "../../../resources/styles"; +import { UNAVAILABLE } from "../../../data/entity"; export interface StateEntity extends EntityRegistryEntry { readonly?: boolean; @@ -280,7 +282,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { for (const entry of entities) { const entity = this.hass.states[entry.entity_id]; - const unavailable = entity?.state === "unavailable"; + const unavailable = entity?.state === UNAVAILABLE; const restored = entity?.attributes.restored; if (!showUnavailable && unavailable) { @@ -378,7 +380,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { "ui.panel.config.entities.picker.disable_selected.button" )} - ${this.hass.localize( "ui.panel.config.entities.picker.remove_selected.button" )} mwc-button, - .header-btns > ha-icon-button { - margin: 8px; - } - .active-filters { - color: var(--primary-text-color); - position: relative; - display: flex; - align-items: center; - padding: 2px 2px 2px 8px; - margin-left: 4px; - font-size: 14px; - } - .active-filters ha-icon { - color: var(--primary-color); - } - .active-filters mwc-button { - margin-left: 8px; - } - .active-filters::before { - background-color: var(--primary-color); - opacity: 0.12; - border-radius: 4px; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - content: ""; - } - `; + static get styles(): CSSResult[] { + return [ + haStyle, + css` + hass-loading-screen { + --app-header-background-color: var(--sidebar-background-color); + --app-header-text-color: var(--sidebar-text-color); + } + a { + color: var(--primary-color); + } + h2 { + margin-top: 0; + font-family: var(--paper-font-headline_-_font-family); + -webkit-font-smoothing: var( + --paper-font-headline_-_-webkit-font-smoothing + ); + font-size: var(--paper-font-headline_-_font-size); + font-weight: var(--paper-font-headline_-_font-weight); + letter-spacing: var(--paper-font-headline_-_letter-spacing); + line-height: var(--paper-font-headline_-_line-height); + opacity: var(--dark-primary-opacity); + } + p { + font-family: var(--paper-font-subhead_-_font-family); + -webkit-font-smoothing: var( + --paper-font-subhead_-_-webkit-font-smoothing + ); + font-weight: var(--paper-font-subhead_-_font-weight); + line-height: var(--paper-font-subhead_-_line-height); + } + ha-data-table { + width: 100%; + --data-table-border-width: 0; + } + :host(:not([narrow])) ha-data-table { + height: calc(100vh - 65px); + display: block; + } + ha-button-menu { + margin-right: 8px; + } + .table-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); + } + search-input { + margin-left: 16px; + flex-grow: 1; + position: relative; + top: 2px; + } + .search-toolbar search-input { + margin-left: 8px; + top: 1px; + } + .search-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + color: var(--secondary-text-color); + } + .search-toolbar ha-button-menu { + position: static; + } + .selected-txt { + font-weight: bold; + padding-left: 16px; + } + .table-header .selected-txt { + margin-top: 20px; + } + .search-toolbar .selected-txt { + font-size: 16px; + } + .header-btns > mwc-button, + .header-btns > ha-icon-button { + margin: 8px; + } + .active-filters { + color: var(--primary-text-color); + position: relative; + display: flex; + align-items: center; + padding: 2px 2px 2px 8px; + margin-left: 4px; + font-size: 14px; + } + .active-filters ha-icon { + color: var(--primary-color); + } + .active-filters mwc-button { + margin-left: 8px; + } + .active-filters::before { + background-color: var(--primary-color); + opacity: 0.12; + border-radius: 4px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + content: ""; + } + `, + ]; } } diff --git a/src/panels/config/helpers/forms/ha-input_number-form.ts b/src/panels/config/helpers/forms/ha-input_number-form.ts index ddc675821f..0439227315 100644 --- a/src/panels/config/helpers/forms/ha-input_number-form.ts +++ b/src/panels/config/helpers/forms/ha-input_number-form.ts @@ -6,9 +6,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import { fireEvent } from "../../../../common/dom/fire_event"; @@ -48,12 +48,12 @@ class HaInputNumberForm extends LitElement { this._max = item.max ?? 100; this._min = item.min ?? 0; this._mode = item.mode || "slider"; - this._step = item.step || 1; + this._step = item.step ?? 1; this._unit_of_measurement = item.unit_of_measurement; } else { this._item = { min: 0, - max: 0, + max: 100, }; this._name = ""; this._icon = ""; @@ -176,8 +176,10 @@ class HaInputNumberForm extends LitElement { return; } ev.stopPropagation(); - const configValue = (ev.target as any).configValue; - const value = ev.detail.value; + const target = ev.target as any; + const configValue = target.configValue; + const value = + target.type === "number" ? Number(ev.detail.value) : ev.detail.value; if (this[`_${configValue}`] === value) { return; } diff --git a/src/panels/config/info/ha-config-info.ts b/src/panels/config/info/ha-config-info.ts index ce3156f055..222ea664fe 100644 --- a/src/panels/config/info/ha-config-info.ts +++ b/src/panels/config/info/ha-config-info.ts @@ -12,6 +12,7 @@ import { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; import "./integrations-card"; import "./system-health-card"; +import { documentationUrl } from "../../../util/documentation-url"; const JS_TYPE = __BUILD__; const JS_VERSION = __VERSION__; @@ -42,7 +43,7 @@ class HaConfigInfo extends LitElement { >

    diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts index dd738133f7..99d7f6b266 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` - -

    -
    -
    - + (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 dddeaf6754..a1625c7c2c 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 f23d7a7114..478ede0f8e 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 { + fetchOZWInstances, + 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"; -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, - networkStartingStatuses, -} from "../../../../../data/ozw"; 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 28402cbe4c..804e2073af 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 82% 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 6d67ed779e..013f2b794b 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 { + fetchOZWNetworkStatistics, + 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 { 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, - networkOfflineStatuses, - networkStartingStatuses, - OZWNetworkStatistics, -} from "../../../../../data/ozw"; +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,40 @@ 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) { + const serviceData = { instance_id: this.ozwInstance }; + return html` + + ${this.hass!.localize(`ui.panel.config.ozw.services.${service}`)} + + `; + } + static get styles(): CSSResultArray { return [ haStyle, @@ -248,6 +249,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 0000000000..f492cf579f --- /dev/null +++ b/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts @@ -0,0 +1,143 @@ +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 { HASSDomEvent } from "../../../../../common/dom/fire_event"; +import { navigate } from "../../../../../common/navigate"; +import "../../../../../components/buttons/ha-call-service-button"; +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 0000000000..f98660bed0 --- /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 0000000000..bd2fa6af3e --- /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 0000000000..baf68ddc96 --- /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/integrations/integration-panels/zwave/zwave-network.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts index 7d220ba93c..b9860c0589 100644 --- a/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts +++ b/src/panels/config/integrations/integration-panels/zwave/zwave-network.ts @@ -1,20 +1,20 @@ -import "../../../../../components/ha-icon-button"; -import "../../../../../components/ha-circular-progress"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import "../../../../../components/buttons/ha-call-api-button"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; +import "../../../../../components/ha-circular-progress"; import "../../../../../components/ha-icon"; +import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-service-description"; import { fetchNetworkStatus, @@ -26,6 +26,7 @@ import { } from "../../../../../data/zwave"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; +import { documentationUrl } from "../../../../../util/documentation-url"; import "../../../ha-config-section"; @customElement("zwave-network") @@ -71,7 +72,10 @@ export class ZwaveNetwork extends LitElement { )}

    diff --git a/src/panels/config/logs/dialog-system-log-detail.ts b/src/panels/config/logs/dialog-system-log-detail.ts index ad5f28c648..34d05afca9 100644 --- a/src/panels/config/logs/dialog-system-log-detail.ts +++ b/src/panels/config/logs/dialog-system-log-detail.ts @@ -1,14 +1,21 @@ +import "@material/mwc-icon-button/mwc-icon-button"; +import { mdiContentCopy } from "@mdi/js"; import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; +import "@polymer/paper-tooltip/paper-tooltip"; +import type { PaperTooltipElement } from "@polymer/paper-tooltip/paper-tooltip"; import { css, CSSResult, html, + internalProperty, LitElement, property, - internalProperty, + query, TemplateResult, } from "lit-element"; +import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/dialog/ha-paper-dialog"; +import "../../../components/ha-svg-icon"; import { domainToName, fetchIntegrationManifest, @@ -16,12 +23,11 @@ import { IntegrationManifest, } from "../../../data/integration"; import { getLoggedErrorIntegration } from "../../../data/system_log"; -import { PolymerChangedEvent } from "../../../polymer-types"; +import type { PolymerChangedEvent } from "../../../polymer-types"; import { haStyleDialog } from "../../../resources/styles"; -import { HomeAssistant } from "../../../types"; -import { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail"; +import type { HomeAssistant } from "../../../types"; +import type { SystemLogDetailDialogParams } from "./show-dialog-system-log-detail"; import { formatSystemLogTime } from "./util"; -import { fireEvent } from "../../../common/dom/fire_event"; class DialogSystemLogDetail extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -30,6 +36,8 @@ class DialogSystemLogDetail extends LitElement { @internalProperty() private _manifest?: IntegrationManifest; + @query("paper-tooltip") private _toolTip?: PaperTooltipElement; + public async showDialog(params: SystemLogDetailDialogParams): Promise { this._params = params; this._manifest = undefined; @@ -66,13 +74,25 @@ class DialogSystemLogDetail extends LitElement { opened @opened-changed="${this._openedChanged}" > -

    - ${this.hass.localize( - "ui.panel.config.logs.details", - "level", - item.level - )} -

    +
    +

    + ${this.hass.localize( + "ui.panel.config.logs.details", + "level", + item.level + )} +

    + + + + ${this.hass.localize("ui.common.copied")} +

    Logger: ${item.name}
    @@ -148,6 +168,25 @@ class DialogSystemLogDetail extends LitElement { } } + private _copyLog(): void { + const copyElement = this.shadowRoot?.querySelector( + "paper-dialog-scrollable" + ) as HTMLElement; + + const selection = window.getSelection()!; + const range = document.createRange(); + + range.selectNodeContents(copyElement); + selection.removeAllRanges(); + selection.addRange(range); + + document.execCommand("copy"); + window.getSelection()!.removeAllRanges(); + + this._toolTip!.show(); + setTimeout(() => this._toolTip?.hide(), 3000); + } + static get styles(): CSSResult[] { return [ haStyleDialog, @@ -164,6 +203,15 @@ class DialogSystemLogDetail extends LitElement { pre { margin-bottom: 0; } + .heading { + display: flex; + display: flex; + align-items: center; + justify-content: space-between; + } + .heading ha-svg-icon { + cursor: pointer; + } `, ]; } diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index 94c50807c1..043d407607 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -4,23 +4,24 @@ import { css, CSSResult, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import memoizeOne from "memoize-one"; -import "../../../components/ha-picture-upload"; -import type { HaPictureUpload } from "../../../components/ha-picture-upload"; import "../../../components/entity/ha-entities-picker"; import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-picture-upload"; +import type { HaPictureUpload } from "../../../components/ha-picture-upload"; import "../../../components/user/ha-user-picker"; import { PersonMutableParams } from "../../../data/person"; +import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; import { PolymerChangedEvent } from "../../../polymer-types"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; import { PersonDetailDialogParams } from "./show-dialog-person-detail"; -import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; const includeDomains = ["device_tracker"]; @@ -153,7 +154,10 @@ class DialogPersonDetail extends LitElement {

    + ${this.scriptEntityId + ? html` +
    + + + ${this.hass.localize("ui.card.script.execute")} + +
    + ` + : ``}
    @@ -208,7 +233,7 @@ export class HaScriptEditor extends LitElement { )}

    @@ -300,6 +325,18 @@ export class HaScriptEditor extends LitElement { } } + private async _runScript(ev) { + ev.stopPropagation(); + await triggerScript(this.hass, this.scriptEntityId); + showToast(this, { + message: this.hass.localize( + "ui.notification_toast.triggered", + "name", + this._config!.alias + ), + }); + } + private _modeChanged(ev: CustomEvent) { const mode = ((ev.target as PaperListboxElement)?.selectedItem as any) ?.mode; diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 99b6dce17e..d7b713596c 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -25,6 +25,7 @@ import { configSections } from "../ha-panel-config"; import "../../../components/ha-svg-icon"; import { mdiPlus } from "@mdi/js"; import { stateIcon } from "../../../common/entity/state_icon"; +import { documentationUrl } from "../../../util/documentation-url"; @customElement("ha-script-picker") class HaScriptPicker extends LitElement { @@ -187,7 +188,7 @@ class HaScriptPicker extends LitElement { ${this.hass.localize("ui.panel.config.script.picker.introduction")}

    diff --git a/src/panels/config/server_control/ha-config-server-control.ts b/src/panels/config/server_control/ha-config-server-control.ts index 196a694e8d..007fcfe7bb 100644 --- a/src/panels/config/server_control/ha-config-server-control.ts +++ b/src/panels/config/server_control/ha-config-server-control.ts @@ -14,6 +14,7 @@ import { } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; import { isServiceLoaded } from "../../../common/config/is_service_loaded"; +import { componentsWithService } from "../../../common/config/components_with_service"; import "../../../components/buttons/ha-call-service-button"; import "../../../components/ha-card"; import { checkCoreConfig } from "../../../data/core"; @@ -51,11 +52,10 @@ export class HaConfigServerControl extends LitElement { changedProperties.has("hass") && (!oldHass || oldHass.config.components !== this.hass.config.components) ) { - this._reloadableDomains = this.hass.config.components.filter( - (component) => - !component.includes(".") && - isServiceLoaded(this.hass, component, "reload") - ); + this._reloadableDomains = componentsWithService( + this.hass, + "reload" + ).sort(); } } diff --git a/src/panels/config/users/ha-config-users.ts b/src/panels/config/users/ha-config-users.ts index 2e9432b8d1..30ed2e8093 100644 --- a/src/panels/config/users/ha-config-users.ts +++ b/src/panels/config/users/ha-config-users.ts @@ -1,3 +1,5 @@ +import "@material/mwc-fab"; +import { mdiPlus } from "@mdi/js"; import { customElement, LitElement, @@ -11,7 +13,7 @@ import { DataTableColumnContainer, RowClickedEvent, } from "../../../components/data-table/ha-data-table"; -import "@material/mwc-fab"; +import "../../../components/ha-svg-icon"; import { deleteUser, fetchUsers, updateUser, User } from "../../../data/user"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-tabs-subpage-data-table"; @@ -19,8 +21,6 @@ import { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; import { showAddUserDialog } from "./show-dialog-add-user"; import { showUserDetailDialog } from "./show-dialog-user-detail"; -import "../../../components/ha-svg-icon"; -import { mdiPlus } from "@mdi/js"; @customElement("ha-config-users") export class HaConfigUsers extends LitElement { @@ -56,7 +56,7 @@ export class HaConfigUsers extends LitElement { ), sortable: true, filterable: true, - width: "25%", + width: "30%", template: (groupIds) => html` ${this.hass.localize(`groups.${groupIds[0]}`)} `, @@ -66,6 +66,7 @@ export class HaConfigUsers extends LitElement { "ui.panel.config.users.picker.headers.system" ), type: "icon", + width: "80px", sortable: true, filterable: true, template: (generated) => html` diff --git a/src/panels/developer-tools/event/developer-tools-event.js b/src/panels/developer-tools/event/developer-tools-event.js index 4466d48f10..ec7526fee9 100644 --- a/src/panels/developer-tools/event/developer-tools-event.js +++ b/src/panels/developer-tools/event/developer-tools-event.js @@ -9,6 +9,7 @@ import "../../../components/ha-code-editor"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { EventsMixin } from "../../../mixins/events-mixin"; import LocalizeMixin from "../../../mixins/localize-mixin"; +import { documentationUrl } from "../../../util/documentation-url"; import "../../../styles/polymer-ha-style"; import "./event-subscribe-card"; import "./events-list"; @@ -61,7 +62,7 @@ class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) {

    [[localize( 'ui.panel.developer-tools.tabs.events.description' )]] @@ -149,6 +150,10 @@ class HaPanelDevEvent extends EventsMixin(LocalizeMixin(PolymerElement)) { } } + _computeDocumentationUrl(hass) { + return documentationUrl(hass, "/docs/configuration/events/"); + } + _computeValidJSON(parsedJSON) { return parsedJSON !== ERROR_SENTINEL; } diff --git a/src/panels/developer-tools/template/developer-tools-template.ts b/src/panels/developer-tools/template/developer-tools-template.ts index 0bb9d0f78e..d41e8fed1e 100644 --- a/src/panels/developer-tools/template/developer-tools-template.ts +++ b/src/panels/developer-tools/template/developer-tools-template.ts @@ -13,9 +13,13 @@ import { classMap } from "lit-html/directives/class-map"; import { debounce } from "../../../common/util/debounce"; import "../../../components/ha-circular-progress"; import "../../../components/ha-code-editor"; -import { subscribeRenderTemplate } from "../../../data/ws-templates"; +import { + RenderTemplateResult, + subscribeRenderTemplate, +} from "../../../data/ws-templates"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; const DEMO_TEMPLATE = `{## Imitate available variables: ##} {% set my_test_json = { @@ -31,10 +35,9 @@ The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}. The sun will rise at {{ as_timestamp(strptime(state_attr("sun.sun", "next_rising"), "")) | timestamp_local }}. {%- endif %} -For loop example getting 3 entity values: +For loop example getting entity values in the weather domain: -{% for states in states | slice(3) -%} - {% set state = states | first %} +{% for state in states.weather -%} {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%} {{ state.name | lower }} is {{state.state_with_unit}} {%- endfor %}.`; @@ -45,11 +48,11 @@ class HaPanelDevTemplate extends LitElement { @property() public narrow!: boolean; - @internalProperty() private _error = false; + @internalProperty() private _error?: string; @internalProperty() private _rendering = false; - @internalProperty() private _processed = ""; + @internalProperty() private _templateResult?: RenderTemplateResult; @internalProperty() private _unsubRenderTemplate?: Promise; @@ -105,7 +108,10 @@ class HaPanelDevTemplate extends LitElement {

  • @@ -140,9 +146,65 @@ class HaPanelDevTemplate extends LitElement { .active=${this._rendering} size="small" > -
    -${this._processed}
    + +
    ${this._error}${this._templateResult
    +            ?.result}
    + ${!this._templateResult?.listeners + ? "" + : this._templateResult.listeners.all + ? html` +

    + ${this.hass.localize( + "ui.panel.developer-tools.tabs.templates.all_listeners" + )} +

    + ` + : this._templateResult.listeners.domains.length || + this._templateResult.listeners.entities.length + ? html` +

    + ${this.hass.localize( + "ui.panel.developer-tools.tabs.templates.listeners" + )} +

    +
      + ${this._templateResult.listeners.domains + .sort() + .map( + (domain) => + html` +
    • + ${this.hass.localize( + "ui.panel.developer-tools.tabs.templates.domain" + )}: ${domain} +
    • + ` + )} + ${this._templateResult.listeners.entities + .sort() + .map( + (entity_id) => + html` +
    • + ${this.hass.localize( + "ui.panel.developer-tools.tabs.templates.entity" + )}: ${entity_id} +
    • + ` + )} +
    + ` + : html` + ${this.hass.localize( + "ui.panel.developer-tools.tabs.templates.no_listeners" + )} + `}
  • `; @@ -190,6 +252,12 @@ ${this._processed} { - this._processed = result; + this._templateResult = result; + this._error = undefined; }, { template: this._template, @@ -231,9 +300,10 @@ ${this._processed}
    ${this._isLoading - ? html`` + ? html`
    + +
    ` : html` - ${this.hass.localize("ui.panel.logbook.entries_not_found")} +
    + ${this.hass.localize("ui.components.logbook.entries_not_found")}
    `; } return html` -
    - ${scroll({ - items: this.entries, - renderItem: (item: LogbookEntry, index?: number) => - this._renderLogbookItem(item, index), - })} +
    + ${this.virtualize + ? scroll({ + items: this.entries, + renderItem: (item: LogbookEntry, index?: number) => + this._renderLogbookItem(item, index), + }) + : this.entries.map((item, index) => + this._renderLogbookItem(item, index) + )}
    `; } @@ -76,12 +104,13 @@ class HaLogbook extends LitElement { if (index === undefined) { return html``; } + const previous = this.entries[index - 1]; const state = item.entity_id ? this.hass.states[item.entity_id] : undefined; const item_username = item.context_user_id && this.userIdToName[item.context_user_id]; return html` -
    +
    ${index === 0 || (item?.when && previous?.when && @@ -98,46 +127,55 @@ class HaLogbook extends LitElement {
    ${formatTimeWithSeconds(new Date(item.when), this.hass.language)}
    - -
    - ${!item.entity_id - ? html` ${item.name} ` - : html` - ${item.name} - `} - ${item.message}${item_username - ? ` (${item_username})` - : ``} - ${!item.context_event_type - ? "" - : item.context_event_type === "call_service" - ? // Service Call - html` by service ${item.context_domain}.${item.context_service}` - : item.context_entity_id === item.entity_id - ? // HomeKit or something that self references - html` by - ${item.context_name - ? item.context_name - : item.context_event_type}` - : // Another entity such as an automation or script - html` by - ${item.context_entity_id_name}`} +
    + ${!this.noIcon + ? html` + + ` + : ""} +
    + ${!this.noName + ? !item.entity_id + ? html`${item.name}` + : html` + ${item.name} + ` + : ""} + ${item.message} + ${item_username + ? ` by ${item_username}` + : !item.context_event_type + ? "" + : item.context_event_type === "call_service" + ? // Service Call + ` by service + ${item.context_domain}.${item.context_service}` + : item.context_entity_id === item.entity_id + ? // HomeKit or something that self references + ` by + ${ + item.context_name + ? item.context_name + : item.context_event_type + }` + : // Another entity such as an automation or script + html` by + ${item.context_entity_id_name}`} +
    @@ -163,26 +201,57 @@ class HaLogbook extends LitElement { height: 100%; } - :host([rtl]) { + .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 { - width: 65px; + display: flex; + justify-content: center; + flex-direction: column; + width: 75px; flex-shrink: 0; - font-size: 0.8em; + font-size: 12px; color: var(--secondary-text-color); } - :host([rtl]) .date { + .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; + color: var(--secondary-text-color); + } + ha-icon { margin: 0 8px 0 16px; flex-shrink: 0; @@ -193,12 +262,12 @@ class HaLogbook extends LitElement { color: var(--primary-text-color); } - a { - color: var(--primary-color); + .no-name .message:first-letter { + text-transform: capitalize; } - .container { - padding: 0 16px; + a { + color: var(--primary-color); } .uni-virtualizer-host { @@ -212,8 +281,22 @@ class HaLogbook extends LitElement { .uni-virtualizer-host > * { box-sizing: border-box; } + + .narrow .entry { + flex-direction: column; + line-height: 1.5; + padding: 8px 0; + } + + .narrow .icon-message ha-icon { + margin-left: 0; + } `; } } -customElements.define("ha-logbook", HaLogbook); +declare global { + interface HTMLElementTagNameMap { + "ha-logbook": HaLogbook; + } +} diff --git a/src/panels/logbook/ha-panel-logbook.ts b/src/panels/logbook/ha-panel-logbook.ts index af57dbc7e6..806cb5ac6b 100644 --- a/src/panels/logbook/ha-panel-logbook.ts +++ b/src/panels/logbook/ha-panel-logbook.ts @@ -1,33 +1,33 @@ +import { mdiRefresh } from "@mdi/js"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; -import "../../components/ha-icon-button"; -import "../../components/ha-circular-progress"; -import { computeRTL } from "../../common/util/compute_rtl"; -import "../../components/entity/ha-entity-picker"; -import "../../components/ha-menu-button"; -import "../../layouts/ha-app-layout"; -import "./ha-logbook"; import { - LitElement, - property, - internalProperty, + css, customElement, html, - css, + internalProperty, + LitElement, + property, PropertyValues, } from "lit-element"; -import { HomeAssistant } from "../../types"; -import { haStyle } from "../../resources/styles"; -import { fetchUsers } from "../../data/user"; -import { fetchPersons } from "../../data/person"; +import { computeRTL } from "../../common/util/compute_rtl"; +import "../../components/entity/ha-entity-picker"; +import "../../components/ha-circular-progress"; +import "../../components/ha-date-range-picker"; +import type { DateRangePickerRanges } from "../../components/ha-date-range-picker"; +import "../../components/ha-icon-button"; +import "../../components/ha-menu-button"; import { clearLogbookCache, getLogbookData, LogbookEntry, } from "../../data/logbook"; -import { mdiRefresh } from "@mdi/js"; -import "../../components/ha-date-range-picker"; -import type { DateRangePickerRanges } from "../../components/ha-date-range-picker"; +import { fetchPersons } from "../../data/person"; +import { fetchUsers } from "../../data/user"; +import "../../layouts/ha-app-layout"; +import { haStyle } from "../../resources/styles"; +import { HomeAssistant } from "../../types"; +import "./ha-logbook"; @customElement("ha-panel-logbook") export class HaPanelLogbook extends LitElement { @@ -125,6 +125,7 @@ export class HaPanelLogbook extends LitElement { .hass=${this.hass} .entries=${this._entries} .userIdToName=${this._userIdToName} + virtualize >`} `; diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts index a2662ab7bd..2d75c7e33b 100644 --- a/src/panels/lovelace/cards/hui-button-card.ts +++ b/src/panels/lovelace/cards/hui-button-card.ts @@ -21,6 +21,7 @@ import { DOMAINS_TOGGLE } from "../../../common/const"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { computeActiveState } from "../../../common/entity/compute_active_state"; import { computeDomain } from "../../../common/entity/compute_domain"; +import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { stateIcon } from "../../../common/entity/state_icon"; @@ -36,7 +37,6 @@ import { hasAction } from "../common/has-action"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { ButtonCardConfig } from "./types"; -import { computeStateDisplay } from "../../../common/entity/compute_state_display"; @customElement("hui-button-card") export class HuiButtonCard extends LitElement implements LovelaceCard { @@ -63,11 +63,9 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { return { type: "button", - tap_action: { action: "toggle" }, - hold_action: { action: "more-info" }, - show_icon: true, - show_name: true, - show_state: false, + tap_action: { + action: "toggle", + }, entity: foundEntities[0] || "", }; } @@ -92,29 +90,18 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { } this._config = { + tap_action: { + action: + config.entity && DOMAINS_TOGGLE.has(computeDomain(config.entity)) + ? "toggle" + : "more-info", + }, hold_action: { action: "more-info" }, - double_tap_action: { action: "none" }, show_icon: true, show_name: true, state_color: true, ...config, }; - - if (config.entity && DOMAINS_TOGGLE.has(computeDomain(config.entity))) { - this._config = { - tap_action: { - action: "toggle", - }, - ...this._config, - }; - } else { - this._config = { - tap_action: { - action: "more-info", - }, - ...this._config, - }; - } } protected shouldUpdate(changedProps: PropertyValues): boolean { diff --git a/src/panels/lovelace/cards/hui-calendar-card.ts b/src/panels/lovelace/cards/hui-calendar-card.ts index 2351454829..ee94fb807c 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-entities-card.ts b/src/panels/lovelace/cards/hui-entities-card.ts index 2e55a41b39..90383f3bdb 100644 --- a/src/panels/lovelace/cards/hui-entities-card.ts +++ b/src/panels/lovelace/cards/hui-entities-card.ts @@ -50,7 +50,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { ["light", "switch", "sensor"] ); - return { type: "entities", title: "My Title", entities: foundEntities }; + return { type: "entities", entities: foundEntities }; } @internalProperty() private _config?: EntitiesCardConfig; diff --git a/src/panels/lovelace/cards/hui-error-card.ts b/src/panels/lovelace/cards/hui-error-card.ts index 9da8f631b7..b995c6c04d 100644 --- a/src/panels/lovelace/cards/hui-error-card.ts +++ b/src/panels/lovelace/cards/hui-error-card.ts @@ -43,8 +43,8 @@ export class HuiErrorCard extends LitElement implements LovelaceCard { return css` :host { display: block; - background-color: #ef5350; - color: white; + background-color: var(--error-color); + color: var(--color-on-error, white); padding: 8px; font-weight: 500; user-select: text; diff --git a/src/panels/lovelace/cards/hui-glance-card.ts b/src/panels/lovelace/cards/hui-glance-card.ts index 46b5be3735..0da08a86a1 100644 --- a/src/panels/lovelace/cards/hui-glance-card.ts +++ b/src/panels/lovelace/cards/hui-glance-card.ts @@ -3,9 +3,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; @@ -20,18 +20,22 @@ import "../../../components/entity/state-badge"; import "../../../components/ha-card"; import "../../../components/ha-icon"; import { UNAVAILABLE_STATES } from "../../../data/entity"; -import { ActionHandlerEvent } from "../../../data/lovelace"; +import { + ActionHandlerEvent, + CallServiceActionConfig, + MoreInfoActionConfig, +} from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; import { findEntities } from "../common/find-entites"; import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { processConfigEntities } from "../common/process-config-entities"; +import "../components/hui-timestamp-display"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; import "../components/hui-warning-element"; import { LovelaceCard, LovelaceCardEditor } from "../types"; -import "../components/hui-timestamp-display"; import { GlanceCardConfig, GlanceConfigEntity } from "./types"; -import { createEntityNotFoundWarning } from "../components/hui-warning"; @customElement("hui-glance-card") export class HuiGlanceCard extends LitElement implements LovelaceCard { @@ -86,7 +90,14 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { state_color: true, ...config, }; - const entities = processConfigEntities(config.entities); + const entities = processConfigEntities( + config.entities + ).map((entityConf) => { + return { + hold_action: { action: "more-info" } as MoreInfoActionConfig, + ...entityConf, + }; + }); for (const entity of entities) { if ( @@ -95,7 +106,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { !entity.tap_action.service) || (entity.hold_action && entity.hold_action.action === "call-service" && - !entity.hold_action.service) + !(entity.hold_action as CallServiceActionConfig).service) ) { throw new Error( 'Missing required property "service" when tap_action or hold_action is call-service' diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index bb65f89e1c..0a9bc908b2 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -1,13 +1,13 @@ -import "../../../components/ha-icon-button"; +import { mdiDotsVertical } from "@mdi/js"; import "@thomasloven/round-slider"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; @@ -20,7 +20,8 @@ import { computeStateName } from "../../../common/entity/compute_state_name"; import { stateIcon } from "../../../common/entity/state_icon"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-card"; -import { UNAVAILABLE_STATES } from "../../../data/entity"; +import "../../../components/ha-icon-button"; +import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../../data/entity"; import { SUPPORT_BRIGHTNESS } from "../../../data/light"; import { ActionHandlerEvent } from "../../../data/lovelace"; import { HomeAssistant, LightEntity } from "../../../types"; @@ -32,7 +33,6 @@ import { hasConfigOrEntityChanged } from "../common/has-changed"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { LightCardConfig } from "./types"; -import { mdiDotsVertical } from "@mdi/js"; @customElement("hui-light-card") export class HuiLightCard extends LitElement implements LovelaceCard { @@ -77,8 +77,9 @@ export class HuiLightCard extends LitElement implements LovelaceCard { } this._config = { - ...config, tap_action: { action: "toggle" }, + hold_action: { action: "more-info" }, + ...config, }; } @@ -133,7 +134,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard { SUPPORT_BRIGHTNESS ), "state-on": stateObj.state === "on", - "state-unavailable": stateObj.state === "unavailable", + "state-unavailable": stateObj.state === UNAVAILABLE, })}" .icon=${this._config.icon || stateIcon(stateObj)} .disabled=${UNAVAILABLE_STATES.includes(stateObj.state)} diff --git a/src/panels/lovelace/cards/hui-markdown-card.ts b/src/panels/lovelace/cards/hui-markdown-card.ts index 2ea9d6c2dc..a802a79c46 100644 --- a/src/panels/lovelace/cards/hui-markdown-card.ts +++ b/src/panels/lovelace/cards/hui-markdown-card.ts @@ -14,7 +14,10 @@ import { classMap } from "lit-html/directives/class-map"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import "../../../components/ha-card"; import "../../../components/ha-markdown"; -import { subscribeRenderTemplate } from "../../../data/ws-templates"; +import { + subscribeRenderTemplate, + RenderTemplateResult, +} from "../../../data/ws-templates"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCard, LovelaceCardEditor } from "../types"; import type { MarkdownCardConfig } from "./types"; @@ -40,7 +43,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard { @internalProperty() private _config?: MarkdownCardConfig; - @internalProperty() private _content = ""; + @internalProperty() private _templateResult?: RenderTemplateResult; @internalProperty() private _unsubRenderTemplate?: Promise; @@ -85,7 +88,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard { class=${classMap({ "no-header": !this._config.title, })} - .content="${this._content}" + .content="${this._templateResult?.result}" > `; @@ -127,7 +130,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard { this._unsubRenderTemplate = subscribeRenderTemplate( this.hass.connection, (result) => { - this._content = result; + this._templateResult = result; }, { template: this._config.content, @@ -139,7 +142,10 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard { } ); } catch (_err) { - this._content = this._config!.content; + this._templateResult = { + result: this._config!.content, + listeners: { all: false, domains: [], entities: [] }, + }; this._unsubRenderTemplate = undefined; } } diff --git a/src/panels/lovelace/cards/hui-media-control-card.ts b/src/panels/lovelace/cards/hui-media-control-card.ts index a176fc251c..0aa6389e21 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/cards/hui-picture-glance-card.ts b/src/panels/lovelace/cards/hui-picture-glance-card.ts index 45c0ea1516..0c04eeccf3 100644 --- a/src/panels/lovelace/cards/hui-picture-glance-card.ts +++ b/src/panels/lovelace/cards/hui-picture-glance-card.ts @@ -3,9 +3,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; @@ -28,10 +28,10 @@ import { hasAction } from "../common/has-action"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import { processConfigEntities } from "../common/process-config-entities"; import "../components/hui-image"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; import "../components/hui-warning-element"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { PictureGlanceCardConfig, PictureGlanceEntityConfig } from "./types"; -import { createEntityNotFoundWarning } from "../components/hui-warning"; const STATES_OFF = new Set(["closed", "locked", "not_home", "off"]); @@ -104,7 +104,10 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard { } }); - this._config = config; + this._config = { + hold_action: { action: "more-info" }, + ...config, + }; } protected shouldUpdate(changedProps: PropertyValues): boolean { @@ -225,6 +228,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard { entityConf = { tap_action: { action: dialog ? "more-info" : "toggle" }, + hold_action: { action: "more-info" }, ...entityConf, }; diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index e8a7f71b48..8470bd4c34 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -1,4 +1,4 @@ -import "../../../components/ha-icon-button"; +import { mdiDotsVertical } from "@mdi/js"; import "@thomasloven/round-slider"; import { HassEntity } from "home-assistant-js-websocket"; import { @@ -6,13 +6,13 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, + query, svg, TemplateResult, - query, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; import { UNIT_F } from "../../../common/const"; @@ -20,6 +20,8 @@ import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_elemen import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; import "../../../components/ha-card"; +import type { HaCard } from "../../../components/ha-card"; +import "../../../components/ha-icon-button"; import { ClimateEntity, CLIMATE_PRESET_NONE, @@ -28,14 +30,11 @@ import { } from "../../../data/climate"; import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; -import { actionHandler } from "../common/directives/action-handler-directive"; import { findEntities } from "../common/find-entites"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { ThermostatCardConfig } from "./types"; -import type { HaCard } from "../../../components/ha-card"; -import { mdiDotsVertical } from "@mdi/js"; const modeIcons: { [mode in HvacMode]: string } = { auto: "hass:calendar-sync", @@ -385,8 +384,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { class="${classMap({ "selected-icon": currentMode === mode })}" .mode="${mode}" .icon="${modeIcons[mode]}" - @action=${this._handleAction} - .actionHandler=${actionHandler()} + @click=${this._handleAction} tabindex="0" > `; diff --git a/src/panels/lovelace/common/directives/action-handler-directive.ts b/src/panels/lovelace/common/directives/action-handler-directive.ts index 09dc3c4351..3b0ea378c8 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,25 +150,22 @@ 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; } + const target = ev.target as HTMLElement; // 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) { - fireEvent(element, "action", { action: "hold" }); + if (options.hasHold && this.held) { + fireEvent(target, "action", { action: "hold" }); } else if (options.hasDoubleClick) { if ( (ev.type === "click" && (ev as MouseEvent).detail < 2) || @@ -170,17 +173,15 @@ class ActionHandler extends HTMLElement implements ActionHandler { ) { this.dblClickTimeout = window.setTimeout(() => { this.dblClickTimeout = undefined; - fireEvent(element, "action", { action: "tap" }); + fireEvent(target, "action", { action: "tap" }); }, 250); } else { clearTimeout(this.dblClickTimeout); this.dblClickTimeout = undefined; - fireEvent(element, "action", { action: "double_tap" }); + fireEvent(target, "action", { action: "double_tap" }); } } else { - fireEvent(element, "action", { action: "tap" }); - // Fire the click we prevented the action for - (ev.target as HTMLElement)?.click(); + fireEvent(target, "action", { action: "tap" }); } }; diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index b5301d37f9..bf49d2116a 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -63,6 +63,8 @@ const HIDE_DOMAIN = new Set([ "zone", ]); +const HIDE_PLATFORM = new Set(["mobile_app"]); + let subscribedRegistries = false; interface SplittedByAreas { @@ -206,11 +208,23 @@ export const computeCards = ( return cards; }; -const computeDefaultViewStates = (entities: HassEntities): HassEntities => { +const computeDefaultViewStates = ( + entities: HassEntities, + entityEntries: EntityRegistryEntry[] +): HassEntities => { const states = {}; + const hiddenEntities = new Set( + entityEntries + .filter((entry) => HIDE_PLATFORM.has(entry.platform)) + .map((entry) => entry.entity_id) + ); + Object.keys(entities).forEach((entityId) => { const stateObj = entities[entityId]; - if (!HIDE_DOMAIN.has(computeStateDomain(stateObj))) { + if ( + !HIDE_DOMAIN.has(computeStateDomain(stateObj)) && + !hiddenEntities.has(stateObj.entity_id) + ) { states[entityId] = entities[entityId]; } }); @@ -317,7 +331,7 @@ export const generateDefaultViewConfig = ( entities: HassEntities, localize: LocalizeFunc ): LovelaceViewConfig => { - const states = computeDefaultViewStates(entities); + const states = computeDefaultViewStates(entities, entityEntries); const path = "default_view"; const title = "Home"; const icon = undefined; diff --git a/src/panels/lovelace/common/load-resources.ts b/src/panels/lovelace/common/load-resources.ts index c996f25562..5b9fb6d316 100644 --- a/src/panels/lovelace/common/load-resources.ts +++ b/src/panels/lovelace/common/load-resources.ts @@ -30,12 +30,6 @@ export const loadLovelaceResources = ( loadModule(normalizedUrl); break; - case "html": - import( - /* webpackChunkName: "import-href-polyfill" */ "../../../resources/html-import/import-href" - ).then(({ importHref }) => importHref(normalizedUrl)); - break; - default: // eslint-disable-next-line console.warn(`Unknown resource type specified: ${resource.type}`); diff --git a/src/panels/lovelace/common/validate-condition.ts b/src/panels/lovelace/common/validate-condition.ts index e50707d871..0aca5f2e11 100644 --- a/src/panels/lovelace/common/validate-condition.ts +++ b/src/panels/lovelace/common/validate-condition.ts @@ -1,4 +1,5 @@ import { HomeAssistant } from "../../../types"; +import { UNAVAILABLE } from "../../../data/entity"; export interface Condition { entity: string; @@ -13,7 +14,7 @@ export function checkConditionsMet( return conditions.every((c) => { const state = hass.states[c.entity] ? hass!.states[c.entity].state - : "unavailable"; + : UNAVAILABLE; return c.state ? state === c.state : state !== c.state_not; }); diff --git a/src/panels/lovelace/components/hui-action-editor.ts b/src/panels/lovelace/components/hui-action-editor.ts index 5899360217..dea53bd0d3 100644 --- a/src/panels/lovelace/components/hui-action-editor.ts +++ b/src/panels/lovelace/components/hui-action-editor.ts @@ -1,4 +1,5 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; +import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-textarea"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; @@ -9,7 +10,7 @@ import { property, TemplateResult, } from "lit-element"; -import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; +import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-service-picker"; import { ActionConfig, @@ -20,17 +21,6 @@ import { import { HomeAssistant } from "../../../types"; import { EditorTarget } from "../editor/types"; -declare global { - // for fire event - interface HASSDomEvents { - "action-changed": undefined; - } - // for add event listener - interface HTMLElementEventMap { - "action-changed": HASSDomEvent; - } -} - @customElement("hui-action-editor") export class HuiActionEditor extends LitElement { @property() public config?: ActionConfig; @@ -42,21 +32,21 @@ export class HuiActionEditor extends LitElement { @property() protected hass?: HomeAssistant; get _action(): string { - return this.config!.action || ""; + return this.config?.action || ""; } get _navigation_path(): string { - const config = this.config! as NavigateActionConfig; + const config = this.config as NavigateActionConfig; return config.navigation_path || ""; } get _url_path(): string { - const config = this.config! as UrlActionConfig; + const config = this.config as UrlActionConfig; return config.url_path || ""; } get _service(): string { - const config = this.config! as CallServiceActionConfig; + const config = this.config as CallServiceActionConfig; return config.service || ""; } @@ -107,13 +97,14 @@ export class HuiActionEditor extends LitElement { .configValue="${"service"}" @value-changed="${this._valueChanged}" > -

    Toggle Editor to input Service Data

    + Service data can only be entered in the code editor ` : ""} `; } private _valueChanged(ev: Event): void { + ev.stopPropagation(); if (!this.hass) { return; } @@ -121,12 +112,12 @@ export class HuiActionEditor extends LitElement { if (this[`_${target.configValue}`] === target.value) { return; } - if (target.configValue === "action") { - this.config = { action: "none" }; - } if (target.configValue) { - this.config = { ...this.config!, [target.configValue!]: target.value }; - fireEvent(this, "action-changed"); + const newConfig = + target.configValue === "action" + ? { action: target.value } + : { ...this.config!, [target.configValue!]: target.value }; + fireEvent(this, "value-changed", { value: newConfig }); } } } diff --git a/src/panels/lovelace/components/hui-entity-editor.ts b/src/panels/lovelace/components/hui-entity-editor.ts index 23ab6eb6f1..ddde4ef768 100644 --- a/src/panels/lovelace/components/hui-entity-editor.ts +++ b/src/panels/lovelace/components/hui-entity-editor.ts @@ -1,27 +1,53 @@ -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 type { HaEntityPicker } from "../../../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; + + @internalProperty() private _renderEmptySortable = false; + + private _sortable?: Sortable; + + public connectedCallback() { + super.connectedCallback(); + this._attached = true; + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this._attached = false; + } + protected render(): TemplateResult { if (!this.entities) { return html``; @@ -36,103 +62,138 @@ export class HuiEntityEditor extends LitElement { ")"}
    - ${this.entities.map((entityConf, index) => { - return html` -
    - - - -
    - `; - })} - + ${guard([this.entities, this._renderEmptySortable], () => + this._renderEmptySortable + ? "" + : this.entities!.map((entityConf, index) => { + return html` +
    + + +
    + `; + }) + )}
    + `; } - private _addEntity(ev: Event): void { - const target = ev.target! as EditorTarget; - if (target.value === "") { + 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._handleEntitiesChanged(); + } + } + + private async _handleEntitiesChanged() { + this._renderEmptySortable = true; + await this.updateComplete; + this._renderEmptySortable = false; + } + + 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: CustomEvent): Promise { + const value = ev.detail.value; + if (value === "") { return; } const newConfigEntities = this.entities!.concat({ - entity: target.value as string, + entity: value as string, }); - target.value = ""; + (ev.target as HaEntityPicker).value = ""; fireEvent(this, "entities-changed", { entities: newConfigEntities }); } - private _entityUp(ev: Event): void { - const target = ev.target! as EditorTarget; + private _entityMoved(ev: SortableEvent): void { + if (ev.oldIndex === ev.newIndex) { + return; + } + 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 }); } - 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], - ]; - - fireEvent(this, "entities-changed", { entities: newEntities }); - } - - private _valueChanged(ev: Event): void { - const target = ev.target! as EditorTarget; + private _valueChanged(ev: CustomEvent): void { + const value = ev.detail.value; + const index = (ev.target as any).index; const newConfigEntities = this.entities!.concat(); - if (target.value === "") { - newConfigEntities.splice(target.index!, 1); + if (value === "") { + newConfigEntities.splice(index, 1); } else { - newConfigEntities[target.index!] = { - ...newConfigEntities[target.index!], - entity: target.value!, + newConfigEntities[index] = { + ...newConfigEntities[index], + entity: value!, }; } 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/components/hui-image.ts b/src/panels/lovelace/components/hui-image.ts index 115e118f83..f9133cc612 100644 --- a/src/panels/lovelace/components/hui-image.ts +++ b/src/panels/lovelace/components/hui-image.ts @@ -3,9 +3,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, query, TemplateResult, @@ -16,6 +16,7 @@ import { STATES_OFF } from "../../../common/const"; import parseAspectRatio from "../../../common/util/parse-aspect-ratio"; import "../../../components/ha-camera-stream"; import { fetchThumbnailUrlWithCache } from "../../../data/camera"; +import { UNAVAILABLE } from "../../../data/entity"; import { CameraEntity, HomeAssistant } from "../../../types"; const UPDATE_INTERVAL = 10000; @@ -73,7 +74,7 @@ export class HuiImage extends LitElement { } const ratio = this.aspectRatio ? parseAspectRatio(this.aspectRatio) : null; const stateObj = this.entity ? this.hass.states[this.entity] : undefined; - const state = stateObj ? stateObj.state : "unavailable"; + const state = stateObj ? stateObj.state : UNAVAILABLE; // Figure out image source to use let imageSrc: string | undefined; @@ -131,8 +132,9 @@ export class HuiImage extends LitElement { ${this.cameraImage && this.cameraView === "live" ? html` ` : html` diff --git a/src/panels/lovelace/editor/card-editor/hui-card-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-editor.ts index f081cc65ae..5c786ad71e 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-editor.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-editor.ts @@ -5,14 +5,16 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, - TemplateResult, query, + TemplateResult, } from "lit-element"; import { fireEvent } from "../../../../common/dom/fire_event"; import { computeRTL } from "../../../../common/util/compute_rtl"; +import { deepEqual } from "../../../../common/util/deep-equal"; +import "../../../../components/ha-circular-progress"; import "../../../../components/ha-code-editor"; import type { HaCodeEditor } from "../../../../components/ha-code-editor"; import type { @@ -20,14 +22,12 @@ import type { LovelaceConfig, } from "../../../../data/lovelace"; import type { HomeAssistant } from "../../../../types"; +import { handleStructError } from "../../common/structs/handle-errors"; import { getCardElementClass } from "../../create-element/create-card-element"; import type { EntityConfig } from "../../entity-rows/types"; import type { LovelaceCardEditor } from "../../types"; -import type { GUIModeChangedEvent } from "../types"; -import "../../../../components/ha-circular-progress"; -import { deepEqual } from "../../../../common/util/deep-equal"; -import { handleStructError } from "../../common/structs/handle-errors"; import { GUISupportError } from "../gui-support-error"; +import type { GUIModeChangedEvent } from "../types"; export interface ConfigChangedEvent { config: LovelaceCardConfig; @@ -78,6 +78,9 @@ export class HuiCardEditor extends LitElement { @query("ha-code-editor") _yamlEditor?: HaCodeEditor; public get yaml(): string { + if (!this._yaml) { + this._yaml = safeDump(this._config); + } return this._yaml || ""; } @@ -101,7 +104,7 @@ export class HuiCardEditor extends LitElement { return; } this._config = config; - this._yaml = safeDump(config); + this._yaml = undefined; this._error = undefined; this._setConfig(); } 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 dce01a77c4..f68b99e1f5 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts @@ -1,40 +1,45 @@ +import "@material/mwc-tab-bar/mwc-tab-bar"; +import "@material/mwc-tab/mwc-tab"; import Fuse from "fuse.js"; 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 { LovelaceCardConfig, LovelaceConfig } from "../../../../data/lovelace"; +import type { + LovelaceCardConfig, + LovelaceConfig, +} from "../../../../data/lovelace"; import { CustomCardEntry, customCards, CUSTOM_TYPE_PREFIX, getCustomCardEntry, } from "../../../../data/lovelace_custom_cards"; -import { HomeAssistant } from "../../../../types"; +import type { HomeAssistant } from "../../../../types"; import { calcUnusedEntities, computeUsedEntities, } from "../../common/compute-unused-entities"; import { tryCreateCardElement } from "../../create-element/create-card-element"; -import { LovelaceCard } from "../../types"; +import type { LovelaceCard } from "../../types"; import { getCardStubConfig } from "../get-card-stub-config"; -import { CardPickTarget, Card } from "../types"; import { coreCards } from "../lovelace-cards"; -import { styleMap } from "lit-html/directives/style-map"; -import "../../../../components/ha-circular-progress"; +import type { Card, CardPickTarget } from "../types"; interface CardElement { card: Card; @@ -53,14 +58,14 @@ export class HuiCardPicker extends LitElement { @internalProperty() private _filter = ""; - private _unusedEntities?: string[]; - - private _usedEntities?: string[]; - @internalProperty() private _width?: number; @internalProperty() private _height?: number; + private _unusedEntities?: string[]; + + private _usedEntities?: string[]; + private _filterCards = memoizeOne( (cardElements: CardElement[], filter?: string): CardElement[] => { if (!filter) { @@ -99,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" )} >
    :first-child { - zoom: 0.6; - display: block; - width: 100%; - } - - .description { - text-align: center; - } - - .spinner { - align-items: center; - justify-content: center; - } - - .overlay { - position: absolute; - width: 100%; - height: 100%; - z-index: 1; - } - - .manual { - max-width: none; - } - `, - ]; - } - private _cardPicked(ev: Event): void { const config: LovelaceCardConfig = (ev.currentTarget! as CardPickTarget) .config; @@ -406,6 +332,90 @@ export class HuiCardPicker extends LitElement {
    `; } + + static get styles(): CSSResult[] { + return [ + css` + search-input { + display: block; + margin: 0 -8px; + } + + .cards-container { + display: grid; + grid-gap: 8px 8px; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + margin-top: 20px; + } + + .card { + height: 100%; + max-width: 500px; + display: flex; + flex-direction: column; + border-radius: 4px; + border: 1px solid var(--divider-color); + background: var(--primary-background-color, #fafafa); + cursor: pointer; + box-sizing: border-box; + position: relative; + } + + .card-header { + color: var(--ha-card-header-color, --primary-text-color); + font-family: var(--ha-card-header-font-family, inherit); + font-size: 16px; + font-weight: bold; + letter-spacing: -0.012em; + line-height: 20px; + padding: 12px 16px; + display: block; + text-align: center; + background: var( + --ha-card-background, + var(--card-background-color, white) + ); + border-radius: 0 0 4px 4px; + border-bottom: 1px solid var(--divider-color); + } + + .preview { + pointer-events: none; + margin: 20px; + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + } + + .preview > :first-child { + zoom: 0.6; + display: block; + width: 100%; + } + + .description { + text-align: center; + } + + .spinner { + align-items: center; + justify-content: center; + } + + .overlay { + position: absolute; + width: 100%; + height: 100%; + z-index: 1; + } + + .manual { + max-width: none; + } + `, + ]; + } } declare global { diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts new file mode 100644 index 0000000000..9b3963485c --- /dev/null +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts @@ -0,0 +1,288 @@ +import "@material/mwc-tab-bar/mwc-tab-bar"; +import "@material/mwc-tab/mwc-tab"; +import { + css, + CSSResultArray, + customElement, + html, + internalProperty, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { cache } from "lit-html/directives/cache"; +import { classMap } from "lit-html/directives/class-map"; +import memoize from "memoize-one"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { computeDomain } from "../../../../common/entity/compute_domain"; +import { computeStateName } from "../../../../common/entity/compute_state_name"; +import { DataTableRowData } from "../../../../components/data-table/ha-data-table"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-header-bar"; +import type { LovelaceViewConfig } from "../../../../data/lovelace"; +import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import "./hui-card-picker"; +import "./hui-entity-picker-table"; +import { CreateCardDialogParams } from "./show-create-card-dialog"; +import { showEditCardDialog } from "./show-edit-card-dialog"; +import { showSuggestCardDialog } from "./show-suggest-card-dialog"; + +declare global { + interface HASSDomEvents { + "selected-changed": SelectedChangedEvent; + } +} + +interface SelectedChangedEvent { + selectedEntities: string[]; +} + +@customElement("hui-dialog-create-card") +export class HuiCreateDialogCard extends LitElement implements HassDialog { + @property({ attribute: false }) protected hass!: HomeAssistant; + + @internalProperty() private _params?: CreateCardDialogParams; + + @internalProperty() private _viewConfig!: LovelaceViewConfig; + + @internalProperty() private _selectedEntities: string[] = []; + + @internalProperty() private _currTabIndex = 0; + + public async showDialog(params: CreateCardDialogParams): Promise { + this._params = params; + const [view] = params.path; + this._viewConfig = params.lovelaceConfig.views[view]; + } + + public closeDialog(): boolean { + this._params = undefined; + this._currTabIndex = 0; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + return true; + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + + return html` + +
    + + + ${this._viewConfig.title + ? this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.pick_card_view_title", + "name", + `"${this._viewConfig.title}"` + ) + : this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.pick_card" + )} + + + + this._handleTabChanged(ev)} + > + + + +
    + ${cache( + this._currTabIndex === 0 + ? html` + + ` + : html` +
    + +
    + ` + )} + +
    + + ${this.hass!.localize("ui.common.cancel")} + + ${this._selectedEntities.length + ? html` + + ${this.hass!.localize("ui.common.continue")} + + ` + : ""} +
    +
    + `; + } + + private _ignoreKeydown(ev: KeyboardEvent) { + ev.stopPropagation(); + } + + static get styles(): CSSResultArray { + return [ + haStyleDialog, + css` + @media all and (max-width: 450px), all and (max-height: 500px) { + /* overrule the ha-style-dialog max-height on small screens */ + ha-dialog { + --mdc-dialog-max-height: 100%; + height: 100%; + } + } + + @media all and (min-width: 850px) { + ha-dialog { + --mdc-dialog-min-width: 845px; + } + } + + 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 { + --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: 1200px) { + ha-dialog { + --mdc-dialog-max-width: calc(100% - 32px); + --mdc-dialog-min-width: 1000px; + } + } + + .header_button { + color: inherit; + text-decoration: none; + } + + mwc-tab-bar { + border-bottom: 1px solid + var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); + } + + .entity-picker-container { + display: flex; + flex-direction: column; + height: 100%; + min-height: calc(100vh - 112px); + margin-top: -20px; + } + `, + ]; + } + + private _handleCardPicked(ev) { + const config = ev.detail.config; + if (this._params!.entities && this._params!.entities.length) { + if (Object.keys(config).includes("entities")) { + config.entities = this._params!.entities; + } else if (Object.keys(config).includes("entity")) { + config.entity = this._params!.entities[0]; + } + } + + showEditCardDialog(this, { + lovelaceConfig: this._params!.lovelaceConfig, + saveConfig: this._params!.saveConfig, + path: this._params!.path, + cardConfig: config, + }); + + this.closeDialog(); + } + + private _handleTabChanged(ev: CustomEvent): void { + const newTab = ev.detail.index; + if (newTab === this._currTabIndex) { + return; + } + + this._currTabIndex = ev.detail.index; + this._selectedEntities = []; + } + + private _handleSelectedChanged(ev: CustomEvent): void { + this._selectedEntities = ev.detail.selectedEntities; + } + + private _cancel(ev?: Event) { + if (ev) { + ev.stopPropagation(); + } + this.closeDialog(); + } + + private _suggestCards(): void { + showSuggestCardDialog(this, { + lovelaceConfig: this._params!.lovelaceConfig, + saveConfig: this._params!.saveConfig, + path: this._params!.path as [number], + entities: this._selectedEntities, + }); + + this.closeDialog(); + } + + private _allEntities = memoize((entities) => + Object.keys(entities).map((entity) => { + const stateObj = this.hass.states[entity]; + return { + icon: "", + entity_id: entity, + stateObj, + name: computeStateName(stateObj), + domain: computeDomain(entity), + last_changed: stateObj!.last_changed, + } as DataTableRowData; + }) + ); +} + +declare global { + interface HTMLElementTagNameMap { + "hui-dialog-create-card": HuiCreateDialogCard; + } +} 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 effc794665..e700cd097b 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 @@ -11,28 +11,32 @@ import { TemplateResult, PropertyValues, } from "lit-element"; -import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-dialog"; +import { mdiHelpCircle } from "@mdi/js"; + +import { fireEvent } from "../../../../common/dom/fire_event"; +import { haStyleDialog } from "../../../../resources/styles"; +import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; +import { addCard, replaceCard } from "../config-util"; +import { getCardDocumentationURL } from "../get-card-documentation-url"; +import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; + +import type { HomeAssistant } from "../../../../types"; +import type { GUIModeChangedEvent } from "../types"; +import type { ConfigChangedEvent, HuiCardEditor } from "./hui-card-editor"; +import type { EditCardDialogParams } from "./show-edit-card-dialog"; +import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import type { LovelaceCardConfig, LovelaceViewConfig, } from "../../../../data/lovelace"; -import { haStyleDialog } from "../../../../resources/styles"; -import "../../../../components/ha-circular-progress"; -import type { HomeAssistant } from "../../../../types"; -import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; -import { addCard, replaceCard } from "../config-util"; -import type { GUIModeChangedEvent } from "../types"; + import "./hui-card-editor"; -import type { ConfigChangedEvent, HuiCardEditor } from "./hui-card-editor"; -import "./hui-card-picker"; import "./hui-card-preview"; -import type { EditCardDialogParams } from "./show-edit-card-dialog"; -import { getCardDocumentationURL } from "../get-card-documentation-url"; -import { mdiHelpCircle } from "@mdi/js"; -import { computeRTLDirection } from "../../../../common/util/compute_rtl"; -import { HassDialog } from "../../../../dialogs/make-dialog-manager"; -import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-header-bar"; +import "../../../../components/ha-circular-progress"; declare global { // for fire event @@ -47,7 +51,7 @@ declare global { @customElement("hui-dialog-edit-card") export class HuiDialogEditCard extends LitElement implements HassDialog { - @property() protected hass!: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; @internalProperty() private _params?: EditCardDialogParams; @@ -111,7 +115,10 @@ export class HuiDialogEditCard extends LitElement implements HassDialog { const oldConfig = changedProps.get("_cardConfig") as LovelaceCardConfig; if (oldConfig?.type !== this._cardConfig!.type) { - this._documentationURL = getCardDocumentationURL(this._cardConfig!.type); + this._documentationURL = getCardDocumentationURL( + this.hass, + this._cardConfig!.type + ); } } @@ -150,62 +157,56 @@ export class HuiDialogEditCard extends LitElement implements HassDialog { @keydown=${this._ignoreKeydown} @closed=${this._cancel} @opened=${this._opened} - .heading=${html`${heading} - ${this._documentationURL !== undefined - ? html` - - - - - - ` - : ""}`} + .heading=${true} > -
    - ${this._cardConfig === undefined - ? html` - - ` - : html` -
    -
    - -
    -
    - - ${this._error - ? html` - - ` - : ``} -
    -
    - `} +
    + +
    ${heading}
    + ${this._documentationURL !== undefined + ? html` + + + + + + ` + : ""} +
    +
    +
    +
    + +
    +
    + + ${this._error + ? html` + + ` + : ``} +
    ${this._cardConfig !== undefined ? html` @@ -256,126 +257,6 @@ export class HuiDialogEditCard extends LitElement implements HassDialog { ev.stopPropagation(); } - static get styles(): CSSResultArray { - return [ - haStyleDialog, - css` - :host { - --code-mirror-max-height: calc(100vh - 176px); - } - - @media all and (max-width: 450px), all and (max-height: 500px) { - /* overrule the ha-style-dialog max-height on small screens */ - ha-dialog { - --mdc-dialog-max-height: 100%; - height: 100%; - } - } - - @media all and (min-width: 850px) { - ha-dialog { - --mdc-dialog-min-width: 845px; - } - } - - ha-dialog { - --mdc-dialog-max-width: 845px; - } - - .center { - margin-left: auto; - margin-right: auto; - } - - .content { - display: flex; - flex-direction: column; - margin: 0 -10px; - } - .content hui-card-preview { - margin: 4px auto; - max-width: 390px; - } - .content .element-editor { - margin: 0 10px; - } - - @media (min-width: 1200px) { - ha-dialog { - --mdc-dialog-max-width: calc(100% - 32px); - --mdc-dialog-min-width: 1000px; - } - - .content { - flex-direction: row; - } - .content > * { - flex-basis: 0; - flex-grow: 1; - flex-shrink: 1; - min-width: 0; - } - .content hui-card-preview { - padding: 8px 10px; - margin: auto 0px; - max-width: 500px; - } - } - - mwc-button ha-circular-progress { - margin-right: 20px; - } - .hidden { - display: none; - } - .element-editor { - margin-bottom: 8px; - } - .blur { - filter: blur(2px) grayscale(100%); - } - .element-preview { - position: relative; - } - .element-preview ha-circular-progress { - top: 50%; - left: 50%; - position: absolute; - z-index: 10; - } - hui-card-preview { - padding-top: 8px; - margin-bottom: 4px; - display: block; - width: 100%; - box-sizing: border-box; - } - .gui-mode-button { - margin-right: auto; - } - .header { - display: flex; - align-items: center; - justify-content: space-between; - } - `, - ]; - } - - private _handleCardPicked(ev) { - const config = ev.detail.config; - if (this._params!.entities && this._params!.entities.length) { - if (Object.keys(config).includes("entities")) { - config.entities = this._params!.entities; - } else if (Object.keys(config).includes("entity")) { - config.entity = this._params!.entities[0]; - } - } - this._cardConfig = deepFreeze(config); - this._error = ev.detail.error; - this._dirty = true; - } - private _handleConfigChanged(ev: HASSDomEvent) { this._cardConfig = deepFreeze(ev.detail.config); this._error = ev.detail.error; @@ -463,6 +344,125 @@ export class HuiDialogEditCard extends LitElement implements HassDialog { showSaveSuccessToast(this, this.hass); this.closeDialog(); } + + static get styles(): CSSResultArray { + return [ + haStyleDialog, + css` + :host { + --code-mirror-max-height: calc(100vh - 176px); + } + + @media all and (max-width: 450px), all and (max-height: 500px) { + /* overrule the ha-style-dialog max-height on small screens */ + ha-dialog { + --mdc-dialog-max-height: 100%; + height: 100%; + } + } + + @media all and (min-width: 850px) { + ha-dialog { + --mdc-dialog-min-width: 845px; + } + } + + ha-dialog { + --mdc-dialog-max-width: 845px; + --dialog-z-index: 5; + } + + 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)); + } + + .center { + margin-left: auto; + margin-right: auto; + } + + .content { + display: flex; + flex-direction: column; + margin: 0 -10px; + } + .content hui-card-preview { + margin: 4px auto; + max-width: 390px; + } + .content .element-editor { + margin: 0 10px; + } + + @media (min-width: 1200px) { + ha-dialog { + --mdc-dialog-max-width: calc(100% - 32px); + --mdc-dialog-min-width: 1000px; + } + + .content { + flex-direction: row; + } + .content > * { + flex-basis: 0; + flex-grow: 1; + flex-shrink: 1; + min-width: 0; + } + .content hui-card-preview { + padding: 8px 10px; + margin: auto 0px; + max-width: 500px; + } + } + + mwc-button ha-circular-progress { + margin-right: 20px; + } + .hidden { + display: none; + } + .element-editor { + margin-bottom: 8px; + } + .blur { + filter: blur(2px) grayscale(100%); + } + .element-preview { + position: relative; + } + .element-preview ha-circular-progress { + top: 50%; + left: 50%; + position: absolute; + z-index: 10; + } + hui-card-preview { + padding-top: 8px; + margin-bottom: 4px; + display: block; + width: 100%; + box-sizing: border-box; + } + .gui-mode-button { + margin-right: auto; + } + .header { + display: flex; + align-items: center; + justify-content: space-between; + } + .header_button { + color: inherit; + text-decoration: none; + } + `, + ]; + } } declare global { 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 a4f222200c..0a23b164e6 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 @@ -5,9 +5,9 @@ import { CSSResultArray, customElement, html, + internalProperty, LitElement, property, - internalProperty, query, TemplateResult, } from "lit-element"; @@ -21,7 +21,7 @@ import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; import { computeCards } from "../../common/generate-lovelace-config"; import { addCards } from "../config-util"; import "./hui-card-preview"; -import { showEditCardDialog } from "./show-edit-card-dialog"; +import { showCreateCardDialog } from "./show-create-card-dialog"; import { SuggestCardDialogParams } from "./show-suggest-card-dialog"; @customElement("hui-dialog-suggest-card") @@ -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; @@ -179,7 +180,8 @@ export class HuiDialogSuggestCard extends LitElement { ) { return; } - showEditCardDialog(this, { + + showCreateCardDialog(this, { lovelaceConfig: this._params!.lovelaceConfig, saveConfig: this._params!.saveConfig, path: this._params!.path, diff --git a/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts b/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts new file mode 100644 index 0000000000..3a724fc6e2 --- /dev/null +++ b/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts @@ -0,0 +1,151 @@ +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { HASSDomEvent } from "../../../../common/dom/fire_event"; +import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import "../../../../components/data-table/ha-data-table"; +import type { + DataTableColumnContainer, + DataTableRowData, + SelectionChangedEvent, +} from "../../../../components/data-table/ha-data-table"; +import "../../../../components/entity/state-badge"; +import "../../../../components/ha-relative-time"; +import type { HomeAssistant } from "../../../../types"; + +@customElement("hui-entity-picker-table") +export class HuiEntityPickerTable extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow?: boolean; + + @property({ type: Array }) public entities!: DataTableRowData[]; + + protected render(): TemplateResult { + return html` + + `; + } + + private _columns = memoizeOne((narrow: boolean) => { + const columns: DataTableColumnContainer = { + icon: { + title: "", + type: "icon", + template: (_icon, entity: any) => html` + + `, + }, + name: { + title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity"), + sortable: true, + filterable: true, + grows: true, + direction: "asc", + template: (name, entity: any) => html` +
    + ${name} + ${narrow + ? html` +
    + ${entity.stateObj.entity_id} +
    + ` + : ""} +
    + `, + }, + }; + + columns.entity_id = { + title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity_id"), + sortable: true, + filterable: true, + width: "30%", + hidden: narrow, + }; + + columns.domain = { + title: this.hass!.localize("ui.panel.lovelace.unused_entities.domain"), + sortable: true, + filterable: true, + width: "15%", + hidden: narrow, + }; + + columns.last_changed = { + title: this.hass!.localize( + "ui.panel.lovelace.unused_entities.last_changed" + ), + type: "numeric", + sortable: true, + width: "15%", + hidden: narrow, + template: (lastChanged: string) => html` + + `, + }; + + return columns; + }); + + private _handleSelectionChanged( + ev: HASSDomEvent + ): void { + const selectedEntities = ev.detail.value; + + fireEvent(this, "selected-changed", { selectedEntities }); + } + + private _handleEntityClicked(ev: Event) { + const entityId = ((ev.target as HTMLElement).closest( + ".mdc-data-table__row" + ) as any).rowId; + fireEvent(this, "hass-more-info", { + entityId, + }); + } + + static get styles(): CSSResult { + return css` + ha-data-table { + --data-table-border-width: 0; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-entity-picker-table": HuiEntityPickerTable; + } +} diff --git a/src/panels/lovelace/editor/card-editor/show-create-card-dialog.ts b/src/panels/lovelace/editor/card-editor/show-create-card-dialog.ts new file mode 100644 index 0000000000..42192a467e --- /dev/null +++ b/src/panels/lovelace/editor/card-editor/show-create-card-dialog.ts @@ -0,0 +1,25 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import { LovelaceConfig } from "../../../../data/lovelace"; + +export interface CreateCardDialogParams { + 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 +} + +const importCreateCardDialog = () => + import( + /* webpackChunkName: "hui-dialog-create-card" */ "./hui-dialog-create-card" + ); + +export const showCreateCardDialog = ( + element: HTMLElement, + createCardDialogParams: CreateCardDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "hui-dialog-create-card", + dialogImport: importCreateCardDialog, + dialogParams: createCardDialogParams, + }); +}; 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 18b0d820e3..46525f1c43 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-button-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts index 36ce2aab96..a2285c087d 100644 --- a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts @@ -2,14 +2,18 @@ import "@polymer/paper-input/paper-input"; import { customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; +import { assert, boolean, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import { stateIcon } from "../../../../common/entity/state_icon"; +import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import "../../../../components/ha-formfield"; import "../../../../components/ha-icon-input"; +import "../../../../components/ha-switch"; import { ActionConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; import { ButtonCardConfig } from "../../cards/types"; @@ -17,16 +21,8 @@ import "../../components/hui-action-editor"; import "../../components/hui-entity-editor"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; -import { - actionConfigStruct, - EditorTarget, - EntitiesEditorEvent, -} from "../types"; -import "../../../../components/ha-switch"; -import "../../../../components/ha-formfield"; +import { actionConfigStruct, EditorTarget } from "../types"; import { configElementStyle } from "./config-elements-style"; -import { computeRTLDirection } from "../../../../common/util/compute_rtl"; -import { assert, object, string, optional, boolean } from "superstruct"; const cardConfigStruct = object({ type: string(), @@ -63,11 +59,11 @@ export class HuiButtonCardEditor extends LitElement } get _show_name(): boolean { - return this._config!.show_name || true; + return this._config!.show_name ?? true; } get _show_state(): boolean { - return this._config!.show_state || false; + return this._config!.show_state ?? false; } get _icon(): string { @@ -75,7 +71,7 @@ export class HuiButtonCardEditor extends LitElement } get _show_icon(): boolean { - return this._config!.show_icon || true; + return this._config!.show_icon ?? true; } get _icon_height(): string { @@ -85,11 +81,11 @@ export class HuiButtonCardEditor extends LitElement } get _tap_action(): ActionConfig { - return this._config!.tap_action || { action: "more-info" }; + return this._config!.tap_action || { action: "toggle" }; } get _hold_action(): ActionConfig { - return this._config!.hold_action || { action: "none" }; + return this._config!.hold_action || { action: "more-info" }; } get _theme(): string { @@ -123,7 +119,7 @@ export class HuiButtonCardEditor extends LitElement .hass=${this.hass} .value="${this._entity}" .configValue=${"entity"} - @change="${this._valueChanged}" + @value-changed="${this._valueChanged}" allow-custom-entity >
    @@ -161,7 +157,7 @@ export class HuiButtonCardEditor extends LitElement
    @@ -175,7 +171,7 @@ export class HuiButtonCardEditor extends LitElement
    @@ -189,7 +185,7 @@ export class HuiButtonCardEditor extends LitElement
    @@ -225,7 +221,7 @@ export class HuiButtonCardEditor extends LitElement .config="${this._tap_action}" .actions="${actions}" .configValue="${"tap_action"}" - @action-changed="${this._valueChanged}" + @value-changed="${this._valueChanged}" >
    `; } - private _valueChanged(ev: EntitiesEditorEvent): void { + private _change(ev: Event) { if (!this._config || !this.hass) { return; } const target = ev.target! as EditorTarget; + const value = target.checked; - if ( - this[`_${target.configValue}`] === target.value || - this[`_${target.configValue}`] === target.config - ) { + if (this[`_${target.configValue}`] === value) { + return; + } + + this._config = { + ...this._config, + [target.configValue!]: value, + }; + fireEvent(this, "config-changed", { config: this._config }); + } + + private _valueChanged(ev: CustomEvent): void { + if (!this._config || !this.hass) { + return; + } + const target = ev.target! as EditorTarget; + const value = ev.detail.value; + + if (this[`_${target.configValue}`] === value) { return; } if (target.configValue) { - if (target.value === "") { + if (value !== false && !value) { this._config = { ...this._config }; delete this._config[target.configValue!]; } else { @@ -266,18 +278,11 @@ export class HuiButtonCardEditor extends LitElement target.configValue === "icon_height" && !isNaN(Number(target.value)) ) { - newValue = `${String(target.value)}px`; + newValue = `${String(value)}px`; } this._config = { ...this._config, - [target.configValue!]: - target.checked !== undefined - ? target.checked - : newValue !== undefined - ? newValue - : target.value - ? target.value - : target.config, + [target.configValue!]: newValue !== undefined ? newValue : value, }; } } 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 cd230be535..49a9db601c 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 >
    - + >
    @@ -145,7 +141,7 @@ export class HuiLightCardEditor extends LitElement .config=${this._hold_action} .actions=${actions} .configValue=${"hold_action"} - @action-changed=${this._valueChanged} + @value-changed=${this._valueChanged} >
    `; } - private _valueChanged(ev: EntitiesEditorEvent): void { + private _valueChanged(ev: CustomEvent): void { if (!this._config || !this.hass) { return; } const target = ev.target! as EditorTarget; + const value = ev.detail.value; - if ( - this[`_${target.configValue}`] === target.value || - this[`_${target.configValue}`] === target.config - ) { + if (this[`_${target.configValue}`] === value) { return; } if (target.configValue) { - if (target.value === "") { + if (value !== false && !value) { this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { ...this._config, - [target.configValue!]: target.value ? target.value : target.config, + [target.configValue!]: value, }; } } diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts index 98fbf6af9a..8858545325 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts @@ -2,11 +2,12 @@ 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 { ActionConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; @@ -14,13 +15,8 @@ import { PictureCardConfig } from "../../cards/types"; import "../../components/hui-action-editor"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; -import { - actionConfigStruct, - EditorTarget, - EntitiesEditorEvent, -} from "../types"; +import { actionConfigStruct, EditorTarget } from "../types"; import { configElementStyle } from "./config-elements-style"; -import { string, object, optional, assert } from "superstruct"; const cardConfigStruct = object({ type: string(), @@ -89,7 +85,7 @@ export class HuiPictureCardEditor extends LitElement .config="${this._tap_action}" .actions="${actions}" .configValue="${"tap_action"}" - @action-changed="${this._valueChanged}" + @value-changed="${this._valueChanged}" > @@ -184,8 +180,7 @@ export class HuiPictureEntityCardEditor extends LitElement )} (${this.hass.localize( "ui.panel.lovelace.editor.card.config.optional" )})" - type="number" - .value="${Number(this._aspect_ratio.replace("%", ""))}" + .value="${this._aspect_ratio}" .configValue="${"aspect_ratio"}" @value-changed="${this._valueChanged}" > @@ -201,7 +196,7 @@ export class HuiPictureEntityCardEditor extends LitElement
    @@ -215,7 +210,7 @@ export class HuiPictureEntityCardEditor extends LitElement @@ -231,7 +226,7 @@ export class HuiPictureEntityCardEditor extends LitElement .config="${this._tap_action}" .actions="${actions}" .configValue="${"tap_action"}" - @action-changed="${this._valueChanged}" + @value-changed="${this._valueChanged}" > @@ -180,8 +171,7 @@ export class HuiPictureGlanceCardEditor extends LitElement )} (${this.hass.localize( "ui.panel.lovelace.editor.card.config.optional" )})" - type="number" - .value="${Number(this._aspect_ratio.replace("%", ""))}" + .value="${this._aspect_ratio}" .configValue="${"aspect_ratio"}" @value-changed="${this._valueChanged}" > @@ -195,7 +185,7 @@ export class HuiPictureGlanceCardEditor extends LitElement .hass=${this.hass} .value="${this._entity}" .configValue=${"entity"} - @change="${this._valueChanged}" + @value-changed="${this._valueChanged}" allow-custom-entity >
    @@ -209,7 +199,7 @@ export class HuiPictureGlanceCardEditor extends LitElement .config="${this._tap_action}" .actions="${actions}" .configValue="${"tap_action"}" - @action-changed="${this._valueChanged}" + @value-changed="${this._valueChanged}" >
    { +export const getCardDocumentationURL = ( + hass: HomeAssistant, + type: string +): string | undefined => { if (type.startsWith(CUSTOM_TYPE_PREFIX)) { return getCustomCardEntry(type)?.documentationURL; } - return `${coreDocumentationURLBase}${type}`; + return `${documentationUrl(hass, "/lovelace/")}${type}`; }; diff --git a/src/panels/lovelace/editor/hui-dialog-save-config.ts b/src/panels/lovelace/editor/hui-dialog-save-config.ts index f056413258..3fd5997ffa 100644 --- a/src/panels/lovelace/editor/hui-dialog-save-config.ts +++ b/src/panels/lovelace/editor/hui-dialog-save-config.ts @@ -1,33 +1,32 @@ import "@material/mwc-button"; import "@material/mwc-icon-button/mwc-icon-button"; +import { mdiHelpCircle } from "@mdi/js"; import { css, CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; -import { mdiHelpCircle } from "@mdi/js"; import { fireEvent } from "../../../common/dom/fire_event"; +import { computeRTLDirection } from "../../../common/util/compute_rtl"; +import "../../../components/ha-circular-progress"; +import "../../../components/ha-dialog"; +import "../../../components/ha-formfield"; +import "../../../components/ha-svg-icon"; +import "../../../components/ha-switch"; +import "../../../components/ha-yaml-editor"; +import type { HassDialog } from "../../../dialogs/make-dialog-manager"; import { haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; import type { SaveDialogParams } from "./show-save-config-dialog"; -import { computeRTLDirection } from "../../../common/util/compute_rtl"; -import type { HassDialog } from "../../../dialogs/make-dialog-manager"; -import "../../../components/ha-switch"; -import "../../../components/ha-formfield"; -import "../../../components/ha-yaml-editor"; -import "../../../components/ha-svg-icon"; -import "../../../components/ha-dialog"; -import "../../../components/ha-circular-progress"; const EMPTY_CONFIG = { views: [] }; -const coreDocumentationURLBase = "https://www.home-assistant.io/lovelace/"; - @customElement("hui-dialog-save-config") export class HuiSaveConfig extends LitElement implements HassDialog { @property({ attribute: false }) public hass?: HomeAssistant; @@ -68,7 +67,7 @@ export class HuiSaveConfig extends LitElement implements HassDialog { "ui.panel.lovelace.editor.save_config.header" )} { - const columns: DataTableColumnContainer = { - icon: { - title: "", - type: "icon", - template: (_icon, entity: any) => html` - - `, - }, - name: { - title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity"), - sortable: true, - filterable: true, - grows: true, - direction: "asc", - template: (name, entity: any) => html` -
    - ${name} - ${narrow - ? html` -
    - ${entity.stateObj.entity_id} -
    - ` - : ""} -
    - `, - }, - }; - - if (narrow) { - return columns; - } - - columns.entity_id = { - title: this.hass!.localize("ui.panel.lovelace.unused_entities.entity_id"), - sortable: true, - filterable: true, - width: "30%", - }; - columns.domain = { - title: this.hass!.localize("ui.panel.lovelace.unused_entities.domain"), - sortable: true, - filterable: true, - width: "15%", - }; - columns.last_changed = { - title: this.hass!.localize( - "ui.panel.lovelace.unused_entities.last_changed" - ), - type: "numeric", - sortable: true, - width: "15%", - template: (lastChanged: string) => html` - - `, - }; - - return columns; - }); - protected updated(changedProperties: PropertyValues): void { super.updated(changedProperties); @@ -161,9 +85,10 @@ export class HuiUnusedEntities extends LitElement { ` : ""} - { + { const stateObj = this.hass!.states[entity]; return { icon: "", @@ -173,18 +98,9 @@ export class HuiUnusedEntities extends LitElement { domain: computeDomain(entity), last_changed: stateObj!.last_changed, }; - })} - .id=${"entity_id"} - selectable - @selection-changed=${this._handleSelectionChanged} - .dir=${computeRTLDirection(this.hass)} - .searchLabel=${this.hass.localize( - "ui.panel.lovelace.unused_entities.search" - )} - .noDataText=${this.hass.localize( - "ui.panel.lovelace.unused_entities.no_data" - )} - > + }) as DataTableRowData[]} + @selected-changed=${this._handleSelectedChanged} + >
    ${this._sortedUsers(this._users).map( (user) => html` - + + ${user.name} - + ` )} `; diff --git a/src/panels/lovelace/elements/hui-icon-element.ts b/src/panels/lovelace/elements/hui-icon-element.ts index 82e7a7fc39..4811f219ac 100644 --- a/src/panels/lovelace/elements/hui-icon-element.ts +++ b/src/panels/lovelace/elements/hui-icon-element.ts @@ -3,8 +3,8 @@ import { CSSResult, customElement, html, - LitElement, internalProperty, + LitElement, TemplateResult, } from "lit-element"; import { ifDefined } from "lit-html/directives/if-defined"; @@ -28,7 +28,7 @@ export class HuiIconElement extends LitElement implements LovelaceElement { throw Error("Invalid Configuration: 'icon' required"); } - this._config = config; + this._config = { hold_action: { action: "more-info" }, ...config }; } protected render(): TemplateResult { diff --git a/src/panels/lovelace/elements/hui-image-element.ts b/src/panels/lovelace/elements/hui-image-element.ts index a42c7bab8f..50e45c153a 100644 --- a/src/panels/lovelace/elements/hui-image-element.ts +++ b/src/panels/lovelace/elements/hui-image-element.ts @@ -3,9 +3,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import { ifDefined } from "lit-html/directives/if-defined"; @@ -29,12 +29,13 @@ export class HuiImageElement extends LitElement implements LovelaceElement { throw Error("Error in element configuration"); } + this._config = { hold_action: { action: "more-info" }, ...config }; + // eslint-disable-next-line wc/no-self-class this.classList.toggle( "clickable", - config.tap_action && config.tap_action.action !== "none" + this._config.tap_action && this._config.tap_action.action !== "none" ); - this._config = config; } protected render(): TemplateResult { diff --git a/src/panels/lovelace/elements/hui-state-badge-element.ts b/src/panels/lovelace/elements/hui-state-badge-element.ts index 2a008921fa..1baa6b0d8d 100644 --- a/src/panels/lovelace/elements/hui-state-badge-element.ts +++ b/src/panels/lovelace/elements/hui-state-badge-element.ts @@ -1,9 +1,9 @@ import { customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; @@ -16,9 +16,9 @@ import { actionHandler } from "../common/directives/action-handler-directive"; import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { hasConfigOrEntityChanged } from "../common/has-changed"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; import "../components/hui-warning-element"; import { LovelaceElement, StateBadgeElementConfig } from "./types"; -import { createEntityNotFoundWarning } from "../components/hui-warning"; @customElement("hui-state-badge-element") export class HuiStateBadgeElement extends LitElement @@ -32,7 +32,7 @@ export class HuiStateBadgeElement extends LitElement throw Error("Invalid Configuration: 'entity' required"); } - this._config = config; + this._config = { hold_action: { action: "more-info" }, ...config }; } protected shouldUpdate(changedProps: PropertyValues): boolean { diff --git a/src/panels/lovelace/elements/hui-state-icon-element.ts b/src/panels/lovelace/elements/hui-state-icon-element.ts index d4a1a2eba1..84c8593ad0 100644 --- a/src/panels/lovelace/elements/hui-state-icon-element.ts +++ b/src/panels/lovelace/elements/hui-state-icon-element.ts @@ -3,9 +3,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; @@ -18,9 +18,9 @@ import { actionHandler } from "../common/directives/action-handler-directive"; import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { hasConfigOrEntityChanged } from "../common/has-changed"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; import "../components/hui-warning-element"; import { LovelaceElement, StateIconElementConfig } from "./types"; -import { createEntityNotFoundWarning } from "../components/hui-warning"; @customElement("hui-state-icon-element") export class HuiStateIconElement extends LitElement implements LovelaceElement { @@ -33,7 +33,11 @@ export class HuiStateIconElement extends LitElement implements LovelaceElement { throw Error("Invalid Configuration: 'entity' required"); } - this._config = { state_color: true, ...config }; + this._config = { + state_color: true, + hold_action: { action: "more-info" }, + ...config, + }; } protected shouldUpdate(changedProps: PropertyValues): boolean { diff --git a/src/panels/lovelace/elements/hui-state-label-element.ts b/src/panels/lovelace/elements/hui-state-label-element.ts index 08de7d4516..7296f0dc64 100644 --- a/src/panels/lovelace/elements/hui-state-label-element.ts +++ b/src/panels/lovelace/elements/hui-state-label-element.ts @@ -3,9 +3,9 @@ import { CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; @@ -18,9 +18,9 @@ import { actionHandler } from "../common/directives/action-handler-directive"; import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { hasConfigOrEntityChanged } from "../common/has-changed"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; import "../components/hui-warning-element"; import { LovelaceElement, StateLabelElementConfig } from "./types"; -import { createEntityNotFoundWarning } from "../components/hui-warning"; @customElement("hui-state-label-element") class HuiStateLabelElement extends LitElement implements LovelaceElement { @@ -33,7 +33,7 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement { throw Error("Invalid Configuration: 'entity' required"); } - this._config = config; + this._config = { hold_action: { action: "more-info" }, ...config }; } protected shouldUpdate(changedProps: PropertyValues): boolean { diff --git a/src/panels/lovelace/entity-rows/hui-group-entity-row.ts b/src/panels/lovelace/entity-rows/hui-group-entity-row.ts index 04d1f817a0..8982640a61 100644 --- a/src/panels/lovelace/entity-rows/hui-group-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-group-entity-row.ts @@ -1,13 +1,14 @@ import { customElement, html, + internalProperty, LitElement, property, - internalProperty, PropertyValues, TemplateResult, } from "lit-element"; import { DOMAINS_TOGGLE } from "../../../common/const"; +import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; import "../../../components/entity/ha-entity-toggle"; import { HomeAssistant } from "../../../types"; @@ -22,6 +23,19 @@ class HuiGroupEntityRow extends LitElement implements LovelaceRow { @internalProperty() private _config?: EntityConfig; + private _computeCanToggle(hass: HomeAssistant, entityIds: string[]): boolean { + return entityIds.some((entityId) => { + const domain = computeDomain(entityId); + if (domain === "group") { + return this._computeCanToggle( + hass, + this.hass?.states[entityId].attributes["entity_id"] + ); + } + return DOMAINS_TOGGLE.has(domain); + }); + } + public setConfig(config: EntityConfig): void { if (!config) { throw new Error("Configuration error"); @@ -50,7 +64,7 @@ class HuiGroupEntityRow extends LitElement implements LovelaceRow { return html` - ${this._computeCanToggle(stateObj.attributes.entity_id) + ${this._computeCanToggle(this.hass, stateObj.attributes.entity_id) ? html` `; } - - private _computeCanToggle(entityIds): boolean { - return entityIds.some((entityId) => - DOMAINS_TOGGLE.has(entityId.split(".", 1)[0]) - ); - } } declare global { diff --git a/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts index f878c33985..46e5a1e88c 100644 --- a/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts @@ -11,7 +11,7 @@ import "../../../components/ha-date-input"; import type { HaDateInput } from "../../../components/ha-date-input"; import "../../../components/paper-time-input"; import type { PaperTimeInput } from "../../../components/paper-time-input"; -import { UNAVAILABLE_STATES } from "../../../data/entity"; +import { UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity"; import { setInputDateTimeValue } from "../../../data/input_datetime"; import type { HomeAssistant } from "../../../types"; import { hasConfigOrEntityChanged } from "../common/has-changed"; @@ -70,10 +70,10 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { ? html` ${stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP && - stateObj.state !== "unavailable" && - stateObj.state !== "unknown" + !UNAVAILABLE_STATES.includes(stateObj.state) ? html`
    + Boolean( + this._editMode || + view.visible === undefined || + view.visible === true || + (Array.isArray(view.visible) && + view.visible.some((show) => show.user === this.hass!.user?.id)) + ); + protected updated(changedProperties: PropertyValues): void { super.updated(changedProperties); @@ -407,9 +417,14 @@ class HUIRoot extends LitElement { if (changedProperties.has("route")) { const views = this.config.views; + if (!viewPath && views.length) { - navigate(this, `${this.route!.prefix}/${views[0].path || 0}`, true); - newSelectView = 0; + newSelectView = views.findIndex(this._isVisible); + navigate( + this, + `${this.route!.prefix}/${views[newSelectView].path || newSelectView}`, + true + ); } else if (viewPath === "hass-unused-entities") { newSelectView = "hass-unused-entities"; } else if (viewPath) { @@ -449,8 +464,14 @@ class HUIRoot extends LitElement { this.lovelace!.mode === "storage" && viewPath === "hass-unused-entities" ) { - navigate(this, `${this.route?.prefix}/${views[0]?.path || 0}`); - newSelectView = 0; + newSelectView = views.findIndex(this._isVisible); + navigate( + this, + `${this.route!.prefix}/${ + views[newSelectView].path || newSelectView + }`, + true + ); } } diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts index 05f991575b..eb627cf1ff 100644 --- a/src/panels/lovelace/views/hui-view.ts +++ b/src/panels/lovelace/views/hui-view.ts @@ -23,11 +23,11 @@ import { computeCardSize } from "../common/compute-card-size"; import { processConfigEntities } from "../common/process-config-entities"; import { createBadgeElement } from "../create-element/create-badge-element"; import { createCardElement } from "../create-element/create-card-element"; -import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; import { Lovelace, LovelaceBadge, LovelaceCard } from "../types"; import "../../../components/ha-svg-icon"; import { mdiPlus } from "@mdi/js"; import { nextRender } from "../../../common/util/render-status"; +import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog"; let editCodeLoaded = false; @@ -186,7 +186,7 @@ export class HUIView extends LitElement { } private _addCard(): void { - showEditCardDialog(this, { + showCreateCardDialog(this, { lovelaceConfig: this.lovelace!.config, saveConfig: this.lovelace!.saveConfig, path: [this.index!], diff --git a/src/panels/media-browser/ha-panel-media-browser.ts b/src/panels/media-browser/ha-panel-media-browser.ts new file mode 100644 index 0000000000..32e8661d52 --- /dev/null +++ b/src/panels/media-browser/ha-panel-media-browser.ts @@ -0,0 +1,163 @@ +import "@material/mwc-icon-button"; +import "@polymer/app-layout/app-header/app-header"; +import "@polymer/app-layout/app-toolbar/app-toolbar"; +import { + css, + CSSResultArray, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { LocalStorage } from "../../common/decorators/local-storage"; +import { HASSDomEvent } from "../../common/dom/fire_event"; +import { computeStateDomain } from "../../common/entity/compute_state_domain"; +import { supportsFeature } from "../../common/entity/supports-feature"; +import "../../components/ha-menu-button"; +import "../../components/media-player/ha-media-player-browse"; +import { + BROWSER_PLAYER, + MediaPickedEvent, + SUPPORT_BROWSE_MEDIA, +} from "../../data/media-player"; +import "../../layouts/ha-app-layout"; +import { haStyle } from "../../resources/styles"; +import type { HomeAssistant } from "../../types"; +import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog"; +import { showSelectMediaPlayerDialog } from "./show-select-media-source-dialog"; + +@customElement("ha-panel-media-browser") +class PanelMediaBrowser extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean, reflect: true }) + public narrow!: boolean; + + // @ts-ignore + @LocalStorage("mediaBrowseEntityId", true) + private _entityId = BROWSER_PLAYER; + + protected render(): TemplateResult { + const stateObj = this._entityId + ? this.hass.states[this._entityId] + : undefined; + + const title = + this._entityId === BROWSER_PLAYER + ? `${this.hass.localize("ui.components.media-browser.web-browser")}` + : stateObj?.attributes.friendly_name + ? `${stateObj?.attributes.friendly_name}` + : undefined; + + return html` + + + + +
    +
    + ${this.hass.localize( + "ui.components.media-browser.media-player-browser" + )} +
    +
    ${title || ""}
    +
    + + ${this.hass.localize("ui.components.media-browser.choose_player")} + +
    +
    +
    + +
    +
    + `; + } + + 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_PLAYER) { + 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` + :host { + --mdc-theme-primary: var(--app-header-text-color); + } + ha-media-player-browse { + height: calc(100vh - 84px); + } + :host([narrow]) app-toolbar mwc-button { + width: 65px; + } + .heading { + overflow: hidden; + white-space: nowrap; + } + .heading .secondary-text { + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + } + `, + ]; + } +} + +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 0000000000..46865415da --- /dev/null +++ b/src/panels/media-browser/hui-dialog-select-media-player.ts @@ -0,0 +1,103 @@ +import "@material/mwc-list/mwc-list"; +import "@material/mwc-list/mwc-list-item"; +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 { computeStateName } from "../../common/entity/compute_state_name"; +import { compare } from "../../common/string/compare"; +import { createCloseHeading } from "../../components/ha-dialog"; +import { UNAVAILABLE_STATES } from "../../data/entity"; +import { BROWSER_PLAYER } from "../../data/media-player"; +import { haStyleDialog } from "../../resources/styles"; +import type { HomeAssistant } from "../../types"; +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 + .sort((a, b) => compare(computeStateName(a), computeStateName(b))) + .map( + (source) => html` + ${computeStateName(source)} + ` + )} + + + `; + } + + private _selectPlayer(ev: CustomEvent): void { + const entityId = (ev.currentTarget as any).player; + this._params!.sourceSelectedCallback(entityId); + this.closeDialog(); + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + ha-dialog { + --dialog-content-padding: 0 24px 20px; + } + mwc-list-item[disabled] { + --mdc-theme-text-primary-on-background: var(--disabled-text-color); + } + `, + ]; + } +} + +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 0000000000..ea04f9033e --- /dev/null +++ b/src/panels/media-browser/hui-dialog-web-browser-play-media.ts @@ -0,0 +1,120 @@ +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 0000000000..161e97fd96 --- /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 0000000000..ec68a67a5b --- /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/panels/profile/ha-long-lived-access-tokens-card.js b/src/panels/profile/ha-long-lived-access-tokens-card.js deleted file mode 100644 index 5943164610..0000000000 --- a/src/panels/profile/ha-long-lived-access-tokens-card.js +++ /dev/null @@ -1,171 +0,0 @@ -import "@material/mwc-button"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import { formatDateTime } from "../../common/datetime/format_date_time"; -import "../../components/ha-card"; -import "../../components/ha-icon-button"; -import { - showAlertDialog, - showPromptDialog, - showConfirmationDialog, -} from "../../dialogs/generic/show-dialog-box"; -import { EventsMixin } from "../../mixins/events-mixin"; -import LocalizeMixin from "../../mixins/localize-mixin"; -import "../../styles/polymer-ha-style"; -import "../../components/ha-settings-row"; - -/* - * @appliesMixin EventsMixin - * @appliesMixin LocalizeMixin - */ -class HaLongLivedTokens extends LocalizeMixin(EventsMixin(PolymerElement)) { - static get template() { - return html` - - -
    - -
    - - [[localize('ui.panel.profile.long_lived_access_tokens.create')]] - -
    - - `; - } - - static get properties() { - return { - hass: Object, - refreshTokens: Array, - _tokens: { - type: Array, - computed: "_computeTokens(refreshTokens)", - }, - }; - } - - _computeTokens(refreshTokens) { - return refreshTokens - .filter((tkn) => tkn.type === "long_lived_access_token") - .reverse(); - } - - _formatTitle(name) { - return this.localize( - "ui.panel.profile.long_lived_access_tokens.token_title", - "name", - name - ); - } - - _formatCreatedAt(created) { - return this.localize( - "ui.panel.profile.long_lived_access_tokens.created_at", - "date", - formatDateTime(new Date(created), this.hass.language) - ); - } - - async _handleCreate() { - const name = await showPromptDialog(this, { - text: this.localize( - "ui.panel.profile.long_lived_access_tokens.prompt_name" - ), - }); - if (!name) return; - try { - const token = await this.hass.callWS({ - type: "auth/long_lived_access_token", - lifespan: 3650, - client_name: name, - }); - await showPromptDialog(this, { - title: name, - text: this.localize( - "ui.panel.profile.long_lived_access_tokens.prompt_copy_token" - ), - defaultValue: token, - }); - this.fire("hass-refresh-tokens"); - } catch (err) { - // eslint-disable-next-line - console.error(err); - showAlertDialog(this, { - text: this.localize( - "ui.panel.profile.long_lived_access_tokens.create_failed" - ), - }); - } - } - - async _handleDelete(ev) { - const token = ev.model.item; - if ( - !(await showConfirmationDialog(this, { - text: this.localize( - "ui.panel.profile.long_lived_access_tokens.confirm_delete", - "name", - token.client_name - ), - })) - ) { - return; - } - try { - await this.hass.callWS({ - type: "auth/delete_refresh_token", - refresh_token_id: token.id, - }); - this.fire("hass-refresh-tokens"); - } catch (err) { - // eslint-disable-next-line - console.error(err); - showAlertDialog(this, { - text: this.localize( - "ui.panel.profile.long_lived_access_tokens.delete_failed" - ), - }); - } - } -} - -customElements.define("ha-long-lived-access-tokens-card", HaLongLivedTokens); diff --git a/src/panels/profile/ha-long-lived-access-tokens-card.ts b/src/panels/profile/ha-long-lived-access-tokens-card.ts new file mode 100644 index 0000000000..99ea0bd7bd --- /dev/null +++ b/src/panels/profile/ha-long-lived-access-tokens-card.ts @@ -0,0 +1,201 @@ +import "@material/mwc-button/mwc-button"; +import "@material/mwc-icon-button/mwc-icon-button"; +import { mdiDelete } from "@mdi/js"; +import { + css, + CSSResultArray, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import memoizeOne from "memoize-one"; +import relativeTime from "../../common/datetime/relative_time"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-card"; +import "../../components/ha-settings-row"; +import "../../components/ha-svg-icon"; +import { RefreshToken } from "../../data/refresh_token"; +import { + showAlertDialog, + showConfirmationDialog, + showPromptDialog, +} from "../../dialogs/generic/show-dialog-box"; +import { haStyle } from "../../resources/styles"; +import "../../styles/polymer-ha-style"; +import { HomeAssistant } from "../../types"; + +@customElement("ha-long-lived-access-tokens-card") +class HaLongLivedTokens extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public refreshTokens?: RefreshToken[]; + + private _accessTokens = memoizeOne( + (refreshTokens: RefreshToken[]): RefreshToken[] => + refreshTokens + ?.filter((token) => token.type === "long_lived_access_token") + .reverse() + ); + + protected render(): TemplateResult { + const accessTokens = this._accessTokens(this.refreshTokens!); + + return html` + +
    + ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.description" + )} + + + ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.learn_auth_requests" + )} + + ${!accessTokens?.length + ? html`

    + ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.empty_state" + )} +

    ` + : accessTokens!.map( + (token) => html` + ${token.client_name} +
    + ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.created", + "date", + relativeTime( + new Date(token.created_at), + this.hass.localize + ) + )} +
    + + + +
    ` + )} +
    + +
    + + ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.create" + )} + +
    +
    + `; + } + + private async _createToken(): Promise { + const name = await showPromptDialog(this, { + text: this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.prompt_name" + ), + inputLabel: this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.name" + ), + }); + + if (!name) { + return; + } + + try { + const token = await this.hass.callWS({ + type: "auth/long_lived_access_token", + lifespan: 3650, + client_name: name, + }); + + showPromptDialog(this, { + title: name, + text: this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.prompt_copy_token" + ), + defaultValue: token, + }); + + fireEvent(this, "hass-refresh-tokens"); + } catch (err) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.create_failed" + ), + text: err.message, + }); + } + } + + private async _deleteToken(ev: Event): Promise { + const token = (ev.currentTarget as any).token; + if ( + !(await showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.confirm_delete", + "name", + token.client_name + ), + })) + ) { + return; + } + try { + await this.hass.callWS({ + type: "auth/delete_refresh_token", + refresh_token_id: token.id, + }); + fireEvent(this, "hass-refresh-tokens"); + } catch (err) { + await showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.delete_failed" + ), + text: err.message, + }); + } + } + + static get styles(): CSSResultArray { + return [ + haStyle, + css` + ha-settings-row { + padding: 0; + } + a { + color: var(--primary-color); + } + mwc-button { + --mdc-theme-primary: var(--primary-color); + } + mwc-icon-button { + color: var(--primary-text-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-long-lived-access-tokens-card": HaLongLivedTokens; + } +} diff --git a/src/panels/profile/ha-panel-profile.ts b/src/panels/profile/ha-panel-profile.ts index 3c429e2e31..c26d92b853 100644 --- a/src/panels/profile/ha-panel-profile.ts +++ b/src/panels/profile/ha-panel-profile.ts @@ -1,5 +1,4 @@ import "@material/mwc-button"; -import "../../layouts/ha-app-layout"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/paper-item/paper-item"; @@ -9,9 +8,9 @@ import { css, CSSResultArray, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; import { fireEvent } from "../../common/dom/fire_event"; @@ -22,7 +21,9 @@ import { CoreFrontendUserData, getOptimisticFrontendUserDataCollection, } from "../../data/frontend"; +import { RefreshToken } from "../../data/refresh_token"; import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; +import "../../layouts/ha-app-layout"; import { haStyle } from "../../resources/styles"; import { HomeAssistant } from "../../types"; import "./ha-advanced-mode-row"; @@ -35,15 +36,15 @@ import "./ha-pick-language-row"; import "./ha-pick-theme-row"; import "./ha-push-notifications-row"; import "./ha-refresh-tokens-card"; -import "./ha-set-vibrate-row"; import "./ha-set-suspend-row"; +import "./ha-set-vibrate-row"; class HaPanelProfile extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public narrow!: boolean; + @property({ type: Boolean }) public narrow!: boolean; - @internalProperty() private _refreshTokens?: unknown[]; + @internalProperty() private _refreshTokens?: RefreshToken[]; @internalProperty() private _coreUserData?: CoreFrontendUserData | null; @@ -106,6 +107,23 @@ class HaPanelProfile extends LitElement { .narrow=${this.narrow} .hass=${this.hass} > + + + ${this.hass.localize( + "ui.panel.profile.customize_sidebar.header" + )} + + + ${this.hass.localize( + "ui.panel.profile.customize_sidebar.description" + )} + + + ${this.hass.localize( + "ui.panel.profile.customize_sidebar.button" + )} + + ${this.hass.dockedSidebar !== "auto" || !this.narrow ? html` diff --git a/src/panels/profile/ha-push-notifications-row.js b/src/panels/profile/ha-push-notifications-row.js index 7f0bbc0af0..528a447298 100644 --- a/src/panels/profile/ha-push-notifications-row.js +++ b/src/panels/profile/ha-push-notifications-row.js @@ -7,6 +7,7 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { pushSupported } from "../../components/ha-push-notifications-toggle"; import LocalizeMixin from "../../mixins/localize-mixin"; import "../../components/ha-settings-row"; +import { documentationUrl } from "../../util/documentation-url"; /* * @appliesMixin LocalizeMixin @@ -26,7 +27,7 @@ class HaPushNotificationsRow extends LocalizeMixin(PolymerElement) { [[_description(_platformLoaded, _pushSupported)]] [[localize('ui.panel.profile.push_notifications.link_promo')]] - ha-icon-button { - color: var(--primary-text-color); - } - ha-icon-button[disabled] { - color: var(--disabled-text-color); - } - - -
    - [[localize('ui.panel.profile.refresh_tokens.description')]] -
    - -
    - `; - } - - static get properties() { - return { - hass: Object, - refreshTokens: Array, - }; - } - - _computeTokens(refreshTokens) { - return refreshTokens.filter((tkn) => tkn.type === "normal").reverse(); - } - - _formatTitle(clientId) { - return this.localize( - "ui.panel.profile.refresh_tokens.token_title", - "clientId", - clientId - ); - } - - _formatCreatedAt(created) { - return this.localize( - "ui.panel.profile.refresh_tokens.created_at", - "date", - formatDateTime(new Date(created), this.hass.language) - ); - } - - _formatLastUsed(item) { - return item.last_used_at - ? this.localize( - "ui.panel.profile.refresh_tokens.last_used", - "date", - formatDateTime(new Date(item.last_used_at), this.hass.language), - "location", - item.last_used_ip - ) - : this.localize("ui.panel.profile.refresh_tokens.not_used"); - } - - async _handleDelete(ev) { - const token = ev.model.item; - if ( - !(await showConfirmationDialog(this, { - text: this.localize( - "ui.panel.profile.refresh_tokens.confirm_delete", - "name", - token.client_id - ), - })) - ) { - return; - } - try { - await this.hass.callWS({ - type: "auth/delete_refresh_token", - refresh_token_id: token.id, - }); - this.fire("hass-refresh-tokens"); - } catch (err) { - // eslint-disable-next-line - console.error(err); - showAlertDialog(this, { - text: this.localize("ui.panel.profile.refresh_tokens.delete_failed"), - }); - } - } -} - -customElements.define("ha-refresh-tokens-card", HaRefreshTokens); diff --git a/src/panels/profile/ha-refresh-tokens-card.ts b/src/panels/profile/ha-refresh-tokens-card.ts new file mode 100644 index 0000000000..f6f1cc3277 --- /dev/null +++ b/src/panels/profile/ha-refresh-tokens-card.ts @@ -0,0 +1,150 @@ +import "@material/mwc-icon-button/mwc-icon-button"; +import { mdiDelete } from "@mdi/js"; +import "@polymer/paper-tooltip/paper-tooltip"; +import { + css, + CSSResultArray, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import memoizeOne from "memoize-one"; +import relativeTime from "../../common/datetime/relative_time"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-card"; +import "../../components/ha-settings-row"; +import "../../components/ha-svg-icon"; +import { RefreshToken } from "../../data/refresh_token"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../dialogs/generic/show-dialog-box"; +import { haStyle } from "../../resources/styles"; +import { HomeAssistant } from "../../types"; + +@customElement("ha-refresh-tokens-card") +class HaRefreshTokens extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public refreshTokens?: RefreshToken[]; + + private _refreshTokens = memoizeOne( + (refreshTokens: RefreshToken[]): RefreshToken[] => + refreshTokens?.filter((token) => token.type === "normal").reverse() + ); + + protected render(): TemplateResult { + const refreshTokens = this._refreshTokens(this.refreshTokens!); + return html` +
    + ${this.hass.localize("ui.panel.profile.refresh_tokens.description")} + ${refreshTokens?.length + ? refreshTokens!.map( + (token) => html` + ${this.hass.localize( + "ui.panel.profile.refresh_tokens.token_title", + "clientId", + token.client_id + )} + +
    + ${this.hass.localize( + "ui.panel.profile.refresh_tokens.created_at", + "date", + relativeTime(new Date(token.created_at), this.hass.localize) + )} +
    +
    + ${token.last_used_at + ? this.hass.localize( + "ui.panel.profile.refresh_tokens.last_used", + "date", + relativeTime( + new Date(token.last_used_at), + this.hass.localize + ), + "location", + token.last_used_ip + ) + : this.hass.localize( + "ui.panel.profile.refresh_tokens.not_used" + )} +
    +
    + ${token.is_current + ? html` + ${this.hass.localize( + "ui.panel.profile.refresh_tokens.current_token_tooltip" + )} + ` + : ""} + + + +
    +
    ` + ) + : ""} +
    +
    `; + } + + private async _deleteToken(ev: Event): Promise { + const token = (ev.currentTarget as any).token; + if ( + !(await showConfirmationDialog(this, { + text: this.hass.localize( + "ui.panel.profile.refresh_tokens.confirm_delete", + "name", + token.client_name + ), + })) + ) { + return; + } + try { + await this.hass.callWS({ + type: "auth/delete_refresh_token", + refresh_token_id: token.id, + }); + fireEvent(this, "hass-refresh-tokens"); + } catch (err) { + await showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.profile.refresh_tokens.delete_failed" + ), + text: err.message, + }); + } + } + + static get styles(): CSSResultArray { + return [ + haStyle, + css` + ha-settings-row { + padding: 0; + } + mwc-icon-button { + color: var(--primary-text-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-refresh-tokens-card": HaRefreshTokens; + } +} diff --git a/src/resources/ha-sortable-style.ts b/src/resources/ha-sortable-style.ts new file mode 100644 index 0000000000..448ca9b7c0 --- /dev/null +++ b/src/resources/ha-sortable-style.ts @@ -0,0 +1,99 @@ +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 a { + height: 48px; + display: flex; + } + + #sortable { + outline: none; + display: block; + } + + .hidden-panel { + display: flex; + } + + .sortable-fallback { + display: none; + } + + .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; + } + } + + .show-panel, + .hide-panel { + display: none; + position: absolute; + top: 0; + right: 0; + --mdc-icon-button-size: 40px; + } + + .hide-panel { + top: 4px; + right: 8px; + } + + :host([expanded]) .hide-panel { + display: block; + } + + :host([expanded]) .show-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/html-import/import-href.js b/src/resources/html-import/import-href.js deleted file mode 100644 index 32083338c3..0000000000 --- a/src/resources/html-import/import-href.js +++ /dev/null @@ -1,101 +0,0 @@ -/* eslint-disable */ -import "./polyfill"; -/** -@license -Copyright (c) 2017 The Polymer Project Authors. All rights reserved. -This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt -The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt -The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt -Code distributed by Google as part of the polymer project is also -subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt -*/ - -// run a callback when HTMLImports are ready or immediately if -// this api is not available. -function whenImportsReady(cb) { - if (window.HTMLImports) { - HTMLImports.whenReady(cb); - } else { - cb(); - } -} - -/** - * Convenience method for importing an HTML document imperatively. - * - * This method creates a new `` element with - * the provided URL and appends it to the document to start loading. - * In the `onload` callback, the `import` property of the `link` - * element will contain the imported document contents. - * - * @param {string} href URL to document to load. - * @param {?function(!Event):void=} onload Callback to notify when an import successfully - * loaded. - * @param {?function(!ErrorEvent):void=} onerror Callback to notify when an import - * unsuccessfully loaded. - * @param {boolean=} optAsync True if the import should be loaded `async`. - * Defaults to `false`. - * @return {!HTMLLinkElement} The link element for the URL to be loaded. - */ -export const importHref = function (href, onload, onerror, optAsync) { - let link /** @type {HTMLLinkElement} */ = document.head.querySelector( - 'link[href="' + href + '"][import-href]' - ); - if (!link) { - link = /** @type {HTMLLinkElement} */ (document.createElement("link")); - link.rel = "import"; - link.href = href; - link.setAttribute("import-href", ""); - } - // always ensure link has `async` attribute if user specified one, - // even if it was previously not async. This is considered less confusing. - if (optAsync) { - link.setAttribute("async", ""); - } - // NOTE: the link may now be in 3 states: (1) pending insertion, - // (2) inflight, (3) already loaded. In each case, we need to add - // event listeners to process callbacks. - const cleanup = function () { - link.removeEventListener("load", loadListener); - link.removeEventListener("error", errorListener); - }; - let loadListener = function (event) { - cleanup(); - // In case of a successful load, cache the load event on the link so - // that it can be used to short-circuit this method in the future when - // it is called with the same href param. - link.__dynamicImportLoaded = true; - if (onload) { - whenImportsReady(() => { - onload(event); - }); - } - }; - let errorListener = function (event) { - cleanup(); - // In case of an error, remove the link from the document so that it - // will be automatically created again the next time `importHref` is - // called. - if (link.parentNode) { - link.parentNode.removeChild(link); - } - if (onerror) { - whenImportsReady(() => { - onerror(event); - }); - } - }; - link.addEventListener("load", loadListener); - link.addEventListener("error", errorListener); - if (link.parentNode == null) { - document.head.appendChild(link); - // if the link already loaded, dispatch a fake load event - // so that listeners are called and get a proper event argument. - } else if (link.__dynamicImportLoaded) { - link.dispatchEvent(new Event("load")); - } - return link; -}; - -export const importHrefPromise = (href) => - new Promise((resolve, reject) => importHref(href, resolve, reject)); diff --git a/src/resources/html-import/polyfill.js b/src/resources/html-import/polyfill.js deleted file mode 100644 index 51844ed272..0000000000 --- a/src/resources/html-import/polyfill.js +++ /dev/null @@ -1,451 +0,0 @@ -/* eslint-disable */ -/* - Copyright (c) 2016 The Polymer Project Authors. All rights reserved. - This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt - The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt - The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt - Code distributed by Google as part of the polymer project is also - subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt -*/ -(function (q) { - function y(a, b) { - if ("function" === typeof window.CustomEvent) return new CustomEvent(a, b); - var c = document.createEvent("CustomEvent"); - c.initCustomEvent(a, !!b.bubbles, !!b.cancelable, b.detail); - return c; - } - function m(a) { - if (u) return a.ownerDocument !== document ? a.ownerDocument : null; - var b = a.__importDoc; - if (!b && a.parentNode) { - b = a.parentNode; - if ("function" === typeof b.closest) b = b.closest("link[rel=import]"); - else for (; !r(b) && (b = b.parentNode); ); - a.__importDoc = b; - } - return b; - } - function D(a) { - var b = k(document, "link[rel=import]:not([import-dependency])"), - c = b.length; - c - ? g(b, function (b) { - return t(b, function () { - 0 === --c && a(); - }); - }) - : a(); - } - function z(a) { - function b() { - "loading" !== document.readyState && - document.body && - (document.removeEventListener("readystatechange", b), a()); - } - document.addEventListener("readystatechange", b); - b(); - } - function A(a) { - z(function () { - return D(function () { - return a && a(); - }); - }); - } - function t(a, b) { - if (a.__loaded) b && b(); - else if ( - ("script" === a.localName && !a.src) || - ("style" === a.localName && !a.firstChild) - ) - (a.__loaded = !0), b && b(); - else { - var c = function (d) { - a.removeEventListener(d.type, c); - a.__loaded = !0; - b && b(); - }; - a.addEventListener("load", c); - (v && "style" === a.localName) || a.addEventListener("error", c); - } - } - function r(a) { - return ( - a.nodeType === Node.ELEMENT_NODE && - "link" === a.localName && - "import" === a.rel - ); - } - function h() { - var a = this; - this.a = {}; - this.b = 0; - this.g = new MutationObserver(function (b) { - return a.w(b); - }); - this.g.observe(document.head, { childList: !0, subtree: !0 }); - this.loadImports(document); - } - function B(a) { - g(k(a, "template"), function (a) { - g( - k( - a.content, - 'script:not([type]),script[type="application/javascript"],script[type="text/javascript"]' - ), - function (a) { - var b = document.createElement("script"); - g(a.attributes, function (a) { - return b.setAttribute(a.name, a.value); - }); - b.textContent = a.textContent; - a.parentNode.replaceChild(b, a); - } - ); - B(a.content); - }); - } - function k(a, b) { - return a.childNodes.length ? a.querySelectorAll(b) : E; - } - function g(a, b, c) { - var d = a ? a.length : 0, - f = c ? -1 : 1; - for (c = c ? d - 1 : 0; c < d && 0 <= c; c += f) b(a[c], c); - } - var n = document.createElement("link"), - u = "import" in n, - E = n.querySelectorAll("*"), - w = null; - !1 === "currentScript" in document && - Object.defineProperty(document, "currentScript", { - get: function () { - return ( - w || - ("complete" !== document.readyState - ? document.scripts[document.scripts.length - 1] - : null) - ); - }, - configurable: !0, - }); - var F = /(url\()([^)]*)(\))/g, - G = /(@import[\s]+(?!url\())([^;]*)(;)/g, - H = /(]*)(rel=['|"]?stylesheet['|"]?[^>]*>)/g, - e = { - u: function (a, b) { - a.href && a.setAttribute("href", e.c(a.getAttribute("href"), b)); - a.src && a.setAttribute("src", e.c(a.getAttribute("src"), b)); - if ("style" === a.localName) { - var c = e.o(a.textContent, b, F); - a.textContent = e.o(c, b, G); - } - }, - o: function (a, b, c) { - return a.replace(c, function (a, c, l, g) { - a = l.replace(/["']/g, ""); - b && (a = e.c(a, b)); - return c + "'" + a + "'" + g; - }); - }, - c: function (a, b) { - if (void 0 === e.f) { - e.f = !1; - try { - var c = new URL("b", "http://a"); - c.pathname = "c%20d"; - e.f = "http://a/c%20d" === c.href; - } catch (d) {} - } - if (e.f) return new URL(a, b).href; - c = e.s; - c || - ((c = document.implementation.createHTMLDocument("temp")), - (e.s = c), - (c.i = c.createElement("base")), - c.head.appendChild(c.i), - (c.h = c.createElement("a"))); - c.i.href = b; - c.h.href = a; - return c.h.href || a; - }, - }, - C = { - async: !0, - load: function (a, b, c) { - if (a) - if (a.match(/^data:/)) { - a = a.split(","); - var d = a[1]; - d = -1 < a[0].indexOf(";base64") ? atob(d) : decodeURIComponent(d); - b(d); - } else { - var f = new XMLHttpRequest(); - f.open("GET", a, C.async); - f.onload = function () { - var a = f.responseURL || f.getResponseHeader("Location"); - a && - 0 === a.indexOf("/") && - (a = - (location.origin || - location.protocol + "//" + location.host) + a); - var d = f.response || f.responseText; - 304 === f.status || - 0 === f.status || - (200 <= f.status && 300 > f.status) - ? b(d, a) - : c(d); - }; - f.send(); - } - else c("error: href must be specified"); - }, - }, - v = - /Trident/.test(navigator.userAgent) || - /Edge\/\d./i.test(navigator.userAgent); - h.prototype.loadImports = function (a) { - var b = this; - g(k(a, "link[rel=import]"), function (a) { - return b.l(a); - }); - }; - h.prototype.l = function (a) { - var b = this, - c = a.href; - if (void 0 !== this.a[c]) { - var d = this.a[c]; - d && d.__loaded && ((a.__import = d), this.j(a)); - } else - this.b++, - (this.a[c] = "pending"), - C.load( - c, - function (a, d) { - a = b.A(a, d || c); - b.a[c] = a; - b.b--; - b.loadImports(a); - b.m(); - }, - function () { - b.a[c] = null; - b.b--; - b.m(); - } - ); - }; - h.prototype.A = function (a, b) { - if (!a) return document.createDocumentFragment(); - v && - (a = a.replace(H, function (a, b, c) { - return -1 === a.indexOf("type=") ? b + " type=import-disable " + c : a; - })); - var c = document.createElement("template"); - c.innerHTML = a; - if (c.content) (a = c.content), B(a); - else - for (a = document.createDocumentFragment(); c.firstChild; ) - a.appendChild(c.firstChild); - if ((c = a.querySelector("base"))) - (b = e.c(c.getAttribute("href"), b)), c.removeAttribute("href"); - var d = 0; - g( - k( - a, - 'link[rel=import],link[rel=stylesheet][href][type=import-disable],style:not([type]),link[rel=stylesheet][href]:not([type]),script:not([type]),script[type="application/javascript"],script[type="text/javascript"]' - ), - function (a) { - t(a); - e.u(a, b); - a.setAttribute("import-dependency", ""); - "script" === a.localName && - !a.src && - a.textContent && - (a.setAttribute( - "src", - "data:text/javascript;charset=utf-8," + - encodeURIComponent( - a.textContent + - ("\n//# sourceURL=" + b + (d ? "-" + d : "") + ".js\n") - ) - ), - (a.textContent = ""), - d++); - } - ); - return a; - }; - h.prototype.m = function () { - var a = this; - if (!this.b) { - this.g.disconnect(); - this.flatten(document); - var b = !1, - c = !1, - d = function () { - c && - b && - (a.loadImports(document), - a.b || - (a.g.observe(document.head, { childList: !0, subtree: !0 }), - a.v())); - }; - this.C(function () { - c = !0; - d(); - }); - this.B(function () { - b = !0; - d(); - }); - } - }; - h.prototype.flatten = function (a) { - var b = this; - g(k(a, "link[rel=import]"), function (a) { - var c = b.a[a.href]; - (a.__import = c) && - c.nodeType === Node.DOCUMENT_FRAGMENT_NODE && - ((b.a[a.href] = a), - (a.readyState = "loading"), - (a.__import = a), - b.flatten(c), - a.appendChild(c)); - }); - }; - h.prototype.B = function (a) { - function b(f) { - if (f < d) { - var l = c[f], - e = document.createElement("script"); - l.removeAttribute("import-dependency"); - g(l.attributes, function (a) { - return e.setAttribute(a.name, a.value); - }); - w = e; - l.parentNode.replaceChild(e, l); - t(e, function () { - w = null; - b(f + 1); - }); - } else a(); - } - var c = k(document, "script[import-dependency]"), - d = c.length; - b(0); - }; - h.prototype.C = function (a) { - var b = k( - document, - "style[import-dependency],link[rel=stylesheet][import-dependency]" - ), - c = b.length; - if (c) { - var d = - v && - !!document.querySelector( - "link[rel=stylesheet][href][type=import-disable]" - ); - g(b, function (b) { - t(b, function () { - b.removeAttribute("import-dependency"); - 0 === --c && a(); - }); - if (d && b.parentNode !== document.head) { - var e = document.createElement(b.localName); - e.__appliedElement = b; - e.setAttribute("type", "import-placeholder"); - b.parentNode.insertBefore(e, b.nextSibling); - for (e = m(b); e && m(e); ) e = m(e); - e.parentNode !== document.head && (e = null); - document.head.insertBefore(b, e); - b.removeAttribute("type"); - } - }); - } else a(); - }; - h.prototype.v = function () { - var a = this; - g( - k(document, "link[rel=import]"), - function (b) { - return a.j(b); - }, - !0 - ); - }; - h.prototype.j = function (a) { - a.__loaded || - ((a.__loaded = !0), - a.import && (a.import.readyState = "complete"), - a.dispatchEvent( - y(a.import ? "load" : "error", { - bubbles: !1, - cancelable: !1, - detail: void 0, - }) - )); - }; - h.prototype.w = function (a) { - var b = this; - g(a, function (a) { - return g(a.addedNodes, function (a) { - a && - a.nodeType === Node.ELEMENT_NODE && - (r(a) ? b.l(a) : b.loadImports(a)); - }); - }); - }; - var x = null; - if (u) - g(k(document, "link[rel=import]"), function (a) { - (a.import && "loading" === a.import.readyState) || (a.__loaded = !0); - }), - (n = function (a) { - a = a.target; - r(a) && (a.__loaded = !0); - }), - document.addEventListener("load", n, !0), - document.addEventListener("error", n, !0); - else { - var p = Object.getOwnPropertyDescriptor(Node.prototype, "baseURI"); - Object.defineProperty( - (!p || p.configurable ? Node : Element).prototype, - "baseURI", - { - get: function () { - var a = r(this) ? this : m(this); - return a - ? a.href - : p && p.get - ? p.get.call(this) - : (document.querySelector("base") || window.location).href; - }, - configurable: !0, - enumerable: !0, - } - ); - Object.defineProperty(HTMLLinkElement.prototype, "import", { - get: function () { - return this.__import || null; - }, - configurable: !0, - enumerable: !0, - }); - z(function () { - x = new h(); - }); - } - A(function () { - return document.dispatchEvent( - y("HTMLImportsLoaded", { cancelable: !0, bubbles: !0, detail: void 0 }) - ); - }); - q.useNative = u; - q.whenReady = A; - q.importForElement = m; - q.loadImports = function (a) { - x && x.loadImports(a); - }; -})((window.HTMLImports = window.HTMLImports || {})); diff --git a/src/resources/styles.ts b/src/resources/styles.ts index 8054abe475..ef04c15548 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-summary/state-card-timer.js b/src/state-summary/state-card-timer.js index 96586e8804..17d39fde5c 100644 --- a/src/state-summary/state-card-timer.js +++ b/src/state-summary/state-card-timer.js @@ -23,7 +23,7 @@ class StateCardTimer extends PolymerElement {
    ${this.stateInfoTemplate} -
    [[_secondsToDuration(timeRemaining)]]
    +
    [[_displayState(timeRemaining, stateObj)]]
    `; } @@ -90,8 +90,10 @@ class StateCardTimer extends PolymerElement { this.timeRemaining = timerTimeRemaining(stateObj); } - _secondsToDuration(time) { - return secondsToDuration(time); + _displayState(time, stateObj) { + return time + ? secondsToDuration(time) + : this.hass.localize(`state.timer.${stateObj.state}`) || stateObj.state; } } customElements.define("state-card-timer", StateCardTimer); diff --git a/src/state/sidebar-mixin.ts b/src/state/sidebar-mixin.ts index b009bbfcb1..e1f1e7653f 100644 --- a/src/state/sidebar-mixin.ts +++ b/src/state/sidebar-mixin.ts @@ -15,15 +15,11 @@ declare global { // for fire event interface HASSDomEvents { "hass-dock-sidebar": DockSidebarParams; - } - interface HASSDomEvents { "hass-default-panel": DefaultPanelParams; } // for add event listener interface HTMLElementEventMap { "hass-dock-sidebar": HASSDomEvent; - } - interface HTMLElementEventMap { "hass-default-panel": HASSDomEvent; } } diff --git a/src/state/themes-mixin.ts b/src/state/themes-mixin.ts index b4470a237a..847f1102f5 100644 --- a/src/state/themes-mixin.ts +++ b/src/state/themes-mixin.ts @@ -45,25 +45,28 @@ export default >(superClass: T) => } private _applyTheme(dark: boolean) { + if (!this.hass) { + return; + } const themeName = - this.hass!.selectedTheme?.theme || - (dark && this.hass!.themes.default_dark_theme - ? this.hass!.themes.default_dark_theme! - : this.hass!.themes.default_theme); + this.hass.selectedTheme?.theme || + (dark && this.hass.themes.default_dark_theme + ? this.hass.themes.default_dark_theme! + : this.hass.themes.default_theme); let options: Partial = this.hass! .selectedTheme; if (themeName === "default" && options?.dark === undefined) { options = { - ...this.hass!.selectedTheme!, + ...this.hass.selectedTheme!, dark, }; } applyThemesOnElement( document.documentElement, - this.hass!.themes, + this.hass.themes, themeName, options ); @@ -71,11 +74,11 @@ export default >(superClass: T) => const darkMode = themeName === "default" ? !!options?.dark - : !!(dark && this.hass!.themes.default_dark_theme); + : !!(dark && this.hass.themes.default_dark_theme); - if (darkMode !== this.hass!.themes.darkMode) { + if (darkMode !== this.hass.themes.darkMode) { this._updateHass({ - themes: { ...this.hass!.themes, darkMode }, + themes: { ...this.hass.themes, darkMode }, }); const schemeMeta = document.querySelector("meta[name=color-scheme]"); @@ -88,9 +91,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 12919204e5..92892f1359 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 fd4952266f..a6080f9359 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": { @@ -260,6 +261,7 @@ }, "common": { "and": "and", + "continue": "Continue", "previous": "Previous", "loading": "Loading", "refresh": "Refresh", @@ -276,14 +278,22 @@ "successfully_saved": "Successfully saved", "successfully_deleted": "Successfully deleted", "back": "Back", - "error_required": "Required" + "error_required": "Required", + "copied": "Copied" }, "components": { + "logbook": { + "entries_not_found": "No logbook entries found." + }, "entity": { "entity-picker": { "entity": "Entity", "clear": "Clear", "show_entities": "Show entities" + }, + "entity-attribute-picker": { + "attribute": "Attribute", + "show_attributes": "Show attributes" } }, "device-picker": { @@ -356,14 +366,41 @@ "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", - "content-type": { - "server": "Server", - "library": "Library", - "artist": "Artist", + "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", + "learn_adding_local_media": "Learn more about adding media in the {documentation}.", + "local_media_files": "Place your video, audio and image files in the media directory to be able to browse and play them in the browser or on supported media players.", + "documentation": "documentation", + "no_local_media_found": "No local media found", + "no_media_folder": "It looks like you have not yet created a media directory.", + "setup_local_help": "Check the {documentation} on how to setup local media.", + "class": { "album": "Album", - "playlist": "Playlist" + "app": "App", + "artist": "Artist", + "channel": "Channel", + "composer": "Composer", + "contributing_artist": "Contributing Artist", + "directory": "Library", + "episode": "Episode", + "game": "Game", + "genre": "Genre", + "image": "Image", + "movie": "Movie", + "music": "Music", + "playlist": "Playlist", + "podcast": "Podcast", + "season": "Season", + "track": "Track", + "tv_show": "TV Show", + "url": "Url", + "video": "Video" } } }, @@ -389,6 +426,8 @@ "dismiss": "Dismiss dialog", "settings": "Entity settings", "edit": "Edit entity", + "details": "Details", + "history": "History", "script": { "last_action": "Last Action", "last_triggered": "Last Triggered" @@ -442,7 +481,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": { @@ -568,7 +607,8 @@ }, "sidebar": { "external_app_configuration": "App Configuration", - "sidebar_toggle": "Sidebar Toggle" + "sidebar_toggle": "Sidebar Toggle", + "done": "Done" }, "panel": { "calendar": { @@ -904,6 +944,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" } @@ -974,6 +1016,7 @@ }, "state": { "label": "State", + "attribute": "Attribute (Optional)", "from": "From", "for": "For", "to": "To" @@ -1010,8 +1053,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", @@ -1087,6 +1132,8 @@ "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" @@ -1121,7 +1168,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" @@ -1599,6 +1652,7 @@ "description": "Manage integrations", "integration": "integration", "discovered": "Discovered", + "attention": "Attention required", "configured": "Configured", "new": "Set up a new integration", "add_integration": "Add integration", @@ -1607,6 +1661,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.", @@ -1629,6 +1684,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", @@ -1667,7 +1723,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", @@ -1755,6 +1811,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.", @@ -1800,6 +1857,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" @@ -1845,7 +1914,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", @@ -1936,10 +2005,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": { @@ -2001,7 +2070,6 @@ } }, "logbook": { - "entries_not_found": "No logbook entries found.", "ranges": { "today": "Today", "yesterday": "Yesterday", @@ -2087,7 +2155,7 @@ "save": "Save", "unsaved_changes": "Unsaved changes", "saved": "Saved", - "confirm_remove_config_title": "Are you sure you want to remove your Lovelace UI configuration? We will automatically generate your Lovelace UI views with your areas and devices.", + "confirm_remove_config_title": "Are you sure you want to remove your Lovelace UI configuration?", "confirm_remove_config_text": "We will automatically generate your Lovelace UI views with your areas and devices if you remove your Lovelace UI configuration.", "confirm_unsaved_changes": "You have unsaved changes, are you sure you want to exit?", "confirm_unsaved_comments": "Your configuration contains comment(s), these will not be saved. Do you want to continue?", @@ -2133,7 +2201,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" @@ -2348,7 +2417,11 @@ }, "cardpicker": { "no_description": "No description available.", - "custom_card": "Custom" + "custom_card": "Custom", + "domain": "Domain", + "entity": "Entity", + "by_entity": "By Entity", + "by_card": "By Card" } }, "warning": { @@ -2380,6 +2453,11 @@ "header": "Always hide the sidebar", "description": "This will hide the sidebar by default, similar to the mobile experience." }, + "customize_sidebar": { + "header": "Change the order and hide items from the sidebar", + "description": "You can also press and hold the header of the sidebar to activate edit mode.", + "button": "Edit" + }, "vibrate": { "header": "Vibrate", "description": "Enable or disable vibration on this device when controlling devices." @@ -2461,14 +2539,13 @@ "header": "Long-Lived Access Tokens", "description": "Create long-lived access tokens to allow your scripts to interact with your Home Assistant instance. Each token will be valid for 10 years from creation. The following long-lived access tokens are currently active.", "learn_auth_requests": "Learn how to make authenticated requests.", - "created_at": "Created at {date}", - "last_used": "Last used at {date} from {location}", - "not_used": "Has never been used", + "created": "Created {date}", "confirm_delete": "Are you sure you want to delete the access token for {name}?", "delete_failed": "Failed to delete the access token.", "create": "Create Token", "create_failed": "Failed to create the access token.", - "prompt_name": "Name?", + "name": "Name", + "prompt_name": "Give the token a name", "prompt_copy_token": "Copy your access token. It will not be shown again.", "empty_state": "You have no long-lived access tokens yet." } @@ -2679,7 +2756,12 @@ "reset": "Reset to demo template", "jinja_documentation": "Jinja2 template documentation", "template_extensions": "Home Assistant template extensions", - "unknown_error_template": "Unknown error rendering template" + "unknown_error_template": "Unknown error rendering template", + "all_listeners": "This template listens for all state changed events.", + "no_listeners": "This template does not listen for any state changed events and will not update automatically.", + "listeners": "This template listens for the following state changed events:", + "entity": "Entity", + "domain": "Domain" } } }, diff --git a/src/util/custom-panel/load-custom-panel.ts b/src/util/custom-panel/load-custom-panel.ts index c674e0f4ed..1286b6588f 100644 --- a/src/util/custom-panel/load-custom-panel.ts +++ b/src/util/custom-panel/load-custom-panel.ts @@ -47,17 +47,6 @@ export const loadCustomPanel = ( ): Promise => { const panelSource = getUrl(panelConfig); - if (panelSource.type === "html") { - const toLoad = [ - import( - /* webpackChunkName: "import-href-polyfill" */ "../../resources/html-import/import-href" - ), - ]; - - return Promise.all(toLoad).then(([{ importHrefPromise }]) => - importHrefPromise(panelSource.url) - ); - } if (panelSource.type === "js") { if (!(panelSource.url in JS_CACHE)) { JS_CACHE[panelSource.url] = loadJS(panelSource.url); diff --git a/src/util/documentation-url.ts b/src/util/documentation-url.ts new file mode 100644 index 0000000000..3ad016be72 --- /dev/null +++ b/src/util/documentation-url.ts @@ -0,0 +1,11 @@ +import { HomeAssistant } from "../types"; + +export const documentationUrl = (hass: HomeAssistant, path: string) => { + return `https://${ + hass.config.version.includes("b") + ? "rc" + : hass.config.version.includes("dev") + ? "next" + : "www" + }.home-assistant.io${path}`; +}; diff --git a/test-mocha/common/entity/compute_state_display.ts b/test-mocha/common/entity/compute_state_display.ts index 0f712aa049..e8663c5a12 100644 --- a/test-mocha/common/entity/compute_state_display.ts +++ b/test-mocha/common/entity/compute_state_display.ts @@ -1,5 +1,6 @@ import { assert } from "chai"; import { computeStateDisplay } from "../../../src/common/entity/compute_state_display"; +import { UNKNOWN } from "../../../src/data/entity"; describe("computeStateDisplay", () => { // Mock Localize function for testing @@ -72,7 +73,7 @@ describe("computeStateDisplay", () => { }; const stateObj: any = { entity_id: "sensor.test", - state: "unknown", + state: UNKNOWN, attributes: { unit_of_measurement: "m", }, diff --git a/test-mocha/util/generate-documentation-url.spec.ts b/test-mocha/util/generate-documentation-url.spec.ts new file mode 100644 index 0000000000..9e34e61448 --- /dev/null +++ b/test-mocha/util/generate-documentation-url.spec.ts @@ -0,0 +1,19 @@ +import * as assert from "assert"; +import { documentationUrl } from "../../src/util/documentation-url"; + +describe("Generate documentation URL", function () { + it("Generate documentation url for stable", function () { + assert.strictEqual( + // @ts-ignore + documentationUrl({ config: { version: "1.0.0" } }, "/blog"), + "https://www.home-assistant.io/blog" + ); + }); + it("Generate documentation url for rc", function () { + assert.strictEqual( + // @ts-ignore + documentationUrl({ config: { version: "1.0.0b0" } }, "/blog"), + "https://rc.home-assistant.io/blog" + ); + }); +}); diff --git a/translations/frontend/bg.json b/translations/frontend/bg.json index f5703b28f4..68302a4647 100644 --- a/translations/frontend/bg.json +++ b/translations/frontend/bg.json @@ -19,6 +19,7 @@ "logbook": "Дневник", "mailbox": "Пощенска кутия", "map": "Карта", + "media_browser": "Медиен браузър", "profile": "Профил", "shopping_list": "Списък за пазаруване", "states": "Състояние" @@ -455,6 +456,7 @@ }, "common": { "cancel": "Отмени", + "continue": "Продължи", "delete": "Изтриване", "loading": "Зареждане", "save": "Запазване", @@ -466,6 +468,10 @@ "device": "Устройство" }, "entity": { + "entity-attribute-picker": { + "attribute": "Атрибут", + "show_attributes": "Показване на атрибутите" + }, "entity-picker": { "entity": "Обект" } @@ -474,6 +480,18 @@ "loading_history": "Зареждане на история за състоянието...", "no_history_found": "Не е намерена история за състоянието" }, + "logbook": { + "entries_not_found": "Липсват логове в дневника" + }, + "media-browser": { + "audio_not_supported": "Браузърът не поддържа този аудио елемент.", + "choose_player": "Изберете плейър", + "media_browsing_error": "Грешка в медийния браузър", + "media_not_supported": "Плейърът на медийния браузър не поддържа този тип медия", + "media_player": "Медиен плейър", + "video_not_supported": "Браузърът не поддържа този видео елемент.", + "web-browser": "Уеб браузър" + }, "relative_time": { "duration": { "day": "{count}{count, plural,\n one {ден}\n other {дни}\n}", @@ -496,6 +514,9 @@ "enable_new_entities_label": "Активирай новодобавените устройства.", "title": "Системни опции за {integration}" }, + "domain_toggler": { + "reset_entities": "Нулиране на обекти" + }, "entity_registry": { "editor": { "delete": "Изтриване", @@ -515,6 +536,9 @@ "yaml_not_editable": "Настройките на този обект не могат да бъдат редактирани. Могат да се конфигурират само обектите, създадени от потребителския интерфейс." }, "more_info_control": { + "controls": "Контроли", + "details": "Детайли", + "history": "История", "script": { "last_action": "Последно задействане" }, @@ -644,7 +668,13 @@ "label": "Извикване на услуга", "service_data": "Данни за услугата" }, + "wait_for_trigger": { + "continue_timeout": "Продължи след изчакване", + "label": "Изчакайте спусък/тригър", + "timeout": "Време на изчакване (опция)" + }, "wait_template": { + "continue_timeout": "Продължи след изчакване", "label": "Изчакване", "timeout": "Изчакване (по избор)", "wait_template": "Шаблон за изчакване" @@ -692,7 +722,9 @@ "time": { "after": "След", "before": "Преди", - "label": "Време" + "label": "Време", + "type_input": "Стойност на помощника за дата/час", + "type_value": "Фиксирано време" }, "zone": { "entity": "Обект с местоположение", @@ -751,6 +783,7 @@ "value_template": "Шаблон за стойност (незадължително)" }, "state": { + "attribute": "Атрибут (опция)", "for": "За период от", "from": "От", "label": "Състояние", @@ -775,7 +808,9 @@ }, "time": { "at": "В", - "label": "Време" + "label": "Време", + "type_input": "Стойност на помощника за дата/час", + "type_value": "Фиксирано време" }, "webhook": { "label": "Webhook", @@ -796,6 +831,8 @@ }, "picker": { "add_automation": "Добавяне на автоматизация", + "duplicate": "Дублиране", + "duplicate_automation": "Дублиране на автоматизация", "header": "Редактор на автоматизации", "headers": { "name": "Име" @@ -815,10 +852,26 @@ "config_documentation": "Документация за конфигурацията" } }, + "alexa": { + "dont_expose_entity": "Не излагай обекта", + "expose_entity": "Изложи обекта", + "exposed": "{избран} изложен", + "follow_domain": "следван домейн", + "manage_domains": "Управление на домейни", + "not_exposed": "{избран} неизложен" + }, "caption": "Home Assistant Cloud", "description_features": "Контролирайте дома си, и когато не сте вкъщи, активирайте интегрирациите с Alexa и Google Assistant.", "description_login": "Влезли сте като {email}", - "description_not_login": "Не сте влезли" + "description_not_login": "Не сте влезли", + "google": { + "dont_expose_entity": "Не излагай обекта", + "expose_entity": "Изложи обекта", + "exposed": "{избран} изложен", + "follow_domain": "следван домейн", + "manage_domains": "Управление на домейни", + "not_exposed": "{избран} неизложен" + } }, "core": { "caption": "Общи", @@ -882,6 +935,7 @@ }, "integrations": { "add_integration": "Добавяне на интеграция", + "attention": "Задълителна проверка", "caption": "Интеграции", "config_entry": { "delete": "Изтриване", @@ -897,6 +951,7 @@ "options": "Настройки", "rename": "Преименуване", "restart_confirm": "Рестартирайте Home Assistant за да завършите премахването на интеграцията", + "services": "{брой} {число, множествено число,\n една {услуга}\n други {услуги}\n}", "system_options": "Системни настройки" }, "config_flow": { @@ -915,6 +970,7 @@ "none": "Нищо не е конфигурирано към момента", "none_found": "Не са намерени интеграции", "none_found_detail": "Коригирайте критериите си за търсене.", + "reconfigure": "Преконфигурирай", "rename_dialog": "Редактирайте името на този запис в конфигурацията" }, "introduction": "Тук е възможно да конфигурирате Вашите компоненти и Home Assistant. Не всичко е възможно да се конфигурира от Интерфейса, но работим по върпоса.", @@ -942,6 +998,23 @@ "mqtt": { "title": "MQTT" }, + "ozw": { + "node": { + "button": "Детайли за възела", + "not_found": "Възел не е намерен" + }, + "nodes_table": { + "failed": "Неуспешно", + "id": "ID", + "manufacturer": "Производител", + "model": "Модел", + "query_stage": "Етап на заявка", + "zwave_plus": "Z-Wave плюс" + }, + "refresh_node": { + "button": "Обнови възела" + } + }, "person": { "caption": "Хора", "description": "Управлявайте хората, които следите от Home Assistant.", @@ -979,8 +1052,13 @@ "group": "Презареждане на гурпите", "heading": "Презареждане на YAML конфигурацията", "introduction": "Някои части от Home Assistant могат да се презаредят без да е необходимо рестартиране. Натискането на Презареди ще отхвърли настоящата конфигурация и ще зареди новата конфигурация.", + "mqtt": "Презареди mqtt обектите", + "reload": "Презарежди {домейн}", + "rpi_gpio": "Презареди GPIO обектите на Raspberry Pi ", "scene": "Презареди сцените", - "script": "Презареждане на скриптовете" + "script": "Презареждане на скриптовете", + "smtp": "Презареди smtp услугите за уведомяване", + "telegram": "Презареди telegram услугите за уведомяване" }, "server_management": { "confirm_restart": "Сигурни ли сте, че искате да рестартирате Home Assistant?", @@ -1005,6 +1083,8 @@ "create": "Създаване", "name": "Име", "password": "Парола", + "password_confirm": "Потвърди парола", + "password_not_match": "Паролите не съвпадат", "username": "Потребителско име" }, "caption": "Потребители", @@ -1017,6 +1097,8 @@ "deactivate_user": "Деактивиране на потребителя", "delete_user": "Изтриване на потребител", "name": "Име", + "new_password": "Нова парола", + "password_changed": "Паролата е променена!", "system_generated_users_not_editable": "Неуспешно обновяване на системно генерираните потребители" }, "picker": { @@ -1102,6 +1184,11 @@ "title": "Състояния" }, "templates": { + "all_listeners": "Този шаблон следи за всички събития на промяна на състояние.", + "domain": "Домейн", + "entity": "Обект", + "listeners": "Този шаблон следи за събития със следните промени на състояния:", + "no_listeners": "Този шаблон не следи за събития на промяна на състояние и не се актуализира автоматично.", "title": "Шаблон" } } @@ -1144,6 +1231,9 @@ "entities": { "toggle": "Превключване на обекти." }, + "generic": { + "state_color": "Да се оцветят ли иконите спрямо състоянието?" + }, "iframe": { "name": "Уеб страница" }, @@ -1155,6 +1245,10 @@ } }, "cardpicker": { + "by_card": "По карта", + "by_entity": "По обект", + "domain": "Домейн", + "entity": "Обект", "no_description": "Няма налично описание." }, "edit_card": { @@ -1166,6 +1260,7 @@ "move": "Преместване", "options": "Още опции", "pick_card": "Изберете картата, която искате да добавите.", + "search_cards": "Търсене на карти", "toggle_editor": "Превключете редактора" }, "edit_lovelace": { @@ -1419,6 +1514,11 @@ "submit": "Промяна" }, "current_user": "В момента сте влезли като {fullName}.", + "customize_sidebar": { + "button": "Редактиране", + "description": "Може също да се задържи водача на страничната лента, за да се активира режим за редактиране.", + "header": "Промяна на реда и скриване на елементи от страничната лента" + }, "dashboard": { "dropdown_label": "Табло", "header": "Табло" @@ -1438,6 +1538,7 @@ "confirm_delete": "Сигурни ли сте, че искате да изтриете кода за достъп за {name}?", "create": "Създай код", "create_failed": "Възникна грешка при създаването на код за достъп.", + "created": "Създаване {дата}", "created_at": "Създаден на {date}", "delete_failed": "Възникна грешка при изтриването на кода за достъп.", "description": "Създайте дългосрочни кодове за достъп за да могат скриптовете Ви да взаимодействат с Home Assistant. Всеки код е валиден за 10 години от създаването си. Следните дългосрочни кодове са активни в момента.", @@ -1445,6 +1546,7 @@ "header": "Дългосрочни кодове за достъп", "last_used": "Последно използван на {date} от {location}", "learn_auth_requests": "Научете се как да правите оторизирани заявки.", + "name": "Име", "not_used": "Никога не е бил използван", "prompt_copy_token": "Копирайте кода си за достъп. Той няма да бъде показан отново.", "prompt_name": "Име?" @@ -1495,6 +1597,7 @@ } }, "sidebar": { + "done": "Готово", "external_app_configuration": "Конфигурация на приложение" } } diff --git a/translations/frontend/ca.json b/translations/frontend/ca.json index 5e49c1fdd1..15df5c0e86 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,8 @@ "back": "Torna", "cancel": "Cancel·la", "close": "Tanca", + "continue": "Continua", + "copied": "Copiat", "delete": "Elimina", "error_required": "Obligatori", "loading": "Carregant", @@ -551,6 +554,10 @@ "toggle": "Commuta" }, "entity": { + "entity-attribute-picker": { + "attribute": "Atribut", + "show_attributes": "Mostra els atributs" + }, "entity-picker": { "clear": "Esborra", "entity": "Entitat", @@ -561,8 +568,35 @@ "loading_history": "Carregant historial d'estats...", "no_history_found": "No s'ha trobat cap historial d'estats." }, + "logbook": { + "entries_not_found": "No s'han trobat entrades al registre." + }, "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", + "class": { + "album": "Àlbum", + "app": "Aplicació", + "artist": "Artista", + "channel": "Canal", + "composer": "Compositor", + "contributing_artist": "Artista col·laborador", + "directory": "Biblioteca", + "episode": "Episodi", + "game": "Joc", + "genre": "Gènere", + "image": "Imatge", + "movie": "Pel·lícula", + "music": "Música", + "playlist": "Llista de reproducció", + "podcast": "Podcast", + "season": "Temporada", + "track": "Pista", + "tv_show": "Programa de TV", + "url": "URL", + "video": "Vídeo" + }, "content-type": { "album": "Àlbum", "artist": "Artista", @@ -570,12 +604,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 +728,11 @@ "crop": "Retalla" }, "more_info_control": { + "controls": "Controls", + "details": "Detalls", "dismiss": "Desestimar el diàleg", "edit": "Edita entitat", + "history": "Historial", "person": { "create_zone": "Crea una zona a partir de la ubicació actual" }, @@ -918,7 +960,13 @@ "label": "Crida servei", "service_data": "Dades de servei" }, + "wait_for_trigger": { + "continue_timeout": "Continua amb temps d'espera", + "label": "Espera disparador", + "timeout": "Temps màxim d'espera (opcional)" + }, "wait_template": { + "continue_timeout": "Continua amb temps d'espera", "label": "Espera", "timeout": "Temps màxim d'espera (opcional)", "wait_template": "Plantilla d'espera" @@ -982,7 +1030,9 @@ "time": { "after": "Després", "before": "Abans", - "label": "Temporal" + "label": "Temporal", + "type_input": "Valor d'un ajudant de data/hora", + "type_value": "Temps fix" }, "zone": { "entity": "Entitat amb ubicació", @@ -1070,6 +1120,7 @@ "value_template": "Plantilla de valor (opcional)" }, "state": { + "attribute": "Atribut (opcional)", "for": "Durant", "from": "Des de", "label": "Estat", @@ -1097,7 +1148,9 @@ }, "time": { "at": "A les", - "label": "Temporal" + "label": "Temporal", + "type_input": "Valor d'un ajudant de data/hora", + "type_value": "Temps fix" }, "webhook": { "label": "Webhook", @@ -1120,6 +1173,8 @@ "add_automation": "Afegeix automatització", "delete_automation": "Elimina l'automatització", "delete_confirm": "Estàs segur que vols eliminar aquesta automatització?", + "duplicate": "Duplica", + "duplicate_automation": "Duplica l'automatització", "edit_automation": "Edita automatització", "header": "Editor d'automatitzacions", "headers": { @@ -1520,6 +1575,7 @@ }, "integrations": { "add_integration": "Afegeix integració", + "attention": "Cal atenció", "caption": "Integracions", "config_entry": { "area": "A {area}", @@ -1543,6 +1599,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}", @@ -1589,6 +1646,7 @@ "none_found_detail": "Ajusta els paràmetres de cerca.", "note_about_integrations": "Encara no es poden configurar totes les integracions a través de la UI.", "note_about_website_reference": "N'hi ha més disponibles al ", + "reconfigure": "Reconfigura", "rename_dialog": "Edita el nom de l'entrada de configuració", "rename_input_label": "Nom de l'entrada", "search": "Cerca integracions" @@ -1762,8 +1820,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", @@ -1895,7 +1966,7 @@ }, "introduction": "L'editor de scripts et permet crear i editar scripts. Vés a l'enllaç de sota per veure'n les instruccions i assegurar-te que has configurat Home Assistant correctament.", "learn_more": "Més informació sobre els scripts", - "no_scripts": "No hem trobat cap script editable", + "no_scripts": "No s'ha pogut trobar cap script editable", "show_info": "Mostra informació sobre l'script", "trigger_script": "Dispara l'script" } @@ -1912,7 +1983,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 +1994,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", @@ -2299,9 +2375,14 @@ "title": "Estats" }, "templates": { + "all_listeners": "Aquesta plantilla escolta a tots els esdeveniments de canvi d'estat.", "description": "Les plantilles es renderitzen mitjançant el motor Jinja2 amb algunes extensions específiques de Home Assistant.", + "domain": "Domini", "editor": "Editor de plantilles", + "entity": "Entitat", "jinja_documentation": "Documentació sobre plantilles amb Jinja2", + "listeners": "Aquesta plantilla escolta als següents esdeveniments de canvi d'estat:", + "no_listeners": "Aquesta plantilla no escolta cap esdeveniment de canvi d'estat i no s'actualitza automàticament.", "reset": "Restableix a la plantilla de demostració", "template_extensions": "Extensions de plantilla de Home Assistant", "title": "Plantilla", @@ -2448,7 +2529,7 @@ "no_theme": "Sense tema", "refresh_interval": "Interval d'actualització", "search": "Cerca", - "secondary_info_attribute": "Atribut d’informació secundària", + "secondary_info_attribute": "Atribut d'informació secundària", "show_icon": "Mostra icona?", "show_name": "Mostra nom?", "show_state": "Mostra estat?", @@ -2549,7 +2630,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 +2648,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", @@ -2883,6 +2969,11 @@ "submit": "Envia" }, "current_user": "Has iniciat la sessió com a {fullName}.", + "customize_sidebar": { + "button": "Edita", + "description": "També pots mantenir premuda la capçalera de la barra lateral per activar el mode d'edició.", + "header": "Canvia l'ordre i/o amaga elements de la barra lateral" + }, "dashboard": { "description": "Tria un panell per defecte per a aquest dispositiu.", "dropdown_label": "Panell", @@ -2905,6 +2996,7 @@ "confirm_delete": "Estàs segur que vols eliminar el token d'autenticació d'accés per {name}?", "create": "Crea un token d'autenticació", "create_failed": "No s'ha pogut crear el token d'autenticació d'accés.", + "created": "Creat el {date}", "created_at": "Creat el {date}", "delete_failed": "No s'ha pogut eliminar el token d'autenticació d'accés.", "description": "Crea tokens d'autenticació d'accés de llarga durada per permetre als teus programes/scripts interactuar amb la instància de Home Assistant. Cada token és vàlid durant deu anys després de la seva creació. Els següents tokens d'autenticació d'accés de llarga durada estan actius actualment.", @@ -2912,9 +3004,10 @@ "header": "Tokens d'autenticació d'accés de llarga durada", "last_used": "Darrer ús el {date} des de {location}", "learn_auth_requests": "Aprèn a fer sol·licituds autenticades.", + "name": "Nom", "not_used": "Mai no s'ha utilitzat", "prompt_copy_token": "Copia't el token d'autenticació d'accés. No es tornarà a mostrar més endavant.", - "prompt_name": "Nom?" + "prompt_name": "Posa un nom al token" }, "mfa_setup": { "close": "Tanca", @@ -2978,6 +3071,7 @@ } }, "sidebar": { + "done": "Fet", "external_app_configuration": "Configuració de l'aplicació", "sidebar_toggle": "Commutació de la barra lateral" } diff --git a/translations/frontend/cs.json b/translations/frontend/cs.json index ca77ec6015..5675a163a4 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,8 @@ "back": "Zpět", "cancel": "Zrušit", "close": "Zavřít", + "continue": "Pokračovat", + "copied": "Zkopírováno", "delete": "Smazat", "error_required": "Povinné", "loading": "Načítání", @@ -551,6 +554,10 @@ "toggle": "Přepnout" }, "entity": { + "entity-attribute-picker": { + "attribute": "Atribut", + "show_attributes": "Zobrazit atributy" + }, "entity-picker": { "clear": "Zrušit", "entity": "Entita", @@ -561,8 +568,35 @@ "loading_history": "Historie stavu se načítá...", "no_history_found": "Historie stavu chybí." }, + "logbook": { + "entries_not_found": "Nenalezeny žádné záznamy." + }, "media-browser": { + "audio_not_supported": "Váš prohlížeč nepodporuje element \"audio\".", + "choose_player": "Vyberte přehrávač", "choose-source": "Zvolte zdroj", + "class": { + "album": "Album", + "app": "Aplikace", + "artist": "Umělec", + "channel": "Kanál", + "composer": "Skladatel", + "contributing_artist": "Přispívající umělec", + "directory": "Knihovna", + "episode": "Epizoda", + "game": "Hra", + "genre": "Žánr", + "image": "Obrázek", + "movie": "Film", + "music": "Hudba", + "playlist": "Seznam skladeb", + "podcast": "Podcast", + "season": "Sezóna", + "track": "Stopa", + "tv_show": "Televizní pořad", + "url": "Url adresa", + "video": "Video" + }, "content-type": { "album": "Album", "artist": "Umělec", @@ -570,12 +604,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 +672,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 +728,11 @@ "crop": "Oříznout" }, "more_info_control": { + "controls": "Ovládací prvky", + "details": "Podrobnosti", "dismiss": "Zavřít dialog", "edit": "Upravit entitu", + "history": "Historie", "person": { "create_zone": "Vytvořit zónu z aktuálního umístění" }, @@ -774,7 +816,7 @@ "services": { "reconfigure": "Překonfigurovat zařízení ZHA (opravit zařízení). Použijte, pokud se zařízením máte problémy. Je-li dotyčné zařízení napájené bateriemi, ujistěte se prosím, že je při používání této služby spuštěné a přijímá příkazy.", "remove": "Odebrat zařízení ze sítě Zigbee.", - "updateDeviceName": "Nastavte vlastní název tohoto zařízení v registru zařízení.", + "updateDeviceName": "Nastavte vlastní název tohoto zařízení v seznamu zařízení.", "zigbee_information": "Zobrazit Zigbee informace zařízení." }, "unknown": "Neznámý", @@ -918,7 +960,13 @@ "label": "Zavolat službu", "service_data": "Data služby" }, + "wait_for_trigger": { + "continue_timeout": "Pokračovat po časovém limitu", + "label": "Počkat na spouštěč", + "timeout": "Časový limit (volitelný)" + }, "wait_template": { + "continue_timeout": "Pokračovat po časovém limitu", "label": "Čekání", "timeout": "Časový limit (volitelné)", "wait_template": "Šablona pro čekání" @@ -982,7 +1030,9 @@ "time": { "after": "Po", "before": "Před", - "label": "Čas" + "label": "Čas", + "type_input": "Hodnota pomocníka \"Datum a/nebo čas\"", + "type_value": "Pevný čas" }, "zone": { "entity": "Entita s umístěním", @@ -1070,6 +1120,7 @@ "value_template": "Šablona hodnoty (volitelné)" }, "state": { + "attribute": "Atribut (volitelný)", "for": "Po dobu", "from": "Z", "label": "Stav", @@ -1097,7 +1148,9 @@ }, "time": { "at": "V", - "label": "Čas" + "label": "Čas", + "type_input": "Hodnota pomocníka \"Datum a/nebo čas\"", + "type_value": "Pevný čas" }, "webhook": { "label": "Webhook", @@ -1120,6 +1173,8 @@ "add_automation": "Přidat automatizaci", "delete_automation": "Odstranit automatizaci", "delete_confirm": "Opravdu chcete odstranit tuto automatizaci?", + "duplicate": "Duplikovat", + "duplicate_automation": "Duplikovat automatizaci", "edit_automation": "Upravit automatizaci", "header": "Editor automatizací", "headers": { @@ -1442,8 +1497,8 @@ "name": "Název", "status": "Stav" }, - "introduction": "Homa Assistant uchovává registr všech entit, které kdy viděl a mohou být jednoznačně identifikovány. Každá z těchto entit bude mít přiděleno ID, které bude rezervováno pouze pro tuto entitu.", - "introduction2": "Pomocí registru entit můžete přepsat název, změnit identifikátor entity nebo odebrat entitu.", + "introduction": "Homa Assistant uchovává záznam o každé entitě, kterou kdy viděl a která může být jednoznačně identifikována. Každá z těchto entit bude mít přiděleno ID, které bude rezervováno pouze pro tuto entitu.", + "introduction2": "Entitě můžete přepsat název, změnit její identifikátor nebo ji odebrat z Home Assistant.", "remove_selected": { "button": "Odstranit vybrané", "confirm_partly_text": "Můžete odebrat pouze {removable} z vybraných {selected} entit. Entity lze odebrat pouze v případě, že integrace již entity neposkytuje. Občas je třeba restartovat Home Assistant před tím, než je možné odstranit entity ze smazané integrace. Opravdu chcete odebrat odstranitelné entity?", @@ -1469,7 +1524,7 @@ "header": "Konfigurace Home Assistant", "helpers": { "caption": "Pomocníci", - "description": "Prvky, které mohou pomoci při vytváření automatizací.", + "description": "Správa prvků, které mohou pomoci při vytváření automatizací", "dialog": { "add_helper": "Přidat pomocníka", "add_platform": "Přidat {platform}", @@ -1520,6 +1575,7 @@ }, "integrations": { "add_integration": "Přidat integraci", + "attention": "Vyžadována pozornost", "caption": "Integrace", "config_entry": { "area": "V {area}", @@ -1543,6 +1599,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}", @@ -1589,6 +1646,7 @@ "none_found_detail": "Upravte kritéria vyhledávání.", "note_about_integrations": "Ne všechny integrace lze prozatím konfigurovat prostřednictvím uživatelského rozhraní.", "note_about_website_reference": "Další jsou k dispozici na ", + "reconfigure": "Překonfigurovat", "rename_dialog": "Upravit název této položky nastavení", "rename_input_label": "Název položky", "search": "Hledat integraci" @@ -1597,7 +1655,7 @@ "logs": { "caption": "Logy", "clear": "Zrušit", - "description": "Zobrazit log Home Assistant", + "description": "Zobrazení logů Home Assistant", "details": "Detaily protokolu ({level})", "load_full_log": "Načíst úplný protokol Home Assistanta", "loading_log": "Načítání protokolu chyb...", @@ -1762,8 +1820,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", @@ -1902,7 +1973,7 @@ }, "server_control": { "caption": "Ovládání serveru", - "description": "Restart a zastavení serveru Home Asistent", + "description": "Restartování a zastavení serveru Home Asistent", "section": { "reloading": { "automation": "Nově načíst automatizace", @@ -1912,7 +1983,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 +1994,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 +2093,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": { @@ -2130,7 +2206,7 @@ "configured_in_yaml": "Zóny konfigurované pomocí configuration.yaml nelze upravovat pomocí UI.", "confirm_delete": "Opravdu chcete tuto zónu smazat?", "create_zone": "Vytvořit zónu", - "description": "Spravujte zóny, ve kterých chcete sledovat osoby.", + "description": "Spravá zón, ve kterých chcete sledovat osoby", "detail": { "create": "Vytvořit", "delete": "Smazat", @@ -2299,9 +2375,14 @@ "title": "Stavy" }, "templates": { + "all_listeners": "Tato šablona naslouchá všem změnám stavu.", "description": "Šablony jsou vykreslovány pomocí Jinja2 šablonového enginu s některými specifickými rozšířeními pro Home Assistant.", + "domain": "Doména", "editor": "Editor šablon", + "entity": "Entita", "jinja_documentation": "Dokumentace šablony Jinja2", + "listeners": "Tato šablona naslouchá následujícím změnám stavu:", + "no_listeners": "Tato šablona neposlouchá žádné změny stavu a nebude se automaticky aktualizovat.", "reset": "Obnovit ukázkovou šablonu", "template_extensions": "Rozšíření šablony Home Assistant", "title": "Šablony", @@ -2549,7 +2630,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 +2648,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", @@ -2883,6 +2969,11 @@ "submit": "Odeslat" }, "current_user": "Nyní jste přihlášeni jako {fullName}.", + "customize_sidebar": { + "button": "Upravit", + "description": "Režim úprav můžete aktivovat také stisknutím a podržením záhlaví postranního panelu.", + "header": "Změna pořadí a skrytí položek postranního panelu" + }, "dashboard": { "description": "Vyberte výchozí dashboard pro toto zařízení.", "dropdown_label": "Dashboard", @@ -2905,6 +2996,7 @@ "confirm_delete": "Opravdu chcete smazat přístupový token pro {name} ?", "create": "Vytvořte token", "create_failed": "Nepodařilo se vytvořit přístupový token.", + "created": "Vytvořeno {date}", "created_at": "Vytvořeno {date}", "delete_failed": "Nepodařilo se odstranit přístupový token.", "description": "Vytvořte přístupové tokeny s dlouhou životností, aby vaše skripty mohly komunikovat s instancí Home Assistant. Každý token bude platný po dobu 10 let od vytvoření. Tyto tokeny s dlouhou životností jsou v současné době aktivní.", @@ -2912,9 +3004,10 @@ "header": "Tokeny s dlouhou životností", "last_used": "Naposledy použito {date} z {location}", "learn_auth_requests": "Naučte se, jak posílat ověřené požadavky.", + "name": "Název", "not_used": "Nikdy nebylo použito", "prompt_copy_token": "Zkopírujte přístupový token. Už nikdy nebude znovu zobrazen.", - "prompt_name": "Název?" + "prompt_name": "Pojmenujte token" }, "mfa_setup": { "close": "Zavřít", @@ -2978,6 +3071,7 @@ } }, "sidebar": { + "done": "Hotovo", "external_app_configuration": "Konfigurace aplikace", "sidebar_toggle": "Přepínač postranního panelu" } diff --git a/translations/frontend/de.json b/translations/frontend/de.json index 4628cab1cc..be5fda9173 100644 --- a/translations/frontend/de.json +++ b/translations/frontend/de.json @@ -419,6 +419,7 @@ "unlock": "Entriegeln" }, "media_player": { + "media_next_track": "Weiter", "media_play": "Abspielen", "sound_mode": "Sound-Modus", "source": "Quelle", @@ -501,6 +502,8 @@ "back": "Zurück", "cancel": "Abbrechen", "close": "Schließen", + "continue": "Weiter", + "copied": "Kopiert", "delete": "Löschen", "error_required": "Benötigt", "loading": "Laden", @@ -557,8 +560,24 @@ "loading_history": "Lade Zustandsverlauf...", "no_history_found": "Kein Zustandsverlauf gefunden." }, + "logbook": { + "entries_not_found": "Keine Logbucheinträge gefunden." + }, "media-browser": { + "audio_not_supported": "Ihr Browser unterstützt das Audioelement nicht.", "choose-source": "Quelle wählen", + "class": { + "album": "Album", + "app": "App", + "directory": "Bibliothek", + "episode": "Episode", + "game": "Spiel", + "movie": "Film", + "music": "Musik", + "season": "Episode", + "url": "URL", + "video": "Video" + }, "content-type": { "album": "Album", "artist": "Künstler", @@ -566,7 +585,13 @@ "playlist": "Playlist", "server": "Server" }, - "play": "Abspielen" + "no_items": "Keine Einträge", + "play": "Abspielen", + "video_not_supported": "Ihr Browser unterstützt das Videoelement nicht." + }, + "picture-upload": { + "label": "Bild", + "unsupported_format": "Nicht unterstütztes Format, bitte wähle ein JPEG-, PNG- oder GIF-Bild." }, "related-items": { "area": "Bereich", @@ -672,8 +697,10 @@ "yaml_not_editable": "Die Einstellungen dieser Entität können nicht über die Benutzeroberfläche bearbeitet werden. Nur über die Benutzeroberfläche eingerichtete Entitäten können über die Benutzeroberfläche konfiguriert werden." }, "more_info_control": { + "details": "Details", "dismiss": "Dialog ausblenden", "edit": "Entität bearbeiten", + "history": "Verlauf", "person": { "create_zone": "Zone vom aktuellen Standort erstellen" }, @@ -849,7 +876,13 @@ "type_select": "Aktionstyp", "type": { "choose": { - "label": "Auswählen" + "add_option": "Option hinzufügen", + "conditions": "Bedingungen", + "default": "Standard-Aktionen", + "label": "Auswählen", + "option": "Option {number}", + "remove_option": "Option entfernen", + "sequence": "Aktionen" }, "condition": { "label": "Bedingung" @@ -872,6 +905,7 @@ }, "repeat": { "label": "Wiederholen", + "sequence": "Aktionen", "type_select": "Wiederholungstyp", "type": { "count": { @@ -1044,6 +1078,7 @@ "value_template": "Wert-Template (optional)" }, "state": { + "attribute": "Attribut (Optional)", "for": "Für", "from": "Von", "label": "Zustand", @@ -1068,7 +1103,8 @@ }, "time": { "at": "Um", - "label": "Zeit" + "label": "Zeit", + "type_value": "Feste Zeit" }, "webhook": { "label": "Webhook", @@ -1091,6 +1127,8 @@ "add_automation": "Automatisierung hinzufügen", "delete_automation": "Automatisierung löschen", "delete_confirm": "Bist du sicher, dass du diese Automatisierung löschen möchtest?", + "duplicate": "Duplizieren", + "duplicate_automation": "Automatisierung kopieren", "edit_automation": "Automatisierung bearbeiten", "header": "Automatisierungseditor", "headers": { @@ -1314,6 +1352,7 @@ } }, "devices": { + "add_prompt": "Mit diesem Gerät wurden noch keine {name} hinzugefügt. Du kannst eins hinzufügen, indem du auf den + Knopf drückst.", "automation": { "actions": { "caption": "Wenn etwas ausgelöst wird ..." @@ -1477,6 +1516,7 @@ }, "integrations": { "add_integration": "Integration hinzufügen", + "attention": "Aufmerksamkeit erforderlich", "caption": "Integrationen", "config_entry": { "area": "In {area}", @@ -1495,6 +1535,8 @@ "no_device": "Entitäten ohne Geräte", "no_devices": "Diese Integration hat keine Geräte.", "options": "Optionen", + "reload": "Neu laden", + "reload_confirm": "Die Integration wurde neu geladen", "rename": "Umbenennen", "restart_confirm": "Starte Home Assistant neu, um das Entfernen dieser Integration abzuschließen", "settings_button": "Einstellungen für {integration} bearbeiten", @@ -1543,6 +1585,7 @@ "none_found_detail": "Passe deine Suchkriterien an.", "note_about_integrations": "Nicht alle Integrationen können über die Benutzeroberfläche konfiguriert werden.", "note_about_website_reference": "Weitere Informationen findest du auf der ", + "reconfigure": "Neu konfigurieren", "rename_dialog": "Bearbeite den Namen dieses Konfigurationseintrags", "rename_input_label": "Eintragsname", "search": "Such-Integrationen" @@ -1653,8 +1696,11 @@ "topic": "Topic" }, "ozw": { + "button": "Konfigurieren", "common": { - "instance": "Instanz" + "instance": "Instanz", + "network": "Netzwerk", + "zwave": "Z-Wave" }, "device_info": { "zwave_info": "Z-Wave Infos" @@ -1664,12 +1710,27 @@ "select_instance": "Instanz auswählen" }, "network_status": { + "details": { + "driverremoved": "Der Treiber wurde entfernt", + "driverreset": "Der Treiber wurde zurückgesetzt", + "ready": "Bereit zum Verbinden", + "started": "Verbindung mit MQTT hergestellt", + "starting": "Verbinde zu MQTT" + }, "offline": "Offline", "online": "Online", + "starting": "Startet", "unknown": "Unbekannt" }, "network": { "header": "Netzwerkverwaltung" + }, + "nodes_table": { + "failed": "Fehlgeschlagen", + "manufacturer": "Hersteller" + }, + "refresh_node": { + "start_refresh_button": "Aktualisierung starten" } }, "person": { @@ -1798,6 +1859,7 @@ "core": "Ort & Anpassungen neu laden", "group": "Gruppen neu laden", "heading": "Neuladen der YAML-Konfiguration", + "homekit": "HomeKit neu laden", "input_boolean": "Eingabe-Booleans neu laden", "input_datetime": "Eingabe-Datums- und Zeitfelder neu laden", "input_number": "Eingabenummern neu laden", @@ -1807,6 +1869,7 @@ "person": "Personen neu laden", "scene": "Szenen neu laden", "script": "Skripte neu laden", + "telegram": "Telegram-Benachrichtigungsdienste neu laden", "zone": "Zonen neu laden" }, "server_management": { @@ -1828,12 +1891,18 @@ }, "tags": { "detail": { - "name": "Name" + "delete": "Löschen", + "description": "Beschreibung", + "name": "Name", + "update": "Aktualisieren" }, + "edit": "Bearbeiten", "headers": { + "last_scanned": "Zuletzt gescannt", "name": "Name" }, - "never_scanned": "Nie gescannt" + "never_scanned": "Nie gescannt", + "write": "Schreiben" }, "users": { "add_user": { @@ -1841,6 +1910,7 @@ "create": "Benutzerkonto anlegen", "name": "Name", "password": "Passwort", + "password_confirm": "Passwort bestätigen", "username": "Benutzername" }, "caption": "Benutzer", @@ -1857,7 +1927,9 @@ "group": "Gruppe", "id": "ID", "name": "Name", + "new_password": "Neues Passwort", "owner": "Besitzer", + "password_changed": "Das Passwort wurde geändert!", "system_generated": "System generiert", "system_generated_users_not_editable": "Systemgenerierte Benutzer können nicht aktualisiert werden.", "system_generated_users_not_removable": "Vom System generierte Benutzer können nicht entfernt werden.", @@ -2728,6 +2800,9 @@ "submit": "Absenden" }, "current_user": "Sie sind derzeit als {fullName} angemeldet.", + "customize_sidebar": { + "button": "Bearbeiten" + }, "dashboard": { "description": "Wähle ein Standard-Dashboard für dieses Gerät.", "dropdown_label": "Dashboard", @@ -2750,6 +2825,7 @@ "confirm_delete": "Möchten Sie den Zugriffs-Token für {name} wirklich löschen?", "create": "Token erstellen", "create_failed": "Das Zugriffs-Token konnte nicht erstellt werden.", + "created": "Erstellt am {date}", "created_at": "Erstellt am {date}", "delete_failed": "Fehler beim Löschen des Zugriffs-Tokens.", "description": "Erstelle langlebige Zugriffstoken, damit deine Skripte mit deiner Home Assistant-Instanz interagieren können. Jedes Token ist ab der Erstellung für 10 Jahre gültig. Die folgenden langlebigen Zugriffstoken sind derzeit aktiv.", @@ -2757,6 +2833,7 @@ "header": "Langlebige Zugangs-Token", "last_used": "Zuletzt verwendet am {date} in {location}", "learn_auth_requests": "Erfahre, wie du authentifizierte Anfragen stellen kannst.", + "name": "Name", "not_used": "Wurde noch nie benutzt", "prompt_copy_token": "Kopiere deinen Zugangs-Token. Er wird nicht wieder angezeigt werden.", "prompt_name": "Name?" @@ -2808,7 +2885,8 @@ "error_no_theme": "Keine Themen verfügbar.", "header": "Thema", "link_promo": "Erfahre mehr über Themen", - "primary_color": "Primärfarbe" + "primary_color": "Primärfarbe", + "reset": "zurücksetzen" }, "vibrate": { "description": "Aktiviere oder deaktiviere die Vibration an diesem Gerät, wenn du Geräte steuerst.", @@ -2822,6 +2900,7 @@ } }, "sidebar": { + "done": "fertig", "external_app_configuration": "App-Konfiguration", "sidebar_toggle": "Seitenleiste umschalten" } diff --git a/translations/frontend/en.json b/translations/frontend/en.json index 014a6c943d..6d3537f927 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" @@ -505,6 +506,8 @@ "back": "Back", "cancel": "Cancel", "close": "Close", + "continue": "Continue", + "copied": "Copied", "delete": "Delete", "error_required": "Required", "loading": "Loading", @@ -551,6 +554,10 @@ "toggle": "Toggle" }, "entity": { + "entity-attribute-picker": { + "attribute": "Attribute", + "show_attributes": "Show attributes" + }, "entity-picker": { "clear": "Clear", "entity": "Entity", @@ -561,8 +568,35 @@ "loading_history": "Loading state history...", "no_history_found": "No state history found." }, + "logbook": { + "entries_not_found": "No logbook entries found." + }, "media-browser": { + "audio_not_supported": "Your browser does not support the audio element.", + "choose_player": "Choose Player", "choose-source": "Choose Source", + "class": { + "album": "Album", + "app": "App", + "artist": "Artist", + "channel": "Channel", + "composer": "Composer", + "contributing_artist": "Contributing Artist", + "directory": "Library", + "episode": "Episode", + "game": "Game", + "genre": "Genre", + "image": "Image", + "movie": "Movie", + "music": "Music", + "playlist": "Playlist", + "podcast": "Podcast", + "season": "Season", + "track": "Track", + "tv_show": "TV Show", + "url": "Url", + "video": "Video" + }, "content-type": { "album": "Album", "artist": "Artist", @@ -570,12 +604,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", @@ -633,7 +672,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" }, @@ -689,8 +728,11 @@ "crop": "Crop" }, "more_info_control": { + "controls": "Controls", + "details": "Details", "dismiss": "Dismiss dialog", "edit": "Edit entity", + "history": "History", "person": { "create_zone": "Create zone from current location" }, @@ -918,7 +960,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" @@ -982,7 +1030,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", @@ -1070,6 +1120,7 @@ "value_template": "Value template (optional)" }, "state": { + "attribute": "Attribute (Optional)", "for": "For", "from": "From", "label": "State", @@ -1096,8 +1147,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", @@ -1120,6 +1173,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": { @@ -1520,6 +1575,7 @@ }, "integrations": { "add_integration": "Add integration", + "attention": "Attention required", "caption": "Integrations", "config_entry": { "area": "In {area}", @@ -1543,6 +1599,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}", @@ -1589,6 +1646,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" @@ -1762,8 +1820,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", @@ -1912,7 +1983,7 @@ "filter": "Reload filter entities", "generic": "Reload generic IP camera entities", "generic_thermostat": "Reload generic thermostat entities", - "group": "Reload groups", + "group": "Reload groups, group entities, and notify services", "heading": "YAML configuration reloading", "history_stats": "Reload history stats entities", "homekit": "Reload HomeKit", @@ -1923,12 +1994,17 @@ "input_text": "Reload input texts", "introduction": "Some parts of Home Assistant can reload without requiring a restart. Hitting reload will unload their current YAML configuration and load the new one.", "min_max": "Reload min/max entities", + "mqtt": "Reload mqtt entities", "person": "Reload persons", "ping": "Reload ping binary sensor entities", - "rest": "Reload rest entities", + "reload": "Reload {domain}", + "rest": "Reload rest entities and notify services", + "rpi_gpio": "Reload Raspberry Pi GPIO entities", "scene": "Reload scenes", "script": "Reload scripts", + "smtp": "Reload smtp notify services", "statistics": "Reload statistics entities", + "telegram": "Reload telegram notify services", "template": "Reload template entities", "trend": "Reload trend entities", "universal": "Reload universal media player entities", @@ -2017,7 +2093,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": { @@ -2055,7 +2131,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", @@ -2188,7 +2264,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", @@ -2201,11 +2277,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": { @@ -2299,9 +2375,14 @@ "title": "States" }, "templates": { + "all_listeners": "This template listens for all state changed events.", "description": "Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.", + "domain": "Domain", "editor": "Template editor", + "entity": "Entity", "jinja_documentation": "Jinja2 template documentation", + "listeners": "This template listens for the following state changed events:", + "no_listeners": "This template does not listen for any state changed events and will not update automatically.", "reset": "Reset to demo template", "template_extensions": "Home Assistant template extensions", "title": "Template", @@ -2549,7 +2630,11 @@ } }, "cardpicker": { + "by_card": "By Card", + "by_entity": "By Entity", "custom_card": "Custom", + "domain": "Domain", + "entity": "Entity", "no_description": "No description available." }, "edit_card": { @@ -2563,6 +2648,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", @@ -2606,7 +2692,7 @@ }, "raw_editor": { "confirm_remove_config_text": "We will automatically generate your Lovelace UI views with your areas and devices if you remove your Lovelace UI configuration.", - "confirm_remove_config_title": "Are you sure you want to remove your Lovelace UI configuration? We will automatically generate your Lovelace UI views with your areas and devices.", + "confirm_remove_config_title": "Are you sure you want to remove your Lovelace UI configuration?", "confirm_unsaved_changes": "You have unsaved changes, are you sure you want to exit?", "confirm_unsaved_comments": "Your configuration contains comment(s), these will not be saved. Do you want to continue?", "error_invalid_config": "Your configuration is not valid: {error}", @@ -2883,6 +2969,11 @@ "submit": "Submit" }, "current_user": "You are currently logged in as {fullName}.", + "customize_sidebar": { + "button": "Edit", + "description": "You can also press and hold the header of the sidebar to activate edit mode.", + "header": "Change the order and hide items from the sidebar" + }, "dashboard": { "description": "Pick a default dashboard for this device.", "dropdown_label": "Dashboard", @@ -2905,6 +2996,7 @@ "confirm_delete": "Are you sure you want to delete the access token for {name}?", "create": "Create Token", "create_failed": "Failed to create the access token.", + "created": "Created {date}", "created_at": "Created at {date}", "delete_failed": "Failed to delete the access token.", "description": "Create long-lived access tokens to allow your scripts to interact with your Home Assistant instance. Each token will be valid for 10 years from creation. The following long-lived access tokens are currently active.", @@ -2912,9 +3004,10 @@ "header": "Long-Lived Access Tokens", "last_used": "Last used at {date} from {location}", "learn_auth_requests": "Learn how to make authenticated requests.", + "name": "Name", "not_used": "Has never been used", "prompt_copy_token": "Copy your access token. It will not be shown again.", - "prompt_name": "Name?" + "prompt_name": "Give the token a name" }, "mfa_setup": { "close": "Close", @@ -2978,6 +3071,7 @@ } }, "sidebar": { + "done": "Done", "external_app_configuration": "App Configuration", "sidebar_toggle": "Sidebar Toggle" } diff --git a/translations/frontend/es-419.json b/translations/frontend/es-419.json index 62e01af2d6..9f6f2ba0a4 100644 --- a/translations/frontend/es-419.json +++ b/translations/frontend/es-419.json @@ -13,9 +13,16 @@ }, "panel": { "calendar": "Calendario", + "config": "", "developer_tools": "Herramientas para desarrolladores", + "history": "", + "logbook": "", + "mailbox": "", + "map": "", + "media_browser": "Navegador de medios", "profile": "Perfil", - "shopping_list": "Lista de compras" + "shopping_list": "Lista de compras", + "states": "" }, "state_attributes": { "climate": { @@ -100,7 +107,7 @@ }, "automation": { "off": "Desactivado", - "on": "Encendido" + "on": "Activada" }, "binary_sensor": { "battery": { @@ -199,7 +206,7 @@ "fan_only": "Sólo ventilador", "heat": "Calentar", "heat_cool": "Calentar/Enfriar", - "off": "Desactivar" + "off": "Apagado" }, "configurator": { "configure": "Configurar", @@ -231,7 +238,7 @@ "home": "En Casa", "locked": "Cerrado", "not_home": "Fuera de Casa", - "off": "Desactivado", + "off": "Apagado", "ok": "OK", "on": "Encendido", "open": "Abierto", @@ -413,9 +420,16 @@ "unlock": "Desbloquear" }, "media_player": { + "browse_media": "Explorar medios", + "media_next_track": "Siguiente", + "media_play": "Reproducir", + "media_play_pause": "Reproducir/pausa", + "media_previous_track": "Anterior", "sound_mode": "Modo de sonido", "source": "Fuente", - "text_to_speak": "Texto a hablar" + "text_to_speak": "Texto a hablar", + "turn_off": "Apagar", + "turn_on": "Encender" }, "persistent_notification": { "dismiss": "Descartar" @@ -492,6 +506,7 @@ "back": "Atrás", "cancel": "Cancelar", "close": "Cerrar", + "continue": "Continuar", "delete": "Eliminar", "error_required": "Requerido", "loading": "Cargando", @@ -538,6 +553,10 @@ "toggle": "Alternar" }, "entity": { + "entity-attribute-picker": { + "attribute": "Atributo", + "show_attributes": "Mostrar atributos" + }, "entity-picker": { "clear": "Limpiar", "entity": "Entidad", @@ -548,6 +567,58 @@ "loading_history": "Cargando historial de estado...", "no_history_found": "No se encontró historial de estado." }, + "logbook": { + "entries_not_found": "No se encontraron entradas en el libro de registro." + }, + "media-browser": { + "audio_not_supported": "Su navegador no soporta el elemento de audio.", + "choose_player": "Elige el reproductor", + "choose-source": "Elige la fuente", + "class": { + "album": "Álbum", + "app": "Aplicación", + "artist": "Artista", + "channel": "Canal", + "composer": "Compositor", + "contributing_artist": "Artista colaborador", + "directory": "Biblioteca", + "episode": "Episodio", + "game": "Juego", + "genre": "Género", + "image": "Imagen", + "movie": "Película", + "music": "Música", + "playlist": "Lista de reproducción", + "podcast": "Podcast", + "season": "Temporada", + "track": "Pista", + "tv_show": "Programa de TV", + "url": "URL", + "video": "Video" + }, + "content-type": { + "album": "Álbum", + "artist": "Artista", + "library": "Biblioteca", + "playlist": "Lista de reproducción", + "server": "Servidor" + }, + "media_browsing_error": "Error de navegación de medios", + "media_not_supported": "El Reproductor multimedia no es compatible con este tipo de medios", + "media_player": "Reproductor multimedia", + "media-player-browser": "Navegador del reproductor multimedia", + "no_items": "No hay elementos", + "pick": "Elegir", + "pick-media": "Elija medios", + "play": "Reproducir", + "play-media": "Reproducir medios", + "video_not_supported": "Su navegador no soporta el elemento de vídeo.", + "web-browser": "Navegador web" + }, + "picture-upload": { + "label": "Imagen", + "unsupported_format": "Formato no admitido, elija una imagen JPEG, PNG o GIF." + }, "related-items": { "area": "Área", "automation": "Parte de las siguientes automatizaciones", @@ -568,6 +639,7 @@ "week": "{count} {count, plural,\n one {semana}\n other {semanas}\n}" }, "future": "en {time}", + "just_now": "Ahora mismo", "never": "Nunca", "past": "Hace {time}" }, @@ -646,13 +718,19 @@ "pattern": "Patrón de expresiones regulares para la validación del lado del cliente", "text": "Texto" }, - "platform_not_loaded": "La integración {platform} no está cargada. Agregue su configuración agregando 'default_config:' o ''{platform}:''.", + "platform_not_loaded": "La integración {platform} no está cargada. Añádela a su archivo de configuración agregando 'default_config:' o ''{platform}:''.", "required_error_msg": "Este campo es requerido", "yaml_not_editable": "La configuración de esta entidad no se puede editar desde la interfaz de usuario. Solo las entidades configuradas desde la interfaz de usuario se pueden configurar desde la interfaz de usuario." }, + "image_cropper": { + "crop": "Cortar" + }, "more_info_control": { + "controls": "Controles", + "details": "Detalles", "dismiss": "Descartar diálogo", "edit": "Editar entidad", + "history": "Historial", "person": { "create_zone": "Crear zona desde ubicación actual" }, @@ -718,11 +796,11 @@ }, "zha_device_info": { "buttons": { - "add": "Agregar dispositivos", + "add": "Agregar dispositivos usando este dispositivo", "clusters": "Administrar clústeres", "reconfigure": "Reconfigurar dispositivo", "remove": "Eliminar dispositivo", - "zigbee_information": "Información de Zigbee" + "zigbee_information": "Firma del dispositivo Zigbee" }, "confirmations": { "remove": "¿Está seguro de que desea eliminar el dispositivo?" @@ -742,7 +820,7 @@ "unknown": "Desconocido", "zha_device_card": { "area_picker_label": "Área", - "device_name_placeholder": "Nombre de usuario", + "device_name_placeholder": "Cambiar el nombre del dispositivo", "update_name_button": "Actualizar Nombre" } } @@ -792,7 +870,7 @@ "confirmation_text": "Todos los dispositivos en esta área quedarán sin asignar.", "confirmation_title": "¿Está seguro de que desea eliminar esta área?" }, - "description": "Visión general de todas las áreas de su casa.", + "description": "Gestione las áreas de su casa.", "editor": { "area_id": "Identificador del área", "create": "Crear", @@ -853,13 +931,19 @@ "label": "Llamar servico", "service_data": "Datos" }, + "wait_for_trigger": { + "continue_timeout": "Continuar cuando el tiempo venza", + "label": "Esperar por un desencadenador", + "timeout": "Tiempo limite (opcional)" + }, "wait_template": { + "continue_timeout": "Continuar cuando el tiempo venza", "label": "Esperar", "timeout": "Tiempo de espera (opcional)", "wait_template": "Plantilla de espera" } }, - "unsupported_action": "Acción no soportada: {action}" + "unsupported_action": "No hay soporte en la interfaz de usuario para la acción: {action}" }, "alias": "Nombre", "conditions": { @@ -917,7 +1001,9 @@ "time": { "after": "Después de", "before": "Antes de", - "label": "Hora" + "label": "Hora", + "type_input": "Valor de un auxiliar de tipo fecha/tiempo", + "type_value": "Tiempo corregido" }, "zone": { "entity": "Entidad con ubicación", @@ -925,7 +1011,7 @@ "zone": "Zona" } }, - "unsupported_condition": "Condición no soportada: {condition}" + "unsupported_condition": "No hay soporte en la interfaz de usuario para la condición: {condition}" }, "default_name": "Nueva Automatización", "description": { @@ -994,6 +1080,7 @@ "start": "Inicio" }, "mqtt": { + "label": "MQTT", "payload": "Payload (opcional)", "topic": "Topic" }, @@ -1004,6 +1091,7 @@ "value_template": "Plantilla de valor (opcional)" }, "state": { + "attribute": "Atributo (opcional)", "for": "Por", "from": "De", "label": "Estado", @@ -1016,6 +1104,9 @@ "sunrise": "Salida del sol", "sunset": "Puesta de sol" }, + "tag": { + "label": "Etiqueta" + }, "template": { "label": "Plantilla", "value_template": "Plantilla de valor" @@ -1027,8 +1118,10 @@ "seconds": "Segundos" }, "time": { - "at": "A", - "label": "Hora" + "at": "A las", + "label": "Hora", + "type_input": "Valor de un auxiliar de tipo fecha/tiempo", + "type_value": "Tiempo corregido" }, "webhook": { "label": "Webhook", @@ -1043,7 +1136,7 @@ "zone": "Zona" } }, - "unsupported_platform": "Plataforma no soportada: {platform}" + "unsupported_platform": "No hay soporte en la interfaz de usuario para la plataforma: {platform}" }, "unsaved_confirm": "Tiene cambios sin guardar. ¿Estás seguro que quieres salir?" }, @@ -1051,6 +1144,8 @@ "add_automation": "Agregar automatización", "delete_automation": "Eliminar automatización", "delete_confirm": "¿Está seguro de que desea eliminar esta automatización?", + "duplicate": "Duplicar", + "duplicate_automation": "Duplicar automatización", "edit_automation": "Editar automatización", "header": "Editor de automatizaciones", "headers": { @@ -1132,8 +1227,13 @@ }, "alexa": { "banner": "La edición de las entidades expuestas a través de esta interfaz de usuario está deshabilitada porque ha configurado filtros de entidad en configuration.yaml.", + "dont_expose_entity": "No exponer la entidad", "expose": "Exponer a Alexa", + "expose_entity": "Exponer la entidad", + "exposed": "{selected} expuesto", "exposed_entities": "Entidades expuestas", + "follow_domain": "Seguir dominio", + "not_exposed": "{selected} no expuesto", "not_exposed_entities": "Entidades no expuestas", "title": "Alexa" }, @@ -1171,8 +1271,14 @@ "google": { "banner": "La edición de las entidades expuestas a través de esta interfaz de usuario está deshabilitada porque ha configurado filtros de entidad en configuration.yaml.", "disable_2FA": "Deshabilitar la autenticación de dos factores", + "dont_expose_entity": "No exponer la entidad", "expose": "Exponer al Asistente de Google", + "expose_entity": "Exponer la entidad", + "exposed": "{selected} expuesto", "exposed_entities": "Entidades expuestas", + "follow_domain": "Seguir dominio", + "manage_domains": "Gestionar dominios", + "not_exposed": "{selected} no expuesto", "not_exposed_entities": "Entidades no expuestas", "sync_to_google": "Sincronizar los cambios con Google.", "title": "Asistente de Google" @@ -1274,6 +1380,7 @@ } }, "devices": { + "add_prompt": "Aún no se ha agregado ningún {name} con este dispositivo. Puede agregar uno haciendo clic en el botón + de arriba.", "automation": { "actions": { "caption": "Cuando algo se desencadena..." @@ -1293,6 +1400,7 @@ "caption": "Dispositivos", "confirm_delete": "¿Está seguro de que desea eliminar este dispositivo?", "confirm_rename_entity_ids": "¿También desea cambiar el nombre de la identificación de la entidad de sus entidades?", + "confirm_rename_entity_ids_warning": "Esto no cambiará ninguna configuración (como automatizaciones, scripts, escenas, Lovelace) que esté usando actualmente estas entidades, tendrás que actualizarlas tú mismo.", "data_table": { "area": "Área", "battery": "Batería", @@ -1334,7 +1442,7 @@ }, "entities": { "caption": "Entidades", - "description": "Visión general de todas las entidades conocidas.", + "description": "Gestione todas las entidades conocidas.", "picker": { "disable_selected": { "button": "Deshabilitar selección", @@ -1386,7 +1494,7 @@ "header": "Configurar Home Assistant", "helpers": { "caption": "Auxiliares", - "description": "Elementos que pueden ayudar a construir automatizaciones.", + "description": "Gestionar elementos ayudan a construir automatizaciones", "dialog": { "add_helper": "Agregar auxiliar", "add_platform": "Añadir {platform}", @@ -1437,6 +1545,7 @@ }, "integrations": { "add_integration": "Agregar integración", + "attention": "Atención requerida", "caption": "Integraciones", "config_entry": { "area": "En {area}", @@ -1457,6 +1566,7 @@ "options": "Opciones", "rename": "Renombrar", "restart_confirm": "Reinicie Home Assistant para terminar de eliminar esta integración.", + "services": "{count} {count, plural,\n one {service}\n other {services}\n}", "settings_button": "Editar configuración para {integration}", "system_options": "Opciones de Sistema", "system_options_button": "Opciones del sistema para {integration}", @@ -1479,7 +1589,7 @@ }, "configure": "Configurar", "configured": "Configurado", - "description": "Administrar y configurar integraciones", + "description": "Gestione las integraciones", "details": "Detalles de integración", "discovered": "Descubierto", "home_assistant_website": "Sitio web de Home Assistant", @@ -1503,6 +1613,7 @@ "none_found_detail": "Ajuste sus criterios de búsqueda", "note_about_integrations": "No todas las integraciones se pueden configurar a través de la interfaz de usuario.", "note_about_website_reference": "Hay más disponibles en ", + "reconfigure": "Reconfigurar", "rename_dialog": "Editar el nombre de esta entrada de configuración", "rename_input_label": "Ingresar Nombre", "search": "Buscar integraciones" @@ -1609,15 +1720,87 @@ "start_listening": "Comenzar a escuchar", "stop_listening": "Deja de escuchar", "subscribe_to": "Tema para suscribirse", + "title": "", "topic": "tema" }, + "ozw": { + "button": "Configurar", + "common": { + "controller": "Controlador", + "instance": "Instancia", + "network": "Red" + }, + "navigation": { + "network": "Red", + "nodes": "Nodos", + "select_instance": "Seleccione la instancia" + }, + "network_status": { + "details": { + "driverallnodesqueried": "Todos los nodos han sido consultados", + "driverallnodesqueriedsomedead": "Todos los nodos han sido consultados. Algunos nodos fueron encontrados inactivos.", + "driverawakenodesqueries": "Se han consultado todos los nodos activos", + "driverfailed": "No se pudo conectar al controlador Z-Wave", + "driverready": "Iniciando el controlador de Z-Wave", + "driverremoved": "El controlador ha sido eliminado", + "driverreset": "El controlador se ha reiniciado", + "offline": "OZWDaemon fuera de línea", + "ready": "Listo para conectar", + "started": "Conectado a MQTT", + "starting": "Conectando con MQTT", + "stopped": "OpenZWave se detuvo" + }, + "offline": "Fuera de línea", + "online": "En línea", + "starting": "Comenzando", + "unknown": "Desconocido" + }, + "network": { + "header": "Gestión de red", + "introduction": "Gestione las funciones de toda la red." + }, + "node_query_stages": { + "complete": "El proceso de consulta está completo", + "configuration": "Obteniendo los valores de configuración del nodo", + "dynamic": "Obteniendo los valores que cambian con frecuencia del nodo" + }, + "node": { + "button": "Detalles del nodo", + "not_found": "Nodo no encontrado" + }, + "nodes_table": { + "failed": "Fallido", + "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úrese de activarlo antes de continuar.", + "button": "Actualizar nodo", + "complete": "Actualización de nodo completa", + "description": "Esto le indicará a OpenZWave que vuelva a consultar un nodo y actualice las clases de comando, las capacidades y los valores del nodo.", + "node_status": "Estado del nodo", + "refreshing_description": "Actualizando la información del nodo ...", + "start_refresh_button": "Iniciar actualización", + "step": "Paso", + "title": "Actualizar la información del nodos", + "wakeup_header": "Instrucciones de activación para", + "wakeup_instructions_source": "Las instrucciones de activación se obtienen de la base de datos de dispositivos de la comunidad OpenZWave." + }, + "select_instance": { + "header": "Seleccione una instancia OpenZWave", + "introduction": "Tiene más de una instancia de OpenZWave en ejecución. ¿Qué instancia te gustaría gestionar?" + } + }, "person": { "add_person": "Agregar persona", "caption": "Personas", "confirm_delete": "¿Está seguro de que desea eliminar a esta persona?", "confirm_delete2": "Todos los dispositivos que pertenecen a esta persona quedarán sin asignar.", "create_person": "Crear persona", - "description": "Gestiona las personas que rastrea Home Assistant.", + "description": "Gestione las personas que rastrea Home Assistant.", "detail": { "create": "Crear", "delete": "Eliminar", @@ -1640,7 +1823,7 @@ "scene": { "activated": "Escena activada {name}.", "caption": "Escenas", - "description": "Crear y editar escenas", + "description": "Gestionar escenas", "editor": { "default_name": "Nueva escena", "devices": { @@ -1735,7 +1918,7 @@ "reloading": { "automation": "Recargar automatizaciones", "core": "Recargar ubicación y personalizaciones", - "group": "Recargar grupos", + "group": "Recargar grupos, entidades grupales, y servicios de notificación", "heading": "Recarga de configuración YAML", "input_boolean": "Recargar controles booleanos", "input_datetime": "Recargar controles de fechas", @@ -1743,9 +1926,16 @@ "input_select": "Recargar controles de selección", "input_text": "Recargar controles de texto", "introduction": "Algunas partes de Home Assistant pueden recargarse sin requerir un reinicio. Al presionar recargar se descargará su configuración YAML actual y se cargará la nueva.", + "mqtt": "Recargar entidades MQTT", "person": "Recargar personas", + "reload": "Recargar {domain}", + "rest": "Recargar entidades \"rest\" y servicios de notificación.", + "rpi_gpio": "Recargue las entidades GPIO de la Raspberry Pi", "scene": "Recargar escenas", "script": "Recargar scripts", + "smtp": "Recargar servicios de notificación smtp", + "telegram": "Recargar servicios de notificación de telegram", + "template": "Recargar las entidades de la plantilla", "zone": "Recargar zonas" }, "server_management": { @@ -1765,6 +1955,32 @@ } } }, + "tags": { + "add_tag": "Añadir etiqueta", + "automation_title": "La etiqueta {name} ha sido escaneada", + "caption": "Etiquetas", + "create_automation": "Crea automatización con etiqueta", + "description": "Gestionar etiquetas", + "detail": { + "create": "Crear", + "create_and_write": "Crear y escribir", + "delete": "Eliminar", + "description": "Descripción", + "name": "Nombre", + "new_tag": "Nueva etiqueta", + "tag_id": "ID de etiqueta", + "tag_id_placeholder": "Autogenerado cuando se deja vacío", + "update": "Actualizar" + }, + "edit": "Editar", + "headers": { + "last_scanned": "Último escaneado", + "name": "Nombre" + }, + "never_scanned": "Nunca escaneado", + "no_tags": "Sin etiquetas", + "write": "Escribir" + }, "users": { "add_user": { "caption": "Agregar usuario", @@ -1801,7 +2017,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 puntos finales de la API de administración para garantizar que limiten correctamente el acceso 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 interfaz de usuario. Todavía estamos auditando todos los puntos finales de la API de administración para garantizar que limiten correctamente el acceso solo a los administradores." }, "zha": { "add_device_page": { @@ -1839,7 +2055,7 @@ "clusters": { "header": "Clústeres", "help_cluster_dropdown": "Seleccione un clúster para ver atributos y comandos.", - "introduction": "Los clústeres son los bloques de construcción para la funcionalidad de Zigbee. Separa la funcionalidad en unidades lógicas. Hay tipos de clientes y servidores que se componen de atributos y comandos." + "introduction": "Los clústeres son los bloques de construcción para la funcionalidad de Zigbee. Ellos separan la funcionalidad en unidades lógicas. Hay tipos de clientes y servidores que se componen de atributos y comandos." }, "common": { "add_devices": "Agregar dispositivos", @@ -1871,7 +2087,7 @@ "create_group": "Zigbee Home Automation - Crear grupo", "create_group_details": "Ingrese los detalles requeridos para crear un nuevo grupo zigbee", "creating_group": "Creando grupo", - "description": "Crear y modificar grupos Zigbee", + "description": "Gestione los grupos Zigbee", "group_details": "Aquí están todos los detalles para el grupo Zigbee seleccionado.", "group_id": "Identificación del grupo", "group_info": "Información del grupo", @@ -1939,6 +2155,7 @@ }, "zwave": { "button": "Configurar", + "caption": "", "common": { "index": "Índice", "instance": "Instancia", @@ -2082,9 +2299,14 @@ "title": "Estados" }, "templates": { + "all_listeners": "Esta plantilla escucha todos los eventos de cambio de estado.", "description": "Las plantillas se representan utilizando el motor de plantillas Jinja2 con algunas extensiones específicas de Home Assistant.", + "domain": "Dominio", "editor": "Editor de plantillas", + "entity": "Entidad", "jinja_documentation": "Documentación de plantillas Jinja2", + "listeners": "Esta plantilla escucha los eventos de los siguientes cambios de estado:", + "no_listeners": "Esta plantilla no escucha ningún evento de cambio de estado y no se actualizará automáticamente.", "template_extensions": "Extensiones de plantilla de Home Assistant", "title": "Plantilla", "unknown_error_template": "Error desconocido al mostrar la plantilla" @@ -2326,7 +2548,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": { @@ -2340,10 +2566,11 @@ "options": "Mas opciones", "pick_card": "¿Qué tarjeta desea agregar?", "pick_card_view_title": "¿Qué tarjeta le gustaría agregar a su vista de {name} ?", + "search_cards": "Buscar tarjetas", "show_code_editor": "Mostrar editor de código", "show_visual_editor": "Mostrar el editor visual", "toggle_editor": "Cambiar editor", - "typed_header": "{tipo} Configuración de la tarjeta", + "typed_header": "{type} Configuración de la tarjeta", "unsaved_changes": "Tiene cambios no guardados" }, "edit_lovelace": { @@ -2420,7 +2647,7 @@ }, "menu": { "close": "Cerrar", - "configure_ui": "Configurar interfaz de usuario", + "configure_ui": "Editar interfaz de usuario", "exit_edit_mode": "Salir del modo de edición de la interfaz de usuario", "help": "Ayuda", "refresh": "Refrescar", @@ -2652,6 +2879,11 @@ "submit": "Enviar" }, "current_user": "Actualmente estás conectado como {fullName} .", + "customize_sidebar": { + "button": "Editar", + "description": "También puede mantener pulsado el encabezado de la barra lateral para activar el modo de edición.", + "header": "Cambiar el orden y ocultar elementos de la barra lateral" + }, "dashboard": { "description": "Elija un tablero predeterminado para este dispositivo.", "dropdown_label": "Tablero", @@ -2674,6 +2906,7 @@ "confirm_delete": "¿Está seguro de que desea eliminar el token de acceso para {name}?", "create": "Crear Token", "create_failed": "No se pudo crear el token de acceso.", + "created": "Creado {date}", "created_at": "Creado en {date}", "delete_failed": "No se pudo eliminar el token de acceso.", "description": "Cree tokens de acceso de larga duración para permitir que sus secuencias de comandos interactúen con la instancia de su Home Assistant. Cada token tendrá una validez de 10 años a partir de su creación. Los siguientes tokens de acceso de larga duración están activos actualmente.", @@ -2681,9 +2914,10 @@ "header": "Tokens de acceso de larga duración", "last_used": "Utilizado por última vez en {date} desde {location}.", "learn_auth_requests": "Aprenda a realizar solicitudes autenticadas.", + "name": "Nombre", "not_used": "Nunca se ha utilizado", "prompt_copy_token": "Copia tu token de acceso. No se volverá a mostrar", - "prompt_name": "¿Nombre?" + "prompt_name": "Dale un nombre al token" }, "mfa_setup": { "close": "Cerrar", @@ -2739,6 +2973,7 @@ } }, "sidebar": { + "done": "Hecho", "external_app_configuration": "Configuración de la aplicación", "sidebar_toggle": "Alternar barra lateral" } diff --git a/translations/frontend/es.json b/translations/frontend/es.json index 1b45b58764..45f8523416 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,8 @@ "back": "Volver", "cancel": "Cancelar", "close": "Cerrar", + "continue": "Continuar", + "copied": "Copiado", "delete": "Eliminar", "error_required": "Obligatorio", "loading": "Cargando", @@ -551,6 +554,10 @@ "toggle": "Interruptor" }, "entity": { + "entity-attribute-picker": { + "attribute": "Atributo", + "show_attributes": "Mostrar atributos" + }, "entity-picker": { "clear": "Limpiar", "entity": "Entidad", @@ -561,8 +568,35 @@ "loading_history": "Cargando historial de estado...", "no_history_found": "No se encontró historial de estado." }, + "logbook": { + "entries_not_found": "No se han encontrado entradas en el registro." + }, "media-browser": { + "audio_not_supported": "Tu navegador no es compatible con el elemento de audio.", + "choose_player": "Elige reproductor", "choose-source": "Elige la fuente", + "class": { + "album": "Álbum", + "app": "Aplicación", + "artist": "Artista", + "channel": "Canal", + "composer": "Compositor", + "contributing_artist": "Artista colaborador", + "directory": "Biblioteca", + "episode": "Episodio", + "game": "Juego", + "genre": "Género", + "image": "Imagen", + "movie": "Película", + "music": "Música", + "playlist": "Lista de reproducción", + "podcast": "Podcast", + "season": "Temporada", + "track": "Pista", + "tv_show": "Programa de TV", + "url": "Url", + "video": "Vídeo" + }, "content-type": { "album": "Álbum", "artist": "Artista", @@ -570,12 +604,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 +728,11 @@ "crop": "Recortar" }, "more_info_control": { + "controls": "Controles", + "details": "Detalles", "dismiss": "Descartar diálogo", "edit": "Editar entidad", + "history": "Historial", "person": { "create_zone": "Crear zona a partir de la ubicación actual" }, @@ -918,7 +960,13 @@ "label": "Llamar servicio", "service_data": "Datos de servicio" }, + "wait_for_trigger": { + "continue_timeout": "Continuar tras el límite de tiempo", + "label": "Esperar al disparador", + "timeout": "Límite de tiempo (opcional)" + }, "wait_template": { + "continue_timeout": "Continuar tras el límite de tiempo", "label": "Esperar", "timeout": "Límite de tiempo (opcional)", "wait_template": "Plantilla de espera" @@ -982,7 +1030,9 @@ "time": { "after": "Después de", "before": "Antes de", - "label": "Hora" + "label": "Hora", + "type_input": "Valor de un ayudante de fecha/hora", + "type_value": "Horario fijo" }, "zone": { "entity": "Entidad con la ubicación", @@ -1070,6 +1120,7 @@ "value_template": "Valor de la plantilla (opcional)" }, "state": { + "attribute": "Atributo (Opcional)", "for": "Durante", "from": "De", "label": "Estado", @@ -1097,7 +1148,9 @@ }, "time": { "at": "A las", - "label": "Hora" + "label": "Hora", + "type_input": "Valor de un ayudante de fecha/hora", + "type_value": "Horario fijo" }, "webhook": { "label": "Webhook", @@ -1120,6 +1173,8 @@ "add_automation": "Añadir automatización", "delete_automation": "Eliminar la automatización", "delete_confirm": "¿Estás seguro de que quieres eliminar esta automatización?", + "duplicate": "Duplicar", + "duplicate_automation": "Duplicar la automatización", "edit_automation": "Editar automatización", "header": "Editor de automatización", "headers": { @@ -1520,6 +1575,7 @@ }, "integrations": { "add_integration": "Añadir integración", + "attention": "Atención requerida", "caption": "Integraciones", "config_entry": { "area": "En {area}", @@ -1543,6 +1599,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}", @@ -1589,6 +1646,7 @@ "none_found_detail": "Ajusta tus criterios de búsqueda.", "note_about_integrations": "Todavía no se pueden configurar todas las integraciones a través de la interfaz de usuario.", "note_about_website_reference": "Hay más disponibles en el ", + "reconfigure": "Reconfigurar", "rename_dialog": "Edita el nombre de esta entrada de configuración", "rename_input_label": "Nombre de la entrada", "search": "Buscar integraciones" @@ -1762,8 +1820,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 +1983,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 +1994,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 +2093,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 +2131,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 +2264,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 +2281,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": { @@ -2299,9 +2375,14 @@ "title": "Estados" }, "templates": { + "all_listeners": "Esta plantilla escucha todos los eventos de cambio de estado.", "description": "Las plantillas se muestran utilizando el motor de plantillas Jinja2 con algunas extensiones específicas de Home Assistant.", + "domain": "Dominio", "editor": "Editor de plantillas", + "entity": "Entidad", "jinja_documentation": "Documentación de plantilla Jinja2", + "listeners": "Esta plantilla escucha los siguientes eventos de cambio de estado:", + "no_listeners": "Esta plantilla no escucha ningún evento de cambio de estado y no se actualizará automáticamente.", "reset": "Reiniciar a la plantilla de demostración", "template_extensions": "Extensiones de plantilla de Home Assistant", "title": "Plantillas", @@ -2549,7 +2630,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 +2648,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", @@ -2883,6 +2969,11 @@ "submit": "Enviar" }, "current_user": "Has iniciado sesión como {fullName}.", + "customize_sidebar": { + "button": "Editar", + "description": "También puedes mantener pulsada la cabecera de la barra lateral para activar el modo de edición.", + "header": "Cambiar el orden y ocultar elementos de la barra lateral" + }, "dashboard": { "description": "Selecciona un panel de control predeterminado para este dispositivo.", "dropdown_label": "Panel de control", @@ -2905,6 +2996,7 @@ "confirm_delete": "¿Estás seguro de que quieres eliminar el token de acceso para {name}?", "create": "Crear Token", "create_failed": "No se ha podido crear el token de acceso.", + "created": "Creado en {date}", "created_at": "Creado el {date}", "delete_failed": "Error al eliminar el token de acceso.", "description": "Crea tokens de acceso de larga duración para permitir que tus scripts interactúen con tu instancia de Home Assistant. Cada token será válido por 10 años desde la creación. Los siguientes tokens de acceso de larga duración están actualmente activos.", @@ -2912,9 +3004,10 @@ "header": "Tokens de acceso de larga duración", "last_used": "Último uso el {date} desde {location}", "learn_auth_requests": "Aprende cómo realizar solicitudes autenticadas.", + "name": "Nombre", "not_used": "Nunca ha sido usado", "prompt_copy_token": "Copia tu token de acceso. No se mostrará de nuevo.", - "prompt_name": "¿Nombre?" + "prompt_name": "Dale un nombre al token" }, "mfa_setup": { "close": "Cerrar", @@ -2978,6 +3071,7 @@ } }, "sidebar": { + "done": "Hecho", "external_app_configuration": "Configuración de la aplicación", "sidebar_toggle": "Alternar barra lateral" } diff --git a/translations/frontend/et.json b/translations/frontend/et.json index 4b3c342d9b..51e3f2f620 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/fr.json b/translations/frontend/fr.json index 939706d0e1..0634745577 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,8 @@ "back": "Retour", "cancel": "Annuler", "close": "Fermer", + "continue": "Continuer", + "copied": "Copié", "delete": "Supprimer", "error_required": "Obligatoire", "loading": "Chargement", @@ -551,6 +554,10 @@ "toggle": "Permuter" }, "entity": { + "entity-attribute-picker": { + "attribute": "Attribut", + "show_attributes": "Afficher les attributs" + }, "entity-picker": { "clear": "Effacer", "entity": "Entité", @@ -561,8 +568,35 @@ "loading_history": "Chargement de l'historique des valeurs ...", "no_history_found": "Aucun historique des valeurs trouvé." }, + "logbook": { + "entries_not_found": "Aucune entrée trouvée dans le journal." + }, "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", + "class": { + "album": "Album", + "app": "App", + "artist": "Artiste", + "channel": "Canal", + "composer": "Compositeur", + "contributing_artist": "Artiste collaborateur", + "directory": "Bibliothèque", + "episode": "Épisode", + "game": "Jeu", + "genre": "Genre", + "image": "Image", + "movie": "Film", + "music": "Musique", + "playlist": "Liste de lecture", + "podcast": "Podcast", + "season": "Saison", + "track": "Piste", + "tv_show": "Émission de télévision", + "url": "Url", + "video": "Vidéo" + }, "content-type": { "album": "Album", "artist": "Artiste", @@ -570,12 +604,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 +728,11 @@ "crop": "Recadrer" }, "more_info_control": { + "controls": "Contrôles", + "details": "Détails", "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" }, @@ -918,7 +960,13 @@ "label": "Appeler un service", "service_data": "Données du service" }, + "wait_for_trigger": { + "continue_timeout": "Continuer à l'expiration du délai", + "label": "Attendre le déclencheur", + "timeout": "Délai d'expiration (optionnel)" + }, "wait_template": { + "continue_timeout": "Continuer à l'expiration du délai", "label": "Attendre", "timeout": "Délai d'expiration (optionnel)", "wait_template": "Template d'attente" @@ -982,7 +1030,9 @@ "time": { "after": "Après", "before": "Avant", - "label": "Heure" + "label": "Heure", + "type_input": "Valeur d'une aide de date/heure", + "type_value": "Temps fixe" }, "zone": { "entity": "Entité avec localisation", @@ -1070,6 +1120,7 @@ "value_template": "Contenu du template (optionnel)" }, "state": { + "attribute": "Attribut (facultatif)", "for": "Pendant", "from": "De", "label": "État", @@ -1097,7 +1148,9 @@ }, "time": { "at": "À", - "label": "Heure" + "label": "Heure", + "type_input": "Valeur d'une aide de date/heure", + "type_value": "Temps fixe" }, "webhook": { "label": "Webhook", @@ -1120,6 +1173,8 @@ "add_automation": "Ajouter une automatisation", "delete_automation": "Supprimer l'automatisation", "delete_confirm": "Voulez-vous vraiment supprimer cette automatisation ?", + "duplicate": "Dupliquer", + "duplicate_automation": "Dupliquer l'automatisation", "edit_automation": "Modifier l'automatisation", "header": "Éditeur d'automatisation", "headers": { @@ -1442,7 +1497,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", @@ -1520,6 +1575,7 @@ }, "integrations": { "add_integration": "Ajouter l'intégration", + "attention": "Attention requise", "caption": "Intégrations", "config_entry": { "area": "Dans {area}", @@ -1534,7 +1590,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", @@ -1543,6 +1599,7 @@ "reload_restart_confirm": "Redémarrer Home Assistant pour finaliser le rechargement de cette intégration", "rename": "Renommer", "restart_confirm": "Redémarrer Home Assistant pour terminer la suppression de cette intégration", + "services": "{count} {count, plural,\n one {service}\n other {services}\n}", "settings_button": "Modifier les paramètres pour {integration}", "system_options": "Options système", "system_options_button": "Options système pour {integration}", @@ -1589,6 +1646,7 @@ "none_found_detail": "Ajustez vos critères de recherche.", "note_about_integrations": "Toutes les intégrations ne peuvent pas encore être configurées via l'interface utilisateur.", "note_about_website_reference": "D'autres sont disponibles sur le ", + "reconfigure": "Reconfigurer", "rename_dialog": "Modifier le nom de cette entrée de configuration", "rename_input_label": "Nom de l'entrée", "search": "Chercher les intégrations" @@ -1740,7 +1798,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 +1820,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 +1983,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 +1994,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 +2093,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 +2131,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", @@ -2299,9 +2375,14 @@ "title": "États" }, "templates": { + "all_listeners": "Ce modèle écoute tous les événements de changement d'état.", "description": "Les modèles sont rendus à l'aide du moteur de modèles Jinja2 avec certaines extensions spécifiques de Home Assistant.", + "domain": "Domaine", "editor": "Éditeur de modèles", + "entity": "Entité", "jinja_documentation": "Documentation de modèle Jinja2", + "listeners": "Ce modèle écoute les événements de changement d'état suivants:", + "no_listeners": "Ce modèle n'écoute aucun événement de changement d'état et ne sera pas mis à jour automatiquement.", "reset": "Réinitialiser au modèle de démonstration", "template_extensions": "Extensions de modèles de Home Assistant", "title": "Modèle", @@ -2496,7 +2577,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 +2630,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 +2648,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 +2851,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 +2871,7 @@ } }, "start_over": "Recommencer", - "unknown_error": "Quelque chose a mal tourné", + "unknown_error": "Un problème est survenu", "working": "Veuillez patienter" }, "initializing": "Initialisation", @@ -2883,6 +2969,11 @@ "submit": "Envoyer" }, "current_user": "Vous êtes actuellement connecté en tant que {fullName}.", + "customize_sidebar": { + "button": "Modifier", + "description": "Vous pouvez également appuyer et maintenir l'en-tête de la barre latérale pour activer le mode d'édition.", + "header": "Modifier l'ordre et masquer les éléments de la barre latérale" + }, "dashboard": { "description": "Choisissez un tableau de bord par défaut pour cet appareil.", "dropdown_label": "Tableau de bord", @@ -2905,6 +2996,7 @@ "confirm_delete": "Êtes-vous sûr de vouloir supprimer le jeton d'accès de {name} ?", "create": "Créer un jeton", "create_failed": "Impossible de créer le jeton d'accès.", + "created": "Créé le {date}", "created_at": "Créé le {date}", "delete_failed": "Impossible de supprimer le jeton d'accès.", "description": "Créez des jetons d'accès de longue durée pour permettre à vos scripts d'interagir avec votre instance de Home Assistant. Chaque jeton sera valable 10 ans à compter de sa création. Les jetons d'accès longue durée suivants sont actuellement actifs.", @@ -2912,6 +3004,7 @@ "header": "Jetons d'accès de longue durée", "last_used": "Dernière utilisation le {date} à partir de {location}", "learn_auth_requests": "Apprenez comment faire des demandes authentifiées.", + "name": "Nom", "not_used": "N'a jamais été utilisé", "prompt_copy_token": "Copiez votre jeton d'accès. Il ne sera plus affiché à nouveau.", "prompt_name": "Nom ?" @@ -2978,6 +3071,7 @@ } }, "sidebar": { + "done": "Terminé", "external_app_configuration": "Configuration de l'application", "sidebar_toggle": "Activer la barre latérale" } diff --git a/translations/frontend/fy.json b/translations/frontend/fy.json index 1f8841d202..918eb995c3 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 9e26dfeeb6..15018eec65 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/hu.json b/translations/frontend/hu.json index 6f4ed3ee3c..0b50127989 100644 --- a/translations/frontend/hu.json +++ b/translations/frontend/hu.json @@ -419,6 +419,8 @@ "unlock": "Kinyit" }, "media_player": { + "media_next_track": "Következő", + "media_previous_track": "Előző", "sound_mode": "Hangzás", "source": "Forrás", "text_to_speak": "Beszéd szövege" @@ -554,6 +556,20 @@ "loading_history": "Állapot előzmények betöltése...", "no_history_found": "Nem található előzmény." }, + "media-browser": { + "choose-source": "Forrás kiválasztása", + "content-type": { + "album": "Album", + "artist": "Előadó", + "library": "Könyvtár", + "playlist": "Lejátszási lista", + "server": "Szerver" + }, + "no_items": "Nincsenek elemek", + "pick-media": "Média kiválasztása", + "play": "Lejátszás", + "play-media": "Média lejátszása" + }, "related-items": { "area": "Terület", "automation": "A következő automatizálások része", @@ -574,6 +590,7 @@ "week": "{count} {count, plural,\n one {héttel}\n other {héttel}\n}" }, "future": "{time} később", + "just_now": "Éppen most", "never": "Soha", "past": "{time} ezelőtt" }, @@ -656,6 +673,9 @@ "required_error_msg": "Ez a mező kötelező", "yaml_not_editable": "Ennek az entitásnak a beállításai nem szerkeszthetők a felhasználói felületről. Csak a felhasználói felületről beállított entitások konfigurálhatók a felhasználói felületről." }, + "image_cropper": { + "crop": "Kivágás" + }, "more_info_control": { "dismiss": "Párbeszédpanel elvetése", "edit": "Entitás szerkesztése", @@ -798,7 +818,7 @@ "confirmation_text": "Minden ebben a területben lévő eszköz hozzárendelés nélküli lesz.", "confirmation_title": "Biztosan törölni szeretnéd ezt a területet?" }, - "description": "Az összes otthoni terület áttekintése", + "description": "Otthoni területek kezelése", "editor": { "area_id": "Terület ID", "create": "Létrehozás", @@ -820,7 +840,7 @@ }, "automation": { "caption": "Automatizálások", - "description": "Automatizálások létrehozása és szerkesztése", + "description": "Automatizálások kezelése", "editor": { "actions": { "add": "Művelet hozzáadása", @@ -852,6 +872,15 @@ "label": "Esemény meghívása", "service_data": "Szolgáltatás adatai" }, + "repeat": { + "label": "Ismétlés", + "type_select": "Ismétlés típusa", + "type": { + "count": { + "label": "Számláló" + } + } + }, "scene": { "label": "Jelenet aktiválása" }, @@ -865,7 +894,7 @@ "wait_template": "Várakozási sablon" } }, - "unsupported_action": "Nem támogatott művelet: {action}" + "unsupported_action": "Nincs felhasználói felület támogatás a művelethez: {action}" }, "alias": "Név", "conditions": { @@ -931,7 +960,7 @@ "zone": "Zóna" } }, - "unsupported_condition": "Nem támogatott feltétel: {condition}" + "unsupported_condition": "Nincs felhasználói felület támogatás a feltételhez: {condition}" }, "default_name": "Új Automatizálás", "description": { @@ -1049,7 +1078,7 @@ "zone": "Zóna" } }, - "unsupported_platform": "Nem támogatott platform: {platform}" + "unsupported_platform": "Nincs felhasználói felület támogatás a platformhoz: {platform}" }, "unsaved_confirm": "Vannak nem mentett módosítások. Biztos, hogy elhagyod az oldalt?" }, @@ -1138,9 +1167,11 @@ }, "alexa": { "banner": "A feltárt entitások szerkesztése funkció le lett tiltva ezen a felületen, mert entitásszűrőket állítottál be a configuration.yaml fájlban.", + "dont_expose_entity": "Entitás feltárásának törlése", "expose": "Feltárás Alexának", + "expose_entity": "Entitás feltárása", "exposed_entities": "Feltárt entitások", - "not_exposed_entities": "Nem feltárt entitások", + "not_exposed_entities": "Feltáratlan entitások", "title": "Alexa" }, "caption": "Home Assistant Felhő", @@ -1177,9 +1208,11 @@ "google": { "banner": "A feltárt entitások szerkesztése funkció le lett tiltva ezen a felületen, mert entitásszűrőket állítottál be a configuration.yaml fájlban.", "disable_2FA": "Kétfaktoros hitelesítés letiltása", + "dont_expose_entity": "Entitás feltárásának törlése", "expose": "Feltárás a Google Asszisztensnek", + "expose_entity": "Entitás feltárása", "exposed_entities": "Feltárt entitások", - "not_exposed_entities": "Nem feltárt entitások", + "not_exposed_entities": "Feltáratlan entitások", "sync_to_google": "A változások szinkronizálása a Google-lal.", "title": "Google Asszisztens" }, @@ -1340,7 +1373,7 @@ }, "entities": { "caption": "Entitások", - "description": "Az összes ismert entitás áttekintése", + "description": "Ismert entitások kezelése", "picker": { "disable_selected": { "button": "Kiválasztottak letiltása", @@ -1392,7 +1425,7 @@ "header": "Home Assistant beállítása", "helpers": { "caption": "Segítők", - "description": "Automatizálások létrehozását segítő elemek", + "description": "Automatizálások létrehozását segítő elemek kezelése", "dialog": { "add_helper": "Segítő hozzáadása", "add_platform": "{platform} hozzáadása", @@ -1420,7 +1453,7 @@ "built_using": "Buildelve:", "caption": "Infó", "custom_uis": "Egyéni felhasználói felületek:", - "description": "Információ a Home Assistant telepítésedről", + "description": "Telepítési információ megtekintése a Home Assistant-ról", "developed_by": "Egy csomó fantasztikus ember által kifejlesztve.", "documentation": "Dokumentáció", "frontend": "frontend-ui", @@ -1459,6 +1492,8 @@ "no_device": "Eszköz nélküli entitások", "no_devices": "Ez az integráció nem rendelkezik eszközökkel.", "options": "Opciók", + "reload": "Újratöltés", + "reload_confirm": "Az integráció újra lett töltve", "rename": "Átnevezés", "restart_confirm": "Indítsd újra a Home Assistant-ot az integráció törlésének befejezéséhez", "settings_button": "{integration} beállításainak szerkesztése", @@ -1483,7 +1518,7 @@ }, "configure": "Beállítás", "configured": "Konfigurálva", - "description": "Integrációk kezelése és beállítása", + "description": "Integrációk kezelése", "details": "Integráció részletei", "discovered": "Felfedezett", "home_assistant_website": "Home Assistant weboldal", @@ -1511,7 +1546,7 @@ "rename_input_label": "Bejegyzés neve", "search": "Integrációk keresése" }, - "introduction": "Itt a komponenseket és a Home Assistant szervert lehet beállítani. Még nem lehet mindent a felületről, de dolgozunk rajta.", + "introduction": "Ebben a nézetben lehetőség van a komponensek és a Home Assistant beállítására. Még nem lehet mindent a felületről, de dolgozunk rajta.", "logs": { "caption": "Napló", "clear": "Törlés", @@ -1567,7 +1602,7 @@ "open": "Megnyitás" } }, - "description": "Lovelace irányítópultjainak konfigurálása", + "description": "Lovelace irányítópultok kezelése", "resources": { "cant_edit_yaml": "A Lovelace-t YAML módban használod, ezért az erőforrásokat nem kezelheted a felhasználói felületről. Kezeld őket a configuration.yaml fájlban.", "caption": "Erőforrások", @@ -1645,7 +1680,7 @@ "scene": { "activated": "Aktivált jelenet: {name}.", "caption": "Jelenetek", - "description": "Jelenetek létrehozása és szerkesztése", + "description": "Jelenetek kezelése", "editor": { "default_name": "Új jelenet", "devices": { @@ -1689,7 +1724,7 @@ }, "script": { "caption": "Szkriptek", - "description": "Szkriptek létrehozása és szerkesztése", + "description": "Szkriptek kezelése", "editor": { "alias": "Név", "default_name": "Új Szkript", @@ -1738,7 +1773,7 @@ "reloading": { "automation": "Automatizálások újratöltése", "core": "Lokáció és testreszabások újratöltése", - "group": "Csoportok újratöltése", + "group": "Csoportok, csoport entitások és értesítési szolgáltatások újratöltése", "heading": "YAML konfiguráció újratöltése", "input_boolean": "Bemeneti logikai változók újratöltése", "input_datetime": "Időpont bemenetek újratöltése", @@ -1768,6 +1803,9 @@ } } }, + "tags": { + "never_scanned": "Sosem volt szkennelve" + }, "users": { "add_user": { "caption": "Felhasználó hozzáadása", @@ -1871,7 +1909,7 @@ "create_group": "Zigbee Home Automation - Csoport létrehozása", "create_group_details": "Add meg a szükséges adatokat egy új zigbee csoport létrehozásához", "creating_group": "Csoport létrehozása", - "description": "Zigbee csoportok létrehozása és módosítása", + "description": "Zigbee csoportok kezelése", "group_details": "Itt van minden részlet a kiválasztott zigbee csoportról.", "group_id": "Csoport ID", "group_info": "Csoportinformációk", @@ -2143,6 +2181,9 @@ "description": "A Gomb kártya gombok hozzáadását teszi lehetővé feladatok végrehajtásához.", "name": "Gomb" }, + "calendar": { + "name": "Naptár" + }, "conditional": { "card": "Kártya", "change_type": "Típus módosítása", @@ -2319,7 +2360,7 @@ "show_code_editor": "Kódszerkesztő megjelenítése", "show_visual_editor": "Vizuális szerkesztő megjelenítése", "toggle_editor": "Szerkesztő", - "typed_header": "{típusú} Kártya Konfiguráció", + "typed_header": "{type} Kártya Konfiguráció", "unsaved_changes": "Vannak nem mentett módosítások" }, "edit_lovelace": { @@ -2396,7 +2437,7 @@ }, "menu": { "close": "Bezárás", - "configure_ui": "Felhasználói felület konfigurálása", + "configure_ui": "Irányítópult szerkesztése", "exit_edit_mode": "Kilépés a felhasználói felület szerkesztési módból", "help": "Súgó", "refresh": "Frissítés", @@ -2589,6 +2630,7 @@ "intro": "Hello {name}, üdvözöl a Home Assistant. Hogyan szeretnéd elnevezni az otthonodat?", "intro_location": "Szeretnénk tudni, hogy hol élsz. Ez segít az információk megjelenítésében és a nap alapú automatizálások beállításában. Ez az adat soha nem lesz megosztva a hálózatodon kívül.", "intro_location_detect": "Segíthetünk neked kitölteni ezt az információt egy külső szolgáltatás egyszeri lekérdezésével.", + "location_name": "A Home Assistant rendszered neve", "location_name_default": "Otthon" }, "integration": { @@ -2659,7 +2701,7 @@ "learn_auth_requests": "Tudj meg többet a hitelesített kérelmek létrehozásáról.", "not_used": "Sosem használt", "prompt_copy_token": "Most másold ki a hozzáférési tokened! Erre később nem lesz lehetőséged.", - "prompt_name": "Név?" + "prompt_name": "Adj nevet a tokennek" }, "mfa_setup": { "close": "Bezárás", diff --git a/translations/frontend/it.json b/translations/frontend/it.json index 13c8d8aeaf..c7976293f9 100644 --- a/translations/frontend/it.json +++ b/translations/frontend/it.json @@ -19,6 +19,7 @@ "logbook": "Registro", "mailbox": "Posta", "map": "Mappa", + "media_browser": "Browser multimediale", "profile": "Profilo", "shopping_list": "Lista della spesa", "states": "Panoramica" @@ -505,6 +506,7 @@ "back": "Indietro", "cancel": "Annulla", "close": "Chiudi", + "continue": "Continua", "delete": "Elimina", "error_required": "Necessario", "loading": "Caricamento", @@ -551,6 +553,10 @@ "toggle": "Azionare" }, "entity": { + "entity-attribute-picker": { + "attribute": "Attributo", + "show_attributes": "Mostra attributi" + }, "entity-picker": { "clear": "Cancella", "entity": "Entità", @@ -561,7 +567,12 @@ "loading_history": "Caricamento storico...", "no_history_found": "Nessuno storico trovato." }, + "logbook": { + "entries_not_found": "Non sono state trovate voci nel registro." + }, "media-browser": { + "audio_not_supported": "Il tuo browser non supporta l'elemento audio.", + "choose_player": "Scegli il lettore", "choose-source": "Scegli origine", "content-type": { "album": "Album", @@ -570,12 +581,17 @@ "playlist": "Elenco di riproduzione", "server": "Server" }, + "media_browsing_error": "Errore di navigazione multimediale", + "media_not_supported": "Il Browser Media Player non supporta questo tipo di file multimediali", + "media_player": "Lettore multimediale", "media-player-browser": "Lettore multimediale", "no_items": "Nessun elemento", "pick": "Scegli", "pick-media": "Seleziona file multimediali", "play": "Riproduci", - "play-media": "Riproduci file multimediali" + "play-media": "Riproduci file multimediali", + "video_not_supported": "Il tuo browser non supporta l'elemento video.", + "web-browser": "Web Browser" }, "picture-upload": { "label": "Immagine", @@ -689,8 +705,11 @@ "crop": "Ritaglia" }, "more_info_control": { + "controls": "Controlli", + "details": "Dettagli", "dismiss": "Chiudi finestra di dialogo", "edit": "Modifica entità", + "history": "Storico", "person": { "create_zone": "Crea zona dalla posizione corrente" }, @@ -918,7 +937,13 @@ "label": "Chiama servizio", "service_data": "Dati del servizio" }, + "wait_for_trigger": { + "continue_timeout": "Continua al timeout", + "label": "In attesa dell'attivazione", + "timeout": "Timeout (opzionale)" + }, "wait_template": { + "continue_timeout": "Continua al timeout", "label": "Attendere", "timeout": "Timeout (opzionale)", "wait_template": "Modello di attesa" @@ -982,7 +1007,9 @@ "time": { "after": "Dopo", "before": "Prima", - "label": "Tempo" + "label": "Tempo", + "type_input": "Valore di un aiutante data/ora", + "type_value": "Tempo fisso" }, "zone": { "entity": "Entità con posizione", @@ -1070,6 +1097,7 @@ "value_template": "Valore modello (opzionale)" }, "state": { + "attribute": "Attributo (opzionale)", "for": "Per", "from": "Da", "label": "Stato", @@ -1096,8 +1124,10 @@ "seconds": "Secondi" }, "time": { - "at": "Alle", - "label": "Ora" + "at": "Al tempo", + "label": "Ora", + "type_input": "Valore di un aiutante data/ora", + "type_value": "Tempo fisso" }, "webhook": { "label": "Webhook", @@ -1120,6 +1150,8 @@ "add_automation": "Aggiungi Automazione", "delete_automation": "Cancellare l'automazione", "delete_confirm": "Sei sicuro di voler eliminare questa automazione?", + "duplicate": "Duplica", + "duplicate_automation": "Duplica automazione", "edit_automation": "Modifica automazione", "header": "Editor di Automazione", "headers": { @@ -1520,6 +1552,7 @@ }, "integrations": { "add_integration": "Aggiungi integrazione", + "attention": "Attenzione richiesta", "caption": "Integrazioni", "config_entry": { "area": "In {area}", @@ -1543,6 +1576,7 @@ "reload_restart_confirm": "Riavvia Home Assistant per completare il ricaricamento di questa integrazione", "rename": "Rinomina", "restart_confirm": "Riavvia Home Assistant per completare la rimozione di questa integrazione", + "services": "{count} {count, plural,\n one {service}\n other {services}\n}", "settings_button": "Modificare le impostazioni per {integration}.", "system_options": "Opzioni di sistema", "system_options_button": "Opzioni di sistema per {integration}", @@ -1589,6 +1623,7 @@ "none_found_detail": "Modifica i criteri di ricerca.", "note_about_integrations": "Non tutte le integrazioni possono ancora essere configurate tramite l'interfaccia utente.", "note_about_website_reference": "Ulteriori informazioni sono disponibili su ", + "reconfigure": "Riconfigurare", "rename_dialog": "Modifica il nome di questa voce di configurazione", "rename_input_label": "Nome della voce", "search": "Cerca integrazioni" @@ -1762,8 +1797,21 @@ "versions": "Recupero di informazioni sulle versioni del firmware e della classe di comando", "wakeup": "Configurazione del supporto per code di riattivazione e messaggi " }, + "node": { + "button": "Dettagli del nodo", + "not_found": "Nodo non trovato" + }, + "nodes_table": { + "failed": "Fallito", + "id": "ID", + "manufacturer": "Produttore", + "model": "Modello", + "query_stage": "Fase di richiesta", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { "battery_note": "Se il nodo è alimentato a batteria, assicurarsi di riattivarlo prima di procedere", + "button": "Aggiorna nodo", "complete": "Aggiornamento del nodo completato", "description": "In questo modo si dirà a OpenZWave di re-interrogare un nodo e aggiornare le classi di comando, le funzionalità e i valori del nodo.", "node_status": "Stato del nodo", @@ -1912,7 +1960,7 @@ "filter": "Ricaricare le entità filtro", "generic": "Ricaricare le entità delle generiche telecamere IP", "generic_thermostat": "Ricaricare le entità termostati generiche", - "group": "Ricarica i Gruppi", + "group": "Ricarica gruppi, entità di gruppo e servizi di notifica", "heading": "Ricarica Configurazione YAML", "history_stats": "Ricarica le entità delle statistiche della cronologia", "homekit": "Ricarica HomeKit", @@ -1923,12 +1971,17 @@ "input_text": "Ricarica input testuali", "introduction": "Alcune parti di Home Assistant possono essere ricaricate senza richiedere un riavvio. Premendo su Ricarica si rimuoverà la loro Configurazione YAML attuale e si caricherà la versione aggiornata.", "min_max": "Ricaricare le entità min/max", + "mqtt": "Ricarica le entità mqtt", "person": "Ricarica le persone", "ping": "Ricarica le entità del sensore binario ping", - "rest": "Ricarica le entità della piattaforma rest", + "reload": "Ricarica {domain}", + "rest": "Ricarica le entità di rest e i servizi di notifica", + "rpi_gpio": "Ricarica le entità GPIO di Raspberry Pi ", "scene": "Ricarica le Scene", "script": "Ricarica gli Script", + "smtp": "Ricarica i servizi di notifica smtp", "statistics": "Ricaricare le entità statistiche", + "telegram": "Ricarica i servizi di notifica telegram", "template": "Ricaricare le entità modello", "trend": "Ricaricare le entità di tendenza", "universal": "Ricarica le entità del lettore multimediale universale", @@ -2017,7 +2070,7 @@ "system": "Sistema" } }, - "users_privileges_note": "Il gruppo di utenti è in fase di elaborazione. L'utente non sarà in grado di amministrare l'istanza tramite l'interfaccia utente. Stiamo ancora verificando tutti gli endpoint dell'API di gestione per garantire che limitino correttamente l'accesso agli amministratori." + "users_privileges_note": "La funzionalità del gruppo di utenti è in fase di elaborazione. L'utente non sarà in grado di amministrare l'istanza tramite l'Interfaccia Utente. Stiamo ancora verificando tutti gli endpoint dell'API di gestione per garantire che limitino correttamente l'accesso solo agli amministratori." }, "zha": { "add_device_page": { @@ -2549,7 +2602,11 @@ } }, "cardpicker": { + "by_card": "Per Scheda", + "by_entity": "Per Entità", "custom_card": "Personalizzato", + "domain": "Dominio", + "entity": "Entità", "no_description": "Nessuna descrizione disponibile." }, "edit_card": { @@ -2563,6 +2620,7 @@ "options": "Altre opzioni", "pick_card": "Quale scheda vorresti aggiungere?", "pick_card_view_title": "Quale scheda vorresti aggiungere alla tua vista {name}?", + "search_cards": "Schede di ricerca", "show_code_editor": "Mostra Editor di Codice", "show_visual_editor": "Mostra Editor Visivo", "toggle_editor": "Attiva / disattiva l'editor", diff --git a/translations/frontend/nb.json b/translations/frontend/nb.json index 235f6930b8..dcb6211a8a 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,8 @@ "back": "Tilbake", "cancel": "Avbryt", "close": "Lukk", + "continue": "Fortsette", + "copied": "Kopiert", "delete": "Slett", "error_required": "Nødvendig", "loading": "Laster", @@ -535,6 +554,10 @@ "toggle": "Veksle" }, "entity": { + "entity-attribute-picker": { + "attribute": "Attributt", + "show_attributes": "Vis attributter" + }, "entity-picker": { "clear": "Tøm", "entity": "Entitet", @@ -545,8 +568,35 @@ "loading_history": "Laster statushistorikk...", "no_history_found": "Ingen statushistorikk funnet." }, + "logbook": { + "entries_not_found": "Ingen loggbokoppføringer funnet." + }, "media-browser": { + "audio_not_supported": "Nettleseren din støtter ikke lydelementet.", + "choose_player": "Velg spiller", "choose-source": "Velg kilde", + "class": { + "album": "Album", + "app": "App", + "artist": "Artist", + "channel": "Kanal", + "composer": "Komponist", + "contributing_artist": "Medvirkende artist", + "directory": "Bibliotek", + "episode": "Episode", + "game": "Spill", + "genre": "Sjanger", + "image": "Bilde", + "movie": "Film", + "music": "Musikk", + "playlist": "Spilleliste", + "podcast": "Podcast", + "season": "Sesong", + "track": "Spor", + "tv_show": "TV Serie", + "url": "Url", + "video": "Video" + }, "content-type": { "album": "Album", "artist": "Artist", @@ -554,12 +604,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", @@ -596,12 +651,12 @@ "dialogs": { "config_entry_system_options": { "enable_new_entities_description": "Hvis den er deaktivert, blir ikke nyoppdagede entiteter for {integration} automatisk lagt til i Home Assistant.", - "enable_new_entities_label": "Aktiver enheter som nylig er lagt til.", + "enable_new_entities_label": "Aktiver entiteter som nylig er lagt til.", "title": "Systemalternativer for {integration}", "update": "Oppdater" }, "domain_toggler": { - "reset_entities": "Tilbakestill enheter", + "reset_entities": "Tilbakestill entiteter", "title": "Veksle domener" }, "entity_registry": { @@ -617,7 +672,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 +683,8 @@ "generic": { "cancel": "Avbryt", "close": "Lukk", - "default_confirmation_title": "Er du sikker?" + "default_confirmation_title": "Er du sikker?", + "ok": "" }, "helper_settings": { "generic": { @@ -672,8 +728,11 @@ "crop": "Beskjære" }, "more_info_control": { + "controls": "Kontroller", + "details": "Detaljer", "dismiss": "Avvis dialogboksen", "edit": "Redigér entitet", + "history": "Historie", "person": { "create_zone": "Opprett sone fra gjeldende plassering" }, @@ -702,14 +761,17 @@ "commands": "Støvsugerkommandoer:", "fan_speed": "Viftehastighet", "locate": "Lokaliser", + "pause": "", "return_home": "Returner hjem", + "start": "", "start_pause": "Start / Pause", + "status": "", "stop": "Stopp" } }, "mqtt_device_debug_info": { "deserialize": "Forsøk å analysere MQTT-meldinger som JSON", - "entities": "Enheter", + "entities": "Entiteter", "no_entities": "Ingen entiteter", "no_triggers": "Ingen utløsere", "payload_display": "Nyttelastvisning", @@ -750,6 +812,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 +941,7 @@ "type_select": "Gjenta type", "type": { "count": { - "label": "Telle" + "label": "Antall" }, "until": { "conditions": "Inntil forholdene", @@ -897,7 +960,13 @@ "label": "Tilkall tjeneste", "service_data": "Tjenestedata" }, + "wait_for_trigger": { + "continue_timeout": "Fortsett ved tidsavbrudd", + "label": "Vent på utløser", + "timeout": "Tidsavbrudd" + }, "wait_template": { + "continue_timeout": "Fortsett ved tidsavbrudd", "label": "Vent", "timeout": "Tidsavbrudd (valgfritt)", "wait_template": "Ventemal" @@ -961,7 +1030,9 @@ "time": { "after": "Etter", "before": "Før", - "label": "Tid" + "label": "Tid", + "type_input": "Verdien til en tid/dato hjelper", + "type_value": "Bestemt tid" }, "zone": { "entity": "Entitet med posisjon", @@ -1033,7 +1104,9 @@ }, "homeassistant": { "event": "Hendelse:", - "shutdown": "Slå av" + "label": "", + "shutdown": "Slå av", + "start": "" }, "mqtt": { "label": "MQTT", @@ -1047,6 +1120,8 @@ "value_template": "Verdi fra mal (valgfritt)" }, "state": { + "attribute": "Attributt (valgfritt)", + "for": "", "from": "Fra", "label": "Tilstand", "to": "Til" @@ -1072,8 +1147,14 @@ "seconds": "Sekunder" }, "time": { - "at": "Klokken", - "label": "Tid" + "at": "På tidspunktet", + "label": "Tid", + "type_input": "Verdien til en tid/dato hjelper", + "type_value": "Bestemt tid" + }, + "webhook": { + "label": "", + "webhook_id": "" }, "zone": { "enter": "Ankommer", @@ -1092,6 +1173,8 @@ "add_automation": "Legg til automasjon", "delete_automation": "Slett automasjon", "delete_confirm": "Er du sikker på at du vil slette denne automasjonen?", + "duplicate": "Dupliser", + "duplicate_automation": "Dupliser automasjon", "edit_automation": "Rediger automasjon", "header": "Automasjonsredigering", "headers": { @@ -1118,7 +1201,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 +1220,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 entitetene dine med Google, be Google 'Hei Google, synkroniser enhetene mine' for å synkronisere entitetene 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,21 +1250,24 @@ "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": { "banner": "Redigere hvilke entiteter som vises via dette grensesnittet er deaktivert fordi du har konfigurert entitetsfilter i configuration.yaml.", - "dont_expose_entity": "Ikke eksponer enheten", + "dont_expose_entity": "Ikke eksponer entiteten", "expose": "Eksponer til Alexa", - "expose_entity": "Vis enhet", + "expose_entity": "Eksponer entitet", "exposed": "{selected} eksponert", "exposed_entities": "Eksponerte entiteter", "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 entiteter", + "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", @@ -1213,16 +1301,17 @@ "google": { "banner": "Redigere hvilke entiteter som vises via dette grensesnittet er deaktivert fordi du har konfigurert entitetsfilter i configuration.yaml.", "disable_2FA": "Deaktiver totrinnsbekreftelse", - "dont_expose_entity": "Ikke eksponer enheten", + "dont_expose_entity": "Ikke eksponer entiteten", "expose": "Eksponer til Google Assistant", - "expose_entity": "Vis enhet", + "expose_entity": "Eksponer entiteten", "exposed": "{selected} eksponert", "exposed_entities": "Eksponerte entiteter", "follow_domain": "Følg domenet", "manage_domains": "Administrer domener", "not_exposed": "{selected} ikke eksponert", - "not_exposed_entities": "Ikke eksponerte enheter", - "sync_to_google": "Synkroniserer endringer til Google." + "not_exposed_entities": "Ikke eksponerte entiteter", + "sync_to_google": "Synkroniserer endringer til Google.", + "title": "" }, "login": { "alert_email_confirm_necessary": "Du må bekrefte e-posten din før du logger inn.", @@ -1321,7 +1410,7 @@ } }, "devices": { - "add_prompt": "Ingen {navn} er lagt til ved hjelp av denne enheten ennå. Du kan legge til en ved å klikke på + knappen ovenfor.", + "add_prompt": "Ingen {name} er lagt til ved hjelp av denne enheten ennå. Du kan legge til en ved å klikke på + knappen ovenfor.", "automation": { "actions": { "caption": "Når noe er utløst..." @@ -1341,7 +1430,7 @@ "caption": "Enheter", "confirm_delete": "Er du sikker på at du vil slette denne enheten?", "confirm_rename_entity_ids": "Vil du også endre navn på entitets-ID-en for entitetene dine?", - "confirm_rename_entity_ids_warning": "Dette vil ikke endre noen konfigurasjon (som automatiseringer, skript, scener, Lovelace) som bruker disse enhetene, må du oppdatere dem selv.", + "confirm_rename_entity_ids_warning": "Dette vil ikke endre noen konfigurasjon (som automatiseringer, skript, scener, Lovelace) som bruker disse entitetene, du må oppdatere dem selv.", "data_table": { "area": "Område", "battery": "Batteri", @@ -1359,9 +1448,9 @@ "entities": { "add_entities_lovelace": "Legg til i Lovelace", "disabled_entities": "+{count} {count, plural,\n one {deaktivert entitet}\n other {deaktiverte entiteter}\n}", - "entities": "Enheter", + "entities": "Entiteter", "hide_disabled": "Skjul deaktiverte", - "none": "Denne enheten har ingen enheter" + "none": "Denne enheten har ingen entiteter" }, "name": "Navn", "no_devices": "Ingen enheter", @@ -1396,15 +1485,17 @@ "confirm_title": "Vil du aktivere {number} enheter?" }, "filter": { + "filter": "", "show_disabled": "Vis deaktiverte entiteter", "show_readonly": "Vis skrivebeskyttede enheter", - "show_unavailable": "Vis utilgjengelige enheter" + "show_unavailable": "Vis utilgjengelige entiteter" }, "header": "Entiteter", "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 +1510,7 @@ "selected": "{number} valgte", "status": { "disabled": "Deaktivert", + "ok": "", "readonly": "Skrivebeskyttet", "restored": "Gjennopprettet", "unavailable": "Utilgjengelig" @@ -1443,7 +1535,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 +1544,7 @@ "input_boolean": "Veksle", "input_datetime": "Dato og/eller klokkeslett", "input_number": "Nummer", + "input_select": "", "input_text": "Tekst" } }, @@ -1463,6 +1557,7 @@ "documentation": "Dokumentasjon", "frontend": "frontend", "frontend_version": "Brukergrensesnittet-versjon: {version} - {type}", + "home_assistant_logo": "", "icons_by": "Ikoner fra", "integrations": "Integrasjoner", "issues": "Problemer", @@ -1480,6 +1575,7 @@ }, "integrations": { "add_integration": "Legg til integrasjon", + "attention": "Oppmerksomhet kreves", "caption": "Integrasjoner", "config_entry": { "area": "I {area}", @@ -1503,6 +1599,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}", @@ -1549,6 +1646,7 @@ "none_found_detail": "Juster dine søkekriterier", "note_about_integrations": "Ikke alle integrasjoner kan konfigureres via brukergrensesnittet ennå.", "note_about_website_reference": "Flere er tilgjengelige på", + "reconfigure": "Konfigurer på nytt", "rename_dialog": "Redigere navnet på denne config-oppføringen", "rename_input_label": "Navn på oppføring", "search": "Søk i integrasjoner" @@ -1593,6 +1691,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 +1719,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å?", @@ -1711,13 +1815,26 @@ "nodeplusinfo": "Innhenting av Z-Wave + informasjon fra noden", "probe": "Kontrollere om noden er våken/levende", "protocolinfo": "Få grunnleggende Z-Wave-funksjoner i denne noden fra kontrolleren", - "session": "\nFå sjelden endrede verdier fra noden", + "session": "Henter verdier fra noden som sjeldent oppdateres", "static": "Innhenting av statiske verdier fra enheten", "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", @@ -1780,7 +1897,7 @@ "delete": "Slett entitet", "device_entities": "Hvis du legger til en entitet som tilhører en enhet, vil enheten også bli lagt til.", "header": "Entiteter", - "introduction": "Enheter som ikke tilhører en enhet kan angis her.", + "introduction": "Entiteter som ikke tilhører en enhet kan angis her.", "without_device": "Entiteter uten enhet" }, "icon": "Ikon", @@ -1866,7 +1983,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,14 +1994,19 @@ "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-entiteter 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 REST entiteter og varslingstjenester på nytt", + "rpi_gpio": "Last inn Raspberry Pi GPIO-entiteter 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", + "trend": "Last inn trend entiteter på nytt", "universal": "Laste inn universelle mediespiller entiteter på nytt", "zone": "Last inn soner på nytt" }, @@ -1946,6 +2068,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 +2089,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 +2110,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 +2131,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 +2218,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 +2264,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 +2281,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 +2348,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", @@ -2248,9 +2375,14 @@ "title": "Tilstander" }, "templates": { + "all_listeners": "Denne malen lytter etter alle tilstandsfornede hendelser.", "description": "Maler blir rendret ved hjelp av Jinja2-malmotoren med noen spesifikke utvidelser for Home Assistant.", + "domain": "Domene", "editor": "Maleditor", + "entity": "Entitet", "jinja_documentation": "Jinja2 mal dokumentasjon", + "listeners": "Denne malen lytter etter følgende tilstandsfor endrede hendelser:", + "no_listeners": "Denne malen lytter ikke etter eventuelle tilstandsfornedne hendelser og oppdateres ikke automatisk.", "reset": "Tilbakestill til demomal", "template_extensions": "Mal utvidelser for Home Assistant", "title": "Mal", @@ -2297,6 +2429,7 @@ }, "picture-elements": { "call_service": "Tilkall tjeneste {name}", + "hold": "", "more_info": "Vis mer info: {name}", "navigate_to": "Naviger til {location}", "tap": "Trykk:", @@ -2362,7 +2495,7 @@ "name": "Entitetsfilter" }, "entity": { - "description": "Enhetskortet gir deg en rask oversikt over enhetens tilstand.", + "description": "Entitetskortet gir deg en rask oversikt over entitetens tilstand.", "name": "Entitet" }, "gauge": { @@ -2391,6 +2524,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 +2538,8 @@ "tap_action": "Trykk handling", "theme": "Tema", "title": "Tittel", - "unit": "Betegnelse" + "unit": "Betegnelse", + "url": "" }, "glance": { "columns": "Kolonner", @@ -2420,7 +2555,7 @@ "name": "Horisontal Stabel" }, "humidifier": { - "description": "Luftfukter kortet gir kontroll over luftfukter enheten din. Lar deg endre fuktigheten og modusen til enheten.", + "description": "Luftfukter kortet gir kontroll over luftfukter entiteten din. Lar deg endre fuktigheten og modusen til entiteten.", "name": "Luftfukter" }, "iframe": { @@ -2442,7 +2577,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 +2607,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 +2630,11 @@ } }, "cardpicker": { + "by_card": "Med kort", + "by_entity": "Etter entitet", "custom_card": "Tilpasset", + "domain": "Domene", + "entity": "Entitet", "no_description": "Ingen beskrivelse tilgjengelig." }, "edit_card": { @@ -2507,6 +2648,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", @@ -2606,14 +2748,14 @@ "refresh_header": "Vil du oppfriske?" }, "unused_entities": { - "available_entities": "Dette er enheter som du har tilgjengelig, men som ikke er i Lovelace brukergrensesnittet ennå.", + "available_entities": "Dette er entitene som du har tilgjengelig, men som ikke er i Lovelace brukergrensesnittet ennå.", "domain": "Domene", "entity": "Entitet", "entity_id": "Entitets-ID", "last_changed": "Sist endret", "no_data": "Ingen ubrukte entiteter funnet", - "search": "Søk etter enheter", - "select_to_add": "Velg enhetene du vil legge til et kort, og klikk deretter på knappen Legg til kort.", + "search": "Søk etter entiteter", + "select_to_add": "Velg entitene du vil legge til et kort, og klikk deretter på knappen Legg til kort.", "title": "Ubrukte entiteter" }, "views": { @@ -2774,6 +2916,7 @@ "upstairs": "Oppe" }, "unit": { + "minutes_abbr": "", "watching": "Ser på" } } @@ -2826,8 +2969,15 @@ "submit": "Send inn" }, "current_user": "Du er logget inn som {fullName}.", + "customize_sidebar": { + "button": "Redigere", + "description": "Du kan også trykke på og holde nede overskriften på sidefeltet for å aktivere redigeringsmodus.", + "header": "Endre rekkefølgen og skjul elementer fra sidepanelet" + }, "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.", @@ -2846,6 +2996,7 @@ "confirm_delete": "Er du sikker på at du vil slette tilgangstoken for {name}?", "create": "Opprett Token", "create_failed": "Kunne ikke opprette tilgangstoken.", + "created": "Opprettet {date}", "created_at": "Opprettet den {date}", "delete_failed": "Kan ikke slette tilgangstokenet.", "description": "Opprett langvarige tilgangstokener slik at skriptene dine kan samhandle med din Home Assistant instans. Hver token vil være gyldig i 10 år fra opprettelsen. Følgende langvarige tilgangstokener er for tiden aktive.", @@ -2853,9 +3004,10 @@ "header": "Langvarige tilgangstokener", "last_used": "Sist brukt den {date} fra {location}", "learn_auth_requests": "Lær hvordan du lager godkjente forespørsler.", + "name": "Navn", "not_used": "Har aldri blitt brukt", "prompt_copy_token": "Kopier tilgangstoken. Det blir ikke vist igjen.", - "prompt_name": "Navn?" + "prompt_name": "Gi tokenet et navn" }, "mfa_setup": { "close": "Lukk", @@ -2919,6 +3071,7 @@ } }, "sidebar": { + "done": "Ferdig", "external_app_configuration": "Appkonfigurasjon", "sidebar_toggle": "Vis/Skjul sidepanel" } diff --git a/translations/frontend/nl.json b/translations/frontend/nl.json index 8e3ce0ba70..f11bf23dca 100644 --- a/translations/frontend/nl.json +++ b/translations/frontend/nl.json @@ -19,6 +19,7 @@ "logbook": "Logboek", "mailbox": "Postvak", "map": "Kaart", + "media_browser": "Mediabrowser", "profile": "Profiel", "shopping_list": "Boodschappenlijst", "states": "Overzicht" @@ -505,6 +506,8 @@ "back": "Terug", "cancel": "Annuleren", "close": "Sluiten", + "continue": "Ga verder", + "copied": "Gekopieerd", "delete": "Verwijderen", "error_required": "Verplicht", "loading": "Bezig met laden", @@ -551,6 +554,10 @@ "toggle": "Omschakelen" }, "entity": { + "entity-attribute-picker": { + "attribute": "Attribuut", + "show_attributes": "Toon attributen" + }, "entity-picker": { "clear": "Wis", "entity": "Entiteit", @@ -561,8 +568,35 @@ "loading_history": "Geschiedenis laden ...", "no_history_found": "Geen geschiedenis gevonden" }, + "logbook": { + "entries_not_found": "Geen logboekvermeldingen gevonden." + }, "media-browser": { + "audio_not_supported": "Uw browser ondersteunt het audio-element niet.", + "choose_player": "Kies speler", "choose-source": "Kies bron", + "class": { + "album": "Album", + "app": "App", + "artist": "Artiest", + "channel": "Kanaal", + "composer": "Componist", + "contributing_artist": "Bijdragende artiest", + "directory": "Bibliotheek", + "episode": "Aflevering", + "game": "Spel", + "genre": "Genre", + "image": "Afbeelding", + "movie": "Film", + "music": "Muziek", + "playlist": "Afspeellijst", + "podcast": "Podcast", + "season": "Seizoen", + "track": "Nummer", + "tv_show": "TV-programma", + "url": "Url", + "video": "Video" + }, "content-type": { "album": "Album", "artist": "Artiest", @@ -570,12 +604,17 @@ "playlist": "Afspeellijst", "server": "Server" }, + "media_browsing_error": "Fout bij bladeren door media", + "media_not_supported": "De Browser Media Player ondersteunt dit type media niet", + "media_player": "Mediaspeler", "media-player-browser": "Mediaspeler-browser", "no_items": "Niets gevonden", "pick": "Kies", "pick-media": "Kies Media", "play": "Speel", - "play-media": "Media afspelen" + "play-media": "Media afspelen", + "video_not_supported": "Uw browser ondersteunt het video-element niet.", + "web-browser": "Webbrowser" }, "picture-upload": { "label": "Afbeelding", @@ -617,6 +656,7 @@ "update": "Bijwerken" }, "domain_toggler": { + "reset_entities": "Entiteiten opnieuw instellen", "title": "Domeinen in- en uitschakelen" }, "entity_registry": { @@ -688,8 +728,11 @@ "crop": "Snijd bij" }, "more_info_control": { + "controls": "Besturing", + "details": "Details", "dismiss": "Dialoogvenster sluiten", "edit": "Entiteit bewerken", + "history": "Geschiedenis", "person": { "create_zone": "Creëer zone van huidige locatie" }, @@ -917,7 +960,13 @@ "label": "Service aanroepen", "service_data": "Service data" }, + "wait_for_trigger": { + "continue_timeout": "Ga verder op time-out", + "label": "Wacht op trigger", + "timeout": "Time-out (optioneel)" + }, "wait_template": { + "continue_timeout": "Ga verder op time-out", "label": "Wacht", "timeout": "Timeout (optioneel)", "wait_template": "Wachtsjabloon" @@ -981,7 +1030,9 @@ "time": { "after": "Nadat", "before": "Voordat", - "label": "Tijd" + "label": "Tijd", + "type_input": "Waarde van een datum/tijdhelper", + "type_value": "Vaste tijd" }, "zone": { "entity": "Entiteit met locatie", @@ -1023,6 +1074,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 +1104,7 @@ }, "homeassistant": { "event": "Gebeurtenis:", + "label": "", "shutdown": "Afsluiten", "start": "Opstarten" }, @@ -1067,6 +1120,7 @@ "value_template": "Waardesjabloon (optioneel)" }, "state": { + "attribute": "Attribuut (Optioneel)", "for": "Voor", "from": "Van", "label": "Staat", @@ -1093,8 +1147,10 @@ "seconds": "Seconden" }, "time": { - "at": "Om", - "label": "Tijd" + "at": "Op tijd", + "label": "Tijd", + "type_input": "Waarde van een datum/tijdhelper", + "type_value": "Vaste tijd" }, "webhook": { "label": "Webhook", @@ -1117,6 +1173,8 @@ "add_automation": "Automatisering toevoegen", "delete_automation": "Verwijder automatisering", "delete_confirm": "Weet je zeker dat je deze automatisering wilt verwijderen?", + "duplicate": "Dupliceren", + "duplicate_automation": "Dupliceer automatisering", "edit_automation": "Bewerk automatisering", "header": "Automatiseringsbewerker", "headers": { @@ -1198,8 +1256,14 @@ }, "alexa": { "banner": "Het bewerken van de entiteiten die via deze gebruikersinterface worden weergegeven is uitgeschakeld, omdat je entiteitenfilters hebt geconfigureerd in configuration.yaml.", + "dont_expose_entity": "Entiteit niet beschikbaar maken", "expose": "Blootstellen aan Alexa", + "expose_entity": "Enititeit beschikbaar maken", + "exposed": "{selected} blootgesteld", "exposed_entities": "Blootgestelde entiteiten", + "follow_domain": "Domein volgen", + "manage_domains": "Beheer domeinen", + "not_exposed": "{selected} niet blootgesteld", "not_exposed_entities": "Niet blootgestelde entiteiten", "title": "Alexa" }, @@ -1237,8 +1301,14 @@ "google": { "banner": "Het bewerken van de entiteiten die via deze gebruikersinterface worden weergegeven is uitgeschakeld, omdat je entiteitenfilters hebt geconfigureerd in configuration.yaml.", "disable_2FA": "Schakel tweestapsverificatie uit", + "dont_expose_entity": "Entiteit niet beschikbaar maken", "expose": "Blootstellen aan Google Assistant", + "expose_entity": "Enititeit beschikbaar maken", + "exposed": "{selected} blootgesteld", "exposed_entities": "Blootgestelde entiteiten", + "follow_domain": "Domein volgen", + "manage_domains": "Beheer domeinen", + "not_exposed": "{selected} niet blootgesteld", "not_exposed_entities": "Niet blootgestelde entiteiten", "sync_to_google": "Wijzigingen synchroniseren met Google.", "title": "Google Assistant" @@ -1505,6 +1575,7 @@ }, "integrations": { "add_integration": "Integratie toevoegen", + "attention": "Aandacht vereist", "caption": "Integraties", "config_entry": { "area": "In {area}", @@ -1528,6 +1599,7 @@ "reload_restart_confirm": "Start Home Assistant opnieuw om het opnieuw laden van deze integratie te voltooien", "rename": "Naam wijzigen", "restart_confirm": "Herstart Home Assistant om het verwijderen van deze integratie te voltooien", + "services": "{count} {count, plural,\n one {service}\n other {services}\n}", "settings_button": "Instellingen bewerken voor {integration}", "system_options": "Systeeminstellingen", "system_options_button": "Systeeminstellingen voor {integration}", @@ -1574,6 +1646,7 @@ "none_found_detail": "Pas uw zoekcriteria aan.", "note_about_integrations": "Nog niet alle integraties kunnen via de UI worden geconfigureerd.", "note_about_website_reference": "Meer zijn beschikbaar op de ", + "reconfigure": "Herconfigureer", "rename_dialog": "Bewerk de naam van dit configuratie item", "rename_input_label": "Invoernaam", "search": "Zoek integraties" @@ -1747,8 +1820,21 @@ "versions": "Het verkrijgen van informatie over firmware en commandoklasseversies", "wakeup": "Ondersteuning instellen voor waakwachtrijen en berichten" }, + "node": { + "button": "Node details", + "not_found": "Node niet gevonden" + }, + "nodes_table": { + "failed": "Mislukt", + "id": "ID", + "manufacturer": "Fabrikant", + "model": "Model", + "query_stage": "Vraagfase", + "zwave_plus": "Z-Wave Plus" + }, "refresh_node": { "battery_note": "Als het knooppunt op batterijen werkt, moet u het uit de sluimerstand halen voordat u verder gaat", + "button": "Node verversen", "complete": "Knooppunt vernieuwen voltooid", "description": "Dit zal OpenZWave vertellen om een knooppunt opnieuw te ondervragen en de opdrachtklassen, mogelijkheden en waarden van het knooppunt bij te werken.", "node_status": "Knooppuntstatus", @@ -1897,7 +1983,7 @@ "filter": "Herlaad filter-entiteiten", "generic": "Herlaad generieke IP camera entiteiten", "generic_thermostat": "Herlaad generieke thermostaat entiteiten", - "group": "Herlaad groepen", + "group": "Herlaad groepen, groepsentiteiten en meld services", "heading": "Configuratie herladen", "history_stats": "Herlaad historische statistieken entiteiten", "homekit": "Herlaad HomeKit", @@ -1908,12 +1994,17 @@ "input_text": "Herlaad input texts", "introduction": "Sommige delen van Home Assistant kunnen opnieuw worden geladen zonder dat een herstart vereist is. Als je herladen gebruikt, wordt de huidige configuratie leeggemaakt en wordt de nieuwe geladen.", "min_max": "Herlaad min/max entiteiten", + "mqtt": "Herlaad mqtt entiteiten", "person": "Herlaad personen", "ping": "Herlaad ping binaire sensor entiteiten", - "rest": "Herlaad rust-entiteiten", + "reload": "Herlaad {domain}", + "rest": "Laad resterende entiteiten opnieuw en stel services op de hoogte", + "rpi_gpio": "Herlaad Raspberry Pi GPIO-entiteiten", "scene": "Herlaad scenes", "script": "Herlaad scripts", + "smtp": "Herlaad telegram notify services", "statistics": "Herlaad statistische entiteiten", + "telegram": "Herlaad telegram notify services", "template": "Herlaad sjabloon-entiteiten", "trend": "Herlaad trend-entiteiten", "universal": "Herlaad universele mediaspeler entiteiten", @@ -2002,7 +2093,7 @@ "system": "Systeem" } }, - "users_privileges_note": "Gebruikersgroepen zijn nog in ontwikkeling. De gebruiker kan de instantie niet beheren via de interface. We zijn bezig met het controleren van de API-eindpunten toegang voor beheerders." + "users_privileges_note": "Gebruikersgroepen zijn nog in ontwikkeling. De gebruiker kan de instantie niet beheren via de interface. We controleren nog steeds alle beheer API eindpunten om ervoor te zorgen dat ze de toegang tot beheerders correct beperken." }, "zha": { "add_device_page": { @@ -2186,7 +2277,7 @@ "node_to_control": "Knooppunt om te besturen", "nodes": "Knooppunten", "nodes_hint": "Selecteer knooppunt om de opties per knooppunt te bekijken", - "nodes_in_group": "Andere knooppunten in deze groep:", + "nodes_in_group": "Andere nodes in deze groep:", "pooling_intensity": "Polling intensiteit", "protection": "Bescherming", "remove_broadcast": "Broadcast verwijderen", @@ -2284,9 +2375,14 @@ "title": "Toestanden" }, "templates": { + "all_listeners": "Deze template luistert naar alle gebeurtenissen met gewijzigde status.", "description": "Sjablonen worden weergegeven met de Jinja2-sjabloonediter samen met enkele extensies van Home Assistant.", + "domain": "Domein", "editor": "Sjabloonediter", + "entity": "Entiteit", "jinja_documentation": "Jinja2-sjabloondocumentatie", + "listeners": "Deze template luistert naar de volgende gebeurtenissen met gewijzigde status:", + "no_listeners": "Deze template luistert niet naar gebeurtenissen met statuswijziging en wordt niet automatisch bijgewerkt.", "reset": "Resetten naar demosjabloon", "template_extensions": "Home Assistant sjabloon extensiesHome Assistant", "title": "Sjablonen", @@ -2534,7 +2630,11 @@ } }, "cardpicker": { + "by_card": "Per kaart", + "by_entity": "Per entiteit", "custom_card": "Aangepaste", + "domain": "Domein", + "entity": "Entiteit", "no_description": "Er is geen beschrijving beschikbaar" }, "edit_card": { @@ -2548,6 +2648,7 @@ "options": "Meer opties", "pick_card": "Welke kaart wil je toevoegen?", "pick_card_view_title": "Welke kaart wil je toevoegen aan je {name} weergave?", + "search_cards": "Zoek kaarten", "show_code_editor": "Code-editor weergeven", "show_visual_editor": "Visual Editor weergeven", "toggle_editor": "Toggle Editor", @@ -2868,6 +2969,11 @@ "submit": "Verzenden" }, "current_user": "Je bent momenteel ingelogd als {fullName}.", + "customize_sidebar": { + "button": "Bewerken", + "description": "U kunt ook de koptekst van de zijbalk ingedrukt houden om de bewerkingsmodus te activeren.", + "header": "Wijzig de volgorde en verberg items van de zijbalk" + }, "dashboard": { "description": "Kies een standaard dashboard voor dit apparaat.", "dropdown_label": "Dashboard", @@ -2890,6 +2996,7 @@ "confirm_delete": "Weet je zeker dat je de toegangstoken voor {name} wilt verwijderen?", "create": "Token aanmaken", "create_failed": "De toegangstoken kon niet worden aangemaakt.", + "created": "Gemaakt op {date}", "created_at": "Gemaakt op {date}", "delete_failed": "Verwijderen van de toegangstoken is mislukt.", "description": "Maak toegangstokens met een lange levensduur zodat je scripts kunnen communiceren met je Home Assistant-instantie. Elk token is tien jaar geldig vanaf de aanmaakdatum. De volgende langlevende toegangstokens zijn momenteel actief.", @@ -2897,9 +3004,10 @@ "header": "Toegangtokens met lange levensduur", "last_used": "Laatst gebruikt op {date} vanaf {location}", "learn_auth_requests": "Kom te weten hoe je geverifieerde verzoeken kunt maken", + "name": "Naam", "not_used": "Is nog nooit gebruikt", "prompt_copy_token": "Kopieer je toegangstoken. Het wordt niet meer getoond.", - "prompt_name": "Naam?" + "prompt_name": "Geef het token een naam" }, "mfa_setup": { "close": "Sluiten", @@ -2963,6 +3071,7 @@ } }, "sidebar": { + "done": "Gedaan", "external_app_configuration": "App configuratie", "sidebar_toggle": "Zijbalk in- en uitschakelen" } diff --git a/translations/frontend/pl.json b/translations/frontend/pl.json index ad4119b2f6..9bc262d724 100644 --- a/translations/frontend/pl.json +++ b/translations/frontend/pl.json @@ -19,6 +19,7 @@ "logbook": "Dziennik", "mailbox": "Poczta", "map": "Mapa", + "media_browser": "Odtwarzacz mediów", "profile": "Profil", "shopping_list": "Lista zakupów", "states": "Przegląd" @@ -379,7 +380,7 @@ "preset_mode": "Ustawienia", "swing_mode": "Tryb ruchu łopatek", "target_humidity": "Wilgotność docelowa", - "target_temperature": "Wilgotność docelowa", + "target_temperature": "Temperatura docelowa", "target_temperature_entity": "{name} temperatura docelowa", "target_temperature_mode": "{name} temperatura docelowa {mode}" }, @@ -505,6 +506,8 @@ "back": "Wstecz", "cancel": "Anuluj", "close": "Zamknij", + "continue": "Kontynuuj", + "copied": "Skopiowano", "delete": "Usuń", "error_required": "To pole jest wymagane", "loading": "Ładowanie", @@ -551,6 +554,10 @@ "toggle": "Przełącz" }, "entity": { + "entity-attribute-picker": { + "attribute": "Atrybut", + "show_attributes": "Pokaż atrybuty" + }, "entity-picker": { "clear": "Wyczyść", "entity": "Encja", @@ -561,8 +568,35 @@ "loading_history": "Ładowanie historii...", "no_history_found": "Nie znaleziono historii." }, + "logbook": { + "entries_not_found": "Nie znaleziono wpisów w dzienniku." + }, "media-browser": { + "audio_not_supported": "Twoja przeglądarka nie obsługuje elementu audio.", + "choose_player": "Wybierz odtwarzacz", "choose-source": "Wybierz źródło", + "class": { + "album": "Album", + "app": "Aplikacja", + "artist": "Artysta", + "channel": "Kanał", + "composer": "Kompozytor", + "contributing_artist": "Artysta współpracujący", + "directory": "Biblioteka", + "episode": "Odcinek", + "game": "Gra", + "genre": "Gatunek", + "image": "Obraz", + "movie": "Film", + "music": "Muzyka", + "playlist": "Playlista", + "podcast": "Podcast", + "season": "Sezon", + "track": "Ścieżka", + "tv_show": "Program telewizyjny", + "url": "URL", + "video": "Wideo" + }, "content-type": { "album": "Album", "artist": "Artysta", @@ -570,12 +604,17 @@ "playlist": "Lista odtwarzania", "server": "Serwer" }, + "media_browsing_error": "Błąd przeglądania multimediów", + "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", @@ -617,6 +656,7 @@ "update": "Aktualizuj" }, "domain_toggler": { + "reset_entities": "Zresetuj encje", "title": "Włączanie domen" }, "entity_registry": { @@ -688,8 +728,11 @@ "crop": "Przytnij" }, "more_info_control": { + "controls": "Sterowanie", + "details": "Szczegóły", "dismiss": "Zamknij okno dialogowe", "edit": "Edytuj encję", + "history": "Historia", "person": { "create_zone": "Utwórz strefę z bieżącej lokalizacji" }, @@ -697,7 +740,7 @@ "confirm_remove_text": "Czy na pewno chcesz usunąć tę encję?", "confirm_remove_title": "Usunąć encję?", "not_provided": "Ta encja jest obecnie niedostępna i jest osierocona po usuniętej, zmienionej lub dysfunkcyjnej integracji, lub urządzeniu.", - "remove_action": "Usuń encje", + "remove_action": "Usuń encję", "remove_intro": "Jeśli encja nie jest używana możesz ją usunąć." }, "script": { @@ -917,7 +960,13 @@ "label": "Wywołanie usługi", "service_data": "Dane usługi" }, + "wait_for_trigger": { + "continue_timeout": "Kontynuuj po przekroczeniu limitu czasu", + "label": "Czekaj na wyzwalacz", + "timeout": "Limit czasu (opcjonalnie)" + }, "wait_template": { + "continue_timeout": "Kontynuuj po przekroczeniu limitu czasu", "label": "Oczekiwanie", "timeout": "Limit czasu (opcjonalnie)", "wait_template": "Szablon czekania" @@ -981,7 +1030,9 @@ "time": { "after": "Po", "before": "Przed", - "label": "Czas" + "label": "Czas", + "type_input": "Wartość pomocnika typu data/czas", + "type_value": "Ustalony czas" }, "zone": { "entity": "Encja z lokalizacją", @@ -1069,6 +1120,7 @@ "value_template": "Szablon wartości (opcjonalnie)" }, "state": { + "attribute": "Atrybut (opcjonalnie)", "for": "Przez", "from": "Z", "label": "Stan", @@ -1096,7 +1148,9 @@ }, "time": { "at": "O", - "label": "Czas" + "label": "Czas", + "type_input": "Wartość pomocnika typu data/czas", + "type_value": "Ustalony czas" }, "webhook": { "label": "Webhook", @@ -1119,6 +1173,8 @@ "add_automation": "Dodaj automatyzację", "delete_automation": "Usuń automatyzację", "delete_confirm": "Czy na pewno chcesz usunąć tę automatyzację?", + "duplicate": "Duplikuj", + "duplicate_automation": "Duplikuj automatyzację", "edit_automation": "Edytuj automatyzację", "header": "Edytor automatyzacji", "headers": { @@ -1200,8 +1256,14 @@ }, "alexa": { "banner": "Edytowanie, które encje są udostępnione za pomocą interfejsu użytkownika, jest wyłączone, ponieważ skonfigurowano filtry encji w pliku configuration.yaml.", + "dont_expose_entity": "Nie udostępniaj encji", "expose": "Udostępnione do Alexy", + "expose_entity": "Udostępniaj encję", + "exposed": "{selected} udostępniona", "exposed_entities": "Udostępnione encje", + "follow_domain": "Obserwuj domenę", + "manage_domains": "Zarządzaj domenami", + "not_exposed": "{selected} nieudostępniona", "not_exposed_entities": "Nieudostępnione encje", "title": "Alexa" }, @@ -1239,8 +1301,14 @@ "google": { "banner": "Edytowanie, które encje są udostępnione za pomocą interfejsu użytkownika, jest wyłączone, ponieważ skonfigurowano filtry encji w pliku configuration.yaml.", "disable_2FA": "Wyłącz uwierzytelnianie dwuskładnikowe", + "dont_expose_entity": "Nie udostępniaj encji", "expose": "Udostępnione do Asystenta Google", + "expose_entity": "Udostępniaj encję", + "exposed": "{selected} udostępniona", "exposed_entities": "Udostępnione encje", + "follow_domain": "Obserwuj domenę", + "manage_domains": "Zarządzaj domenami", + "not_exposed": "{selected} nieudostępniona", "not_exposed_entities": "Nieudostępnione encje", "sync_to_google": "Synchronizowanie zmian z Google.", "title": "Asystent Google" @@ -1474,7 +1542,7 @@ }, "types": { "input_boolean": "Przełącznik", - "input_datetime": "Data i/lub godzina", + "input_datetime": "Data i/lub czas", "input_number": "Numer", "input_select": "Pole wyboru", "input_text": "Tekst" @@ -1507,6 +1575,7 @@ }, "integrations": { "add_integration": "Dodaj integrację", + "attention": "Wymaga uwagi", "caption": "Integracje", "config_entry": { "area": "obszar: {area}", @@ -1530,6 +1599,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}", @@ -1576,6 +1646,7 @@ "none_found_detail": "Dostosuj kryteria wyszukiwania.", "note_about_integrations": "Jeszcze nie wszystkie integracje można skonfigurować za pomocą interfejsu użytkownika.", "note_about_website_reference": "Więcej jest dostępnych na stronie integracji ", + "reconfigure": "Zmień konfigurację", "rename_dialog": "Edytuj nazwę tego wpisu konfiguracji", "rename_input_label": "Nazwa wpisu", "search": "Szukaj integracji" @@ -1733,7 +1804,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", @@ -1749,8 +1820,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", @@ -1894,7 +1978,7 @@ "reloading": { "automation": "Automatyzacje", "command_line": "Encje komponentu linia komend", - "core": "Lokalizacja i dostosowywanie", + "core": "Lokalizację i dostosowywanie", "filesize": "Encje komponentu wielkość pliku", "filter": "Encje komponentu filtr", "generic": "Encje komponentu kamera IP generic", @@ -1910,12 +1994,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", @@ -2286,9 +2375,14 @@ "title": "Stany" }, "templates": { + "all_listeners": "Ten szablon nasłuchuje wszystkich zdarzeń zmiany stanu.", "description": "Szablony są renderowane przy użyciu silnika szablonów Jinja2 z kilkoma specyficznymi rozszerzeniami Home Assistanta.", + "domain": "Domena", "editor": "Edytor szablonów", + "entity": "Encja", "jinja_documentation": "Dokumentacja szablonów Jinja2", + "listeners": "Ten szablon nasłuchuje następujących zdarzeń zmiany stanu:", + "no_listeners": "Ten szablon nie nasłuchuje żadnych zdarzeń zmiany stanu i nie zostanie zaktualizowany automatycznie.", "reset": "Zresetuj do szablonu demonstracyjnego", "template_extensions": "Rozszerzenia szablonów Home Assistanta", "title": "Szablon", @@ -2536,7 +2630,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": { @@ -2550,6 +2648,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", @@ -2870,6 +2969,11 @@ "submit": "Zatwierdź" }, "current_user": "Jesteś obecnie zalogowany jako {fullName}.", + "customize_sidebar": { + "button": "Edytuj", + "description": "Możesz także nacisnąć i przytrzymać nagłówek paska bocznego, aby aktywować tryb edycji.", + "header": "Zmień kolejność i ukryj elementy na pasku bocznym" + }, "dashboard": { "description": "Wybierz domyślny dashboard dla tego urządzenia.", "dropdown_label": "Dashboard", @@ -2892,6 +2996,7 @@ "confirm_delete": "Czy na pewno chcesz usunąć token dla {name}?", "create": "Utwórz token", "create_failed": "Nie udało się utworzyć tokena.", + "created": "Utworzony {date}", "created_at": "Utworzony {date}", "delete_failed": "Nie udało się usunąć tokena.", "description": "Długoterminowe tokeny dostępu umożliwiają skryptom interakcję z Home Assistantem. Każdy token będzie ważny przez 10 lat od utworzenia. Następujące tokeny są obecnie aktywne.", @@ -2899,6 +3004,7 @@ "header": "Tokeny dostępu", "last_used": "Ostatnio używany {date} z {location}", "learn_auth_requests": "Dowiedz się, jak tworzyć uwierzytelnione żądania.", + "name": "Nazwa", "not_used": "Nigdy nie był używany", "prompt_copy_token": "Skopiuj token. Nie będzie on już ponownie wyświetlany.", "prompt_name": "Nazwa" @@ -2965,6 +3071,7 @@ } }, "sidebar": { + "done": "Gotowe", "external_app_configuration": "Konfiguracja aplikacji", "sidebar_toggle": "Przełącz pasek boczny" } diff --git a/translations/frontend/pt-BR.json b/translations/frontend/pt-BR.json index 078f61a7fc..8326daffff 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 f34d128cfd..2d315f637e 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/ro.json b/translations/frontend/ro.json index b0bcdb48cd..0262cb7b0d 100644 --- a/translations/frontend/ro.json +++ b/translations/frontend/ro.json @@ -489,6 +489,7 @@ "back": "Înapoi", "cancel": "Revocare", "close": "Închide", + "copied": "Copiat", "delete": "Șterge", "error_required": "Necesar", "loading": "Se încarcă", @@ -544,6 +545,32 @@ "loading_history": "Încărcarea istoricului de stare ...", "no_history_found": "Nici un istoric de stare nu a fost găsit." }, + "logbook": { + "entries_not_found": "Nu s-au găsit intrări în jurnal." + }, + "media-browser": { + "class": { + "album": "Album", + "app": "Aplicație", + "artist": "Artist", + "channel": "Canal", + "composer": "Compozitor", + "contributing_artist": "Artist contribuitor", + "directory": "Bibliotecă", + "episode": "Episod", + "game": "Joc", + "genre": "Gen", + "image": "Imagine", + "movie": "Film", + "music": "Muzică", + "playlist": "Playlist", + "podcast": "Podcast", + "season": "Sezon", + "tv_show": "Emisiune TV", + "url": "Url", + "video": "Video" + } + }, "related-items": { "area": "Zonă", "automation": "Parte din următoarele automatizări", @@ -647,6 +674,7 @@ "yaml_not_editable": "Setările acestei entități nu se pot edita din interfața grafica. Numai entitățile configurate in interfața grafica sunt configurabile din interfața grafica." }, "more_info_control": { + "details": "Detalii", "dismiss": "Se respinge dialogul", "edit": "Editează entitatea", "person": { @@ -850,7 +878,13 @@ "label": "Cheama serviciu", "service_data": "Date serviciu" }, + "wait_for_trigger": { + "continue_timeout": "Continua la timeout", + "label": "Așteptați declanșatorul", + "timeout": "Timeout (optional)" + }, "wait_template": { + "continue_timeout": "Continuați la expirare", "label": "Asteptare", "timeout": "Timeout (opțional)", "wait_template": "Sablon Asteptare" @@ -1260,6 +1294,7 @@ }, "integrations": { "add_integration": "Adăugați integrare", + "attention": "Atenție necesară", "caption": "Integrări", "config_entry": { "delete": "Șterge", @@ -1317,6 +1352,7 @@ "none": "Nimic nu a fost configurat încă", "none_found": "Nu s-au găsit integrări", "none_found_detail": "Ajustați criteriile de căutare.", + "reconfigure": "Reconfigurați", "rename_dialog": "Editați numele acestei intrări de configurare", "rename_input_label": "Introdu nume", "search": "Căutare integrari" @@ -1848,9 +1884,14 @@ "title": "Status" }, "templates": { + "all_listeners": "Acest șablon ascultă următoarele evenimente modificate de stare:", "description": "Șabloanele sunt redate utilizând motorul de șablon Jinja2 cu unele extensii specifice Home Assistant.", + "domain": "Domeniu", "editor": "Editor șabloane", + "entity": "Entitate", "jinja_documentation": "Șablon documentație Jinja2", + "listeners": "Acest șablon ascultă următoarele evenimente modificate de stare:", + "no_listeners": "Acest șablon ascultă următoarele evenimente modificate de stare:", "template_extensions": "Șabloane de extensie pentru Home Assistant", "title": "Sabloane", "unknown_error_template": "Sa produs o eroare de randare necunoscută." @@ -2353,6 +2394,11 @@ "submit": "Trimite" }, "current_user": "În prezent sunteți conectat ca {fullName}.", + "customize_sidebar": { + "button": "Editeaza", + "description": "De asemenea, puteți apăsa și ține apăsat antetul barei laterale pentru a activa modul de editare.", + "header": "Schimbați ordinea și ascundeți elementele din bara laterală" + }, "dashboard": { "description": "Alegeți un tablou de bord implicit pentru acest dispozitiv.", "dropdown_label": "Tablou de bord", @@ -2375,6 +2421,7 @@ "confirm_delete": "Sigur doriti sa stergeti tokenul de acces pentru {name}?", "create": "Creaza un Token", "create_failed": "Crearea tokenului de acces eşuată", + "created": "Creat in {data}", "created_at": "Creat in {date}", "delete_failed": "Ştergerea tokenului de acces eşuată", "description": "Creați tokenuri de acces cu durată lungă de viață pentru a permite script-urilor dvs. să interacționeze cu instanța dvs. Home Assistant. Fiecare token va fi valabil timp de 10 ani de la creatie. Următoarele tokenuri de acces de lungă durată sunt active la ora actuala.", @@ -2382,6 +2429,7 @@ "header": "Tokenuri de acces de lunga durata", "last_used": "Ultima utilizare la {date} din {location}", "learn_auth_requests": "Aflați cum să faceți cereri autentificate.", + "name": "Nume", "not_used": "Nu a fost utilizat niciodata", "prompt_copy_token": "Copia token-ul de acces. Acesta nu va fi afișat din nou.", "prompt_name": "Nume?" @@ -2443,6 +2491,7 @@ } }, "sidebar": { + "done": "Terminat", "external_app_configuration": "Configurație aplicație", "sidebar_toggle": "Schimbati bara laterală" } diff --git a/translations/frontend/ru.json b/translations/frontend/ru.json index b1acc8ea5f..04aa43066e 100644 --- a/translations/frontend/ru.json +++ b/translations/frontend/ru.json @@ -19,6 +19,7 @@ "logbook": "Журнал событий", "mailbox": "Почта", "map": "Карта", + "media_browser": "Браузер мультимедиа", "profile": "Профиль", "shopping_list": "Список покупок", "states": "Обзор" @@ -505,6 +506,8 @@ "back": "Назад", "cancel": "Отменить", "close": "Закрыть", + "continue": "Продолжить", + "copied": "Скопировано", "delete": "Удалить", "error_required": "Обязательное поле", "loading": "Загрузка", @@ -551,6 +554,10 @@ "toggle": "Переключить" }, "entity": { + "entity-attribute-picker": { + "attribute": "Атрибут", + "show_attributes": "Показать атрибуты" + }, "entity-picker": { "clear": "Очистить", "entity": "Объект", @@ -561,8 +568,35 @@ "loading_history": "Загрузка истории...", "no_history_found": "История не найдена." }, + "logbook": { + "entries_not_found": "В журнале нет записей." + }, "media-browser": { + "audio_not_supported": "Ваш браузер не поддерживает аудио.", + "choose_player": "Выберите медиаплеер", "choose-source": "Выбрать источник", + "class": { + "album": "Альбом", + "app": "Приложение", + "artist": "Исполнитель", + "channel": "Канал", + "composer": "Композитор", + "contributing_artist": "Соисполнитель", + "directory": "Библиотека", + "episode": "Эпизод", + "game": "Игра", + "genre": "Жанр", + "image": "Изображение", + "movie": "Фильм", + "music": "Музыка", + "playlist": "Плейлист", + "podcast": "Подкаст", + "season": "Сезон", + "track": "Трек", + "tv_show": "Сериалы", + "url": "URL-адрес", + "video": "Видео" + }, "content-type": { "album": "Альбом", "artist": "Исполнитель", @@ -570,12 +604,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": "Изображение", @@ -617,7 +656,7 @@ "update": "Обновить" }, "domain_toggler": { - "reset_entities": "Сбросить объекты", + "reset_entities": "Сбросить настройки доступа объектов", "title": "Переключить домены" }, "entity_registry": { @@ -689,8 +728,11 @@ "crop": "Обрезать" }, "more_info_control": { + "controls": "Управление", + "details": "Свойства", "dismiss": "Закрыть диалог", "edit": "Изменить объект", + "history": "История", "person": { "create_zone": "Создать зону из текущего местоположения" }, @@ -858,7 +900,7 @@ "add": "Добавить действие", "delete": "Удалить", "delete_confirm": "Вы уверены, что хотите удалить?", - "duplicate": "Дублировать", + "duplicate": "Копировать", "header": "Действия", "introduction": "Действия — это то, что сделает Home Assistant, когда правило автоматизации сработает.", "learn_more": "Узнайте больше о действиях", @@ -918,7 +960,13 @@ "label": "Вызов службы", "service_data": "Данные" }, + "wait_for_trigger": { + "continue_timeout": "Продолжить по истечении времени", + "label": "Ожидать триггера", + "timeout": "Тайм-аут (необязательно)" + }, "wait_template": { + "continue_timeout": "Продолжить по истечении времени", "label": "Ожидание", "timeout": "Тайм-аут (необязательно)", "wait_template": "Шаблон ожидания" @@ -931,7 +979,7 @@ "add": "Добавить условие", "delete": "Удалить", "delete_confirm": "Вы уверены, что хотите удалить?", - "duplicate": "Дублировать", + "duplicate": "Копировать", "header": "Условия", "introduction": "Условия — это необязательная часть правила автоматизации. Действие автоматизации не будет выполнено, пока не будут удовлетворены все условия.", "learn_more": "Узнайте больше об условиях", @@ -982,7 +1030,9 @@ "time": { "after": "После", "before": "До", - "label": "Время" + "label": "Время", + "type_input": "Вспомогательный элемент даты и времени", + "type_value": "Фиксированное время" }, "zone": { "entity": "Объект с местоположением", @@ -1023,7 +1073,7 @@ "add": "Добавить триггер", "delete": "Удалить", "delete_confirm": "Вы уверены, что хотите удалить?", - "duplicate": "Дублировать", + "duplicate": "Копировать", "header": "Триггеры", "introduction": "Триггеры — это то, что запускает процесс автоматизации. Можно указать несколько триггеров на одно и то же правило. Когда триггер сработает, Home Assistant будет проверять условия (если таковые имеются), и выполнять действия.", "learn_more": "Узнайте больше о триггерах", @@ -1070,6 +1120,7 @@ "value_template": "Шаблон значения (необязательно)" }, "state": { + "attribute": "Атрибут (необязательно)", "for": "В течение", "from": "С", "label": "Состояние", @@ -1096,8 +1147,10 @@ "seconds": "Секунд" }, "time": { - "at": "В", - "label": "Время" + "at": "Время", + "label": "Время", + "type_input": "Вспомогательный элемент даты и времени", + "type_value": "Фиксированное время" }, "webhook": { "label": "Webhook", @@ -1120,6 +1173,8 @@ "add_automation": "Добавить автоматизацию", "delete_automation": "Удалить автоматизацию", "delete_confirm": "Вы уверены, что хотите удалить эту автоматизацию?", + "duplicate": "Копировать", + "duplicate_automation": "Копировать автоматизацию", "edit_automation": "Редактировать автоматизацию", "header": "Редактор автоматизаций", "headers": { @@ -1201,15 +1256,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 +1301,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 +1320,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 +1467,7 @@ }, "scripts": "Сценарии", "unknown_error": "Неизвестная ошибка.", - "unnamed_device": "Безымянное устройство", + "unnamed_device": "Устройство без названия", "update": "Обновить" }, "entities": { @@ -1510,6 +1575,7 @@ }, "integrations": { "add_integration": "Добавить интеграцию", + "attention": "Требуется внимание", "caption": "Интеграции", "config_entry": { "area": "Помещение: {area}", @@ -1533,6 +1599,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 +1646,8 @@ "none_found_detail": "Измените критерии поиска", "note_about_integrations": "Пока что не все интеграции могут быть настроены через пользовательский интерфейс.", "note_about_website_reference": "Все доступные интеграции Вы можете найти на ", - "rename_dialog": "Изменение названия интеграции", + "reconfigure": "Перенастроить", + "rename_dialog": "Название интеграции", "rename_input_label": "Название", "search": "Поиск интеграций" }, @@ -1752,8 +1820,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 +1983,7 @@ "filter": "Перезагрузить объекты интеграции \"Filter\"", "generic": "Перезагрузить объекты интеграции \"Generic IP Camera\"", "generic_thermostat": "Перезагрузить объекты интеграции \"Generic Thermostat\"", - "group": "Перезагрузить группы", + "group": "Перезагрузить группы, объекты групп и службы уведомлений", "heading": "Перезагрузка конфигурации YAML", "history_stats": "Перезагрузить объекты интеграции \"History Stats\"", "homekit": "Перезагрузить HomeKit", @@ -1913,12 +1994,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 +2093,7 @@ "system": "Системный" } }, - "users_privileges_note": "Группа пользователей находится в стадии разработки. В дальнейшем пользователи не смогут администрировать сервер через пользовательский интерфейс. Мы все еще проверяем все конечные точки API управления, чтобы убедиться, что они правильно ограничивают доступ." + "users_privileges_note": "Функционал пользователей всё ещё в стадии разработки. В дальнейшем пользователи не смогут администрировать сервер через пользовательский интерфейс. Мы все еще проверяем все конечные точки API управления, чтобы убедиться, что они правильно ограничивают доступ." }, "zha": { "add_device_page": { @@ -2289,9 +2375,14 @@ "title": "Состояния" }, "templates": { + "all_listeners": "Этот шаблон отслеживает все события изменения состояния.", "description": "Здесь Вы можете протестировать поведение шаблонов. В Home Assistant используется шаблонизатор Jinja2 с некоторыми специальными расширениями.", + "domain": "Домен", "editor": "Редактор шаблонов", + "entity": "Объект", "jinja_documentation": "Узнайте больше о шаблонизаторе Jinja2", + "listeners": "Этот шаблон отслеживает следующие события изменения состояния:", + "no_listeners": "Этот шаблон не отслеживает события изменения состояния и не обновляется автоматически.", "reset": "Вернуться к демонстрационному шаблону", "template_extensions": "Узнайте больше о шаблонах Home Assistant", "title": "Шаблоны", @@ -2539,7 +2630,11 @@ } }, "cardpicker": { + "by_card": "Карточки", + "by_entity": "Объекты", "custom_card": "Custom", + "domain": "Домен", + "entity": "Объект", "no_description": "Описание недоступно." }, "edit_card": { @@ -2553,6 +2648,7 @@ "options": "Больше параметров", "pick_card": "Какую карточку Вы хотели бы добавить?", "pick_card_view_title": "Какую карточку Вы хотели бы добавить на вкладку {name}?", + "search_cards": "Поиск карточек", "show_code_editor": "Текстовый редактор", "show_visual_editor": "Форма ввода", "toggle_editor": "Переключить редактор", @@ -2838,7 +2934,7 @@ }, "integration": { "finish": "Готово", - "intro": "Устройства и сервисы представлены в Home Assistant как интеграции. Вы можете добавить их сейчас или сделать это позже в разделе настроек.", + "intro": "Устройства и службы представлены в Home Assistant как интеграции. Вы можете добавить их сейчас или сделать это позже в разделе настроек.", "more_integrations": "Ещё" }, "intro": "Готовы ли Вы разбудить свой дом, вернуть свою конфиденциальность и присоединиться к всемирному сообществу?", @@ -2873,6 +2969,11 @@ "submit": "Подтвердить" }, "current_user": "Добро пожаловать, {fullName}! Вы вошли в систему.", + "customize_sidebar": { + "button": "Изменить", + "description": "Режим редактирования можно включить, нажав и удерживая заголовок боковой панели", + "header": "Изменить элементы боковой панели" + }, "dashboard": { "description": "Панель, используемая по умолчанию для этого устройства", "dropdown_label": "Панель", @@ -2895,6 +2996,7 @@ "confirm_delete": "Вы уверены, что хотите удалить токен доступа для {name}?", "create": "Создать токен", "create_failed": "Не удалось создать токен доступа.", + "created": "Создан {date}", "created_at": "Создан {date}", "delete_failed": "Не удалось удалить токен доступа.", "description": "Создайте долгосрочные токены доступа, чтобы Ваши скрипты могли взаимодействовать с Home Assistant. Каждый токен будет действителен в течение 10 лет с момента создания. Ниже Вы можете просмотреть долгосрочные токены доступа, которые в настоящее время активны.", @@ -2902,9 +3004,10 @@ "header": "Долгосрочные токены доступа", "last_used": "Последнее использование {date} из {location}", "learn_auth_requests": "Узнайте, как выполнять аутентифицированные запросы.", + "name": "Название", "not_used": "Никогда не использовался", "prompt_copy_token": "Скопируйте Ваш токен доступа. Он больше не будет показан.", - "prompt_name": "Введите название для токена" + "prompt_name": "Название токена" }, "mfa_setup": { "close": "Закрыть", @@ -2968,6 +3071,7 @@ } }, "sidebar": { + "done": "Готово", "external_app_configuration": "Настройки приложения", "sidebar_toggle": "Переключатель в боковой панели" } diff --git a/translations/frontend/sk.json b/translations/frontend/sk.json index 6e865645c7..9e3bacaab7 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/sv.json b/translations/frontend/sv.json index f684b51098..cf1615ecc1 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/uk.json b/translations/frontend/uk.json index 4757374de4..b3501dd8da 100644 --- a/translations/frontend/uk.json +++ b/translations/frontend/uk.json @@ -810,6 +810,9 @@ "label": "Викликати сервіс", "service_data": "Дані сервісу" }, + "wait_for_trigger": { + "timeout": "Таймаут (опціонально)" + }, "wait_template": { "label": "Чекати", "timeout": "Тайм-аут (необов'язково)", @@ -873,7 +876,8 @@ "time": { "after": "Після", "before": "До", - "label": "Час" + "label": "Час", + "type_value": "Фіксований час" }, "zone": { "entity": "Об'єкт з місцем розташування", @@ -982,7 +986,8 @@ }, "time": { "at": "У", - "label": "Час" + "label": "Час", + "type_value": "Фіксований час" }, "webhook": { "label": "Webhook", diff --git a/translations/frontend/zh-Hans.json b/translations/frontend/zh-Hans.json index 96f370e5df..d8bc96cb89 100644 --- a/translations/frontend/zh-Hans.json +++ b/translations/frontend/zh-Hans.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": "加载中", @@ -551,6 +553,10 @@ "toggle": "切换" }, "entity": { + "entity-attribute-picker": { + "attribute": "属性", + "show_attributes": "显示属性" + }, "entity-picker": { "clear": "清除", "entity": "实体", @@ -561,8 +567,35 @@ "loading_history": "正在加载历史状态...", "no_history_found": "没有找到历史状态。" }, + "logbook": { + "entries_not_found": "未找到日志条目。" + }, "media-browser": { + "audio_not_supported": "您的浏览器不支持音频元素。", + "choose_player": "选择播放器", "choose-source": "选择媒体源", + "class": { + "album": "专辑", + "app": "应用", + "artist": "艺术家", + "channel": "频道", + "composer": "作曲家", + "contributing_artist": "参与创作者", + "directory": "媒体库", + "episode": "分集", + "game": "游戏", + "genre": "体裁", + "image": "图片", + "movie": "电影", + "music": "音乐", + "playlist": "播放列表", + "podcast": "播客", + "season": "播出季", + "track": "音轨", + "tv_show": "电视节目", + "url": "网址", + "video": "视频" + }, "content-type": { "album": "专辑", "artist": "艺术家", @@ -570,12 +603,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 +727,11 @@ "crop": "剪裁" }, "more_info_control": { + "controls": "控制项", + "details": "详情", "dismiss": "关闭对话框", "edit": "编辑实体", + "history": "历史", "person": { "create_zone": "从当前位置创建地点" }, @@ -918,7 +959,13 @@ "label": "调用服务", "service_data": "服务数据" }, + "wait_for_trigger": { + "continue_timeout": "超时继续", + "label": "等待触发", + "timeout": "超时(可选)" + }, "wait_template": { + "continue_timeout": "超时继续", "label": "等待", "timeout": "超时(可选)", "wait_template": "等待模板" @@ -982,7 +1029,9 @@ "time": { "after": "晚于", "before": "早于", - "label": "时间" + "label": "时间", + "type_input": "“日期/时间”辅助元素的值", + "type_value": "固定时间" }, "zone": { "entity": "位置追踪设备", @@ -1070,6 +1119,7 @@ "value_template": "自定义值(可选)" }, "state": { + "attribute": "属性(可选)", "for": "持续", "from": "从", "label": "状态", @@ -1096,8 +1146,10 @@ "seconds": "秒" }, "time": { - "at": "当", - "label": "时间" + "at": "时间为", + "label": "时间", + "type_input": "“日期/时间”辅助元素的值", + "type_value": "固定时间" }, "webhook": { "label": "Webhook", @@ -1120,6 +1172,8 @@ "add_automation": "添加自动化", "delete_automation": "删除自动化", "delete_confirm": "您确定要删除此自动化吗?", + "duplicate": "制作副本", + "duplicate_automation": "复制自动化", "edit_automation": "编辑自动化", "header": "自动化编辑器", "headers": { @@ -1520,6 +1574,7 @@ }, "integrations": { "add_integration": "添加集成", + "attention": "需要注意", "caption": "集成", "config_entry": { "area": "位于:{area}", @@ -1543,6 +1598,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}系统选项", @@ -1589,6 +1645,7 @@ "none_found_detail": "请调整搜索条件。", "note_about_integrations": "并非所有集成都可以通过 UI 进行配置。", "note_about_website_reference": "更多可用信息,尽在 ", + "reconfigure": "重新配置", "rename_dialog": "编辑此配置项的名称", "rename_input_label": "条目名称", "search": "搜索集成" @@ -1762,8 +1819,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": "节点状态", @@ -1776,7 +1846,7 @@ }, "select_instance": { "header": "选择一个 OpenZWave 实例", - "introduction": "您正在运行多个 OpenZWave 实例。您要管理哪个实例?" + "introduction": "有多个 OpenZWave 实例正在运行。您要管理哪个实例?" }, "services": { "add_node": "添加节点", @@ -1912,7 +1982,7 @@ "filter": "重载 filter 实体", "generic": "重载通用 IP 摄像机实体", "generic_thermostat": "重载通用恒温器实体", - "group": "重载分组", + "group": "重载分组、分组实体及通知服务", "heading": "配置重载", "history_stats": "重载历史记录统计实体", "homekit": "重载 HomeKit", @@ -1923,12 +1993,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": "重载通用媒体播放器实体", @@ -2299,9 +2374,14 @@ "title": "状态" }, "templates": { + "all_listeners": "此模板监听所有的状态改变事件。", "description": "模板使用 jinja2 模板引擎和一些 Home Assistant 特定的插件进行渲染。", + "domain": "域", "editor": "模板编辑器", + "entity": "实体", "jinja_documentation": "Jinja2 模板文档", + "listeners": "此模板监听以下状态改变事件:", + "no_listeners": "此模板不监听任何状态改变事件,并且不会自动更新。", "reset": "重置为演示模板", "template_extensions": "Home Assistant 模板插件", "title": "模板", @@ -2549,7 +2629,11 @@ } }, "cardpicker": { + "by_card": "按卡片", + "by_entity": "按实体", "custom_card": "自定义", + "domain": "域", + "entity": "实体", "no_description": "没有描述。" }, "edit_card": { @@ -2563,6 +2647,7 @@ "options": "更多选项", "pick_card": "请选择要添加的卡片。", "pick_card_view_title": "您想将哪张卡片添加到 {name} 视图?", + "search_cards": "搜索卡片", "show_code_editor": "显示代码编辑器", "show_visual_editor": "显示可视化编辑器", "toggle_editor": "切换编辑器", @@ -2883,6 +2968,11 @@ "submit": "提交" }, "current_user": "您目前以 {fullName} 的身份登录。", + "customize_sidebar": { + "button": "编辑", + "description": "您也可以长按侧边栏标题来进入编辑模式。", + "header": "排序和隐藏侧边栏中的项目" + }, "dashboard": { "description": "选择此设备的默认仪表盘。", "dropdown_label": "仪表盘", @@ -2905,6 +2995,7 @@ "confirm_delete": "确定要删除 {name} 的访问令牌吗?", "create": "创建令牌", "create_failed": "无法创建访问令牌。", + "created": "创建于 {date}", "created_at": "创建于 {date}", "delete_failed": "无法删除访问令牌。", "description": "创建长期访问令牌以允许脚本与 Home Assistant 实例进行交互。每个令牌在创建后有效期为 10 年。以下是处于激活状态的长期访问令牌。", @@ -2912,9 +3003,10 @@ "header": "长期访问令牌", "last_used": "上次使用于 {date} 来自 {location}", "learn_auth_requests": "了解如何创建经过身份验证的请求。", + "name": "名称", "not_used": "从未使用过", "prompt_copy_token": "请复制您的访问令牌。它不会再显示出来。", - "prompt_name": "名称?" + "prompt_name": "为令牌指定名称" }, "mfa_setup": { "close": "关闭", @@ -2978,6 +3070,7 @@ } }, "sidebar": { + "done": "完成", "external_app_configuration": "应用配置", "sidebar_toggle": "侧边栏切换" } diff --git a/translations/frontend/zh-Hant.json b/translations/frontend/zh-Hant.json index 78b42e133e..452cf1c2e5 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,8 @@ "back": "上一步", "cancel": "取消", "close": "關閉", + "continue": "繼續", + "copied": "已複製", "delete": "刪除", "error_required": "必填", "loading": "讀取中", @@ -551,6 +554,10 @@ "toggle": "觸發" }, "entity": { + "entity-attribute-picker": { + "attribute": "屬性", + "show_attributes": "顯示屬性" + }, "entity-picker": { "clear": "清除", "entity": "實體", @@ -561,8 +568,35 @@ "loading_history": "正在載入狀態歷史...", "no_history_found": "找不到狀態歷史。" }, + "logbook": { + "entries_not_found": "找不到實體日誌。" + }, "media-browser": { + "audio_not_supported": "瀏覽器不支援音效元件。", + "choose_player": "選擇播放器", "choose-source": "選擇來源", + "class": { + "album": "專輯", + "app": "App", + "artist": "演唱者", + "channel": "頻道", + "composer": "作曲者", + "contributing_artist": "參與藝術家", + "directory": "媒體庫", + "episode": "集", + "game": "遊戲", + "genre": "類型", + "image": "圖像", + "movie": "電影", + "music": "音樂", + "playlist": "播放列表", + "podcast": "Podcast", + "season": "季", + "track": "音軌", + "tv_show": "電視節目", + "url": "網址", + "video": "影片" + }, "content-type": { "album": "專輯", "artist": "演唱者", @@ -570,12 +604,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 +728,11 @@ "crop": "裁切" }, "more_info_control": { + "controls": "控制", + "details": "詳細資訊", "dismiss": "忽略對話", "edit": "編輯實體", + "history": "歷史", "person": { "create_zone": "使用目前位置新增區域" }, @@ -918,7 +960,13 @@ "label": "執行服務", "service_data": "服務資料" }, + "wait_for_trigger": { + "continue_timeout": "繼續逾時計算", + "label": "等候觸發", + "timeout": "超時(選項)" + }, "wait_template": { + "continue_timeout": "繼續逾時計算", "label": "等待", "timeout": "超時(選項)", "wait_template": "等待模板" @@ -982,7 +1030,9 @@ "time": { "after": "在...之後", "before": "在...之前", - "label": "時間" + "label": "時間", + "type_input": "日期/時間協助數值", + "type_value": "固定時間" }, "zone": { "entity": "區域實體", @@ -1070,6 +1120,7 @@ "value_template": "數值模板(選項)" }, "state": { + "attribute": "屬性(選項)", "for": "持續", "from": "從...狀態", "label": "狀態", @@ -1096,8 +1147,10 @@ "seconds": "秒鐘" }, "time": { - "at": "在...時間", - "label": "時間" + "at": "時間於", + "label": "時間", + "type_input": "日期/時間協助數值", + "type_value": "固定時間" }, "webhook": { "label": "Webhook", @@ -1120,6 +1173,8 @@ "add_automation": "新增一個自動化", "delete_automation": "刪除自動化", "delete_confirm": "確定要刪除此自動化?", + "duplicate": "複製", + "duplicate_automation": "複製自動化", "edit_automation": "編輯自動化", "header": "自動化編輯器", "headers": { @@ -1520,6 +1575,7 @@ }, "integrations": { "add_integration": "新增整合", + "attention": "需要注意", "caption": "整合", "config_entry": { "area": "於 {area}", @@ -1543,6 +1599,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} 系統選項", @@ -1589,6 +1646,7 @@ "none_found_detail": "調整搜尋條件。", "note_about_integrations": "目前並非所有整合皆可以透過 UI 進行設定。", "note_about_website_reference": "更多資訊請參閱", + "reconfigure": "重新設定", "rename_dialog": "編輯設定實體名稱", "rename_input_label": "實體名稱", "search": "搜尋整合" @@ -1762,8 +1820,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 +1983,7 @@ "filter": "重新載入過濾器實體", "generic": "重新載入通用 IP 攝影機實體", "generic_thermostat": "重新載入通用溫控器實體", - "group": "重新載入群組", + "group": "重新載入群組、群組實體及通知服務", "heading": "YAML 設定新載入中", "history_stats": "重新載入歷史狀態實體", "homekit": "重新載入 Homekit", @@ -1923,13 +1994,18 @@ "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": "重新載入統計資訊實體", - "template": "重新載入範例實體", + "telegram": "重新載入 Telegram 通知服務", + "template": "重新載入模板實體", "trend": "重新載入趨勢實體", "universal": "重新載入通用媒體播放器實體", "zone": "重新載入區域" @@ -2017,7 +2093,7 @@ "system": "系統" } }, - "users_privileges_note": "使用者群組功能進行中。將無法透過 UI 進行使用者管理,仍在檢視所有管理 API Endpoint 以確保能夠正確符合管理員存取需求。" + "users_privileges_note": "使用者群組功能仍在開發中。將無法透過 UI 進行使用者管理,仍在檢視所有管理 API Endpoint 以確保能夠正確符合管理員存取需求。" }, "zha": { "add_device_page": { @@ -2299,9 +2375,14 @@ "title": "狀態" }, "templates": { + "all_listeners": "此模板監聽所有狀態變更事件。", "description": "模版使用 Jinja2 模板引擎及 Home Assistant 特殊擴充進行模板渲染。", + "domain": "區域", "editor": "模板編輯器", + "entity": "實體", "jinja_documentation": "Jinja2 模版文件", + "listeners": "此模板監聽以下狀態變更事件:", + "no_listeners": "此模板不監聽任何狀態變更事件,將不會自動更新。", "reset": "重置示範範模板", "template_extensions": "Home Assistant 模板擴充", "title": "模板", @@ -2549,7 +2630,11 @@ } }, "cardpicker": { + "by_card": "以面板", + "by_entity": "以實體", "custom_card": "自訂面板", + "domain": "區域", + "entity": "實體", "no_description": "無描述可使用。" }, "edit_card": { @@ -2563,6 +2648,7 @@ "options": "更多選項", "pick_card": "選擇所要新增的面板?", "pick_card_view_title": "要加入 {name} 視圖的面板?", + "search_cards": "搜尋面板", "show_code_editor": "顯示編碼編輯器", "show_visual_editor": "顯示視覺編輯器", "toggle_editor": "切換編輯器", @@ -2883,6 +2969,11 @@ "submit": "傳送" }, "current_user": "目前登入身份:{fullName}。", + "customize_sidebar": { + "button": "編輯", + "description": "同時也能長按側邊列的標題以啟動編輯模式。", + "header": "自側邊列中變更順序或隱藏項目" + }, "dashboard": { "description": "選擇此設備的預設主面板。", "dropdown_label": "主面板", @@ -2905,6 +2996,7 @@ "confirm_delete": "確定要刪除{name}存取密鑰嗎?", "create": "創建密鑰", "create_failed": "創建存取密鑰失敗。", + "created": "新增日期:{date}", "created_at": "於{date}創建", "delete_failed": "刪除存取密鑰失敗。", "description": "創建長效存取密鑰,可供運用腳本與 Home Assistant 實體進行互動。每個密鑰於創建後,有效期為十年。目前已啟用之永久有效密鑰如下。", @@ -2912,9 +3004,10 @@ "header": "永久有效存取密鑰", "last_used": "上次使用:於{date}、位置{location}", "learn_auth_requests": "學習如何進行驗證請求。", + "name": "名稱", "not_used": "從未使用過", "prompt_copy_token": "複製存取密鑰,將不會再次顯示。", - "prompt_name": "名稱?" + "prompt_name": "為密鑰命名" }, "mfa_setup": { "close": "關閉", @@ -2978,6 +3071,7 @@ } }, "sidebar": { + "done": "完成", "external_app_configuration": "App 設定", "sidebar_toggle": "側邊欄開關" } diff --git a/yarn.lock b/yarn.lock index 8b8451ef9c..1a6af52a72 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" @@ -4937,7 +4942,7 @@ debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -debug@3.2.6, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: +debug@3.2.6, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -5706,11 +5711,16 @@ event-target-shim@^5.0.1: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter3@3.1.0, eventemitter3@^3.0.0: +eventemitter3@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + events@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" @@ -6218,11 +6228,9 @@ fn-name@~2.0.1: integrity sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc= follow-redirects@^1.0.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" - integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ== - dependencies: - debug "^3.2.6" + version "1.13.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" + integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" @@ -6972,11 +6980,11 @@ http-proxy-middleware@0.19.1: micromatch "^3.1.10" http-proxy@^1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" - integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== dependencies: - eventemitter3 "^3.0.0" + eventemitter3 "^4.0.0" follow-redirects "^1.0.0" requires-port "^1.0.0"