Compare commits

...
Author SHA1 Message Date
Joakim Sørensen 09f4922ad3 change 2021-06-15 15:44:59 +00:00
Joakim Sørensen 36831d26e4 fix typing 2021-06-15 09:51:27 +00:00
Joakim Sørensen 5829660894 Better event handling 2021-06-15 09:48:19 +00:00
Joakim Sørensen 4e3fbc1169 Add applying update "screen" 2021-06-14 18:18:54 +00:00
GitHub Action 38640c99e3 Translation update 2021-06-14 00:48:26 +00:00
GitHub Action d6df8bddea Translation update 2021-06-13 00:48:28 +00:00
GitHub Action ddfc4bd98e Translation update 2021-06-12 00:48:12 +00:00
3d6674325c Fix the issue that logbook card doesn't translate context.user_id to name if it's a user's id. (#9383)
Co-authored-by: Bram Kragten <[email protected]>
2021-06-10 14:51:24 +02:00
Bram KragtenandGitHub 194829f5b1 Generalize map (#9331)
* Generalize map

* Fix path opacity

* Add fitZones
2021-06-10 14:22:44 +02:00
GitHub Action 11a77253f4 Translation update 2021-06-10 00:49:02 +00:00
GitHub Action 67be2343f8 Translation update 2021-06-09 00:48:19 +00:00
e9b1b3d853 Fix issues with restoring snapshot during onboarding (#9385)
Co-authored-by: Bram Kragten <[email protected]>
2021-06-08 17:57:53 +02:00
8a33d174d7 FIx selecting service/url path action after choosing default action (#9376)
Co-authored-by: Joakim Sørensen <[email protected]>
2021-06-08 14:24:58 +02:00
Joakim SørensenandGitHub 226d6216b7 Build wheels for 3.9-alpine3.13 (#9390) 2021-06-08 13:07:02 +02:00
GitHub Action 1925bb01be Translation update 2021-06-08 00:49:18 +00:00
Bram Kragten 82a4806e01 Change line logic 2021-06-07 10:45:57 +02:00
ce419fae7b Add password confirmation to snapshot creation (#9349)
* Add password confirmation to snapshot creation

* Remove confirm_password before sending

* change layout

* style changes

* Adjust styling

Co-authored-by: Bram Kragten <[email protected]>
2021-06-07 10:45:20 +02:00
c68b76e2da Add hardware dialog (#9348)
Co-authored-by: Bram Kragten <[email protected]>
2021-06-07 10:16:33 +02:00
Joakim SørensenandGitHub 342020b420 Fix downloads on mobile (#9375) 2021-06-07 10:15:43 +02:00
GitHub Action 1e6e99e3c7 Translation update 2021-06-07 00:49:13 +00:00
GitHub Action 2e9aafc377 Translation update 2021-06-06 00:49:09 +00:00
dependabot[bot]andGitHub 299c863f49 Bump ws from 6.2.1 to 6.2.2 (#9372) 2021-06-05 23:52:13 +02:00
Bram KragtenandGitHub c2792a28ba Move attributes down in more info person and timer (#9368) 2021-06-05 12:52:27 +02:00
GitHub Action 635a027a8e Translation update 2021-06-05 02:40:15 +00:00
Will AdlerandGitHub a45b8ca8e7 Add period to end of sentence (#9361) 2021-06-04 08:57:03 +02:00
GitHub Action 1e6e945a07 Translation update 2021-06-04 02:52:56 +00:00
Bram KragtenandGitHub f71157c24d Remove tsc from pre commit (#9359) 2021-06-03 22:57:03 +02:00
Bram Kragten e87a2b36cf Bumped version to 20210603.0 2021-06-03 22:51:53 +02:00
Bram KragtenandGitHub 5418474f64 Polyfill globalThis in latest build (#9352) 2021-06-03 22:50:33 +02:00
Philip AllgaierandGitHub 8836ba6ceb Pick the correct backend-selected active theme (#9357) 2021-06-03 22:48:52 +02:00
Bram KragtenandGitHub 509c5b497a Disable babel compact option (#9335) 2021-06-03 12:34:30 -07:00
Joakim SørensenandGitHub e00bcc9f48 Better exit navigation for my-ingress (#9342) 2021-06-03 10:01:12 -07:00
Joakim SørensenandGitHub bdef9fd040 Add missing media folder to snapshot (#9341) 2021-06-03 10:21:04 +02:00
GitHub Action c956491ec5 Translation update 2021-06-03 03:48:04 +00:00
Bram KragtenandGitHub 68bc549d6a Use HLS light build (#9338)
* Use HLS light build

* Bump hls, backBufferLength
2021-06-02 18:34:18 +02:00
Bram KragtenandGitHub 9c64eafc21 Fix ZHA visualization (#9337) 2021-06-02 18:33:55 +02:00
Bram KragtenandGitHub b05e86d442 Fix noUnderline in search input (#9339) 2021-06-02 18:33:44 +02:00
Bram Kragten fe5f9576c6 Fix dev 2021-06-02 10:11:29 +02:00
1b282b65b7 Add QR code to long lived access tokens dialog (#8948)
* Add QR code to long lived access tokens dialog

* Apply suggestions from code review

Co-authored-by: Bram Kragten <[email protected]>

* Further changes from code review

Co-authored-by: Bram Kragten <[email protected]>
2021-06-02 09:59:22 +02:00
GitHub Action e49664bad3 Translation update 2021-06-02 04:10:37 +00:00
Bram Kragten 2a30b55a43 Bumped version to 20210601.1 2021-06-01 21:30:51 +02:00
9d0b20adce Add support for system options v2 (#9332)
* Add support for system options v2

* Update src/dialogs/config-entry-system-options/dialog-config-entry-system-options.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update dialog-config-entry-system-options.ts

Co-authored-by: Bram Kragten <[email protected]>
2021-06-01 12:22:25 -07:00
Bram KragtenandGitHub acd5e1c081 Fix back button too wide on mobile (#9333) 2021-06-01 12:12:00 -07:00
Bram KragtenandGitHub cc1c5e45b2 Display error when enabling/disabling config entries (#9325) 2021-06-01 21:03:00 +02:00
Bram KragtenandGitHub 038199c447 Change the type of debounce, use arrow functions (#9328) 2021-06-01 11:53:45 -07:00
Bram KragtenandGitHub 8a1eab7ceb Cleanup virtualizer styles (#9327) 2021-06-01 11:51:30 -07:00
bc5bd35448 Filter adapters with information from the Supervisor (#9322)
Co-authored-by: Bram Kragten <[email protected]>
2021-06-01 20:12:54 +02:00
Bram KragtenandGitHub 1795fd56b7 Don't rotate chart axis labels (#9329) 2021-06-01 10:28:03 -07:00
105 changed files with 3871 additions and 2439 deletions
+4 -2
View File
@@ -6,7 +6,6 @@ on:
- published
env:
WHEELS_TAG: 3.8-alpine3.12
PYTHON_VERSION: 3.8
NODE_VERSION: 12.1
@@ -64,6 +63,9 @@ jobs:
strategy:
matrix:
arch: ["aarch64", "armhf", "armv7", "amd64", "i386"]
tag:
- "3.8-alpine3.12"
- "3.9-alpine3.13"
steps:
- name: Download requirements.txt
uses: actions/download-artifact@v2
@@ -73,7 +75,7 @@ jobs:
- name: Build wheels
uses: home-assistant/wheels@master
with:
tag: ${{ env.WHEELS_TAG }}
tag: ${{ matrix.tag }}
arch: ${{ matrix.arch }}
wheels-host: ${{ secrets.WHEELS_HOST }}
wheels-key: ${{ secrets.WHEELS_KEY }}
+1 -6
View File
@@ -52,6 +52,7 @@ module.exports.terserOptions = (latestBuild) => ({
module.exports.babelOptions = ({ latestBuild }) => ({
babelrc: false,
compact: false,
presets: [
!latestBuild && [
"@babel/preset-env",
@@ -79,12 +80,6 @@ module.exports.babelOptions = ({ latestBuild }) => ({
].filter(Boolean),
});
// Are already ES5, cause warnings when babelified.
module.exports.babelExclude = () => [
require.resolve("@mdi/js/mdi.js"),
require.resolve("hls.js"),
];
const outputPath = (outputRoot, latestBuild) =>
path.resolve(outputRoot, latestBuild ? "frontend_latest" : "frontend_es5");
-1
View File
@@ -57,7 +57,6 @@ const createRollupConfig = ({
babel({
...bundle.babelOptions({ latestBuild }),
extensions,
exclude: bundle.babelExclude(),
babelHelpers: isWDS ? "inline" : "bundled",
}),
string({
-1
View File
@@ -47,7 +47,6 @@ const createWebpackConfig = ({
rules: [
{
test: /\.m?js$|\.ts$/,
exclude: bundle.babelExclude(),
use: {
loader: "babel-loader",
options: bundle.babelOptions({ latestBuild }),
+3 -8
View File
@@ -31,10 +31,8 @@ const createConfigEntry = (
supports_options: false,
supports_unload: true,
disabled_by: null,
system_options: {
disable_new_entities: false,
disable_polling: false,
},
pref_disable_new_entities: false,
pref_disable_polling: false,
reason: null,
...override,
});
@@ -68,10 +66,7 @@ const optionsFlowEntry = createConfigEntry("Options Flow", {
supports_options: true,
});
const disabledPollingEntry = createConfigEntry("Disabled Polling", {
system_options: {
disable_new_entities: false,
disable_polling: true,
},
pref_disable_polling: true,
});
const setupErrorEntry = createConfigEntry("Setup Error", {
state: "setup_error",
@@ -29,7 +29,6 @@ class SupervisorFormfieldLabel extends LitElement {
static get styles(): CSSResultGroup {
return css`
:host {
cursor: pointer;
display: flex;
align-items: center;
}
@@ -5,6 +5,7 @@ import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version";
import { formatDate } from "../../../src/common/datetime/format_date";
import { formatDateTime } from "../../../src/common/datetime/format_date_time";
import { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-radio";
@@ -44,6 +45,9 @@ const _computeFolders = (folders): CheckboxItem[] => {
if (folders.includes("share")) {
list.push({ slug: "share", name: "Share", checked: false });
}
if (folders.includes("media")) {
list.push({ slug: "media", name: "Media", checked: false });
}
if (folders.includes("addons/local")) {
list.push({ slug: "addons/local", name: "Local add-ons", checked: false });
}
@@ -64,6 +68,8 @@ const _computeAddons = (addons): AddonCheckboxItem[] =>
export class SupervisorSnapshotContent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public localize?: LocalizeFunc;
@property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public snapshot?: HassioSnapshotDetail;
@@ -78,10 +84,14 @@ export class SupervisorSnapshotContent extends LitElement {
@property({ type: Boolean }) public snapshotHasPassword = false;
@property({ type: Boolean }) public onboarding = false;
@property() public snapshotName = "";
@property() public snapshotPassword = "";
@property() public confirmSnapshotPassword = "";
public willUpdate(changedProps) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
@@ -101,8 +111,12 @@ export class SupervisorSnapshotContent extends LitElement {
}
}
private _localize = (string: string) =>
this.supervisor?.localize(`snapshot.${string}`) ||
this.localize!(`ui.panel.page-onboarding.restore.${string}`);
protected render(): TemplateResult {
if (!this.supervisor) {
if (!this.onboarding && !this.supervisor) {
return html``;
}
const foldersSection =
@@ -114,14 +128,16 @@ export class SupervisorSnapshotContent extends LitElement {
${this.snapshot
? html`<div class="details">
${this.snapshot.type === "full"
? this.supervisor.localize("snapshot.full_snapshot")
: this.supervisor.localize("snapshot.partial_snapshot")}
? this._localize("full_snapshot")
: this._localize("partial_snapshot")}
(${Math.ceil(this.snapshot.size * 10) / 10 + " MB"})<br />
${formatDateTime(new Date(this.snapshot.date), this.hass.locale)}
${this.hass
? formatDateTime(new Date(this.snapshot.date), this.hass.locale)
: this.snapshot.date}
</div>`
: html`<paper-input
name="snapshotName"
.label=${this.supervisor.localize("snapshot.name")}
.label=${this.supervisor?.localize("snapshot.name") || "Name"}
.value=${this.snapshotName}
@value-changed=${this._handleTextValueChanged}
>
@@ -129,13 +145,11 @@ export class SupervisorSnapshotContent extends LitElement {
${!this.snapshot || this.snapshot.type === "full"
? html`<div class="sub-header">
${!this.snapshot
? this.supervisor.localize("snapshot.type")
: this.supervisor.localize("snapshot.select_type")}
? this._localize("type")
: this._localize("select_type")}
</div>
<div class="snapshot-types">
<ha-formfield
.label=${this.supervisor.localize("snapshot.full_snapshot")}
>
<ha-formfield .label=${this._localize("full_snapshot")}>
<ha-radio
@change=${this._handleRadioValueChanged}
value="full"
@@ -144,9 +158,7 @@ export class SupervisorSnapshotContent extends LitElement {
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.supervisor!.localize("snapshot.partial_snapshot")}
>
<ha-formfield .label=${this._localize("partial_snapshot")}>
<ha-radio
@change=${this._handleRadioValueChanged}
value="partial"
@@ -157,9 +169,9 @@ export class SupervisorSnapshotContent extends LitElement {
</ha-formfield>
</div>`
: ""}
${this.snapshot && this.snapshotType === "partial"
? html`
${this.snapshot.homeassistant
${this.snapshotType === "partial"
? html`<div class="partial-picker">
${this.snapshot && this.snapshot.homeassistant
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
@@ -179,15 +191,11 @@ export class SupervisorSnapshotContent extends LitElement {
</ha-formfield>
`
: ""}
`
: ""}
${this.snapshotType === "partial"
? html`
${foldersSection?.templates.length
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this.supervisor.localize("snapshot.folders")}
.label=${this._localize("folders")}
.iconPath=${mdiFolder}
>
</supervisor-formfield-label>`}
@@ -207,7 +215,7 @@ export class SupervisorSnapshotContent extends LitElement {
? html`
<ha-formfield
.label=${html`<supervisor-formfield-label
.label=${this.supervisor.localize("snapshot.addons")}
.label=${this._localize("addons")}
.iconPath=${mdiPuzzle}
>
</supervisor-formfield-label>`}
@@ -223,29 +231,44 @@ export class SupervisorSnapshotContent extends LitElement {
<div class="section-content">${addonsSection.templates}</div>
`
: ""}
`
</div> `
: ""}
${this.snapshotType === "partial" &&
(!this.snapshot || this.snapshotHasPassword)
? html`<hr />`
: ""}
${!this.snapshot
? html`<ha-formfield
.label=${this.supervisor.localize("snapshot.password_protection")}
class="password"
.label=${this._localize("password_protection")}
>
<ha-checkbox
.checked=${this.snapshotHasPassword}
@change=${this._toggleHasPassword}
>
</ha-checkbox
></ha-formfield>`
</ha-checkbox>
</ha-formfield>`
: ""}
${this.snapshotHasPassword
? html`
<paper-input
.label=${this.supervisor.localize("snapshot.password")}
.label=${this._localize("password")}
type="password"
name="snapshotPassword"
.value=${this.snapshotPassword}
@value-changed=${this._handleTextValueChanged}
>
</paper-input>
${!this.snapshot
? html` <paper-input
.label=${this.supervisor?.localize("confirm_password")}
type="password"
name="confirmSnapshotPassword"
.value=${this.confirmSnapshotPassword}
@value-changed=${this._handleTextValueChanged}
>
</paper-input>`
: ""}
`
: ""}
`;
@@ -253,21 +276,24 @@ export class SupervisorSnapshotContent extends LitElement {
static get styles(): CSSResultGroup {
return css`
ha-checkbox {
--mdc-checkbox-touch-target-size: 16px;
.partial-picker ha-formfield {
display: block;
margin: 4px 12px 8px 0;
}
ha-formfield {
display: contents;
.partial-picker ha-checkbox {
--mdc-checkbox-touch-target-size: 32px;
}
.partial-picker {
display: block;
margin: 0px -6px;
}
supervisor-formfield-label {
display: inline-flex;
align-items: center;
}
paper-input[type="password"] {
display: block;
margin: 4px 0 4px 16px;
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
}
.details {
color: var(--secondary-text-color);
@@ -275,13 +301,15 @@ export class SupervisorSnapshotContent extends LitElement {
.section-content {
display: flex;
flex-direction: column;
margin-left: 16px;
margin-left: 30px;
}
.security {
margin-top: 16px;
ha-formfield.password {
display: block;
margin: 0 -14px -16px;
}
.snapshot-types {
display: flex;
margin-left: -13px;
}
.sub-header {
margin-top: 8px;
@@ -300,6 +328,9 @@ export class SupervisorSnapshotContent extends LitElement {
if (this.snapshotHasPassword) {
data.password = this.snapshotPassword;
if (!this.snapshot) {
data.confirm_password = this.confirmSnapshotPassword;
}
}
if (this.snapshotType === "full") {
@@ -331,7 +362,7 @@ export class SupervisorSnapshotContent extends LitElement {
const addons =
section === "addons"
? new Map(
this.supervisor!.addon.addons.map((item) => [item.slug, item])
this.supervisor?.addon.addons.map((item) => [item.slug, item])
)
: undefined;
let checkedItems = 0;
@@ -341,6 +372,7 @@ export class SupervisorSnapshotContent extends LitElement {
.label=${item.name}
.iconPath=${section === "addons" ? mdiPuzzle : mdiFolder}
.imageUrl=${section === "addons" &&
!this.onboarding &&
atLeastVersion(this.hass.config.version, 0, 105) &&
addons?.get(item.slug)?.icon
? `/api/hassio/addons/${item.slug}/icon`
+20 -1
View File
@@ -220,10 +220,29 @@ export class HassioUpdate extends LitElement {
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
try {
await updateCore(this.hass);
} catch (err) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Home Assistant Core"
),
text: extractApiErrorMessage(err),
});
return;
}
}
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
fireEvent(this, "supervisor-applying-update", {
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
});
}
static get styles(): CSSResultGroup {
+194
View File
@@ -0,0 +1,194 @@
import { mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/common/search/search-input";
import { compare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { dump } from "../../../../src/resources/js-yaml-dump";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
const _filterDevices = memoizeOne(
(showAdvanced: boolean, hardware: HassioHardwareInfo, filter: string) =>
hardware.devices
.filter(
(device) =>
(showAdvanced ||
["tty", "gpio", "input"].includes(device.subsystem)) &&
(device.by_id?.toLowerCase().includes(filter) ||
device.name.toLowerCase().includes(filter) ||
device.dev_path.toLocaleLowerCase().includes(filter) ||
JSON.stringify(device.attributes)
.toLocaleLowerCase()
.includes(filter))
)
.sort((a, b) => compare(a.name, b.name))
);
@customElement("dialog-hassio-hardware")
class HassioHardwareDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _dialogParams?: HassioHardwareDialogParams;
@state() private _filter?: string;
public showDialog(params: HassioHardwareDialogParams) {
this._dialogParams = params;
}
public closeDialog() {
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._dialogParams) {
return html``;
}
const devices = _filterDevices(
this.hass.userData?.showAdvanced || false,
this._dialogParams.hardware,
(this._filter || "").toLowerCase()
);
return html`
<ha-dialog
open
scrimClickAction
hideActions
@closed=${this.closeDialog}
.heading=${true}
>
<div class="header" slot="heading">
<h2>
${this._dialogParams.supervisor.localize("dialog.hardware.title")}
</h2>
<mwc-icon-button dialogAction="close">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
<search-input
autofocus
no-label-float
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
.label=${this._dialogParams.supervisor.localize(
"dialog.hardware.search"
)}
>
</search-input>
</div>
${devices.map(
(device) =>
html`<ha-expansion-panel
.header=${device.name}
.secondary=${device.by_id || undefined}
outlined
>
<div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.subsystem"
)}:
</span>
<span>${device.subsystem}</span>
</div>
<div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.device_path"
)}:
</span>
<code>${device.dev_path}</code>
</div>
${device.by_id
? html` <div class="device-property">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.id"
)}:
</span>
<code>${device.by_id}</code>
</div>`
: ""}
<div class="attributes">
<span>
${this._dialogParams!.supervisor.localize(
"dialog.hardware.attributes"
)}:
</span>
<pre>${dump(device.attributes, { indent: 2 })}</pre>
</div>
</ha-expansion-panel>`
)}
</ha-dialog>
`;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
mwc-icon-button {
position: absolute;
right: 16px;
top: 10px;
text-decoration: none;
color: var(--primary-text-color);
}
h2 {
margin: 18px 42px 0 18px;
color: var(--primary-text-color);
}
ha-expansion-panel {
margin: 4px 0;
}
pre,
code {
background-color: var(--markdown-code-background-color, none);
border-radius: 3px;
}
pre {
padding: 16px;
overflow: auto;
line-height: 1.45;
font-family: var(--code-font-family, monospace);
}
code {
font-size: 85%;
padding: 0.2em 0.4em;
}
search-input {
margin: 0 16px;
display: block;
}
.device-property {
display: flex;
justify-content: space-between;
}
.attributes {
margin-top: 12px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-hassio-hardware": HassioHardwareDialog;
}
}
@@ -0,0 +1,19 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioHardwareDialogParams {
supervisor: Supervisor;
hardware: HassioHardwareInfo;
}
export const showHassioHardwareDialog = (
element: HTMLElement,
dialogParams: HassioHardwareDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-hardware",
dialogImport: () => import("./dialog-hassio-hardware"),
dialogParams,
});
};
@@ -95,16 +95,25 @@ class HassioCreateSnapshotDialog extends LitElement {
this._creatingSnapshot = true;
this._error = "";
if (
this._snapshotContent.snapshotHasPassword &&
!this._snapshotContent.snapshotPassword.length
) {
if (snapshotDetails.password && !snapshotDetails.password.length) {
this._error = this._dialogParams!.supervisor.localize(
"snapshot.enter_password"
);
this._creatingSnapshot = false;
return;
}
if (
snapshotDetails.password &&
snapshotDetails.password !== snapshotDetails.confirm_password
) {
this._error = this._dialogParams!.supervisor.localize(
"snapshot.passwords_not_matching"
);
this._creatingSnapshot = false;
return;
}
delete snapshotDetails.confirm_password;
try {
if (this._snapshotContent.snapshotType === "full") {
@@ -1,13 +1,13 @@
import { ActionDetail } from "@material/mwc-list";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import { mdiClose, mdiDotsVertical } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { slugify } from "../../../../src/common/string/slugify";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-button-menu";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-svg-icon";
import { getSignedPath } from "../../../../src/data/auth";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
@@ -22,6 +22,7 @@ import {
import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { fileDownload } from "../../../../src/util/file_download";
import "../../components/supervisor-snapshot-content";
import type { SupervisorSnapshotContent } from "../../components/supervisor-snapshot-content";
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
@@ -66,14 +67,24 @@ class HassioSnapshotDialog
open
scrimClickAction
@closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this._computeName)}
.heading=${true}
>
<div slot="heading">
<ha-header-bar>
<span slot="title">${this._snapshot.name}</span>
<mwc-icon-button slot="actionItems" dialogAction="cancel">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
</ha-header-bar>
</div>
${this._restoringSnapshot
? html` <ha-circular-progress active></ha-circular-progress>`
: html`<supervisor-snapshot-content
.hass=${this.hass}
.supervisor=${this._dialogParams.supervisor}
.snapshot=${this._snapshot}
.onboarding=${this._dialogParams.onboarding || false}
.localize=${this._dialogParams.localize}
>
</supervisor-snapshot-content>`}
${this._error ? html`<p class="error">Error: ${this._error}</p>` : ""}
@@ -86,18 +97,20 @@ class HassioSnapshotDialog
Restore
</mwc-button>
<ha-button-menu
fixed
slot="primaryAction"
@action=${this._handleMenuAction}
@closed=${(ev: Event) => ev.stopPropagation()}
>
<mwc-icon-button slot="trigger" alt="menu">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>Download Snapshot</mwc-list-item>
<mwc-list-item class="error">Delete Snapshot</mwc-list-item>
</ha-button-menu>
${!this._dialogParams.onboarding
? html`<ha-button-menu
fixed
slot="primaryAction"
@action=${this._handleMenuAction}
@closed=${(ev: Event) => ev.stopPropagation()}
>
<mwc-icon-button slot="trigger" alt="menu">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>Download Snapshot</mwc-list-item>
<mwc-list-item class="error">Delete Snapshot</mwc-list-item>
</ha-button-menu>`
: ""}
</ha-dialog>
`;
}
@@ -114,6 +127,12 @@ class HassioSnapshotDialog
display: block;
text-align: center;
}
ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0;
display: block;
}
`,
];
}
@@ -288,12 +307,11 @@ class HassioSnapshotDialog
}
}
const a = document.createElement("a");
a.href = signedPath.path;
a.download = `home_assistant_snapshot_${slugify(this._computeName)}.tar`;
this.shadowRoot!.appendChild(a);
a.click();
this.shadowRoot!.removeChild(a);
fileDownload(
this,
signedPath.path,
`home_assistant_snapshot_${slugify(this._computeName)}.tar`
);
}
private get _computeName() {
@@ -1,4 +1,5 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LocalizeFunc } from "../../../../src/common/translations/localize";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioSnapshotDialogParams {
@@ -6,6 +7,7 @@ export interface HassioSnapshotDialogParams {
onDelete?: () => void;
onboarding?: boolean;
supervisor?: Supervisor;
localize?: LocalizeFunc;
}
export const showHassioSnapshotDialog = (
@@ -7,10 +7,7 @@ import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../../src/data/hassio/common";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@@ -153,16 +150,7 @@ class DialogSupervisorUpdate extends LitElement {
}
this._action = "update";
try {
await this._dialogParams!.updateHandler!();
} catch (err) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
}
this._action = null;
return;
}
await this._dialogParams!.updateHandler!();
this.closeDialog();
}
@@ -1,17 +1,18 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioPartialSnapshotCreateParams } from "../../../../src/data/hassio/snapshot";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface SupervisorDialogSupervisorUpdateParams {
supervisor: Supervisor;
name: string;
version: string;
snapshotParams: any;
snapshotParams: HassioPartialSnapshotCreateParams;
updateHandler: () => Promise<void>;
}
export const showDialogSupervisorUpdate = (
element: HTMLElement,
dialogParams: SupervisorDialogSupervisorUpdateParams
dialogParams: Partial<SupervisorDialogSupervisorUpdateParams>
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-supervisor-update",
+36 -2
View File
@@ -1,12 +1,26 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import {
Supervisor,
supervisorApplyUpdateDetails,
supervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router";
declare global {
interface HASSDomEvents {
"supervisor-applying-update": supervisorApplyUpdateDetails;
}
}
@customElement("hassio-panel")
class HassioPanel extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -17,6 +31,16 @@ class HassioPanel extends LitElement {
@property({ attribute: false }) public route!: Route;
@state() private _applyingUpdate?: supervisorApplyUpdateDetails;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._applyingUpdate = undefined;
this.addEventListener("supervisor-applying-update", (ev) => {
this._applyingUpdate = ev.detail;
});
}
protected render(): TemplateResult {
if (!this.hass) {
return html`<hass-loading-screen></hass-loading-screen>`;
@@ -29,6 +53,16 @@ class HassioPanel extends LitElement {
) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
if (this._applyingUpdate !== undefined) {
return html`<hass-loading-screen no-toolbar>
${this.supervisor.localize("dialog.update.updating", {
name: this._applyingUpdate.name,
version: this._applyingUpdate.version,
})}
</hass-loading-screen>`;
}
return html`
<hassio-panel-router
.hass=${this.hass}
+10 -3
View File
@@ -97,16 +97,23 @@ class HassioIngressView extends LitElement {
title: requestedAddon,
});
await nextRender();
history.back();
navigate("/hassio/store", { replace: true });
return;
}
if (!addonInfo.ingress) {
if (!addonInfo.version) {
await showAlertDialog(this, {
text: this.supervisor.localize("my.error_addon_not_installed"),
title: addonInfo.name,
});
await nextRender();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
} else if (!addonInfo.ingress) {
await showAlertDialog(this, {
text: this.supervisor.localize("my.error_addon_no_ingress"),
title: addonInfo.name,
});
await nextRender();
history.back();
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
} else {
navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true });
}
+22 -2
View File
@@ -11,6 +11,7 @@ import {
extractApiErrorMessage,
fetchHassioStats,
HassioStats,
ignoreSupervisorError,
} from "../../../src/data/hassio/common";
import { restartCore, updateCore } from "../../../src/data/supervisor/core";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
@@ -150,7 +151,7 @@ class HassioCoreInfo extends LitElement {
title: this.supervisor.localize(
"common.failed_to_restart_name",
"name",
"Home AssistantCore"
"Home Assistant Core"
),
text: extractApiErrorMessage(err),
});
@@ -175,10 +176,29 @@ class HassioCoreInfo extends LitElement {
}
private async _updateCore(): Promise<void> {
await updateCore(this.hass);
try {
await updateCore(this.hass);
} catch (err) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Home Assistant Core"
),
text: extractApiErrorMessage(err),
});
return;
}
}
fireEvent(this, "supervisor-collection-refresh", {
collection: "core",
});
fireEvent(this, "supervisor-applying-update", {
name: "Home Assistant Core",
version: this.supervisor.core.version_latest,
});
}
static get styles(): CSSResultGroup {
+6 -8
View File
@@ -2,7 +2,6 @@ import "@material/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
import { dump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -41,8 +40,8 @@ import {
roundWithOneDecimal,
} from "../../../src/util/calculate";
import "../components/supervisor-metric";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import { showNetworkDialog } from "../dialogs/network/show-dialog-network";
import { showHassioHardwareDialog } from "../dialogs/hardware/show-dialog-hassio-hardware";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-host-info")
@@ -229,20 +228,19 @@ class HassioHostInfo extends LitElement {
}
private async _showHardware(): Promise<void> {
let hardware;
try {
const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: this.supervisor.localize("system.host.hardware"),
content: `<pre>${dump(content, { indent: 2 })}</pre>`,
});
hardware = await fetchHassioHardwareInfo(this.hass);
} catch (err) {
showAlertDialog(this, {
await showAlertDialog(this, {
title: this.supervisor.localize(
"system.host.failed_to_get_hardware_list"
),
text: extractApiErrorMessage(err),
});
return;
}
showHassioHardwareDialog(this, { supervisor: this.supervisor, hardware });
}
private async _hostReboot(ev: CustomEvent): Promise<void> {
-1
View File
@@ -1,5 +1,4 @@
module.exports = {
"*.ts": () => "tsc -p tsconfig.json",
"*.{js,ts}": "eslint --fix",
"!(/translations)*.{js,ts,json,css,md,html}": "prettier --write",
};
+1 -3
View File
@@ -66,9 +66,7 @@
"@polymer/iron-autogrow-textarea": "^3.0.1",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",
"@polymer/iron-image": "^3.0.1",
"@polymer/iron-input": "^3.0.1",
"@polymer/iron-label": "^3.0.1",
"@polymer/iron-overlay-behavior": "^3.0.2",
"@polymer/iron-resizable-behavior": "^3.0.1",
"@polymer/paper-checkbox": "^3.1.0",
@@ -108,7 +106,7 @@
"fecha": "^4.2.0",
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.0.4",
"hls.js": "^1.0.5",
"home-assistant-js-websocket": "^5.10.0",
"idb-keyval": "^5.0.5",
"intl-messageformat": "^9.6.16",
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20210601.0",
version="20210603.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
+1 -6
View File
@@ -6,8 +6,7 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async (
mapElement: HTMLElement,
darkMode?: boolean,
draw = false
darkMode?: boolean
): Promise<[Map, LeafletModuleType, TileLayer]> => {
if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element");
@@ -17,10 +16,6 @@ export const setupLeafletMap = async (
.default as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
if (draw) {
await import("leaflet-draw");
}
const map = Leaflet.map(mapElement);
const style = document.createElement("link");
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
+1 -3
View File
@@ -89,8 +89,6 @@ export const domainIcon = (
}
// eslint-disable-next-line
console.warn(
"Unable to find icon for domain " + domain + " (" + stateObj + ")"
);
console.warn(`Unable to find icon for domain ${domain}`);
return DEFAULT_DOMAIN_ICON;
};
+23 -12
View File
@@ -1,9 +1,16 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMagnify } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, query } from "lit/decorators";
import "../../components/ha-svg-icon";
import { fireEvent } from "../dom/fire_event";
@@ -27,18 +34,11 @@ class SearchInput extends LitElement {
this.shadowRoot!.querySelector("paper-input")!.focus();
}
@query("paper-input", true) private _input!: PaperInputElement;
protected render(): TemplateResult {
return html`
<style>
.no-underline:not(.focused) {
--paper-input-container-underline: {
display: none;
height: 0;
}
}
</style>
<paper-input
class=${classMap({ "no-underline": this.noUnderline })}
.autofocus=${this.autofocus}
.label=${this.label || "Search"}
.value=${this.filter}
@@ -62,6 +62,17 @@ class SearchInput extends LitElement {
`;
}
protected updated(changedProps: PropertyValues) {
if (
changedProps.has("noUnderline") &&
(this.noUnderline || changedProps.get("noUnderline") !== undefined)
) {
(this._input.inputElement!.parentElement!.shadowRoot!.querySelector(
"div.unfocused-line"
) as HTMLElement).style.display = this.noUnderline ? "none" : "block";
}
}
private async _filterChanged(value: string) {
fireEvent(this, "value-changed", { value: String(value) });
}
+11 -15
View File
@@ -4,29 +4,25 @@
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// eslint-disable-next-line: ban-types
export const debounce = <T extends (...args) => unknown>(
func: T,
wait,
export const debounce = <T extends any[]>(
func: (...args: T) => void,
wait: number,
immediate = false
): T => {
let timeout;
// @ts-ignore
return function (...args) {
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-this-alias
const context = this;
) => {
let timeout: number | undefined;
return (...args: T): void => {
const later = () => {
timeout = null;
timeout = undefined;
if (!immediate) {
func.apply(context, args);
func(...args);
}
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
timeout = window.setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
func(...args);
}
};
};
+4 -6
View File
@@ -246,7 +246,7 @@ export class HaDataTable extends LitElement {
aria-rowcount=${this._filteredData.length + 1}
style=${styleMap({
height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 57}px`
? `${(this._filteredData.length || 1) * 53 + 53}px`
: `calc(100% - ${this._headerHeight}px)`,
})}
>
@@ -919,13 +919,11 @@ export class HaDataTable extends LitElement {
color: var(--secondary-text-color);
}
.scroller {
display: flex;
position: relative;
contain: strict;
height: calc(100% - 57px);
}
.mdc-data-table__table:not(.auto-height) .scroller {
overflow: auto;
.mdc-data-table__table.auto-height .scroller {
overflow-y: hidden !important;
}
.grows {
flex-grow: 1;
+16 -1
View File
@@ -14,12 +14,17 @@ class HaExpansionPanel extends LitElement {
@property() header?: string;
@property() secondary?: string;
@query(".container") private _container!: HTMLDivElement;
protected render(): TemplateResult {
return html`
<div class="summary" @click=${this._toggleContainer}>
<slot name="header">${this.header}</slot>
<slot class="header" name="header">
${this.header}
<slot class="secondary" name="secondary">${this.secondary}</slot>
</slot>
<ha-svg-icon
.path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}"
@@ -106,6 +111,16 @@ class HaExpansionPanel extends LitElement {
.container.expanded {
height: auto;
}
.header {
display: block;
}
.secondary {
display: block;
color: var(--secondary-text-color);
font-size: 12px;
}
`;
}
}
+3 -3
View File
@@ -7,14 +7,14 @@ import { afterNextRender } from "../common/util/render-status";
import { FrontendLocaleData } from "../data/translation";
import { getValueInPercentage, normalize } from "../util/calculate";
// Workaround for https://github.com/home-assistant/frontend/issues/6467
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const getAngle = (value: number, min: number, max: number) => {
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
return (percentage * 180) / 100;
};
// Workaround for https://github.com/home-assistant/frontend/issues/6467
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
@customElement("ha-gauge")
export class Gauge extends LitElement {
@property({ type: Number }) public min = 0;
+9 -3
View File
@@ -13,6 +13,11 @@ import { nextRender } from "../common/util/render-status";
import { getExternalConfig } from "../external_app/external_config";
import type { HomeAssistant } from "../types";
type HlsLite = Omit<
HlsType,
"subtitleTrackController" | "audioTrackController" | "emeController"
>;
@customElement("ha-hls-player")
class HaHLSPlayer extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -39,7 +44,7 @@ class HaHLSPlayer extends LitElement {
@state() private _attached = false;
private _hlsPolyfillInstance?: HlsType;
private _hlsPolyfillInstance?: HlsLite;
private _useExoPlayer = false;
@@ -103,7 +108,8 @@ class HaHLSPlayer extends LitElement {
const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url);
const Hls = (await import("hls.js")).default;
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.min.js"))
.default;
let hlsSupported = Hls.isSupported();
if (!hlsSupported) {
@@ -182,7 +188,7 @@ class HaHLSPlayer extends LitElement {
url: string
) {
const hls = new Hls({
liveBackBufferLength: 60,
backBufferLength: 60,
fragLoadingTimeOut: 30000,
manifestLoadingTimeOut: 30000,
levelLoadingTimeOut: 30000,
+69
View File
@@ -0,0 +1,69 @@
import { LitElement, html, css } from "lit";
import { property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
class HaEntityMarker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "entity-id" }) public entityId?: string;
@property({ attribute: "entity-name" }) public entityName?: string;
@property({ attribute: "entity-picture" }) public entityPicture?: string;
@property({ attribute: "entity-color" }) public entityColor?: string;
protected render() {
return html`
<div
class="marker"
style=${styleMap({ "border-color": this.entityColor })}
@click=${this._badgeTap}
>
${this.entityPicture
? html`<div
class="entity-picture"
style=${styleMap({
"background-image": `url(${this.entityPicture})`,
})}
></div>`
: this.entityName}
</div>
`;
}
private _badgeTap(ev: Event) {
ev.stopPropagation();
if (this.entityId) {
fireEvent(this, "hass-more-info", { entityId: this.entityId });
}
}
static get styles() {
return css`
.marker {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
overflow: hidden;
width: 48px;
height: 48px;
font-size: var(--ha-marker-font-size, 1.5em);
border-radius: 50%;
border: 1px solid var(--ha-marker-color, var(--primary-color));
color: var(--primary-text-color);
background-color: var(--card-background-color);
}
.entity-picture {
background-size: cover;
height: 100%;
width: 100%;
}
`;
}
}
customElements.define("ha-entity-marker", HaEntityMarker);
-299
View File
@@ -1,299 +0,0 @@
import {
Circle,
DivIcon,
DragEndEvent,
LatLng,
LeafletMouseEvent,
Map,
Marker,
TileLayer,
} from "leaflet";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import {
LeafletModuleType,
replaceTileLayer,
setupLeafletMap,
} from "../../common/dom/setup-leaflet-map";
import { nextRender } from "../../common/util/render-status";
import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
@customElement("ha-location-editor")
class LocationEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Array }) public location?: [number, number];
@property({ type: Number }) public radius?: number;
@property() public radiusColor?: string;
@property() public icon?: string;
@property({ type: Boolean }) public darkMode?: boolean;
public fitZoom = 16;
private _iconEl?: DivIcon;
private _ignoreFitToMap?: [number, number];
// eslint-disable-next-line
private Leaflet?: LeafletModuleType;
private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _locationMarker?: Marker | Circle;
public fitMap(): void {
if (!this._leafletMap || !this.location) {
return;
}
if (this._locationMarker && "getBounds" in this._locationMarker) {
this._leafletMap.fitBounds(this._locationMarker.getBounds());
} else {
this._leafletMap.setView(this.location, this.fitZoom);
}
this._ignoreFitToMap = this.location;
}
protected render(): TemplateResult {
return html` <div id="map"></div> `;
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._initMap();
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
// Still loading.
if (!this.Leaflet) {
return;
}
if (changedProps.has("location")) {
this._updateMarker();
if (
this.location &&
(!this._ignoreFitToMap ||
this._ignoreFitToMap[0] !== this.location[0] ||
this._ignoreFitToMap[1] !== this.location[1])
) {
this.fitMap();
}
}
if (changedProps.has("radius")) {
this._updateRadius();
}
if (changedProps.has("radiusColor")) {
this._updateRadiusColor();
}
if (changedProps.has("icon")) {
this._updateIcon();
}
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return;
}
if (!this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes.darkMode
);
}
}
private get _mapEl(): HTMLDivElement {
return this.shadowRoot!.querySelector("div")!;
}
private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
this.darkMode ?? this.hass.themes.darkMode,
Boolean(this.radius)
);
this._leafletMap.addEventListener(
"click",
// @ts-ignore
(ev: LeafletMouseEvent) => this._locationUpdated(ev.latlng)
);
this._updateIcon();
this._updateMarker();
this.fitMap();
this._leafletMap.invalidateSize();
}
private _locationUpdated(latlng: LatLng) {
let longitude = latlng.lng;
if (Math.abs(longitude) > 180.0) {
// Normalize longitude if map provides values beyond -180 to +180 degrees.
longitude = (((longitude % 360.0) + 540.0) % 360.0) - 180.0;
}
this.location = this._ignoreFitToMap = [latlng.lat, longitude];
fireEvent(this, "change", undefined, { bubbles: false });
}
private _radiusUpdated() {
this._ignoreFitToMap = this.location;
this.radius = (this._locationMarker as Circle).getRadius();
fireEvent(this, "change", undefined, { bubbles: false });
}
private _updateIcon() {
if (!this.icon) {
this._iconEl = undefined;
return;
}
// create icon
let iconHTML = "";
const el = document.createElement("ha-icon");
el.setAttribute("icon", this.icon);
iconHTML = el.outerHTML;
this._iconEl = this.Leaflet!.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: "light leaflet-edit-move",
});
this._setIcon();
}
private _setIcon() {
if (!this._locationMarker || !this._iconEl) {
return;
}
if (!this.radius) {
(this._locationMarker as Marker).setIcon(this._iconEl);
return;
}
// @ts-ignore
const moveMarker = this._locationMarker.editing._moveMarker;
moveMarker.setIcon(this._iconEl);
}
private _setupEdit() {
// @ts-ignore
this._locationMarker.editing.enable();
// @ts-ignore
const moveMarker = this._locationMarker.editing._moveMarker;
// @ts-ignore
const resizeMarker = this._locationMarker.editing._resizeMarkers[0];
this._setIcon();
moveMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng())
);
resizeMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._radiusUpdated(ev)
);
}
private async _updateMarker(): Promise<void> {
if (!this.location) {
if (this._locationMarker) {
this._locationMarker.remove();
this._locationMarker = undefined;
}
return;
}
if (this._locationMarker) {
this._locationMarker.setLatLng(this.location);
if (this.radius) {
// @ts-ignore
this._locationMarker.editing.disable();
await nextRender();
this._setupEdit();
}
return;
}
if (!this.radius) {
this._locationMarker = this.Leaflet!.marker(this.location, {
draggable: true,
});
this._setIcon();
this._locationMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._locationUpdated(ev.target.getLatLng())
);
this._leafletMap!.addLayer(this._locationMarker);
} else {
this._locationMarker = this.Leaflet!.circle(this.location, {
color: this.radiusColor || defaultRadiusColor,
radius: this.radius,
});
this._leafletMap!.addLayer(this._locationMarker);
this._setupEdit();
}
}
private _updateRadius(): void {
if (!this._locationMarker || !this.radius) {
return;
}
(this._locationMarker as Circle).setRadius(this.radius);
}
private _updateRadiusColor(): void {
if (!this._locationMarker || !this.radius) {
return;
}
(this._locationMarker as Circle).setStyle({ color: this.radiusColor });
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
height: 300px;
}
#map {
height: 100%;
background: inherit;
}
.leaflet-edit-move {
border-radius: 50%;
cursor: move !important;
}
.leaflet-edit-resize {
border-radius: 50%;
cursor: nesw-resize !important;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-location-editor": LocationEditor;
}
}
+102 -134
View File
@@ -3,10 +3,8 @@ import {
DivIcon,
DragEndEvent,
LatLng,
Map,
Marker,
MarkerOptions,
TileLayer,
} from "leaflet";
import {
css,
@@ -16,15 +14,13 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import {
LeafletModuleType,
replaceTileLayer,
setupLeafletMap,
} from "../../common/dom/setup-leaflet-map";
import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import type { HomeAssistant } from "../../types";
import "./ha-map";
import type { HaMap } from "./ha-map";
declare global {
// for fire event
@@ -51,38 +47,40 @@ export interface MarkerLocation {
export class HaLocationsEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public locations?: MarkerLocation[];
@property({ attribute: false }) public locations?: MarkerLocation[];
public fitZoom = 16;
@property({ type: Boolean }) public autoFit = false;
@property({ type: Number }) public zoom = 16;
@property({ type: Boolean }) public darkMode?: boolean;
@state() private _locationMarkers?: Record<string, Marker | Circle>;
@state() private _circles: Record<string, Circle> = {};
@query("ha-map", true) private map!: HaMap;
// eslint-disable-next-line
private Leaflet?: LeafletModuleType;
// eslint-disable-next-line
private _leafletMap?: Map;
constructor() {
super();
private _tileLayer?: TileLayer;
private _locationMarkers?: { [key: string]: Marker | Circle };
private _circles: Record<string, Circle> = {};
import("leaflet").then((module) => {
import("leaflet-draw").then(() => {
this.Leaflet = module.default as LeafletModuleType;
this._updateMarkers();
this.updateComplete.then(() => this.fitMap());
});
});
}
public fitMap(): void {
if (
!this._leafletMap ||
!this._locationMarkers ||
!Object.keys(this._locationMarkers).length
) {
return;
}
const bounds = this.Leaflet!.latLngBounds(
Object.values(this._locationMarkers).map((item) => item.getLatLng())
);
this._leafletMap.fitBounds(bounds.pad(0.5));
this.map.fitMap();
}
public fitMarker(id: string): void {
if (!this._leafletMap || !this._locationMarkers) {
if (!this.map.leafletMap || !this._locationMarkers) {
return;
}
const marker = this._locationMarkers[id];
@@ -90,29 +88,44 @@ export class HaLocationsEditor extends LitElement {
return;
}
if ("getBounds" in marker) {
this._leafletMap.fitBounds(marker.getBounds());
this.map.leafletMap.fitBounds(marker.getBounds());
(marker as Circle).bringToFront();
} else {
const circle = this._circles[id];
if (circle) {
this._leafletMap.fitBounds(circle.getBounds());
this.map.leafletMap.fitBounds(circle.getBounds());
} else {
this._leafletMap.setView(marker.getLatLng(), this.fitZoom);
this.map.leafletMap.setView(marker.getLatLng(), this.zoom);
}
}
}
protected render(): TemplateResult {
return html` <div id="map"></div> `;
return html`<ha-map
.hass=${this.hass}
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
.darkMode=${this.darkMode}
></ha-map>`;
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._initMap();
}
private _getLayers = memoizeOne(
(
circles: Record<string, Circle>,
markers?: Record<string, Marker | Circle>
): Array<Marker | Circle> => {
const layers: Array<Marker | Circle> = [];
Array.prototype.push.apply(layers, Object.values(circles));
if (markers) {
Array.prototype.push.apply(layers, Object.values(markers));
}
return layers;
}
);
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
// Still loading.
if (!this.Leaflet) {
@@ -122,37 +135,6 @@ export class HaLocationsEditor extends LitElement {
if (changedProps.has("locations")) {
this._updateMarkers();
}
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return;
}
if (!this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes.darkMode
);
}
}
private get _mapEl(): HTMLDivElement {
return this.shadowRoot!.querySelector("div")!;
}
private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
this.hass.themes.darkMode,
true
);
this._updateMarkers();
this.fitMap();
this._leafletMap.invalidateSize();
}
private _updateLocation(ev: DragEndEvent) {
@@ -189,21 +171,18 @@ export class HaLocationsEditor extends LitElement {
}
private _updateMarkers(): void {
if (this._locationMarkers) {
Object.values(this._locationMarkers).forEach((marker) => {
marker.remove();
});
this._locationMarkers = undefined;
Object.values(this._circles).forEach((circle) => circle.remove());
this._circles = {};
}
if (!this.locations || !this.locations.length) {
this._circles = {};
this._locationMarkers = undefined;
return;
}
this._locationMarkers = {};
const locationMarkers = {};
const circles = {};
const defaultZoneRadiusColor = getComputedStyle(this).getPropertyValue(
"--accent-color"
);
this.locations.forEach((location: MarkerLocation) => {
let icon: DivIcon | undefined;
@@ -228,45 +207,46 @@ export class HaLocationsEditor extends LitElement {
const circle = this.Leaflet!.circle(
[location.latitude, location.longitude],
{
color: location.radius_color || defaultRadiusColor,
color: location.radius_color || defaultZoneRadiusColor,
radius: location.radius,
}
);
circle.addTo(this._leafletMap!);
if (location.radius_editable || location.location_editable) {
// @ts-ignore
circle.editing.enable();
// @ts-ignore
const moveMarker = circle.editing._moveMarker;
// @ts-ignore
const resizeMarker = circle.editing._resizeMarkers[0];
if (icon) {
moveMarker.setIcon(icon);
}
resizeMarker.id = moveMarker.id = location.id;
moveMarker
.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._updateLocation(ev)
)
.addEventListener(
"click",
// @ts-ignore
(ev: MouseEvent) => this._markerClicked(ev)
);
if (location.radius_editable) {
resizeMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._updateRadius(ev)
);
} else {
resizeMarker.remove();
}
this._locationMarkers![location.id] = circle;
circle.addEventListener("add", () => {
// @ts-ignore
const moveMarker = circle.editing._moveMarker;
// @ts-ignore
const resizeMarker = circle.editing._resizeMarkers[0];
if (icon) {
moveMarker.setIcon(icon);
}
resizeMarker.id = moveMarker.id = location.id;
moveMarker
.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._updateLocation(ev)
)
.addEventListener(
"click",
// @ts-ignore
(ev: MouseEvent) => this._markerClicked(ev)
);
if (location.radius_editable) {
resizeMarker.addEventListener(
"dragend",
// @ts-ignore
(ev: DragEndEvent) => this._updateRadius(ev)
);
} else {
resizeMarker.remove();
}
});
locationMarkers[location.id] = circle;
} else {
this._circles[location.id] = circle;
circles[location.id] = circle;
}
}
if (
@@ -275,6 +255,7 @@ export class HaLocationsEditor extends LitElement {
) {
const options: MarkerOptions = {
title: location.name,
draggable: location.location_editable,
};
if (icon) {
@@ -293,13 +274,14 @@ export class HaLocationsEditor extends LitElement {
"click",
// @ts-ignore
(ev: MouseEvent) => this._markerClicked(ev)
)
.addTo(this._leafletMap!);
);
(marker as any).id = location.id;
this._locationMarkers![location.id] = marker;
locationMarkers[location.id] = marker;
}
});
this._circles = circles;
this._locationMarkers = locationMarkers;
}
static get styles(): CSSResultGroup {
@@ -308,23 +290,9 @@ export class HaLocationsEditor extends LitElement {
display: block;
height: 300px;
}
#map {
ha-map {
height: 100%;
}
.leaflet-marker-draggable {
cursor: move !important;
}
.leaflet-edit-resize {
border-radius: 50%;
cursor: nesw-resize !important;
}
.named-icon {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
}
`;
}
}
+302 -162
View File
@@ -1,13 +1,15 @@
import { Circle, Layer, Map, Marker, TileLayer } from "leaflet";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
Circle,
CircleMarker,
LatLngTuple,
Layer,
Map,
Marker,
Polyline,
TileLayer,
} from "leaflet";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
LeafletModuleType,
replaceTileLayer,
@@ -15,194 +17,324 @@ import {
} from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { debounce } from "../../common/util/debounce";
import "../../panels/map/ha-entity-marker";
import "./ha-entity-marker";
import { HomeAssistant } from "../../types";
import "../ha-icon-button";
import { installResizeObserver } from "../../panels/lovelace/common/install-resize-observer";
const getEntityId = (entity: string | HaMapEntity): string =>
typeof entity === "string" ? entity : entity.entity_id;
export interface HaMapPaths {
points: LatLngTuple[];
color?: string;
gradualOpacity?: number;
}
export interface HaMapEntity {
entity_id: string;
color: string;
}
@customElement("ha-map")
class HaMap extends LitElement {
export class HaMap extends ReactiveElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public entities?: string[];
@property({ attribute: false }) public entities?: string[] | HaMapEntity[];
@property() public darkMode?: boolean;
@property({ attribute: false }) public paths?: HaMapPaths[];
@property() public zoom?: number;
@property({ attribute: false }) public layers?: Layer[];
@property({ type: Boolean }) public autoFit = false;
@property({ type: Boolean }) public fitZones?: boolean;
@property({ type: Boolean }) public darkMode?: boolean;
@property({ type: Number }) public zoom = 14;
@state() private _loaded = false;
public leafletMap?: Map;
// eslint-disable-next-line
private Leaflet?: LeafletModuleType;
private _leafletMap?: Map;
private _tileLayer?: TileLayer;
// @ts-ignore
private _resizeObserver?: ResizeObserver;
private _debouncedResizeListener = debounce(
() => {
if (!this._leafletMap) {
return;
}
this._leafletMap.invalidateSize();
},
100,
false
);
private _mapItems: Array<Marker | Circle> = [];
private _mapZones: Array<Marker | Circle> = [];
private _connected = false;
private _mapPaths: Array<Polyline | CircleMarker> = [];
public connectedCallback(): void {
super.connectedCallback();
this._connected = true;
if (this.hasUpdated) {
this.loadMap();
this._attachObserver();
}
this._loadMap();
this._attachObserver();
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._connected = false;
if (this._leafletMap) {
this._leafletMap.remove();
this._leafletMap = undefined;
if (this.leafletMap) {
this.leafletMap.remove();
this.leafletMap = undefined;
this.Leaflet = undefined;
}
this._loaded = false;
if (this._resizeObserver) {
this._resizeObserver.unobserve(this._mapEl);
} else {
window.removeEventListener("resize", this._debouncedResizeListener);
this._resizeObserver.unobserve(this);
}
}
protected render(): TemplateResult {
if (!this.entities) {
return html``;
}
return html` <div id="map"></div> `;
}
protected update(changedProps: PropertyValues) {
super.update(changedProps);
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this.loadMap();
if (this._connected) {
this._attachObserver();
}
}
protected shouldUpdate(changedProps) {
if (!changedProps.has("hass") || changedProps.size > 1) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || !this.entities) {
return true;
}
// Check if any state has changed
for (const entity of this.entities) {
if (oldHass.states[entity] !== this.hass!.states[entity]) {
return true;
}
}
return false;
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("hass")) {
this._drawEntities();
this._fitMap();
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return;
}
if (!this.Leaflet || !this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes.darkMode
);
}
}
private get _mapEl(): HTMLDivElement {
return this.shadowRoot!.getElementById("map") as HTMLDivElement;
}
private async loadMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
this.darkMode ?? this.hass.themes.darkMode
);
this._drawEntities();
this._leafletMap.invalidateSize();
this._fitMap();
}
private _fitMap(): void {
if (!this._leafletMap || !this.Leaflet || !this.hass) {
if (!this._loaded) {
return;
}
if (this._mapItems.length === 0) {
this._leafletMap.setView(
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (changedProps.has("_loaded") || changedProps.has("entities")) {
this._drawEntities();
} else if (this._loaded && oldHass && this.entities) {
// Check if any state has changed
for (const entity of this.entities) {
if (
oldHass.states[getEntityId(entity)] !==
this.hass!.states[getEntityId(entity)]
) {
this._drawEntities();
break;
}
}
}
if (changedProps.has("_loaded") || changedProps.has("paths")) {
this._drawPaths();
}
if (changedProps.has("_loaded") || changedProps.has("layers")) {
this._drawLayers(changedProps.get("layers") as Layer[] | undefined);
}
if (
changedProps.has("_loaded") ||
((changedProps.has("entities") || changedProps.has("layers")) &&
this.autoFit)
) {
this.fitMap();
}
if (changedProps.has("zoom")) {
this.leafletMap!.setZoom(this.zoom);
}
if (
!changedProps.has("darkMode") &&
(!changedProps.has("hass") ||
(oldHass && oldHass.themes.darkMode === this.hass.themes.darkMode))
) {
return;
}
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
this._tileLayer = replaceTileLayer(
this.Leaflet!,
this.leafletMap!,
this._tileLayer!,
darkMode
);
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
}
private async _loadMap(): Promise<void> {
let map = this.shadowRoot!.getElementById("map");
if (!map) {
map = document.createElement("div");
map.id = "map";
this.shadowRoot!.append(map);
}
const darkMode = this.darkMode ?? this.hass.themes.darkMode;
[this.leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
map,
darkMode
);
this.shadowRoot!.getElementById("map")!.classList.toggle("dark", darkMode);
this._loaded = true;
}
public fitMap(): void {
if (!this.leafletMap || !this.Leaflet || !this.hass) {
return;
}
if (!this._mapItems.length && !this.layers?.length) {
this.leafletMap.setView(
new this.Leaflet.LatLng(
this.hass.config.latitude,
this.hass.config.longitude
),
this.zoom || 14
this.zoom
);
return;
}
const bounds = this.Leaflet.latLngBounds(
let bounds = this.Leaflet.latLngBounds(
this._mapItems ? this._mapItems.map((item) => item.getLatLng()) : []
);
this._leafletMap.fitBounds(bounds.pad(0.5));
if (this.zoom && this._leafletMap.getZoom() > this.zoom) {
this._leafletMap.setZoom(this.zoom);
if (this.fitZones) {
this._mapZones?.forEach((zone) => {
bounds.extend(
"getBounds" in zone ? zone.getBounds() : zone.getLatLng()
);
});
}
this.layers?.forEach((layer: any) => {
bounds.extend(
"getBounds" in layer ? layer.getBounds() : layer.getLatLng()
);
});
if (!this.layers) {
bounds = bounds.pad(0.5);
}
this.leafletMap.fitBounds(bounds, { maxZoom: this.zoom });
}
private _drawLayers(prevLayers: Layer[] | undefined): void {
if (prevLayers) {
prevLayers.forEach((layer) => layer.remove());
}
if (!this.layers) {
return;
}
const map = this.leafletMap!;
this.layers.forEach((layer) => {
map.addLayer(layer);
});
}
private _drawPaths(): void {
const hass = this.hass;
const map = this.leafletMap;
const Leaflet = this.Leaflet;
if (!hass || !map || !Leaflet) {
return;
}
if (this._mapPaths.length) {
this._mapPaths.forEach((marker) => marker.remove());
this._mapPaths = [];
}
if (!this.paths) {
return;
}
const darkPrimaryColor = getComputedStyle(this).getPropertyValue(
"--dark-primary-color"
);
this.paths.forEach((path) => {
let opacityStep: number;
let baseOpacity: number;
if (path.gradualOpacity) {
opacityStep = path.gradualOpacity / (path.points.length - 2);
baseOpacity = 1 - path.gradualOpacity;
}
for (
let pointIndex = 0;
pointIndex < path.points.length - 1;
pointIndex++
) {
const opacity = path.gradualOpacity
? baseOpacity! + pointIndex * opacityStep!
: undefined;
// DRAW point
this._mapPaths.push(
Leaflet!.circleMarker(path.points[pointIndex], {
radius: 3,
color: path.color || darkPrimaryColor,
opacity,
fillOpacity: opacity,
interactive: false,
})
);
// DRAW line between this and next point
this._mapPaths.push(
Leaflet!.polyline(
[path.points[pointIndex], path.points[pointIndex + 1]],
{
color: path.color || darkPrimaryColor,
opacity,
interactive: false,
}
)
);
}
const pointIndex = path.points.length - 1;
if (pointIndex >= 0) {
const opacity = path.gradualOpacity
? baseOpacity! + pointIndex * opacityStep!
: undefined;
// DRAW end path point
this._mapPaths.push(
Leaflet!.circleMarker(path.points[pointIndex], {
radius: 3,
color: path.color || darkPrimaryColor,
opacity,
fillOpacity: opacity,
interactive: false,
})
);
}
this._mapPaths.forEach((marker) => map.addLayer(marker));
});
}
private _drawEntities(): void {
const hass = this.hass;
const map = this._leafletMap;
const map = this.leafletMap;
const Leaflet = this.Leaflet;
if (!hass || !map || !Leaflet) {
return;
}
if (this._mapItems) {
if (this._mapItems.length) {
this._mapItems.forEach((marker) => marker.remove());
this._mapItems = [];
}
const mapItems: Layer[] = (this._mapItems = []);
if (this._mapZones) {
if (this._mapZones.length) {
this._mapZones.forEach((marker) => marker.remove());
this._mapZones = [];
}
const mapZones: Layer[] = (this._mapZones = []);
const allEntities = this.entities!.concat();
if (!this.entities) {
return;
}
for (const entity of allEntities) {
const entityId = entity;
const stateObj = hass.states[entityId];
const computedStyles = getComputedStyle(this);
const zoneColor = computedStyles.getPropertyValue("--accent-color");
const darkPrimaryColor = computedStyles.getPropertyValue(
"--dark-primary-color"
);
const className =
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light";
for (const entity of this.entities) {
const stateObj = hass.states[getEntityId(entity)];
if (!stateObj) {
continue;
}
@@ -240,13 +372,12 @@ class HaMap extends LitElement {
}
// create marker with the icon
mapZones.push(
this._mapZones.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className:
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light",
className,
}),
interactive: false,
title,
@@ -254,10 +385,10 @@ class HaMap extends LitElement {
);
// create circle around it
mapZones.push(
this._mapZones.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#FF9800",
color: zoneColor,
radius,
})
);
@@ -273,17 +404,20 @@ class HaMap extends LitElement {
.join("")
.substr(0, 3);
// create market with the icon
mapItems.push(
// create marker with the icon
this._mapItems.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
// Leaflet clones this element before adding it to the map. This messes up
// our Polymer object and we can't pass data through. Thus we hack like this.
html: `
<ha-entity-marker
entity-id="${entityId}"
entity-id="${getEntityId(entity)}"
entity-name="${entityName}"
entity-picture="${entityPicture || ""}"
${
typeof entity !== "string"
? `entity-color="${entity.color}"`
: ""
}
></ha-entity-marker>
`,
iconSize: [48, 48],
@@ -295,10 +429,10 @@ class HaMap extends LitElement {
// create circle around if entity has accuracy
if (gpsAccuracy) {
mapItems.push(
this._mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#0288D1",
color: darkPrimaryColor,
radius: gpsAccuracy,
})
);
@@ -309,20 +443,14 @@ class HaMap extends LitElement {
this._mapZones.forEach((marker) => map.addLayer(marker));
}
private _attachObserver(): void {
// Observe changes to map size and invalidate to prevent broken rendering
// Uses ResizeObserver in Chrome, otherwise window resize event
// @ts-ignore
if (typeof ResizeObserver === "function") {
// @ts-ignore
this._resizeObserver = new ResizeObserver(() =>
this._debouncedResizeListener()
);
this._resizeObserver.observe(this._mapEl);
} else {
window.addEventListener("resize", this._debouncedResizeListener);
private async _attachObserver(): Promise<void> {
if (!this._resizeObserver) {
await installResizeObserver();
this._resizeObserver = new ResizeObserver(() => {
this.leafletMap?.invalidateSize({ debounceMoveend: true });
});
}
this._resizeObserver.observe(this);
}
static get styles(): CSSResultGroup {
@@ -337,13 +465,25 @@ class HaMap extends LitElement {
#map.dark {
background: #090909;
}
.light {
color: #000000;
}
.dark {
color: #ffffff;
}
.light {
color: #000000;
.leaflet-marker-draggable {
cursor: move !important;
}
.leaflet-edit-resize {
border-radius: 50%;
cursor: nesw-resize !important;
}
.named-icon {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
}
`;
}
@@ -377,7 +377,10 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
major: {
fontStyle: "bold",
},
source: "auto",
sampleSize: 5,
autoSkipPadding: 20,
maxRotation: 0,
},
},
],
@@ -236,7 +236,9 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
major: {
fontStyle: "bold",
},
sampleSize: 5,
autoSkipPadding: 50,
maxRotation: 0,
},
categoryPercentage: undefined,
barPercentage: undefined,
+15 -28
View File
@@ -14,19 +14,18 @@ export interface ConfigEntry {
| "failed_unload";
supports_options: boolean;
supports_unload: boolean;
system_options: ConfigEntrySystemOptions;
pref_disable_new_entities: boolean;
pref_disable_polling: boolean;
disabled_by: "user" | null;
reason: string | null;
}
export interface ConfigEntryMutableParams {
title: string;
}
export interface ConfigEntrySystemOptions {
disable_new_entities: boolean;
disable_polling: boolean;
}
export type ConfigEntryMutableParams = Partial<
Pick<
ConfigEntry,
"title" | "pref_disable_new_entities" | "pref_disable_polling"
>
>;
export const getConfigEntries = (hass: HomeAssistant) =>
hass.callApi<ConfigEntry[]>("GET", "config/config_entries/entry");
@@ -34,9 +33,9 @@ export const getConfigEntries = (hass: HomeAssistant) =>
export const updateConfigEntry = (
hass: HomeAssistant,
configEntryId: string,
updatedValues: Partial<ConfigEntryMutableParams>
updatedValues: ConfigEntryMutableParams
) =>
hass.callWS<ConfigEntry>({
hass.callWS<{ require_restart: boolean; config_entry: ConfigEntry }>({
type: "config_entries/update",
entry_id: configEntryId,
...updatedValues,
@@ -52,13 +51,15 @@ export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
require_restart: boolean;
}>("POST", `config/config_entries/entry/${configEntryId}/reload`);
export interface DisableConfigEntryResult {
require_restart: boolean;
}
export const disableConfigEntry = (
hass: HomeAssistant,
configEntryId: string
) =>
hass.callWS<{
require_restart: boolean;
}>({
hass.callWS<DisableConfigEntryResult>({
type: "config_entries/disable",
entry_id: configEntryId,
disabled_by: "user",
@@ -72,17 +73,3 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
entry_id: configEntryId,
disabled_by: null,
});
export const updateConfigEntrySystemOptions = (
hass: HomeAssistant,
configEntryId: string,
params: Partial<ConfigEntrySystemOptions>
) =>
hass.callWS<{
require_restart: boolean;
system_options: ConfigEntrySystemOptions;
}>({
type: "config_entries/system_options/update",
entry_id: configEntryId,
...params,
});
+10 -5
View File
@@ -14,12 +14,17 @@ interface HassioHardwareAudioList {
};
}
interface HardwareDevice {
attributes: Record<string, string>;
by_id: null | string;
dev_path: string;
name: string;
subsystem: string;
sysfs: string;
}
export interface HassioHardwareInfo {
serial: string[];
input: string[];
disk: string[];
gpio: string[];
audio: Record<string, unknown>;
devices: HardwareDevice[];
}
export const fetchHassioHardwareAudio = async (
+1
View File
@@ -41,6 +41,7 @@ export interface HassioSnapshotDetail extends HassioSnapshot {
export interface HassioFullSnapshotCreateParams {
name: string;
password?: string;
confirm_password?: string;
}
export interface HassioPartialSnapshotCreateParams
extends HassioFullSnapshotCreateParams {
+5
View File
@@ -13,6 +13,11 @@ import {
} from "../hassio/supervisor";
import { SupervisorStore } from "./store";
export interface supervisorApplyUpdateDetails {
name: string;
version: string;
}
export const supervisorWSbaseCommand = {
type: "supervisor/api",
method: "GET",
-8
View File
@@ -1,14 +1,6 @@
import { navigate } from "../common/navigate";
import {
DEFAULT_ACCENT_COLOR,
DEFAULT_PRIMARY_COLOR,
} from "../resources/ha-style";
import { HomeAssistant } from "../types";
export const defaultRadiusColor = DEFAULT_ACCENT_COLOR;
export const homeRadiusColor = DEFAULT_PRIMARY_COLOR;
export const passiveRadiusColor = "#9b9b9b";
export interface Zone {
id: string;
name: string;
@@ -7,7 +7,10 @@ import "../../components/ha-dialog";
import "../../components/ha-formfield";
import "../../components/ha-switch";
import type { HaSwitch } from "../../components/ha-switch";
import { updateConfigEntrySystemOptions } from "../../data/config_entries";
import {
ConfigEntryMutableParams,
updateConfigEntry,
} from "../../data/config_entries";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box";
@@ -32,8 +35,8 @@ class DialogConfigEntrySystemOptions extends LitElement {
): Promise<void> {
this._params = params;
this._error = undefined;
this._disableNewEntities = params.entry.system_options.disable_new_entities;
this._disablePolling = params.entry.system_options.disable_polling;
this._disableNewEntities = params.entry.pref_disable_new_entities;
this._disablePolling = params.entry.pref_disable_polling;
}
public closeDialog(): void {
@@ -147,14 +150,14 @@ class DialogConfigEntrySystemOptions extends LitElement {
private async _updateEntry(): Promise<void> {
this._submitting = true;
const data: Parameters<typeof updateConfigEntrySystemOptions>[2] = {
disable_new_entities: this._disableNewEntities,
const data: ConfigEntryMutableParams = {
pref_disable_new_entities: this._disableNewEntities,
};
if (this._allowUpdatePolling()) {
data.disable_polling = this._disablePolling;
data.pref_disable_polling = this._disablePolling;
}
try {
const result = await updateConfigEntrySystemOptions(
const result = await updateConfigEntry(
this.hass,
this._params!.entry.entry_id,
data
@@ -166,15 +169,8 @@ class DialogConfigEntrySystemOptions extends LitElement {
),
});
}
const curEntry = this._params!.entry;
this._params!.entryUpdated({
...curEntry,
system_options: {
...curEntry.system_options,
...data,
},
});
this._params = undefined;
this._params!.entryUpdated(result.config_entry);
this.closeDialog();
} catch (err) {
this._error = err.message || "Unknown error";
} finally {
@@ -23,16 +23,12 @@ class MoreInfoPerson extends LitElement {
}
return html`
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="id,user_id,editable"
></ha-attributes>
${this.stateObj.attributes.latitude && this.stateObj.attributes.longitude
? html`
<ha-map
.hass=${this.hass}
.entities=${this._entityArray(this.stateObj.entity_id)}
autoFit
></ha-map>
`
: ""}
@@ -51,6 +47,11 @@ class MoreInfoPerson extends LitElement {
</div>
`
: ""}
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="id,user_id,editable"
></ha-attributes>
`;
}
@@ -17,11 +17,6 @@ class MoreInfoTimer extends LitElement {
}
return html`
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="remaining"
></ha-attributes>
<div class="actions">
${this.stateObj.state === "idle" || this.stateObj.state === "paused"
? html`
@@ -57,6 +52,11 @@ class MoreInfoTimer extends LitElement {
`
: ""}
</div>
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}
extra-filters="remaining"
></ha-attributes>
`;
}
+27 -5
View File
@@ -5,6 +5,7 @@ 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 { fetchUsers } from "../../data/user";
import { getLogbookData, LogbookEntry } from "../../data/logbook";
import { loadTraceContexts, TraceContexts } from "../../data/trace";
import "../../panels/logbook/ha-logbook";
@@ -22,10 +23,12 @@ export class MoreInfoLogbook extends LitElement {
@state() private _traceContexts?: TraceContexts;
@state() private _persons = {};
@state() private _userIdToName = {};
private _lastLogbookDate?: Date;
private _fetchUserPromise?: Promise<void>;
private _throttleGetLogbookEntries = throttle(() => {
this._getLogBookData();
}, 10000);
@@ -59,7 +62,7 @@ export class MoreInfoLogbook extends LitElement {
.hass=${this.hass}
.entries=${this._logbookEntries}
.traceContexts=${this._traceContexts}
.userIdToName=${this._persons}
.userIdToName=${this._userIdToName}
></ha-logbook>
`
: html`<div class="no-entries">
@@ -70,7 +73,7 @@ export class MoreInfoLogbook extends LitElement {
}
protected firstUpdated(): void {
this._fetchPersonNames();
this._fetchUserPromise = this._fetchUserNames();
this.addEventListener("click", (ev) => {
if ((ev.composedPath()[0] as HTMLElement).tagName === "A") {
setTimeout(() => closeDialog("ha-more-info-dialog"), 500);
@@ -125,6 +128,7 @@ export class MoreInfoLogbook extends LitElement {
true
),
loadTraceContexts(this.hass),
this._fetchUserPromise,
]);
this._logbookEntries = this._logbookEntries
? [...newEntries, ...this._logbookEntries]
@@ -133,16 +137,34 @@ export class MoreInfoLogbook extends LitElement {
this._traceContexts = traceContexts;
}
private _fetchPersonNames() {
private async _fetchUserNames() {
const userIdToName = {};
// Start loading users
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
// Process persons
Object.values(this.hass.states).forEach((entity) => {
if (
entity.attributes.user_id &&
computeStateDomain(entity) === "person"
) {
this._persons[entity.attributes.user_id] =
this._userIdToName[entity.attributes.user_id] =
entity.attributes.friendly_name;
}
});
// Process users
if (userProm) {
const users = await userProm;
for (const user of users) {
if (!(user.id in userIdToName)) {
userIdToName[user.id] = user.name;
}
}
}
this._userIdToName = userIdToName;
}
static get styles() {
-12
View File
@@ -638,18 +638,6 @@ export class QuickBar extends LitElement {
margin-left: 8px;
}
.uni-virtualizer-host {
display: block;
position: relative;
contain: strict;
overflow: auto;
height: 100%;
}
.uni-virtualizer-host > * {
box-sizing: border-box;
}
mwc-list-item {
width: 100%;
}
+3
View File
@@ -48,6 +48,9 @@
window.providersPromise = fetch("/auth/providers", {
credentials: "same-origin",
});
if (!window.globalThis) {
window.globalThis = window;
}
</script>
<script>
+3
View File
@@ -71,6 +71,9 @@
import("<%= latestAppJS %>");
window.customPanelJS = "<%= latestCustomPanelJS %>";
window.latestJS = true;
if (!window.globalThis) {
window.globalThis = window;
}
</script>
<script>
{% for extra_module in extra_modules -%}
+3
View File
@@ -80,6 +80,9 @@
window.stepsPromise = fetch("/api/onboarding", {
credentials: "same-origin",
});
if (!window.globalThis) {
window.globalThis = window;
}
</script>
<script>
+2
View File
@@ -39,6 +39,7 @@ class HassLoadingScreen extends LitElement {
</div>`}
<div class="content">
<ha-circular-progress active></ha-circular-progress>
<slot></slot>
</div>
`;
}
@@ -76,6 +77,7 @@ class HassLoadingScreen extends LitElement {
.content {
height: calc(100% - var(--header-height));
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
+1 -1
View File
@@ -229,7 +229,7 @@ class HassTabsSubpage extends LitElement {
color: var(--sidebar-text-color);
text-decoration: none;
}
:host([narrow]) .toolbar a {
.bottom-bar a {
width: 25%;
}
+7 -1
View File
@@ -12,7 +12,10 @@ import { HASSDomEvent } from "../common/dom/fire_event";
import { extractSearchParamsObject } from "../common/url/search-params";
import { subscribeOne } from "../common/util/subscribe-one";
import { AuthUrlSearchParams, hassUrl } from "../data/auth";
import { fetchDiscoveryInformation } from "../data/discovery";
import {
DiscoveryInformation,
fetchDiscoveryInformation,
} from "../data/discovery";
import {
fetchOnboardingOverview,
OnboardingResponses,
@@ -68,6 +71,8 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@state() private _steps?: OnboardingStep[];
@state() private _discoveryInformation?: DiscoveryInformation;
protected render(): TemplateResult {
const step = this._curStep()!;
@@ -87,6 +92,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
? html`<onboarding-restore-snapshot
.localize=${this.localize}
.restoring=${this._restoring}
.discoveryInformation=${this._discoveryInformation}
@restoring=${this._restoringSnapshot}
>
</onboarding-restore-snapshot>`
+20 -7
View File
@@ -5,9 +5,11 @@ import "@polymer/paper-radio-button/paper-radio-button";
import "@polymer/paper-radio-group/paper-radio-group";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/map/ha-location-editor";
import "../components/map/ha-locations-editor";
import type { MarkerLocation } from "../components/map/ha-locations-editor";
import { createTimezoneListEl } from "../components/timezone-datalist";
import {
ConfigUpdateValues,
@@ -81,14 +83,14 @@ class OnboardingCoreConfig extends LitElement {
</div>
<div class="row">
<ha-location-editor
<ha-locations-editor
class="flex"
.hass=${this.hass}
.location=${this._locationValue}
.fitZoom=${14}
.locations=${this._markerLocation(this._locationValue)}
zoom="14"
.darkMode=${mql.matches}
@change=${this._locationChanged}
></ha-location-editor>
@location-updated=${this._locationChanged}
></ha-locations-editor>
</div>
<div class="row">
@@ -208,13 +210,24 @@ class OnboardingCoreConfig extends LitElement {
return this._unitSystem !== undefined ? this._unitSystem : "metric";
}
private _markerLocation = memoizeOne(
(location: [number, number]): MarkerLocation[] => [
{
id: "location",
latitude: location[0],
longitude: location[1],
location_editable: true,
},
]
);
private _handleChange(ev: PolymerChangedEvent<string>) {
const target = ev.currentTarget as PaperInputElement;
this[`_${target.name}`] = target.value;
}
private _locationChanged(ev) {
this._location = ev.currentTarget.location;
this._location = ev.detail.location;
}
private _unitSystemChanged(
+16 -8
View File
@@ -4,9 +4,12 @@ import { customElement, property } from "lit/decorators";
import "../../hassio/src/components/hassio-ansi-to-html";
import { showHassioSnapshotDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-hassio-snapshot";
import { showSnapshotUploadDialog } from "../../hassio/src/dialogs/snapshot/show-dialog-snapshot-upload";
import { navigate } from "../common/navigate";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-card";
import {
DiscoveryInformation,
fetchDiscoveryInformation,
} from "../data/discovery";
import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
import { haStyle } from "../resources/styles";
@@ -26,6 +29,9 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
@property({ type: Boolean }) public restoring = false;
@property({ attribute: false })
public discoveryInformation?: DiscoveryInformation;
protected render(): TemplateResult {
return this.restoring
? html`<ha-card
@@ -58,13 +64,14 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
private async _checkRestoreStatus(): Promise<void> {
if (this.restoring) {
try {
const response = await fetch("/api/hassio/supervisor/info", {
method: "GET",
});
if (response.status === 401) {
// If we get a unauthorized response, the restore is done
navigate("/", { replace: true });
location.reload();
const response = await fetchDiscoveryInformation();
if (
!this.discoveryInformation ||
this.discoveryInformation.uuid !== response.uuid
) {
// When the UUID changes, the restore is complete
window.location.replace("/");
}
} catch (err) {
// We fully expected issues with fetching info untill restore is complete.
@@ -76,6 +83,7 @@ class OnboardingRestoreSnapshot extends ProvideHassLitMixin(LitElement) {
showHassioSnapshotDialog(this, {
slug,
onboarding: true,
localize: this.localize,
});
}
+29 -18
View File
@@ -8,7 +8,8 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { UNIT_C } from "../../../common/const";
import "../../../components/ha-card";
import "../../../components/map/ha-location-editor";
import "../../../components/map/ha-locations-editor";
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
import { createTimezoneListEl } from "../../../components/timezone-datalist";
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
import type { PolymerChangedEvent } from "../../../polymer-types";
@@ -20,13 +21,13 @@ class ConfigCoreForm extends LitElement {
@state() private _working = false;
@state() private _location!: [number, number];
@state() private _location?: [number, number];
@state() private _elevation!: string;
@state() private _elevation?: string;
@state() private _unitSystem!: ConfigUpdateValues["unit_system"];
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
@state() private _timeZone!: string;
@state() private _timeZone?: string;
protected render(): TemplateResult {
const canEdit = ["storage", "default"].includes(
@@ -52,16 +53,16 @@ class ConfigCoreForm extends LitElement {
: ""}
<div class="row">
<ha-location-editor
<ha-locations-editor
class="flex"
.hass=${this.hass}
.location=${this._locationValue(
this._location,
.locations=${this._markerLocation(
this.hass.config.latitude,
this.hass.config.longitude
this.hass.config.longitude,
this._location
)}
@change=${this._locationChanged}
></ha-location-editor>
@location-updated=${this._locationChanged}
></ha-locations-editor>
</div>
<div class="row">
@@ -162,8 +163,19 @@ class ConfigCoreForm extends LitElement {
input.inputElement.appendChild(createTimezoneListEl());
}
private _locationValue = memoizeOne(
(location, lat, lng) => location || [Number(lat), Number(lng)]
private _markerLocation = memoizeOne(
(
lat: number,
lng: number,
location?: [number, number]
): MarkerLocation[] => [
{
id: "location",
latitude: location ? location[0] : lat,
longitude: location ? location[1] : lng,
location_editable: true,
},
]
);
private get _elevationValue() {
@@ -192,7 +204,7 @@ class ConfigCoreForm extends LitElement {
}
private _locationChanged(ev) {
this._location = ev.currentTarget.location;
this._location = ev.detail.location;
}
private _unitSystemChanged(
@@ -204,11 +216,10 @@ class ConfigCoreForm extends LitElement {
private async _save() {
this._working = true;
try {
const location = this._locationValue(
this._location,
const location = this._location || [
this.hass.config.latitude,
this.hass.config.longitude
);
this.hass.config.longitude,
];
await saveCoreConfig(this.hass, {
latitude: location[0],
longitude: location[1],
+16 -3
View File
@@ -9,13 +9,14 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-network";
import "../../../components/ha-card";
import "../../../components/ha-checkbox";
import "../../../components/ha-network";
import "../../../components/ha-settings-row";
import { fetchNetworkInfo } from "../../../data/hassio/network";
import {
NetworkConfig,
getNetworkConfig,
NetworkConfig,
setNetworkConfig,
} from "../../../data/network";
import { haStyle } from "../../../resources/styles";
@@ -73,7 +74,19 @@ class ConfigNetwork extends LitElement {
private async _load() {
this._error = undefined;
try {
this._networkConfig = await getNetworkConfig(this.hass);
const coreNetwork = await getNetworkConfig(this.hass);
if (isComponentLoaded(this.hass, "hassio")) {
const supervisorNetwork = await fetchNetworkInfo(this.hass);
const interfaces = new Set(
supervisorNetwork.interfaces.map((int) => int.interface)
);
if (interfaces.size) {
coreNetwork.adapters = coreNetwork.adapters.filter((adapter) =>
interfaces.has(adapter.name)
);
}
}
this._networkConfig = coreNetwork;
} catch (err) {
this._error = err.message || err;
}
@@ -11,7 +11,11 @@ import { slugify } from "../../../common/string/slugify";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-icon-next";
import { AreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry, disableConfigEntry } from "../../../data/config_entries";
import {
ConfigEntry,
disableConfigEntry,
DisableConfigEntryResult,
} from "../../../data/config_entries";
import {
computeDeviceName,
DeviceRegistryEntry,
@@ -25,7 +29,10 @@ import {
} from "../../../data/entity_registry";
import { SceneEntities, showSceneEditor } from "../../../data/scene";
import { findRelated, RelatedResult } from "../../../data/search";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles";
@@ -671,13 +678,41 @@ export class HaConfigDevicePage extends LitElement {
dismissText: this.hass.localize("ui.common.no"),
}))
) {
disableConfigEntry(this.hass, cnfg_entry);
let result: DisableConfigEntryResult;
try {
// eslint-disable-next-line no-await-in-loop
result = await disableConfigEntry(this.hass, cnfg_entry);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_error"
),
text: err.message,
});
return;
}
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_restart_confirm"
),
});
}
delete updates.disabled_by;
}
}
}
}
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
try {
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.devices.update_device_error"
),
text: err.message,
});
}
if (
!oldDeviceName ||
@@ -18,6 +18,7 @@ import {
ConfigEntry,
deleteConfigEntry,
disableConfigEntry,
DisableConfigEntryResult,
enableConfigEntry,
reloadConfigEntry,
updateConfigEntry,
@@ -487,7 +488,18 @@ export class HaIntegrationCard extends LitElement {
if (!confirmed) {
return;
}
const result = await disableConfigEntry(this.hass, entryId);
let result: DisableConfigEntryResult;
try {
result = await disableConfigEntry(this.hass, entryId);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_error"
),
text: err.message,
});
return;
}
if (result.require_restart) {
showAlertDialog(this, {
text: this.hass.localize(
@@ -503,7 +515,18 @@ export class HaIntegrationCard extends LitElement {
private async _enableIntegration(configEntry: ConfigEntry) {
const entryId = configEntry.entry_id;
const result = await enableConfigEntry(this.hass, entryId);
let result: DisableConfigEntryResult;
try {
result = await enableConfigEntry(this.hass, entryId);
} catch (err) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_entry.disable_error"
),
text: err.message,
});
return;
}
if (result.require_restart) {
showAlertDialog(this, {
@@ -567,10 +590,10 @@ export class HaIntegrationCard extends LitElement {
if (newName === null) {
return;
}
const newEntry = await updateConfigEntry(this.hass, configEntry.entry_id, {
const result = await updateConfigEntry(this.hass, configEntry.entry_id, {
title: newName,
});
fireEvent(this, "entry-updated", { entry: newEntry });
fireEvent(this, "entry-updated", { entry: result.config_entry });
}
static get styles(): CSSResultGroup {
@@ -63,7 +63,7 @@ export class HaIntegrationHeader extends LitElement {
]);
}
if (this.configEntry?.system_options.disable_polling) {
if (this.configEntry?.pref_disable_polling) {
icons.push([
mdiSyncOff,
this.hass.localize(
@@ -9,7 +9,12 @@ import {
} from "../../../../../data/zha";
import "../../../../../layouts/hass-tabs-subpage";
import type { HomeAssistant, Route } from "../../../../../types";
import { Network, Edge, Node, EdgeOptions } from "vis-network/peer";
import {
Network,
Edge,
Node,
EdgeOptions,
} from "vis-network/peer/esm/vis-network";
import "../../../../../common/search/search-input";
import "../../../../../components/device/ha-device-picker";
import "../../../../../components/ha-button-menu";
@@ -21,6 +26,7 @@ import "../../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
import { zhaTabs } from "./zha-config-dashboard";
import { customElement, property, query, state } from "lit/decorators";
import "../../../../../components/ha-formfield";
@customElement("zha-network-visualization-page")
export class ZHANetworkVisualizationPage extends LitElement {
@@ -28,7 +34,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
@property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
@property({ type: Boolean }) public isWide!: boolean;
@@ -67,8 +73,6 @@ export class ZHANetworkVisualizationPage extends LitElement {
{},
{
autoResize: true,
height: window.innerHeight + "px",
width: window.innerWidth + "px",
layout: {
improvedLayout: true,
},
@@ -135,17 +139,35 @@ export class ZHANetworkVisualizationPage extends LitElement {
"ui.panel.config.zha.visualization.header"
)}
>
<div class="table-header">
<search-input
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
.filter=${this._filter}
.label=${this.hass.localize(
"ui.panel.config.zha.visualization.highlight_label"
)}
>
</search-input>
${this.narrow
? html`
<div slot="header">
<search-input
no-label-float
no-underline
class="header"
@value-changed=${this._handleSearchChange}
.filter=${this._filter}
.label=${this.hass.localize(
"ui.panel.config.zha.visualization.highlight_label"
)}
>
</search-input>
</div>
`
: ""}
<div class="header">
${!this.narrow
? html`<search-input
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
.filter=${this._filter}
.label=${this.hass.localize(
"ui.panel.config.zha.visualization.highlight_label"
)}
></search-input>`
: ""}
<ha-device-picker
.hass=${this.hass}
.value=${this.zoomedDeviceId}
@@ -155,16 +177,24 @@ export class ZHANetworkVisualizationPage extends LitElement {
.deviceFilter=${(device) => this._filterDevices(device)}
@value-changed=${this._onZoomToDevice}
></ha-device-picker>
<ha-checkbox
@change=${this._handleCheckboxChange}
.checked=${this._autoZoom}
></ha-checkbox
>${this.hass!.localize("ui.panel.config.zha.visualization.auto_zoom")}
<mwc-button @click=${this._refreshTopology}
>${this.hass!.localize(
"ui.panel.config.zha.visualization.refresh_topology"
)}</mwc-button
>
<div class="controls">
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.config.zha.visualization.auto_zoom"
)}
>
<ha-checkbox
@change=${this._handleCheckboxChange}
.checked=${this._autoZoom}
>
</ha-checkbox>
</ha-formfield>
<mwc-button @click=${this._refreshTopology}>
${this.hass!.localize(
"ui.panel.config.zha.visualization.refresh_topology"
)}
</mwc-button>
</div>
</div>
<div id="visualization"></div>
</hass-tabs-subpage>
@@ -352,30 +382,23 @@ export class ZHANetworkVisualizationPage extends LitElement {
return [
css`
.header {
font-family: var(--paper-font-display1_-_font-family);
-webkit-font-smoothing: var(
--paper-font-display1_-_-webkit-font-smoothing
);
font-size: var(--paper-font-display1_-_font-size);
font-weight: var(--paper-font-display1_-_font-weight);
letter-spacing: var(--paper-font-display1_-_letter-spacing);
line-height: var(--paper-font-display1_-_line-height);
opacity: var(--dark-primary-opacity);
}
.table-header {
border-bottom: 1px solid --divider-color;
padding: 0 16px;
border-bottom: 1px solid var(--divider-color);
padding: 0 8px;
display: flex;
align-items: center;
flex-direction: row;
justify-content: space-between;
height: var(--header-height);
box-sizing: border-box;
}
:host([narrow]) .table-header {
.header > * {
padding: 0 8px;
}
:host([narrow]) .header {
flex-direction: column;
align-items: stretch;
height: var(--header-height) * 3;
height: var(--header-height) * 2;
}
.search-toolbar {
@@ -386,34 +409,34 @@ export class ZHANetworkVisualizationPage extends LitElement {
}
search-input {
position: relative;
top: 2px;
flex: 1;
}
:host(:not([narrow])) search-input {
margin: 5px;
}
search-input.header {
left: -8px;
display: block;
position: relative;
top: -2px;
color: var(--secondary-text-color);
}
ha-device-picker {
flex: 1;
position: relative;
top: -4px;
}
:host(:not([narrow])) ha-device-picker {
margin: 5px;
.controls {
display: flex;
align-items: center;
justify-content: space-between;
}
mwc-button {
font-weight: 500;
color: var(--primary-color);
#visualization {
height: calc(100% - var(--header-height));
width: 100%;
}
:host(:not([narrow])) mwc-button {
margin: 5px;
:host([narrow]) #visualization {
height: calc(100% - (var(--header-height) * 2));
}
`,
];
@@ -24,6 +24,7 @@ import {
import "../../../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { fileDownload } from "../../../../../util/file_download";
import "../../../ha-config-section";
import { showZWaveJSAddNodeDialog } from "./show-dialog-zwave_js-add-node";
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
@@ -312,12 +313,7 @@ class ZWaveJSConfigDashboard extends LitElement {
return;
}
const a = document.createElement("a");
a.href = signedPath.path;
a.download = `zwave_js_dump.jsonl`;
this.shadowRoot!.appendChild(a);
a.click();
this.shadowRoot!.removeChild(a);
fileDownload(this, signedPath.path, `zwave_js_dump.jsonl`);
}
static get styles(): CSSResultGroup {
+48 -21
View File
@@ -9,13 +9,9 @@ import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield";
import "../../../components/ha-switch";
import "../../../components/map/ha-location-editor";
import {
defaultRadiusColor,
getZoneEditorInitData,
passiveRadiusColor,
ZoneMutableParams,
} from "../../../data/zone";
import "../../../components/map/ha-locations-editor";
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
import { getZoneEditorInitData, ZoneMutableParams } from "../../../data/zone";
import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { ZoneDetailDialogParams } from "./show-dialog-zone-detail";
@@ -132,17 +128,19 @@ class DialogZoneDetail extends LitElement {
)}"
.invalid=${iconValid}
></paper-input>
<ha-location-editor
<ha-locations-editor
class="flex"
.hass=${this.hass}
.location=${this._locationValue(this._latitude, this._longitude)}
.radius=${this._radius}
.radiusColor=${this._passive
? passiveRadiusColor
: defaultRadiusColor}
.icon=${this._icon}
@change=${this._locationChanged}
></ha-location-editor>
.locations=${this._location(
this._latitude,
this._longitude,
this._radius,
this._passive,
this._icon
)}
@location-updated=${this._locationChanged}
@radius-updated=${this._radiusChanged}
></ha-locations-editor>
<div class="location">
<paper-input
.value=${this._latitude}
@@ -222,11 +220,40 @@ class DialogZoneDetail extends LitElement {
`;
}
private _locationValue = memoizeOne((lat, lng) => [Number(lat), Number(lng)]);
private _location = memoizeOne(
(
lat: number,
lng: number,
radius: number,
passive: boolean,
icon: string
): MarkerLocation[] => {
const computedStyles = getComputedStyle(this);
const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color");
const passiveRadiusColor = computedStyles.getPropertyValue(
"--secondary-text-color"
);
return [
{
id: "location",
latitude: Number(lat),
longitude: Number(lng),
radius,
radius_color: passive ? passiveRadiusColor : zoneRadiusColor,
icon,
location_editable: true,
radius_editable: true,
},
];
}
);
private _locationChanged(ev) {
[this._latitude, this._longitude] = ev.currentTarget.location;
this._radius = ev.currentTarget.radius;
private _locationChanged(ev: CustomEvent) {
[this._latitude, this._longitude] = ev.detail.location;
}
private _radiusChanged(ev: CustomEvent) {
this._radius = ev.detail.radius;
}
private _passiveChanged(ev) {
@@ -292,7 +319,7 @@ class DialogZoneDetail extends LitElement {
.location > *:last-child {
margin-left: 4px;
}
ha-location-editor {
ha-locations-editor {
margin-top: 16px;
}
a {
+16 -8
View File
@@ -31,11 +31,8 @@ import { saveCoreConfig } from "../../../data/core";
import { subscribeEntityRegistry } from "../../../data/entity_registry";
import {
createZone,
defaultRadiusColor,
deleteZone,
fetchZones,
homeRadiusColor,
passiveRadiusColor,
updateZone,
Zone,
ZoneMutableParams,
@@ -73,6 +70,15 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
private _getZones = memoizeOne(
(storageItems: Zone[], stateItems: HassEntity[]): MarkerLocation[] => {
const computedStyles = getComputedStyle(this);
const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color");
const passiveRadiusColor = computedStyles.getPropertyValue(
"--secondary-text-color"
);
const homeRadiusColor = computedStyles.getPropertyValue(
"--primary-color"
);
const stateLocations: MarkerLocation[] = stateItems.map(
(entityState) => ({
id: entityState.entity_id,
@@ -86,7 +92,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
? homeRadiusColor
: entityState.attributes.passive
? passiveRadiusColor
: defaultRadiusColor,
: zoneRadiusColor,
location_editable:
entityState.entity_id === "zone.home" && this._canEditCore,
radius_editable: false,
@@ -94,7 +100,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
);
const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({
...zone,
radius_color: zone.passive ? passiveRadiusColor : defaultRadiusColor,
radius_color: zone.passive ? passiveRadiusColor : zoneRadiusColor,
location_editable: true,
radius_editable: true,
}));
@@ -274,7 +280,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
}
}
protected updated(changedProps: PropertyValues) {
public willUpdate(changedProps: PropertyValues) {
super.updated(changedProps);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass && this._stateItems) {
@@ -410,8 +416,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
if (this.narrow) {
return;
}
await this.updateComplete;
this._activeEntry = created.id;
await this.updateComplete;
await this._map?.updateComplete;
this._map?.fitMarker(created.id);
}
@@ -427,8 +434,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
if (this.narrow || !fitMap) {
return;
}
await this.updateComplete;
this._activeEntry = entry.id;
await this.updateComplete;
await this._map?.updateComplete;
this._map?.fitMarker(entry.id);
}
-8
View File
@@ -353,15 +353,7 @@ class HaLogbook extends LitElement {
}
:host([virtualize]) .container {
display: block;
position: relative;
contain: strict;
height: 100%;
overflow: auto;
}
.container > * {
box-sizing: border-box;
}
.narrow .entry {
+15 -19
View File
@@ -1,22 +1,22 @@
import { mdiRefresh } from "@mdi/js";
import "@material/mwc-icon-button";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { css, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
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 { fetchPersons } from "../../data/person";
import { loadTraceContexts, TraceContexts } from "../../data/trace";
import { fetchUsers } from "../../data/user";
import "../../layouts/ha-app-layout";
@@ -44,7 +44,7 @@ export class HaPanelLogbook extends LitElement {
@state() private _ranges?: DateRangePickerRanges;
private _fetchUserDone?: Promise<unknown>;
private _fetchUserPromise?: Promise<void>;
@state() private _userIdToName = {};
@@ -136,7 +136,7 @@ export class HaPanelLogbook extends LitElement {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("title");
this._fetchUserDone = this._fetchUserNames();
this._fetchUserPromise = this._fetchUserNames();
const today = new Date();
today.setHours(0, 0, 0, 0);
@@ -198,23 +198,19 @@ export class HaPanelLogbook extends LitElement {
private async _fetchUserNames() {
const userIdToName = {};
// Start loading all the data
const personProm = fetchPersons(this.hass);
const userProm = this.hass.user!.is_admin && fetchUsers(this.hass);
// Start loading users
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
// Process persons
const persons = await personProm;
for (const person of persons.storage) {
if (person.user_id) {
userIdToName[person.user_id] = person.name;
Object.values(this.hass.states).forEach((entity) => {
if (
entity.attributes.user_id &&
computeStateDomain(entity) === "person"
) {
this._userIdToName[entity.attributes.user_id] =
entity.attributes.friendly_name;
}
}
for (const person of persons.config) {
if (person.user_id) {
userIdToName[person.user_id] = person.name;
}
}
});
// Process users
if (userProm) {
@@ -262,7 +258,7 @@ export class HaPanelLogbook extends LitElement {
this._entityId
),
isComponentLoaded(this.hass, "trace") ? loadTraceContexts(this.hass) : {},
this._fetchUserDone,
this._fetchUserPromise,
]);
this._entries = entries;
+38 -18
View File
@@ -9,11 +9,12 @@ import {
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { throttle } from "../../../common/util/throttle";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import { fetchUsers } from "../../../data/user";
import { getLogbookData, LogbookEntry } from "../../../data/logbook";
import type { HomeAssistant } from "../../../types";
import "../../logbook/ha-logbook";
@@ -51,18 +52,20 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
};
}
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: LogbookCardConfig;
@state() private _logbookEntries?: LogbookEntry[];
@state() private _persons = {};
@state() private _configEntities?: EntityConfig[];
@state() private _userIdToName = {};
private _lastLogbookDate?: Date;
private _fetchUserPromise?: Promise<void>;
private _throttleGetLogbookEntries = throttle(() => {
this._getLogBookData();
}, 10000);
@@ -114,7 +117,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
}
protected firstUpdated(): void {
this._fetchPersonNames();
this._fetchUserPromise = this._fetchUserNames();
}
protected updated(changedProperties: PropertyValues) {
@@ -199,7 +202,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
virtualize
.hass=${this.hass}
.entries=${this._logbookEntries}
.userIdToName=${this._persons}
.userIdToName=${this._userIdToName}
></ha-logbook>
`
: html`
@@ -229,13 +232,16 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
const lastDate = this._lastLogbookDate || hoursToShowDate;
const now = new Date();
const newEntries = await getLogbookData(
this.hass,
lastDate.toISOString(),
now.toISOString(),
this._configEntities!.map((entity) => entity.entity).toString(),
true
);
const [newEntries] = await Promise.all([
getLogbookData(
this.hass,
lastDate.toISOString(),
now.toISOString(),
this._configEntities!.map((entity) => entity.entity).toString(),
true
),
this._fetchUserPromise,
]);
const logbookEntries = this._logbookEntries
? [...newEntries, ...this._logbookEntries]
@@ -248,20 +254,34 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
this._lastLogbookDate = now;
}
private _fetchPersonNames() {
if (!this.hass) {
return;
}
private async _fetchUserNames() {
const userIdToName = {};
// Start loading users
const userProm = this.hass.user?.is_admin && fetchUsers(this.hass);
// Process persons
Object.values(this.hass!.states).forEach((entity) => {
if (
entity.attributes.user_id &&
computeStateDomain(entity) === "person"
) {
this._persons[entity.attributes.user_id] =
this._userIdToName[entity.attributes.user_id] =
entity.attributes.friendly_name;
}
});
// Process users
if (userProm) {
const users = await userProm;
for (const user of users) {
if (!(user.id in userIdToName)) {
userIdToName[user.id] = user.name;
}
}
}
this._userIdToName = userIdToName;
}
static get styles(): CSSResultGroup {
+175 -478
View File
@@ -1,14 +1,5 @@
import { HassEntity } from "home-assistant-js-websocket";
import {
Circle,
CircleMarker,
LatLngTuple,
Layer,
Map,
Marker,
Polyline,
TileLayer,
} from "leaflet";
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
import { LatLngTuple } from "leaflet";
import {
css,
CSSResultGroup,
@@ -17,32 +8,106 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import {
LeafletModuleType,
replaceTileLayer,
setupLeafletMap,
} from "../../../common/dom/setup-leaflet-map";
import { customElement, property, query, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { debounce } from "../../../common/util/debounce";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import { fetchRecent } from "../../../data/history";
import { HomeAssistant } from "../../../types";
import "../../map/ha-entity-marker";
import "../../../components/map/ha-entity-marker";
import { findEntities } from "../common/find-entities";
import { installResizeObserver } from "../common/install-resize-observer";
import { processConfigEntities } from "../common/process-config-entities";
import { EntityConfig } from "../entity-rows/types";
import { LovelaceCard } from "../types";
import { MapCardConfig } from "./types";
import "../../../components/map/ha-map";
import { mdiImageFilterCenterFocus } from "@mdi/js";
import type { HaMap, HaMapPaths } from "../../../components/map/ha-map";
import memoizeOne from "memoize-one";
const MINUTE = 60000;
const COLORS = [
"#0288D1",
"#00AA00",
"#984ea3",
"#00d2d5",
"#ff7f00",
"#af8d00",
"#7f80cd",
"#b3e900",
"#c42e60",
"#a65628",
"#f781bf",
"#8dd3c7",
];
@customElement("hui-map-card")
class HuiMapCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
public isPanel = false;
@state()
private _history?: HassEntity[][];
@state()
private _config?: MapCardConfig;
@query("ha-map")
private _map?: HaMap;
private _date?: Date;
private _configEntities?: string[];
private _colorDict: Record<string, string> = {};
private _colorIndex = 0;
public setConfig(config: MapCardConfig): void {
if (!config) {
throw new Error("Error in card configuration.");
}
if (!config.entities?.length && !config.geo_location_sources) {
throw new Error(
"Either entities or geo_location_sources must be specified"
);
}
if (config.entities && !Array.isArray(config.entities)) {
throw new Error("Entities need to be an array");
}
if (
config.geo_location_sources &&
!Array.isArray(config.geo_location_sources)
) {
throw new Error("Geo_location_sources needs to be an array");
}
this._config = config;
this._configEntities = (config.entities
? processConfigEntities<EntityConfig>(config.entities)
: []
).map((entity) => entity.entity);
this._cleanupHistory();
}
public getCardSize(): number {
if (!this._config?.aspect_ratio) {
return 7;
}
const ratio = parseAspectRatio(this._config.aspect_ratio);
const ar =
ratio && ratio.w > 0 && ratio.h > 0
? `${((100 * ratio.h) / ratio.w).toFixed(2)}`
: "100";
return 1 + Math.floor(Number(ar) / 25) || 3;
}
public static async getConfigElement() {
await import("../editor/config-elements/hui-map-card-editor");
return document.createElement("hui-map-card-editor");
@@ -66,129 +131,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return { type: "map", entities: foundEntities };
}
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
public isPanel = false;
@property()
private _history?: HassEntity[][];
private _date?: Date;
@property()
private _config?: MapCardConfig;
private _configEntities?: EntityConfig[];
// eslint-disable-next-line
private Leaflet?: LeafletModuleType;
private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _resizeObserver?: ResizeObserver;
private _debouncedResizeListener = debounce(
() => {
if (!this.isConnected || !this._leafletMap) {
return;
}
this._leafletMap.invalidateSize();
},
250,
false
);
private _mapItems: Array<Marker | Circle> = [];
private _mapZones: Array<Marker | Circle> = [];
private _mapPaths: Array<Polyline | CircleMarker> = [];
private _colorDict: Record<string, string> = {};
private _colorIndex = 0;
private _colors: string[] = [
"#0288D1",
"#00AA00",
"#984ea3",
"#00d2d5",
"#ff7f00",
"#af8d00",
"#7f80cd",
"#b3e900",
"#c42e60",
"#a65628",
"#f781bf",
"#8dd3c7",
];
public setConfig(config: MapCardConfig): void {
if (!config) {
throw new Error("Error in card configuration.");
}
if (!config.entities?.length && !config.geo_location_sources) {
throw new Error(
"Either entities or geo_location_sources must be specified"
);
}
if (config.entities && !Array.isArray(config.entities)) {
throw new Error("Entities need to be an array");
}
if (
config.geo_location_sources &&
!Array.isArray(config.geo_location_sources)
) {
throw new Error("Geo_location_sources needs to be an array");
}
this._config = config;
this._configEntities = config.entities
? processConfigEntities(config.entities)
: [];
this._cleanupHistory();
}
public getCardSize(): number {
if (!this._config?.aspect_ratio) {
return 7;
}
const ratio = parseAspectRatio(this._config.aspect_ratio);
const ar =
ratio && ratio.w > 0 && ratio.h > 0
? `${((100 * ratio.h) / ratio.w).toFixed(2)}`
: "100";
return 1 + Math.floor(Number(ar) / 25) || 3;
}
public connectedCallback(): void {
super.connectedCallback();
this._attachObserver();
if (this.hasUpdated) {
this.loadMap();
}
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this._leafletMap) {
this._leafletMap.remove();
this._leafletMap = undefined;
this.Leaflet = undefined;
}
if (this._resizeObserver) {
this._resizeObserver.unobserve(this._mapEl);
}
}
protected render(): TemplateResult {
if (!this._config) {
return html``;
@@ -196,22 +138,29 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return html`
<ha-card id="card" .header=${this._config.title}>
<div id="root">
<div
id="map"
class=${classMap({ dark: this._config.dark_mode === true })}
></div>
<ha-icon-button
<ha-map
.hass=${this.hass}
.entities=${this._getEntities(
this.hass.states,
this._config,
this._configEntities
)}
.paths=${this._getHistoryPaths(this._config, this._history)}
.darkMode=${this._config.dark_mode}
></ha-map>
<mwc-icon-button
@click=${this._fitMap}
tabindex="0"
icon="hass:image-filter-center-focus"
title="Reset focus"
></ha-icon-button>
>
<ha-svg-icon .path=${mdiImageFilterCenterFocus}></ha-svg-icon>
</mwc-icon-button>
</div>
</ha-card>
`;
}
protected shouldUpdate(changedProps) {
protected shouldUpdate(changedProps: PropertyValues) {
if (!changedProps.has("hass") || changedProps.size > 1) {
return true;
}
@@ -228,7 +177,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
// Check if any state has changed
for (const entity of this._configEntities) {
if (oldHass.states[entity.entity] !== this.hass!.states[entity.entity]) {
if (oldHass.states[entity] !== this.hass!.states[entity]) {
return true;
}
}
@@ -238,17 +187,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
if (this.isConnected) {
this.loadMap();
}
const root = this.shadowRoot!.getElementById("root");
if (!this._config || this.isPanel || !root) {
return;
}
this._attachObserver();
if (!this._config.aspect_ratio) {
root.style.paddingBottom = "100%";
return;
@@ -263,172 +207,86 @@ class HuiMapCard extends LitElement implements LovelaceCard {
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("hass") || changedProps.has("_history")) {
this._drawEntities();
this._fitMap();
}
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass && oldHass.themes.darkMode !== this.hass.themes.darkMode) {
this._replaceTileLayer();
}
}
if (
changedProps.has("_config") &&
changedProps.get("_config") !== undefined
) {
this.updateMap(changedProps.get("_config") as MapCardConfig);
}
if (this._config?.hours_to_show && this._configEntities?.length) {
const minute = 60000;
if (changedProps.has("_config")) {
this._getHistory();
} else if (Date.now() - this._date!.getTime() >= minute) {
} else if (Date.now() - this._date!.getTime() >= MINUTE) {
this._getHistory();
}
}
}
private get _mapEl(): HTMLDivElement {
return this.shadowRoot!.getElementById("map") as HTMLDivElement;
private _fitMap() {
this._map?.fitMap();
}
private async loadMap(): Promise<void> {
[this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl,
this._config!.dark_mode ?? this.hass.themes.darkMode
);
this._drawEntities();
this._leafletMap.invalidateSize();
this._fitMap();
}
private _replaceTileLayer() {
const map = this._leafletMap;
const config = this._config;
const Leaflet = this.Leaflet;
if (!map || !config || !Leaflet || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
Leaflet,
map,
this._tileLayer,
this._config!.dark_mode ?? this.hass.themes.darkMode
);
}
private updateMap(oldConfig: MapCardConfig): void {
const map = this._leafletMap;
const config = this._config;
const Leaflet = this.Leaflet;
if (!map || !config || !Leaflet || !this._tileLayer) {
return;
}
if (this._config!.dark_mode !== oldConfig.dark_mode) {
this._replaceTileLayer();
}
if (
config.entities !== oldConfig.entities ||
config.geo_location_sources !== oldConfig.geo_location_sources
) {
this._drawEntities();
}
map.invalidateSize();
this._fitMap();
}
private _fitMap(): void {
if (!this._leafletMap || !this.Leaflet || !this._config || !this.hass) {
return;
}
const zoom = this._config.default_zoom;
if (this._mapItems.length === 0) {
this._leafletMap.setView(
new this.Leaflet.LatLng(
this.hass.config.latitude,
this.hass.config.longitude
),
zoom || 14
);
return;
}
const bounds = this.Leaflet.featureGroup(this._mapItems).getBounds();
this._leafletMap.fitBounds(bounds.pad(0.5));
if (zoom && this._leafletMap.getZoom() > zoom) {
this._leafletMap.setZoom(zoom);
}
}
private _getColor(entityId: string) {
let color;
if (this._colorDict[entityId]) {
color = this._colorDict[entityId];
} else {
color = this._colors[this._colorIndex];
this._colorIndex = (this._colorIndex + 1) % this._colors.length;
this._colorDict[entityId] = color;
private _getColor(entityId: string): string {
let color = this._colorDict[entityId];
if (color) {
return color;
}
color = COLORS[this._colorIndex % COLORS.length];
this._colorIndex++;
this._colorDict[entityId] = color;
return color;
}
private _drawEntities(): void {
const hass = this.hass;
const map = this._leafletMap;
const config = this._config;
const Leaflet = this.Leaflet;
if (!hass || !map || !config || !Leaflet) {
return;
}
if (this._mapItems) {
this._mapItems.forEach((marker) => marker.remove());
}
const mapItems: Layer[] = (this._mapItems = []);
if (this._mapZones) {
this._mapZones.forEach((marker) => marker.remove());
}
const mapZones: Layer[] = (this._mapZones = []);
if (this._mapPaths) {
this._mapPaths.forEach((marker) => marker.remove());
}
const mapPaths: Layer[] = (this._mapPaths = []);
const allEntities = this._configEntities!.concat();
// Calculate visible geo location sources
if (config.geo_location_sources) {
const includesAll = config.geo_location_sources.includes("all");
for (const entityId of Object.keys(hass.states)) {
const stateObj = hass.states[entityId];
if (
computeDomain(entityId) === "geo_location" &&
(includesAll ||
config.geo_location_sources.includes(stateObj.attributes.source))
) {
allEntities.push({ entity: entityId });
}
private _getEntities = memoizeOne(
(
states: HassEntities,
config: MapCardConfig,
configEntities?: string[]
) => {
if (!states || !config) {
return undefined;
}
}
// DRAW history
if (this._config!.hours_to_show && this._history) {
for (const entityStates of this._history) {
let entities = configEntities || [];
if (config.geo_location_sources) {
const geoEntities: string[] = [];
// Calculate visible geo location sources
const includesAll = config.geo_location_sources.includes("all");
for (const stateObj of Object.values(states)) {
if (
computeDomain(stateObj.entity_id) === "geo_location" &&
(includesAll ||
config.geo_location_sources.includes(stateObj.attributes.source))
) {
geoEntities.push(stateObj.entity_id);
}
}
entities = [...entities, ...geoEntities];
}
return entities.map((entity) => ({
entity_id: entity,
color: this._getColor(entity),
}));
}
);
private _getHistoryPaths = memoizeOne(
(
config: MapCardConfig,
history?: HassEntity[][]
): HaMapPaths[] | undefined => {
if (!config.hours_to_show || !history) {
return undefined;
}
const paths: HaMapPaths[] = [];
for (const entityStates of history) {
if (entityStates?.length <= 1) {
continue;
}
const entityId = entityStates[0].entity_id;
// filter location data from states and remove all invalid locations
const path = entityStates.reduce(
(accumulator: LatLngTuple[], state) => {
const latitude = state.attributes.latitude;
const longitude = state.attributes.longitude;
const points = entityStates.reduce(
(accumulator: LatLngTuple[], entityState) => {
const latitude = entityState.attributes.latitude;
const longitude = entityState.attributes.longitude;
if (latitude && longitude) {
accumulator.push([latitude, longitude] as LatLngTuple);
}
@@ -437,162 +295,15 @@ class HuiMapCard extends LitElement implements LovelaceCard {
[]
) as LatLngTuple[];
// DRAW HISTORY
for (
let markerIndex = 0;
markerIndex < path.length - 1;
markerIndex++
) {
const opacityStep = 0.8 / (path.length - 2);
const opacity = 0.2 + markerIndex * opacityStep;
// DRAW history path dots
mapPaths.push(
Leaflet.circleMarker(path[markerIndex], {
radius: 3,
color: this._getColor(entityId),
opacity,
interactive: false,
})
);
// DRAW history path lines
const line = [path[markerIndex], path[markerIndex + 1]];
mapPaths.push(
Leaflet.polyline(line, {
color: this._getColor(entityId),
opacity,
interactive: false,
})
);
}
paths.push({
points,
color: this._getColor(entityStates[0].entity_id),
gradualOpacity: 0.8,
});
}
return paths;
}
// DRAW entities
for (const entity of allEntities) {
const entityId = entity.entity;
const stateObj = hass.states[entityId];
if (!stateObj) {
continue;
}
const title = computeStateName(stateObj);
const {
latitude,
longitude,
passive,
icon,
radius,
entity_picture: entityPicture,
gps_accuracy: gpsAccuracy,
} = stateObj.attributes;
if (!(latitude && longitude)) {
continue;
}
if (computeStateDomain(stateObj) === "zone") {
// DRAW ZONE
if (passive) {
continue;
}
// create icon
let iconHTML = "";
if (icon) {
const el = document.createElement("ha-icon");
el.setAttribute("icon", icon);
iconHTML = el.outerHTML;
} else {
const el = document.createElement("span");
el.innerHTML = title;
iconHTML = el.outerHTML;
}
// create marker with the icon
mapZones.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: this._config!.dark_mode
? "dark"
: this._config!.dark_mode === false
? "light"
: "",
}),
interactive: false,
title,
})
);
// create circle around it
mapZones.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: "#FF9800",
radius,
})
);
continue;
}
// DRAW ENTITY
// create icon
const entityName = title
.split(" ")
.map((part) => part[0])
.join("")
.substr(0, 3);
// create market with the icon
mapItems.push(
Leaflet.marker([latitude, longitude], {
icon: Leaflet.divIcon({
// Leaflet clones this element before adding it to the map. This messes up
// our Polymer object and we can't pass data through. Thus we hack like this.
html: `
<ha-entity-marker
entity-id="${entityId}"
entity-name="${entityName}"
entity-picture="${entityPicture || ""}"
entity-color="${this._getColor(entityId)}"
></ha-entity-marker>
`,
iconSize: [48, 48],
className: "",
}),
title: computeStateName(stateObj),
})
);
// create circle around if entity has accuracy
if (gpsAccuracy) {
mapItems.push(
Leaflet.circle([latitude, longitude], {
interactive: false,
color: this._getColor(entityId),
radius: gpsAccuracy,
})
);
}
}
this._mapItems.forEach((marker) => map.addLayer(marker));
this._mapZones.forEach((marker) => map.addLayer(marker));
this._mapPaths.forEach((marker) => map.addLayer(marker));
}
private async _attachObserver(): Promise<void> {
// Observe changes to map size and invalidate to prevent broken rendering
if (!this._resizeObserver) {
await installResizeObserver();
this._resizeObserver = new ResizeObserver(this._debouncedResizeListener);
}
this._resizeObserver.observe(this);
}
);
private async _getHistory(): Promise<void> {
this._date = new Date();
@@ -601,9 +312,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return;
}
const entityIds = this._configEntities!.map((entity) => entity.entity).join(
","
);
const entityIds = this._configEntities!.join(",");
const endTime = new Date();
const startTime = new Date();
startTime.setHours(endTime.getHours() - this._config!.hours_to_show!);
@@ -624,7 +333,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
if (stateHistory.length < 1) {
return;
}
this._history = stateHistory;
}
@@ -636,13 +344,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
this._history = undefined;
} else {
// remove unused entities
const configEntityIds = this._configEntities?.map(
(configEntity) => configEntity.entity
);
this._history = this._history!.reduce(
(accumulator: HassEntity[][], entityStates) => {
const entityId = entityStates[0].entity_id;
if (configEntityIds?.includes(entityId)) {
if (this._configEntities?.includes(entityId)) {
accumulator.push(entityStates);
}
return accumulator;
@@ -660,7 +365,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
height: 100%;
}
#map {
ha-map {
z-index: 0;
border: none;
position: absolute;
@@ -671,7 +376,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
background: inherit;
}
ha-icon-button {
mwc-icon-button {
position: absolute;
top: 75px;
left: 3px;
@@ -685,14 +390,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
:host([ispanel]) #root {
height: 100%;
}
.dark {
color: #ffffff;
}
.light {
color: #000000;
}
`;
}
}
@@ -33,18 +33,18 @@ export class HuiActionEditor extends LitElement {
@property() protected hass?: HomeAssistant;
get _navigation_path(): string {
const config = this.config as NavigateActionConfig;
return config.navigation_path || "";
const config = this.config as NavigateActionConfig | undefined;
return config?.navigation_path || "";
}
get _url_path(): string {
const config = this.config as UrlActionConfig;
return config.url_path || "";
const config = this.config as UrlActionConfig | undefined;
return config?.url_path || "";
}
get _service(): string {
const config = this.config as CallServiceActionConfig;
return config.service || "";
return config?.service || "";
}
private _serviceAction = memoizeOne(
@@ -29,6 +29,7 @@ export class HuiInputListEditor extends LitElement {
.index=${index}
@value-changed=${this._valueChanged}
@blur=${this._consolidateEntries}
@keydown=${this._handleKeyDown}
><ha-icon-button
slot="suffix"
class="clear-button"
@@ -70,6 +71,13 @@ export class HuiInputListEditor extends LitElement {
});
}
private _handleKeyDown(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.stopPropagation();
this._consolidateEntries(ev);
}
}
private _consolidateEntries(ev: Event): void {
const target = ev.target! as EditorTarget;
if (target.value === "") {
@@ -34,7 +34,7 @@ const cardConfigStruct = object({
dark_mode: optional(boolean()),
entities: array(entitiesConfigStruct),
hours_to_show: optional(number()),
geo_location_sources: optional(array()),
geo_location_sources: optional(array(string())),
});
@customElement("hui-map-card-editor")
-88
View File
@@ -1,88 +0,0 @@
import "@polymer/iron-image/iron-image";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { EventsMixin } from "../../mixins/events-mixin";
/*
* @appliesMixin EventsMixin
*/
class HaEntityMarker extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="iron-positioning"></style>
<style>
.marker {
position: relative;
display: block;
margin: 0 auto;
width: 2.5em;
text-align: center;
height: 2.5em;
line-height: 2.5em;
font-size: 1.5em;
border-radius: 50%;
border: 0.1em solid var(--ha-marker-color, var(--primary-color));
color: var(--primary-text-color);
background-color: var(--card-background-color);
}
iron-image {
border-radius: 50%;
}
</style>
<div class="marker" style$="border-color:{{entityColor}}">
<template is="dom-if" if="[[entityName]]">[[entityName]]</template>
<template is="dom-if" if="[[entityPicture]]">
<iron-image
sizing="cover"
class="fit"
src="[[entityPicture]]"
></iron-image>
</template>
</div>
`;
}
static get properties() {
return {
hass: {
type: Object,
},
entityId: {
type: String,
value: "",
},
entityName: {
type: String,
value: null,
},
entityPicture: {
type: String,
value: null,
},
entityColor: {
type: String,
value: null,
},
};
}
ready() {
super.ready();
this.addEventListener("click", (ev) => this.badgeTap(ev));
}
badgeTap(ev) {
ev.stopPropagation();
if (this.entityId) {
this.fire("hass-more-info", { entityId: this.entityId });
}
}
}
customElements.define("ha-entity-marker", HaEntityMarker);
-263
View File
@@ -1,263 +0,0 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import {
replaceTileLayer,
setupLeafletMap,
} from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { navigate } from "../../common/navigate";
import "../../components/ha-icon";
import "../../components/ha-menu-button";
import { defaultRadiusColor } from "../../data/zone";
import "../../layouts/ha-app-layout";
import LocalizeMixin from "../../mixins/localize-mixin";
import "../../styles/polymer-ha-style";
import "./ha-entity-marker";
/*
* @appliesMixin LocalizeMixin
*/
class HaPanelMap extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
#map {
height: calc(100vh - var(--header-height));
width: 100%;
z-index: 0;
background: inherit;
}
.icon {
color: var(--primary-text-color);
}
</style>
<ha-app-layout>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button
hass="[[hass]]"
narrow="[[narrow]]"
></ha-menu-button>
<div main-title>[[localize('panel.map')]]</div>
<template is="dom-if" if="[[computeShowEditZone(hass)]]">
<ha-icon-button
icon="hass:pencil"
on-click="openZonesEditor"
></ha-icon-button>
</template>
</app-toolbar>
</app-header>
<div id="map"></div>
</ha-app-layout>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: "drawEntities",
},
narrow: Boolean,
};
}
connectedCallback() {
super.connectedCallback();
this.loadMap();
}
async loadMap() {
this._darkMode = this.hass.themes.darkMode;
[this._map, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this.$.map,
this._darkMode
);
this.drawEntities(this.hass);
this._map.invalidateSize();
this.fitMap();
}
disconnectedCallback() {
if (this._map) {
this._map.remove();
}
}
computeShowEditZone(hass) {
return !__DEMO__ && hass.user.is_admin;
}
openZonesEditor() {
navigate("/config/zone");
}
fitMap() {
let bounds;
if (this._mapItems.length === 0) {
this._map.setView(
new this.Leaflet.LatLng(
this.hass.config.latitude,
this.hass.config.longitude
),
14
);
} else {
bounds = new this.Leaflet.latLngBounds(
this._mapItems.map((item) => item.getLatLng())
);
this._map.fitBounds(bounds.pad(0.5));
}
}
drawEntities(hass) {
/* eslint-disable vars-on-top */
const map = this._map;
if (!map) return;
if (this._darkMode !== this.hass.themes.darkMode) {
this._darkMode = this.hass.themes.darkMode;
this._tileLayer = replaceTileLayer(
this.Leaflet,
map,
this._tileLayer,
this.hass.themes.darkMode
);
}
if (this._mapItems) {
this._mapItems.forEach(function (marker) {
marker.remove();
});
}
const mapItems = (this._mapItems = []);
if (this._mapZones) {
this._mapZones.forEach(function (marker) {
marker.remove();
});
}
const mapZones = (this._mapZones = []);
Object.keys(hass.states).forEach((entityId) => {
const entity = hass.states[entityId];
if (
entity.state === "home" ||
!("latitude" in entity.attributes) ||
!("longitude" in entity.attributes)
) {
return;
}
const title = computeStateName(entity);
let icon;
if (computeStateDomain(entity) === "zone") {
// DRAW ZONE
if (entity.attributes.passive) return;
// create icon
let iconHTML = "";
if (entity.attributes.icon) {
const el = document.createElement("ha-icon");
el.setAttribute("icon", entity.attributes.icon);
iconHTML = el.outerHTML;
} else {
const el = document.createElement("span");
el.innerHTML = title;
iconHTML = el.outerHTML;
}
icon = this.Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
className: "icon",
});
// create marker with the icon
mapZones.push(
this.Leaflet.marker(
[entity.attributes.latitude, entity.attributes.longitude],
{
icon: icon,
interactive: false,
title: title,
}
).addTo(map)
);
// create circle around it
mapZones.push(
this.Leaflet.circle(
[entity.attributes.latitude, entity.attributes.longitude],
{
interactive: false,
color: defaultRadiusColor,
radius: entity.attributes.radius,
}
).addTo(map)
);
return;
}
// DRAW ENTITY
// create icon
const entityPicture = entity.attributes.entity_picture || "";
const entityName = title
.split(" ")
.map(function (part) {
return part.substr(0, 1);
})
.join("");
/* Leaflet clones this element before adding it to the map. This messes up
our Polymer object and we can't pass data through. Thus we hack like this. */
icon = this.Leaflet.divIcon({
html:
"<ha-entity-marker entity-id='" +
entity.entity_id +
"' entity-name='" +
entityName +
"' entity-picture='" +
entityPicture +
"'></ha-entity-marker>",
iconSize: [45, 45],
className: "",
});
// create market with the icon
mapItems.push(
this.Leaflet.marker(
[entity.attributes.latitude, entity.attributes.longitude],
{
icon: icon,
title: computeStateName(entity),
}
).addTo(map)
);
// create circle around if entity has accuracy
if (entity.attributes.gps_accuracy) {
mapItems.push(
this.Leaflet.circle(
[entity.attributes.latitude, entity.attributes.longitude],
{
interactive: false,
color: "#0288D1",
radius: entity.attributes.gps_accuracy,
}
).addTo(map)
);
}
});
}
}
customElements.define("ha-panel-map", HaPanelMap);
+103
View File
@@ -0,0 +1,103 @@
import { mdiPencil } from "@mdi/js";
import "@material/mwc-icon-button";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { property } from "lit/decorators";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { navigate } from "../../common/navigate";
import "../../components/ha-svg-icon";
import "../../components/ha-menu-button";
import "../../layouts/ha-app-layout";
import { HomeAssistant } from "../../types";
import "../../components/map/ha-map";
import { haStyle } from "../../resources/styles";
class HaPanelMap extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
private _entities: string[] = [];
protected render() {
return html`
<ha-app-layout>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
<div main-title>${this.hass.localize("panel.map")}</div>
${!__DEMO__ && this.hass.user?.is_admin
? html`<mwc-icon-button @click=${this._openZonesEditor}
><ha-svg-icon .path=${mdiPencil}></ha-svg-icon
></mwc-icon-button>`
: ""}
</app-toolbar>
</app-header>
<ha-map .hass=${this.hass} .entities=${this._entities} autoFit></ha-map>
</ha-app-layout>
`;
}
private _openZonesEditor() {
navigate("/config/zone");
}
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
this._getStates(oldHass);
}
private _getStates(oldHass?: HomeAssistant) {
let changed = false;
const personSources = new Set<string>();
const locationEntities: string[] = [];
Object.values(this.hass!.states).forEach((entity) => {
if (
entity.state === "home" ||
!("latitude" in entity.attributes) ||
!("longitude" in entity.attributes)
) {
return;
}
locationEntities.push(entity.entity_id);
if (computeStateDomain(entity) === "person" && entity.attributes.source) {
personSources.add(entity.attributes.source);
}
if (oldHass?.states[entity.entity_id] !== entity) {
changed = true;
}
});
if (changed) {
this._entities = locationEntities.filter(
(entity) => !personSources.has(entity)
);
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-map {
height: calc(100vh - var(--header-height));
}
`,
];
}
}
customElements.define("ha-panel-map", HaPanelMap);
declare global {
interface HTMLElementTagNameMap {
"ha-panel-map": HaPanelMap;
}
}
@@ -0,0 +1,106 @@
import "@material/mwc-button";
import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { createCloseHeading } from "../../components/ha-dialog";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { LongLivedAccessTokenDialogParams } from "./show-long-lived-access-token-dialog";
const QR_LOGO_URL = "/static/icons/favicon-192x192.png";
@customElement("ha-long-lived-access-token-dialog")
export class HaLongLivedAccessTokenDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: LongLivedAccessTokenDialogParams;
@state() private _qrCode?: TemplateResult;
public showDialog(params: LongLivedAccessTokenDialogParams): void {
this._params = params;
}
public closeDialog() {
this._params = undefined;
this._qrCode = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params || !this._params.token) {
return html``;
}
return html`
<ha-dialog
open
hideActions
.heading=${createCloseHeading(this.hass, this._params.name)}
@closed=${this.closeDialog}
>
<div>
<paper-input
dialogInitialFocus
.value=${this._params.token}
.label=${this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.prompt_copy_token"
)}
type="text"
></paper-input>
<div id="qr">
${this._qrCode
? this._qrCode
: html`
<mwc-button @click=${this._generateQR}>
Generate QR code
</mwc-button>
`}
</div>
</div>
</ha-dialog>
`;
}
private async _generateQR() {
const qrcode = await import("qrcode");
const canvas = await qrcode.toCanvas(this._params?.token, {
width: 180,
errorCorrectionLevel: "Q",
});
const context = canvas.getContext("2d");
const imageObj = new Image();
imageObj.src = QR_LOGO_URL;
await new Promise((resolve) => {
imageObj.onload = resolve;
});
context.drawImage(
imageObj,
canvas.width / 3,
canvas.height / 3,
canvas.width / 3,
canvas.height / 3
);
this._qrCode = html`<img src=${canvas.toDataURL()}></img>`;
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
#qr {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-long-lived-access-token-dialog": HaLongLivedAccessTokenDialog;
}
}
@@ -18,6 +18,7 @@ import {
import { haStyle } from "../../resources/styles";
import "../../styles/polymer-ha-style";
import { HomeAssistant } from "../../types";
import { showLongLivedAccessTokenDialog } from "./show-long-lived-access-token-dialog";
@customElement("ha-long-lived-access-tokens-card")
class HaLongLivedTokens extends LitElement {
@@ -118,13 +119,7 @@ class HaLongLivedTokens extends LitElement {
client_name: name,
});
showPromptDialog(this, {
title: name,
text: this.hass.localize(
"ui.panel.profile.long_lived_access_tokens.prompt_copy_token"
),
defaultValue: token,
});
showLongLivedAccessTokenDialog(this, { token, name });
fireEvent(this, "hass-refresh-tokens");
} catch (err) {
+3 -1
View File
@@ -36,7 +36,9 @@ export class HaPickThemeRow extends LitElement {
const hasThemes =
this.hass.themes.themes && Object.keys(this.hass.themes.themes).length;
const curTheme =
this.hass.selectedTheme?.theme || this.hass.themes.default_theme;
this.hass.selectedTheme?.theme || this.hass.themes.darkMode
? this.hass.themes.default_dark_theme || this.hass.themes.default_theme
: this.hass.themes.default_theme;
const themeSettings = this.hass.selectedTheme;
@@ -1,5 +1,4 @@
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
import "@polymer/iron-label/iron-label";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -0,0 +1,17 @@
import { fireEvent } from "../../common/dom/fire_event";
export interface LongLivedAccessTokenDialogParams {
token: string;
name: string;
}
export const showLongLivedAccessTokenDialog = (
element: HTMLElement,
longLivedAccessTokenDialogParams: LongLivedAccessTokenDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-long-lived-access-token-dialog",
dialogImport: () => import("./ha-long-lived-access-token-dialog"),
dialogParams: longLivedAccessTokenDialogParams,
});
};
+16 -3
View File
@@ -1,10 +1,10 @@
// For localize
import "@formatjs/intl-getcanonicallocales/polyfill";
import "lit/polyfill-support";
import "core-js";
import "regenerator-runtime/runtime";
import "lit/polyfill-support";
import "@formatjs/intl-getcanonicallocales/polyfill";
// To use comlink under ES5
import "proxy-polyfill";
import "regenerator-runtime/runtime";
import "unfetch/polyfill";
// Source: https://github.com/jserz/js_piece/blob/master/DOM/ParentNode/append()/append().md
@@ -32,3 +32,16 @@ import "unfetch/polyfill";
});
});
})([Element.prototype, Document.prototype, DocumentFragment.prototype]);
// Source: https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttributeNames
if (Element.prototype.getAttributeNames === undefined) {
Element.prototype.getAttributeNames = function () {
const attributes = this.attributes;
const length = attributes.length;
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = attributes[i].name;
}
return result;
};
}
+2 -2
View File
@@ -29,10 +29,10 @@ documentContainer.innerHTML = `<custom-style>
--disabled-text-color: #bdbdbd;
/* main interface colors */
--primary-color: #03a9f4;
--primary-color: ${DEFAULT_PRIMARY_COLOR};
--dark-primary-color: #0288d1;
--light-primary-color: #b3e5fC;
--accent-color: #ff9800;
--accent-color: ${DEFAULT_ACCENT_COLOR};
--divider-color: rgba(0, 0, 0, .12);
--scrollbar-thumb-color: rgb(194, 194, 194);
@@ -2,6 +2,7 @@
const isSafari14 = /^((?!chrome|android).)*version\/14\.0\s.*safari/i.test(
navigator.userAgent
);
if (isSafari14) {
const origAttachShadow = window.Element.prototype.attachShadow;
window.Element.prototype.attachShadow = function (init) {
+25 -2
View File
@@ -1155,7 +1155,7 @@
"section": {
"validation": {
"heading": "Configuration validation",
"introduction": "Validate your configuration if you recently made some changes to your configuration and want to make sure that it is all valid",
"introduction": "Validate your configuration if you recently made some changes to your configuration and want to make sure that it is all valid.",
"check_config": "Check configuration",
"valid": "Configuration valid!",
"invalid": "Configuration invalid"
@@ -1975,6 +1975,7 @@
"confirm_rename_entity_ids": "Do you also want to rename the entity IDs of your entities?",
"confirm_rename_entity_ids_warning": "This will not change any configuration (like automations, scripts, scenes, dashboards) that is currently using these entities! You will have to update them yourself to use the new entity IDs!",
"confirm_disable_config_entry": "There are no more devices for the config entry {entry_name}, do you want to instead disable the config entry?",
"update_device_error": "Updating the device failed",
"disabled": "Disabled",
"data_table": {
"device": "Device",
@@ -2163,6 +2164,7 @@
"reload_restart_confirm": "Restart Home Assistant to finish reloading this integration",
"disable_restart_confirm": "Restart Home Assistant to finish disabling this integration",
"enable_restart_confirm": "Restart Home Assistant to finish enabling this integration",
"disable_error": "Enabling or disabling of the integration failed",
"manuf": "by {manufacturer}",
"hub": "Connected via",
"firmware": "Firmware: {version}",
@@ -3567,7 +3569,16 @@
"description": "Alternatively you can restore from a previous snapshot.",
"in_progress": "Restore in progress",
"show_log": "Show full log",
"hide_log": "Hide full log"
"hide_log": "Hide full log",
"full_snapshot": "[%key:supervisor::snapshot::full_snapshot%]",
"partial_snapshot": "[%key:supervisor::snapshot::partial_snapshot%]",
"type": "[%key:supervisor::snapshot::type%]",
"select_type": "[%key:supervisor::snapshot::select_type%]",
"folders": "[%key:supervisor::snapshot::folders%]",
"addons": "[%key:supervisor::snapshot::addons%]",
"password_protection": "[%key:supervisor::snapshot::password_protection%]",
"password": "[%key:supervisor::snapshot::password%]",
"confirm_password": "[%key:supervisor::snapshot::confirm_password%]"
}
},
"custom": {
@@ -3811,6 +3822,8 @@
"faq_link": "[%key:ui::panel::my::faq_link%]",
"error": "[%key:ui::panel::my::error%]",
"error_addon_not_found": "Add-on not found",
"error_addon_not_started": "The requested add-on are not running. Please start it first",
"error_addon_not_installed": "The requested add-on is not installed. Please install it first",
"error_addon_no_ingress": "The requested add-on does not support ingress"
},
"system": {
@@ -3920,9 +3933,11 @@
"addons": "Add-ons",
"folders": "Folders",
"password": "Snapshot password",
"confirm_password": "Confirm Snapshot password",
"password_protection": "Password protection",
"password_protected": "password protected",
"enter_password": "Please enter a password.",
"passwords_not_matching": "The passwords does not match",
"folder": {
"homeassistant": "Home Assistant configuration",
"ssl": "SSL",
@@ -3976,6 +3991,14 @@
"create_snapshot": "Create a snapshot of {name} before updating",
"updating": "Updating {name} to version {version}",
"snapshotting": "Creating snapshot of {name}"
},
"hardware": {
"title": "Hardware",
"search": "Search hardware",
"subsystem": "Subsystem",
"id": "ID",
"attributes": "Attributes",
"device_path": "Device path"
}
}
}
+14
View File
@@ -0,0 +1,14 @@
export const fileDownload = (
element: HTMLElement,
href: string,
filename: string
): void => {
const a = document.createElement("a");
a.target = "_blank";
a.href = href;
a.download = filename;
element.shadowRoot!.appendChild(a);
a.dispatchEvent(new MouseEvent("click"));
element.shadowRoot!.removeChild(a);
};
View File
+77 -21
View File
@@ -130,12 +130,12 @@
"action_error": {
"get_changelog": "Неуспешно получаване на списък с промени на добавката",
"go_to_config": "Грешка при стартиране на добавката - неуспешна проверка на конфигурацията!",
"install": "Инсталирането на добавката не бе успешно",
"restart": "Рестартирането на добавката не бе успешно",
"start": "Стартирането на добавката не бе успешно",
"install": "Инсталирането на добавката не е успешно",
"restart": "Рестартирането на добавката не е успешно",
"start": "Стартирането на добавката не е успешно",
"start_invalid_config": "Към конфигурацията",
"stop": "Спирането на добавката не бе успешно",
"uninstall": "Деинсталирането на добавката не бе успешно",
"stop": "Спирането на добавката не е успешно",
"uninstall": "Деинсталирането на добавката не е успешно",
"validate_config": "Грешка при проверка на конфигурацията на добавката"
},
"capability": {
@@ -218,8 +218,8 @@
"documentation": {
"get_documentation": "Неуспешно получаване на документация за добавката, {error}"
},
"failed_to_reset": "Нулирането на конфигурацията на добавката не бе успешно, {error}",
"failed_to_save": "Запазването на конфигурацията на добавката не бе успешно, {error}",
"failed_to_reset": "Нулирането на конфигурацията на добавката не е успешно, {error}",
"failed_to_save": "Запазването на конфигурацията на добавката не е успешно, {error}",
"logs": {
"get_logs": "Неуспешно получаване журнали на добавка, {грешка}"
},
@@ -241,10 +241,10 @@
"description": "Описание",
"error": {
"unknown": "Неизвестна грешка",
"update_failed": "Актуализацията не бе успешна"
"update_failed": "Актуализацията не е успешна"
},
"failed_to_restart_name": "Рестартирането на {name} не бе успешно",
"failed_to_update_name": "Актуализирането на {name} не бе успешно",
"failed_to_restart_name": "Рестартирането на {name} не е успешно",
"failed_to_update_name": "Актуализирането на {name} не е успешно",
"learn_more": "Научете повече",
"new_version_available": "Налична е нова версия",
"newest_version": "Последна версия",
@@ -260,6 +260,7 @@
"save": "Запис",
"show_more": "Показване на повече информация за това",
"update": "Актуализиране",
"update_available": "{count}{count, plural,\n one {обновление изчаква}\n other {{count} обновления изчакват}\n}",
"version": "Версия",
"yes": "Да"
},
@@ -285,12 +286,20 @@
"no_addons": "Все още нямате инсталирани добавки. Насочете се към хранилището за добавки, за да започнете!"
},
"dialog": {
"hardware": {
"attributes": "Атрибути",
"device_path": "Път до устройството",
"id": "ID",
"search": "Търсене на хардуер",
"subsystem": "Подсистема",
"title": "Хардуер"
},
"network": {
"connected_to": "Свързан с {ssid}",
"dhcp": "DHCP",
"disabled": "Деактивиран",
"dns_servers": "DNS Сървъри",
"failed_to_change": "Промяната на мрежовите настройки не бе успешна",
"failed_to_change": "Промяната на мрежовите настройки не е успешна",
"gateway": "Адрес на шлюза",
"ip_netmask": "IP адрес/Мрежова маска",
"open": "Отворена",
@@ -305,8 +314,8 @@
"registries": {
"add_new_registry": "Добавяне на нов регистър",
"add_registry": "Добавяне на регистър",
"failed_to_add": "Добавянето на регистър не бе успешно",
"failed_to_remove": "Премахването на регистър не бе успешно",
"failed_to_add": "Добавянето на регистър не е успешно",
"failed_to_remove": "Премахването на регистър не е успешно",
"no_registries": "Няма конфигурирани регистри",
"password": "Парола",
"registry": "Регистър",
@@ -334,6 +343,8 @@
"my": {
"error": "Възникна неизвестна грешка",
"error_addon_not_found": "Не е намерена добавка",
"error_addon_not_installed": "Исканата добавка не е инсталирана. Моля, първо я инсталирайте",
"error_addon_not_started": "Заявените добавки не е стартирана. Моля, първо я стартирайте",
"faq_link": "My Home Assistant ЧЗВ",
"not_supported": "Тази препратка не се поддържа от вашата Home Assistant инсталация. Последвайте {link} за поддържани препратки както и версиите при тяхното пускане."
},
@@ -346,13 +357,19 @@
"snapshot": {
"addons": "Добавки",
"available_snapshots": "Налични снапшоти",
"confirm_password": "Потвърдете паролата за снапшота",
"could_not_create": "Не можа да се създаде снапшот",
"create": "Създаване",
"create_blocked_not_running": "Създаването на снапшот в момента не е възможно, тъй като системата е в състояние {state}.",
"create_snapshot": "Създаване на снапшот",
"created": "Създаден",
"delete_selected": "Изтриване на избраните снапшоти",
"delete_snapshot_confirm": "Изтрий",
"delete_snapshot_text": "Искате ли да изтриете {number} {number, plural,\n one {резервно копие}\n other {резервни копия}\n}?",
"delete_snapshot_title": "Изтриване на снапшота",
"description": "Снапшотите ви позволяват лесно да архивирате и възстановявате всички данни от вашия екземпляр на Home Assistant.",
"enter_password": "Моля, въведете парола.",
"failed_to_delete": "Изтриването не е успешно",
"folder": {
"addons/local": "Локални добавки",
"homeassistant": "Конфигурация на Home Assistant",
@@ -368,7 +385,10 @@
"password": "Парола",
"password_protected": "защитен с парола",
"password_protection": "Защита с парола",
"passwords_not_matching": "Паролите не съвпадат",
"security": "Сигурност",
"select_type": "Изберете какво да възстановите",
"selected": "{number} избрани",
"type": "Тип",
"upload_snapshot": "Качване на снапшот"
},
@@ -435,7 +455,7 @@
"unhealthy_reason": {
"docker": "Средата на Docker не работи правилно",
"privileged": "Supervisor не е привилегирован",
"setup": "Настройката на Supervisor не бе успешна",
"setup": "Настройката на Supervisor не е успешна",
"supervisor": "Supervisor не успя да се актуализира",
"untrusted": "Открито e ненадежден съдържание"
},
@@ -494,7 +514,7 @@
"heating": "{name} отопление",
"high": "високо",
"low": "ниско",
"on_off": "Вкл. / Изкл",
"on_off": "Вкл. / Изкл.",
"operation": "Режим",
"preset_mode": "Предварително зададени настройки",
"swing_mode": "Режим на люлеене",
@@ -874,6 +894,9 @@
"config_entry_system_options": {
"enable_new_entities_description": "Ако е изключено, новооткритите обекти за {integration} няма да бъдат автоматично добавяни в Home Assistant",
"enable_new_entities_label": "Активирай новодобавените обекти.",
"enable_polling_description": "Дали Home Assistant трябва да обновява автоматично обектите от {integration}.",
"enable_polling_label": "Включи автоматично опресняване",
"restart_home_assistant": "Трябва да рестартирате Home Assistant, за да влязат в сила промените.",
"title": "Системни опции за {integration}",
"update": "Актуализация"
},
@@ -1121,6 +1144,7 @@
"cluster_header": "Клъстер",
"configuration_complete": "Преконфигурирането на устройството завърши.",
"configuration_failed": "Преконфигурирането на устройството не бе успешно. Допълнителна информация може да бъде налична в дневниците.",
"configuring_alt": "Конфигуриране",
"heading": "Преконфигуриране на устройство",
"in_progress": "Устройството се преконфигурира. Това може да отнеме известно време.",
"min_max_change": "мин/макс/промяна",
@@ -1637,7 +1661,8 @@
"link_learn_how_it_works": "Научете как работи",
"not_connected": "Не е свързан",
"remote_enabled": {
"caption": "Автоматично свързване"
"caption": "Автоматично свързване",
"description": "Активирайте тази опция, за да сте сигурни, че вашият Home Assistant е винаги достъпен от разстояние."
},
"title": "Дистанционен контрол"
},
@@ -1916,7 +1941,8 @@
"scripts": "Скриптове",
"unknown_error": "Неизвестна грешка",
"unnamed_device": "Устройство без име",
"update": "Актуализация"
"update": "Актуализация",
"update_device_error": "Актуализирането на устройството не е успешно"
},
"entities": {
"caption": "Обекти",
@@ -2028,6 +2054,7 @@
"depends_on_cloud": "Зависи от облака",
"device_unavailable": "Устройството е недостъпно",
"devices": "{count} {count, plural,\n one {устройство}\n other {устройства}\n}",
"disable_error": "Активирането или деактивирането на интеграцията не бе успешно",
"disable_restart_confirm": "Рестартирайте Home Assistant за да завършите деактивирането на тази интеграция",
"disable": {
"disable_confirm": "Наистина ли искате да забраните този конфигурационен запис? Устройствата и обекти му ще бъдат деактивирани.",
@@ -2039,6 +2066,7 @@
},
"disabled_cause": "Деактивирано от {cause}"
},
"disabled_polling": "Автоматичното обновяване за данни е забранено",
"documentation": "Документация",
"enable_restart_confirm": "Рестартирайте Home Assistant за да завършите активирането на тази интеграция",
"entities": "{count} {count, plural,\n one {обект}\n other {обекта}\n}",
@@ -2093,7 +2121,7 @@
"confirm_delete_ignore_title": "Да се спре ли игнорирането на {name}?",
"confirm_ignore": "Наистина ли не искате да настроите тази интеграция? Можете да отмените това, като кликнете върху „Показване на игнорирани интеграции“ в менюто горе вдясно.",
"hide_ignored": "Скриване на игнорираните интеграции",
"ignore": "Игнорирайте",
"ignore": "Игнориране",
"ignored": "Игнорирана",
"show_ignored": "Показване на игнорираните интеграции",
"stop_ignore": "Спрете да игнорирате"
@@ -2240,7 +2268,8 @@
"product_manual": "Ръководство за продукта"
},
"node_query_stages": {
"associations": "Обновяване на свързаните групи и членства"
"associations": "Обновяване на свързаните групи и членства",
"complete": "Разпита приключи"
},
"node": {
"button": "Детайли за възела",
@@ -2256,6 +2285,7 @@
},
"refresh_node": {
"button": "Обнови възела",
"description": "Това ще накара OpenZWave да разпита дадено устройство и да актуализира командните му класове, възможностите и стойностите му.",
"node_status": "Състояние на възела",
"refreshing_description": "Опресняване на информацията за възела...",
"step": "Стъпка"
@@ -2325,6 +2355,8 @@
"add_scene": "Добавяне на сцена",
"delete_confirm": "Сигурни ли сте, че искате да изтриете тази сцена?",
"delete_scene": "Изтриване на сцената",
"duplicate": "Дублиране",
"duplicate_scene": "Дублиране на сцената",
"edit_scene": "Редактиране на сцената",
"header": "Редактор на сцени",
"headers": {
@@ -2528,8 +2560,10 @@
"CONFIGURED_status_text": "Инициализация",
"INITIALIZED": "Инициализацията завърши",
"INITIALIZED_status_text": "Устройството е готово за употреба",
"INTERVIEW_COMPLETE": "Разпита приключи",
"INTERVIEW_COMPLETE_status_text": "Конфигуриране",
"PAIRED": "Намерено устройство"
"PAIRED": "Намерено устройство",
"PAIRED_status_text": "Започване на разпит"
},
"groups": {
"add_group": "Добавяне на група",
@@ -2585,6 +2619,8 @@
"zwave_js": {
"add_node": {
"inclusion_failed": "Възелът не може да бъде добавен. Моля, проверете журналите за повече информация.",
"interview_failed": "Разпита на устройството не бе успешен. Допълнителна информация може да е налична в логовете.",
"interview_started": "Устройството се разпитва. Това може да отнеме известно време.",
"title": "Добавяне на Z-Wave възел"
},
"button": "Конфигуриране",
@@ -2628,6 +2664,16 @@
"node_status": {
"unknown": "Неизвестен"
},
"reinterview_node": {
"battery_device_warning": "Ще трябва да събудите устройствата, захранвани с батерии, преди да започнете повторното интервю. Вижте ръководството на вашето устройство за инструкции как да го събудите.",
"in_progress": "Устройството се разпитва. Това може да отнеме известно време.",
"interview_complete": "Разпита на устройството приключи.",
"interview_failed": "Разпита на устройството не бе успешен. Допълнителна информация може да е налична в логовете.",
"introduction": "Повторно разпитване на устройство във вашата Z-Wave мрежа. Използвайте тази функция, ако устройството има липсваща или неправилна функционалност.",
"run_in_background": "Можете да затворите този диалогов прозорец и разпита ще продължи във фонов режим.",
"start_reinterview": "Започване на нов разпит",
"title": "Повторен разпит на Z-Wave устройство"
},
"remove_node": {
"exclusion_failed": "Възелът не може да бъде премахнат. Моля, проверете журналите за повече информация.",
"title": "Премахване на Z-Wave възел"
@@ -2678,6 +2724,7 @@
"node_protection": "Защита на възела",
"nodes": "Възли",
"nodes_in_group": "Други възли в тази група:",
"pooling_intensity": "Честота на опресняване",
"protection": "Защита",
"remove_from_group": "Премахване от групата"
},
@@ -3346,10 +3393,19 @@
"intro": "Готови ли сте да събудите дома си, да отвоювате независимостта си и да се присъедините към световна общност от хора автоматизиращи домовете си?",
"next": "Следващ",
"restore": {
"addons": "Добавки",
"confirm_password": "Потвърдете паролата за снапшота",
"description": "Като алтернатива можете да възстановите от предишен снапшот.",
"folders": "Папки",
"full_snapshot": "Пълен снапшот",
"hide_log": "Скриване на пълния дневник",
"in_progress": "Възстановяването е в ход",
"show_log": "Показване на пълния дневник"
"partial_snapshot": "Частичен снапшот",
"password": "Парола",
"password_protection": "Защита с парола",
"select_type": "Изберете какво да възстановите",
"show_log": "Показване на пълния дневник",
"type": "Тип"
},
"user": {
"create_account": "Създай акаунт",
+36 -9
View File
@@ -284,7 +284,7 @@
"refresh": "Actualitza",
"release_notes": "Notes de la versió",
"reload": "Torna a carregar",
"reset_defaults": "Restableix als valors per defecte",
"reset_defaults": "Restableix els valors per defecte",
"reset_options": "Opcions de reinici",
"restart": "Reinicia",
"restart_name": "Reinicia {name}",
@@ -318,6 +318,14 @@
"no_addons": "Encara no tens cap complement instal·lat. Vés al directori de complements per començar!"
},
"dialog": {
"hardware": {
"attributes": "Atributs",
"device_path": "Ruta del dispositiu",
"id": "ID",
"search": "Cerca maquinari",
"subsystem": "Subsistema",
"title": "Maquinari"
},
"network": {
"connected_to": "Connectat a {ssid}",
"dhcp": "DHCP",
@@ -368,6 +376,8 @@
"error": "S'ha produït un error desconegut",
"error_addon_no_ingress": "El complement sol·licitat no admet ingress",
"error_addon_not_found": "No s'ha trobat el complement",
"error_addon_not_installed": "El complement sol·licitat no està instal·lat. Instal·la'l primer",
"error_addon_not_started": "El complement sol·licitat no s'està executant. Inicia'l primer",
"faq_link": "Preguntes freqüents de My Home Assistant",
"not_supported": "La instància de Home Assistant no admet aquesta redirecció. Consulta {link} per veure les redireccions compatibles i en quina versió es van introduir."
},
@@ -380,6 +390,7 @@
"snapshot": {
"addons": "Complements",
"available_snapshots": "Instantànies disponibles",
"confirm_password": "Confirma la contrasenya de la instantània",
"could_not_create": "No s'ha pogut crear la instantània",
"create": "Crea",
"create_blocked_not_running": "Ara mateix no és possible crear una instantània perquè el sistema es troba en estat {state}.",
@@ -407,6 +418,7 @@
"password": "Contrasenya de la instantània",
"password_protected": "protegit amb contrasenya",
"password_protection": "Protecció amb contrasenya",
"passwords_not_matching": "Les contrasenyes no coincideixen",
"security": "Seguretat",
"select_type": "Selecciona què vols restaurar",
"selected": "{number} seleccionada/es",
@@ -929,8 +941,11 @@
},
"dialogs": {
"config_entry_system_options": {
"enable_new_entities_description": "Si es desactiva, les entitats descobertes recentment per a {integration} no s'afegiran automàticament a Home Assistant.",
"enable_new_entities_description": "Si esactivat, les noves entitats descobertes per {integration} s'afegiran automàticament a Home Assistant.",
"enable_new_entities_label": "Activa entitats afegides recentment.",
"enable_polling_description": "Si Home Assistant ha de sondejar automàticament {integration} per obtenir actualitzacions de les entitats.",
"enable_polling_label": "Activa el sondeig per obtenir actualitzacions.",
"restart_home_assistant": "Has de reiniciar Home Assistant per aplicar els canvis.",
"title": "Opcions del sistema per a {integration}",
"update": "Actualitza"
},
@@ -1388,7 +1403,7 @@
"type_select": "Tipus de repetició",
"type": {
"count": {
"label": "Compta"
"label": "Comptador"
},
"until": {
"conditions": "Condicions de \"Fins que\"",
@@ -1540,7 +1555,7 @@
"delete_confirm": "Segur que vols eliminar-ho?",
"duplicate": "Duplica",
"header": "Disparadors",
"introduction": "Els activadors són les regles que fan que es dispari una automatització. Pots definir més d'un activador per a cada automatització. Una vegada s'iniciï un activador, Home Assistant validarà les condicions (si n'hi ha) i finalment cridarà l'acció.",
"introduction": "Els disparadors són les regles que fan que s'executi una automatització. Pots definir més d'un disparador per a cada automatització. Una vegada salti un disparador, Home Assistant validarà les condicions (si n'hi ha) i finalment cridarà l'acció.",
"learn_more": "Més informació sobre els activadors",
"name": "Disparador",
"type_select": "Tipus de disparador",
@@ -2073,7 +2088,8 @@
"scripts": "Programes (scripts)",
"unknown_error": "Error desconegut",
"unnamed_device": "Dispositiu sense nom",
"update": "Actualitza"
"update": "Actualitza",
"update_device_error": "No s'ha pogut actualitzar el dispositiu"
},
"entities": {
"caption": "Entitats",
@@ -2206,6 +2222,7 @@
"depends_on_cloud": "Depèn del núvol",
"device_unavailable": "Dispositiu no disponible",
"devices": "{count} {count, plural,\n one {dispositiu}\n other {dispositius}\n}",
"disable_error": "S'ha produït un error en activar o desactivar la integració",
"disable_restart_confirm": "Reinicia Home Assistant per acabar de desactivar aquesta integració",
"disable": {
"disable_confirm": "Estàs segur que vols desactivar aquesta entrada de configuració? Els seus dispositius i entitats es desactivaran.",
@@ -2217,6 +2234,7 @@
},
"disabled_cause": "Desactivada per {cause}"
},
"disabled_polling": "Sondeig automàtic per l'obtenció de dades actualitzades desactivat",
"documentation": "Documentació",
"enable_restart_confirm": "Reinicia Home Assistant per acabar d'activar aquesta integració",
"entities": "{count} {count, plural,\n one {entitat}\n other {entitats}\n}",
@@ -2713,7 +2731,7 @@
"validation": {
"check_config": "Comprova la configuració",
"heading": "Validació de la configuració",
"introduction": "Valida la configuració si recentment has fet algun canvi a la configuració i vols assegurar-te de que sigui vàlida.",
"introduction": "Valida la configuració si recentment n'has fet algun canvi i vols assegurar-te de que sigui vàlida.",
"invalid": "Configuració invàlida",
"valid": "Configuració vàlida!"
}
@@ -3708,7 +3726,7 @@
},
"page-authorize": {
"abort_intro": "S'ha avortat l'inici de sessió",
"authorizing_client": "Esteu a punt de permetre l'accés a la vostra instància de Home Assistant al client {clientId}.",
"authorizing_client": "Estàs a punt de concedir al client {clientId} l'accés a la teva instància de Home Assistant.",
"form": {
"error": "Error: {error}",
"next": "Següent",
@@ -3802,7 +3820,7 @@
"working": "Si us plau, espereu"
},
"initializing": "S'està inicialitzant",
"logging_in_to_with": "S'escriuen els registes amb **{authProviderName}** a **{locationName}**.",
"logging_in_to_with": "Iniciant sessió a **{locationName}** amb **{authProviderName}**.",
"logging_in_with": "Iniciant sessió amb **{authProviderName}**.",
"pick_auth_provider": "O bé inicieu sessió amb"
},
@@ -3873,10 +3891,19 @@
"intro": "Estàs preparat donar vida pròpia a la teva llar, recuperar la teva privacitat i unir-te a una comunitat mundial de \"tinkerers\"?",
"next": "Següent",
"restore": {
"addons": "Complements",
"confirm_password": "Confirma la contrasenya de la instantània",
"description": "També pots restaurar des d'una instantània anterior.",
"folders": "Carpetes",
"full_snapshot": "Instantània completa",
"hide_log": "Amaga el registre complet",
"in_progress": "Restauració en curs",
"show_log": "Mostra el registre complet"
"partial_snapshot": "Instantània parcial",
"password": "Contrasenya de la instantània",
"password_protection": "Protecció amb contrasenya",
"select_type": "Selecciona què vols restaurar",
"show_log": "Mostra el registre complet",
"type": "Tipus d'instantània"
},
"user": {
"create_account": "Crear compte",
+36 -7
View File
@@ -318,6 +318,14 @@
"no_addons": "Zatím nemáte nainstalované žádné doplňky. Chcete-li začít, přejděte do obchodu s doplňky."
},
"dialog": {
"hardware": {
"attributes": "Atributy",
"device_path": "Cesta k zařízení",
"id": "ID",
"search": "Hledat hardware",
"subsystem": "Subsystém",
"title": "Hardware"
},
"network": {
"connected_to": "Připojeno k {ssid}",
"dhcp": "DHCP",
@@ -368,6 +376,8 @@
"error": "Nastala neznámá chyba",
"error_addon_no_ingress": "Požadovaný doplněk nepodporuje ingress",
"error_addon_not_found": "Doplněk nebyl nalezen",
"error_addon_not_installed": "Požadovaný doplněk není nainstalován. Nejprve jej prosím nainstalujte",
"error_addon_not_started": "Požadovaný doplněk není spuštěn. Nejprve jej prosím spusťte",
"faq_link": "Časté dotazy týkající se My Home Assistant",
"not_supported": "Toto přesměrování není vaší instancí Home Assistant podporováno. Zkontrolujte {link} pro podporovaná přesměrování a verzi, ve které byla zavedena."
},
@@ -380,6 +390,7 @@
"snapshot": {
"addons": "Doplňky",
"available_snapshots": "Dostupné zálohy",
"confirm_password": "Potvrďte heslo zálohy",
"could_not_create": "Nelze vytvořit zálohu",
"create": "Vytvořit",
"create_blocked_not_running": "Vytvoření zálohy není momentálně možné, protože systém je ve \"{state}\".",
@@ -407,6 +418,7 @@
"password": "Heslo zálohy",
"password_protected": "chráněno heslem",
"password_protection": "Ochrana heslem",
"passwords_not_matching": "Hesla se neshodují",
"security": "Zabezpečení",
"select_type": "Vyberte, co chcete obnovit",
"selected": "{number} vybraných",
@@ -929,8 +941,11 @@
},
"dialogs": {
"config_entry_system_options": {
"enable_new_entities_description": "Pokud je zakázáno, nově objevené entity pro {integration} nebudou automaticky přidány do Home Assistant.",
"enable_new_entities_description": "Pokud se mají automaticky přidat nově objevená zařízení integrace {integration}.",
"enable_new_entities_label": "Povolit nově přidané entity.",
"enable_polling_description": "Má-li Home Assistant automaticky zjišťovat aktualizace entit integrace {integration}.",
"enable_polling_label": "Povolit dotazování na aktualizace.",
"restart_home_assistant": "Aby se změny projevily, je třeba restartovat Home Assistant.",
"title": "Upravit nastavení pro {integration}",
"update": "Aktualizovat"
},
@@ -1164,7 +1179,7 @@
},
"types": {
"navigation": "Navigovat",
"reload": "Znovu načíst",
"reload": "Nově načíst",
"server_control": "Server"
}
},
@@ -2073,7 +2088,8 @@
"scripts": "Skripty",
"unknown_error": "Neznámá chyba",
"unnamed_device": "Nepojmenované zařízení",
"update": "Aktualizovat"
"update": "Aktualizovat",
"update_device_error": "Aktualizace zařízení se nezdařila"
},
"entities": {
"caption": "Entity",
@@ -2206,6 +2222,7 @@
"depends_on_cloud": "Závisí na cloudu",
"device_unavailable": "Zařízení není dostupné",
"devices": "{count} {count, plural,\n one {zařízení}\n other {zařízení}\n}",
"disable_error": "Povolení nebo zakázání integrace se nezdařilo",
"disable_restart_confirm": "Restartujte Home Assistant pro dokončení odstranění této integrace",
"disable": {
"disable_confirm": "Opravdu chcete zakázat tuto položku konfigurace? Její zařízení a entity budou zakázány.",
@@ -2217,6 +2234,7 @@
},
"disabled_cause": "Zakázáno {cause}"
},
"disabled_polling": "Automatické dotazování na aktualizovaná data je zakázáno",
"documentation": "Dokumentace",
"enable_restart_confirm": "Restartujte Home Assistant pro dokončení přidání této integrace",
"entities": "{count} {count, plural,\n one {entita}\n other {entit}\n}",
@@ -2713,7 +2731,7 @@
"validation": {
"check_config": "Zkontrolujte konfiguraci",
"heading": "Ověření konfigurace",
"introduction": "Pokud jste nedávno provedli změny konfigurace a chcete se ujistit, že je vše v pořádku, můžete zde konfiguraci ověřit",
"introduction": "Pokud jste nedávno provedli nějaké změny v konfiguraci a chcete se ujistit, že jsou všechny platné, proveďte ověření konfigurace.",
"invalid": "Konfigurace není v pořádku!",
"valid": "Konfigurace je v pořádku!"
}
@@ -2928,6 +2946,7 @@
"follow_device_instructions": "Podle pokynů dodaných se zařízením aktivujte párování na zařízení.",
"inclusion_failed": "Uzel nelze přidat. Další informace najdete v protokolech.",
"inclusion_finished": "Uzel byl přidán.",
"interview_failed": "Komunikace se zařízením se nezdařila. V logách mohou být k dispozici další informace.",
"introduction": "Tento průvodce vás provede přidáním uzlu do vaší sítě Z-Wave.",
"secure_inclusion_warning": "Zabezpečená zařízení vyžadují větší šířku pásma; příliš mnoho zabezpečených zařízení může zpomalit vaši síť Z-Wave. Bezpečné začlenění doporučujeme používat pouze u zařízení, která to vyžadují, jako jsou zámky nebo otvírače garážových vrat.",
"start_inclusion": "Zahájit začlenění",
@@ -3092,7 +3111,7 @@
"heal_node": "Uzdravit uzel",
"node_info": "Informace o uzlu",
"print_node": "Otisk uzlu",
"refresh_entity": "Znovu načíst Entitu",
"refresh_entity": "Nově načíst Entitu",
"refresh_node": "Obnovit uzel",
"remove_failed_node": "Odebrat selhaný uzel",
"remove_node": "Odebrat uzel",
@@ -3157,6 +3176,7 @@
"copy_id": "Zkopírovat ID do schránky",
"current_entities": "Současné entity",
"description1": "Nastavte stav zařízení v Home Assistant.",
"description2": "Pokud entita patří k zařízení, neprobíhá s tímto zařízením žádná skutečná komunikace.",
"entity": "Entita",
"filter_attributes": "Filtrovat atributy",
"filter_entities": "Filtrovat entity",
@@ -3534,7 +3554,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",
"search_cards": "Hledat karty",
"show_code_editor": "Zobrazit editor kódu",
"show_visual_editor": "Zobrazit vizuální editor",
"toggle_editor": "Přepnout Editor",
@@ -3866,10 +3886,19 @@
"intro": "Jste připraveni oživit svůj domov, zachovat si své soukromí a připojit se k celosvětové komunitě tvůrců?",
"next": "Další",
"restore": {
"addons": "Doplňky",
"confirm_password": "Potvrďte heslo zálohy",
"description": "Případně můžete Home Assistant obnovit z poslední zálohy.",
"folders": "Složky",
"full_snapshot": "Úplná záloha",
"hide_log": "Skrýt celý log",
"in_progress": "Probíhá obnovení",
"show_log": "Zobrazit celý log"
"partial_snapshot": "Částečná záloha",
"password": "Heslo zálohy",
"password_protection": "Ochrana heslem",
"select_type": "Vyberte, co chcete obnovit",
"show_log": "Zobrazit celý log",
"type": "Typ zálohy"
},
"user": {
"create_account": "Vytvořit účet",
+29 -2
View File
@@ -318,6 +318,14 @@
"no_addons": "Du har endnu ikke installeret nogen tilføjelsesprogrammer. Gå over til butikken for tilføjelsesprogrammer for at komme i gang!"
},
"dialog": {
"hardware": {
"attributes": "Attributter",
"device_path": "Enhedssti",
"id": "Identifikationsnummer",
"search": "Søg efter hardware",
"subsystem": "Delsystem",
"title": "Hardware"
},
"network": {
"connected_to": "Forbundet til {ssid}",
"dhcp": "DHCP",
@@ -368,6 +376,8 @@
"error": "Der opstod en ukendt fejl",
"error_addon_no_ingress": "Tilføjelsesprogrammet understøtter ikke ingress",
"error_addon_not_found": "Tilføjelsesprogrammet blev ikke fundet",
"error_addon_not_installed": "Tilføjelsesprogrammet er ikke installeret venligst installer det først",
"error_addon_not_started": "Tilføjelsesprogrammet kører ikke venligst start det først",
"faq_link": "Ofte stillede spørgsmål om Home Assistant",
"not_supported": "Denne omdirigering understøttes ikke af din Home Assistant installation. Kontroller {link} for de understøttede omdirigeringer og den version, de blev introduceret i."
},
@@ -380,6 +390,7 @@
"snapshot": {
"addons": "Tilføjelsesprogrammer",
"available_snapshots": "Tilgængelige snapshots",
"confirm_password": "Bekræft snapshot kodeord",
"could_not_create": "Kunne ikke oprette snapshot",
"create": "Opret",
"create_blocked_not_running": "Det er ikke muligt at oprette et snapshot lige nu, fordi systemet er i tilstanden {state}.",
@@ -407,6 +418,7 @@
"password": "Kodeord",
"password_protected": "beskyttet med kodeord",
"password_protection": "Kodeordsbeskyttelse",
"passwords_not_matching": "Kodeordene er forskellige",
"security": "Sikkerhed",
"select_type": "Vælg, hvad der skal gendannes",
"selected": "{number} valgt",
@@ -931,6 +943,9 @@
"config_entry_system_options": {
"enable_new_entities_description": "Hvis deaktiveret, tilføjes nyligt opdagede entiteter fra {integration} ikke automatisk til Home Assistant.",
"enable_new_entities_label": "Aktivér nyligt tilføjede entiteter.",
"enable_polling_description": "Hvis Home Assistant automatisk skal hente {integration} enheder for opdateringer.",
"enable_polling_label": "Aktiver automatisk henting af opdateringer",
"restart_home_assistant": "Du skal genstarte Home Assistant før dine ændringer aktiveres",
"title": "Systemindstillinger for {integration}",
"update": "Opdater"
},
@@ -2073,7 +2088,8 @@
"scripts": "Scripts",
"unknown_error": "Ukendt fejl",
"unnamed_device": "Enhed uden navn",
"update": "Opdater"
"update": "Opdater",
"update_device_error": "Enhedsopdateringen fejlede"
},
"entities": {
"caption": "Entiteter",
@@ -2206,6 +2222,7 @@
"depends_on_cloud": "Afhængig af Cloud tjenester",
"device_unavailable": "Enheden er utilgængelig",
"devices": "{count} {count, plural,\n one {enhed}\n other {enheder}\n}",
"disable_error": "Aktivering eller deaktivering af integrationen fejlede",
"disable_restart_confirm": "Genstart Home Assistant for at fuldføre deaktivering af denne integration",
"disable": {
"disable_confirm": "Er du sikker på, at du vil deaktivere denne integration? Integrationens enheder og entiteter vil blive deaktiveret.",
@@ -2217,6 +2234,7 @@
},
"disabled_cause": "Deaktiveret af {cause}"
},
"disabled_polling": "Den automatiske hentning af opdateret data er frakoblet",
"documentation": "Dokumentation",
"enable_restart_confirm": "Genstart Home Assistant for at fuldføre aktivering af denne integration",
"entities": "{count} {count, plural,\n one {entitet}\n other {entiteter}\n}",
@@ -3873,10 +3891,19 @@
"intro": "Er du klar til at vække dit hjem til live, genvinde dit privatliv og blive medlem af et verdensomspændende fællesskab af tinkerers?",
"next": "Næste",
"restore": {
"addons": "Tilføjelse",
"confirm_password": "Bekræft Snapshot kodeord",
"description": "Alternativt kan du gendanne fra et tidligere snapshot.",
"folders": "Mappe",
"full_snapshot": "Fuld snapshot",
"hide_log": "Skjul fuld log",
"in_progress": "Gendannelse er i gang",
"show_log": "Vis den fulde log"
"partial_snapshot": "Delvis snapshot",
"password": "Snapshot kodeord",
"password_protection": "Kodeordsbeskyttelse",
"select_type": "Vælg det der skal genetableres",
"show_log": "Vis den fulde log",
"type": "Snapshot type"
},
"user": {
"create_account": "Opret konto",
+45 -6
View File
@@ -314,10 +314,18 @@
"addon_new_version": "Neue Version verfügbar",
"addon_running": "Add-on wird ausgeführt",
"addon_stopped": "Add-on ist gestoppt",
"addons": "Add-ons",
"addons": "Installierte Add-ons",
"no_addons": "Du hast noch keine Add-ons installiert. Gehe zum Add-on Store, um loszulegen!"
},
"dialog": {
"hardware": {
"attributes": "Attribute",
"device_path": "Gerätepfad",
"id": "ID",
"search": "Hardware suchen",
"subsystem": "Subsystem",
"title": "Hardware"
},
"network": {
"connected_to": "Verbunden mit {ssid}",
"dhcp": "DHCP",
@@ -368,6 +376,8 @@
"error": "Ein unbekannter Fehler ist aufgetreten.",
"error_addon_no_ingress": "Das angeforderte Add-on unterstützt keinen Ingress",
"error_addon_not_found": "Add-on nicht gefunden",
"error_addon_not_installed": "Das angeforderte Add-on ist nicht installiert. Bitte installiere es zuerst",
"error_addon_not_started": "Das angeforderte Add-on läuft nicht. Bitte starte es zuerst",
"faq_link": "Häufig gestellten Fragen zu Home Assistant",
"not_supported": "Diese Weiterleitung wird von deiner Home Assistant-Instanz nicht unterstützt. Überprüfe den {link} auf die unterstützten Weiterleitungen und die Version, in der sie eingeführt wurden."
},
@@ -380,13 +390,19 @@
"snapshot": {
"addons": "Add-ons",
"available_snapshots": "Verfügbare Datensicherungen",
"confirm_password": "Snapshot-Passwort bestätigen",
"could_not_create": "Datensicherung konnte nicht erstellt werden",
"create": "Erstellen",
"create_blocked_not_running": "Das Erstellen eines Snapshots ist derzeit nicht möglich, da sich das System im Zustand {state} befindet.",
"create_snapshot": "Datensicherung erstellen",
"created": "Erstellt",
"delete_selected": "Ausgewählten Snapshot löschen",
"delete_snapshot_confirm": "löschen",
"delete_snapshot_text": "Möchtest du {number} {number, plural,\n one {den Snapshot}\n other {die Snapshots}\n} löschen?",
"delete_snapshot_title": "Snapshot löschen",
"description": "Datensicherungen ermöglichen dir das leichte Speichern und Wiederherstellen von allen Daten aus Home Assistant.",
"enter_password": "Bitte Passwort eingeben.",
"failed_to_delete": "Löschen fehlgeschlagen",
"folder": {
"addons/local": "Lokale Add-ons",
"homeassistant": "Home Assistant-Konfiguration",
@@ -396,14 +412,17 @@
},
"folders": "Ordner",
"full_snapshot": "Vollständige Datensicherung",
"name": "Name",
"name": "Snapshot-Name",
"no_snapshots": "Du hast bisher keine Datensicherungen erstellt.",
"partial_snapshot": "Selektive Datensicherung",
"password": "Passwort",
"password": "Snapshot-Passwort",
"password_protected": "Passwort geschützt",
"password_protection": "Passwortschutz",
"passwords_not_matching": "Passwörter stimmen nicht überein",
"security": "Sicherheit",
"type": "Typ",
"select_type": "Wähle aus, was wiederhergestellt werden soll",
"selected": "{number} ausgewählt",
"type": "Snapshot-Typ",
"upload_snapshot": "Datensicherung hochladen"
},
"store": {
@@ -720,6 +739,9 @@
"no_match": "Keine übereinstimmende Bereiche gefunden",
"show_areas": "Bereiche anzeigen"
},
"attributes": {
"expansion_header": "Attribute"
},
"blueprint-picker": {
"add_user": "Benutzer hinzufügen",
"remove_user": "Benutzer entfernen",
@@ -921,6 +943,9 @@
"config_entry_system_options": {
"enable_new_entities_description": "Wenn deaktiviert werden neu erkannte Entitäten für {integration} nicht automatisch zu Home Assistant hinzugefügt.",
"enable_new_entities_label": "Neu hinzugefügte Entitäten aktivieren.",
"enable_polling_description": "Ob Home Assistant automatisch {integration} Entitäten nach Updates abfragen soll.",
"enable_polling_label": "Aktiviere Polling für Updates.",
"restart_home_assistant": "Du musst Home Assistant neu starten, damit deine Änderungen wirksam werden.",
"title": "Einstellungen für {integration}",
"update": "Aktualisieren"
},
@@ -1719,6 +1744,7 @@
"title": "Alexa"
},
"connected": "Verbunden",
"connecting": "Verbinde...",
"connection_status": "Cloud-Verbindungsstatus",
"fetching_subscription": "Abo wird abgerufen ...",
"google": {
@@ -2062,7 +2088,8 @@
"scripts": "Skripte",
"unknown_error": "Unbekannter Fehler",
"unnamed_device": "Unbenanntes Gerät",
"update": "Aktualisieren"
"update": "Aktualisieren",
"update_device_error": "Aktualisieren des Geräts fehlgeschlagen"
},
"entities": {
"caption": "Entitäten",
@@ -2195,6 +2222,7 @@
"depends_on_cloud": "Abhängig von der Cloud",
"device_unavailable": "Gerät nicht verfügbar",
"devices": "{count} {count, plural,\n one {Gerät}\n other {Geräte}\n}",
"disable_error": "Aktivieren oder Deaktivieren der Integration fehlgeschlagen",
"disable_restart_confirm": "Home Assistant neu starten, um das Deaktivieren dieser Integration abzuschließen",
"disable": {
"disable_confirm": "Möchtest du diesen Konfigurationseintrag wirklich deaktivieren? Die Geräte und Entitäten werden deaktiviert.",
@@ -2206,6 +2234,7 @@
},
"disabled_cause": "Deaktiviert durch {cause}."
},
"disabled_polling": "Automatisches Abfragen nach aktualisierten Daten deaktiviert",
"documentation": "Dokumentation",
"enable_restart_confirm": "Home Assistant neu starten, um das Aktivieren dieser Integration abzuschließen",
"entities": "{count} {count, plural,\none {Entität}\nother {Entitäten}\n}",
@@ -2959,6 +2988,7 @@
},
"logs": {
"log_level": "Protokollstufe",
"log_level_changed": "Log Level geändert auf: {level}",
"subscribed_to_logs": "Abonniert Z-Wave JS-Protokollnachrichten ...",
"title": "Z-Wave JS Protokolle"
},
@@ -3861,10 +3891,19 @@
"intro": "Sind Sie bereit, dein Zuhause zu wecken, Ihre Privatsphäre zurückzugewinnen und einer weltweiten Gemeinschaft von Tüftlern beizutreten?",
"next": "Weiter",
"restore": {
"addons": "Add-ons",
"confirm_password": "Snapshot-Passwort bestätigen",
"description": "Alternativ kannst du von einem vorherigen Snapshot wiederherstellen.",
"folders": "Ordner",
"full_snapshot": "Vollständige Datensicherung",
"hide_log": "Vollständiges Protokoll ausblenden",
"in_progress": "Wiederherstellung im Gange",
"show_log": "Vollständiges Protokoll anzeigen"
"partial_snapshot": "Selektive Datensicherung",
"password": "Snapshot-Passwort",
"password_protection": "Passwortschutz",
"select_type": "Wähle aus, was wiederhergestellt werden soll",
"show_log": "Vollständiges Protokoll anzeigen",
"type": "Snapshot-Typ"
},
"user": {
"create_account": "Benutzerkonto anlegen",
+26 -3
View File
@@ -318,6 +318,14 @@
"no_addons": "You don't have any add-ons installed yet. Head over to the add-on store to get started!"
},
"dialog": {
"hardware": {
"attributes": "Attributes",
"device_path": "Device path",
"id": "ID",
"search": "Search hardware",
"subsystem": "Subsystem",
"title": "Hardware"
},
"network": {
"connected_to": "Connected to {ssid}",
"dhcp": "DHCP",
@@ -368,6 +376,8 @@
"error": "An unknown error occurred",
"error_addon_no_ingress": "The requested add-on does not support ingress",
"error_addon_not_found": "Add-on not found",
"error_addon_not_installed": "The requested add-on is not installed. Please install it first",
"error_addon_not_started": "The requested add-on are not running. Please start it first",
"faq_link": "My Home Assistant FAQ",
"not_supported": "This redirect is not supported by your Home Assistant instance. Check the {link} for the supported redirects and the version they where introduced."
},
@@ -380,6 +390,7 @@
"snapshot": {
"addons": "Add-ons",
"available_snapshots": "Available Snapshots",
"confirm_password": "Confirm Snapshot password",
"could_not_create": "Could not create snapshot",
"create": "Create",
"create_blocked_not_running": "Creating a snapshot is not possible right now because the system is in {state} state.",
@@ -407,6 +418,7 @@
"password": "Snapshot password",
"password_protected": "password protected",
"password_protection": "Password protection",
"passwords_not_matching": "The passwords does not match",
"security": "Security",
"select_type": "Select what to restore",
"selected": "{number} selected",
@@ -2076,7 +2088,8 @@
"scripts": "Scripts",
"unknown_error": "Unknown error",
"unnamed_device": "Unnamed device",
"update": "Update"
"update": "Update",
"update_device_error": "Updating the device failed"
},
"entities": {
"caption": "Entities",
@@ -2209,6 +2222,7 @@
"depends_on_cloud": "Depends on the cloud",
"device_unavailable": "Device unavailable",
"devices": "{count} {count, plural,\n one {device}\n other {devices}\n}",
"disable_error": "Enabling or disabling of the integration failed",
"disable_restart_confirm": "Restart Home Assistant to finish disabling this integration",
"disable": {
"disable_confirm": "Are you sure you want to disable this config entry? Its devices and entities will be disabled.",
@@ -2717,7 +2731,7 @@
"validation": {
"check_config": "Check configuration",
"heading": "Configuration validation",
"introduction": "Validate your configuration if you recently made some changes to your configuration and want to make sure that it is all valid",
"introduction": "Validate your configuration if you recently made some changes to your configuration and want to make sure that it is all valid.",
"invalid": "Configuration invalid",
"valid": "Configuration valid!"
}
@@ -3877,10 +3891,19 @@
"intro": "Are you ready to awaken your home, reclaim your privacy and join a worldwide community of tinkerers?",
"next": "Next",
"restore": {
"addons": "Add-ons",
"confirm_password": "Confirm Snapshot password",
"description": "Alternatively you can restore from a previous snapshot.",
"folders": "Folders",
"full_snapshot": "Full snapshot",
"hide_log": "Hide full log",
"in_progress": "Restore in progress",
"show_log": "Show full log"
"partial_snapshot": "Partial snapshot",
"password": "Snapshot password",
"password_protection": "Password protection",
"select_type": "Select what to restore",
"show_log": "Show full log",
"type": "Snapshot type"
},
"user": {
"create_account": "Create Account",
+5 -1
View File
@@ -367,6 +367,8 @@
"my": {
"error": "Ha ocurrido un error desconocido",
"error_addon_not_found": "Complemento no encontrado",
"error_addon_not_installed": "El complemento solicitado no está instalado. Por favor instálelo primero",
"error_addon_not_started": "El complemento solicitado no se están ejecutando. Por favor inícielo antes.",
"faq_link": "Mis preguntas frecuentes de Home Assistant",
"not_supported": "Esta redirección no está soportada por su instancia de Home Assistant. Consulte las redirecciones soportadas y la versión en la que fueron introducidas en {link}."
},
@@ -2031,7 +2033,8 @@
"scripts": "Scripts",
"unknown_error": "Error desconocido",
"unnamed_device": "Dispositivo sin nombre",
"update": "Actualizar"
"update": "Actualizar",
"update_device_error": "Error al actualizar el dispositivo"
},
"entities": {
"caption": "Entidades",
@@ -2163,6 +2166,7 @@
"depends_on_cloud": "Depende de la nube",
"device_unavailable": "Dispositivo no disponible",
"devices": "{count} {count, plural,\n one {dispositivo}\n other {dispositivos}\n}",
"disable_error": "Ha fallado la habilitación o deshabilitación de la integración",
"disable_restart_confirm": "Reinicie Home Assistant para terminar de deshabilitar esta integración",
"disable": {
"disable_confirm": "¿Está seguro de que desea deshabilitar esta entrada de configuración? Sus dispositivos y entidades serán deshabilitados.",
+31 -4
View File
@@ -318,6 +318,14 @@
"no_addons": "Aún no tienes ningún complemento instalado. ¡Dirígete a la tienda de complementos para comenzar!"
},
"dialog": {
"hardware": {
"attributes": "Atributos",
"device_path": "Ruta del dispositivo",
"id": "ID",
"search": "Buscar hardware",
"subsystem": "Subsistema",
"title": "Hardware"
},
"network": {
"connected_to": "Conectado a {ssid}",
"dhcp": "DHCP",
@@ -368,6 +376,8 @@
"error": "Se ha producido un error desconocido",
"error_addon_no_ingress": "El complemento solicitado no admite la entrada",
"error_addon_not_found": "Complemento no encontrado",
"error_addon_not_installed": "El complemento solicitado no está instalado. Por favor instálalo primero",
"error_addon_not_started": "El complemento solicitado no se está ejecutando. Por favor, inícialo primero",
"faq_link": "Preguntas frecuentes sobre mi Home Assistant",
"not_supported": "Esta redirección no es compatible con tu instancia de Home Assistant. Consulta el {link} para conocer las redirecciones admitidas y la versión en la que se introdujeron."
},
@@ -380,6 +390,7 @@
"snapshot": {
"addons": "Complementos",
"available_snapshots": "Instantáneas disponibles",
"confirm_password": "Confirma la contraseña de la instantánea",
"could_not_create": "No se pudo crear la instantánea",
"create": "Crear",
"create_blocked_not_running": "No es posible crear una instantánea en este momento porque el sistema está en el estado {state}.",
@@ -407,6 +418,7 @@
"password": "Contraseña de la instantánea",
"password_protected": "protegida por contraseña",
"password_protection": "Protección con contraseña",
"passwords_not_matching": "Las contraseñas no coinciden",
"security": "Seguridad",
"select_type": "Selecciona qué restaurar",
"selected": "{number} {number, plural,\n one {seleccionada}\n other {seleccionadas}\n}",
@@ -929,8 +941,11 @@
},
"dialogs": {
"config_entry_system_options": {
"enable_new_entities_description": "Si está deshabilitada, las nuevas entidades que se descubran para {integration} no se añadirán automáticamente a Home Assistant.",
"enable_new_entities_description": "Si los dispositivos recién descubiertos para {integration} deberían agregarse automáticamente.",
"enable_new_entities_label": "Habilitar entidades recién añadidas.",
"enable_polling_description": "Si Home Assistant debería sondear automáticamente las entidades de {integration} para obtener actualizaciones.",
"enable_polling_label": "Habilitar el sondeo de actualizaciones.",
"restart_home_assistant": "Debes reiniciar Home Assistant para que los cambios surtan efecto.",
"title": "Opciones del sistema para {integration}",
"update": "Actualizar"
},
@@ -2073,7 +2088,8 @@
"scripts": "Scripts",
"unknown_error": "Error desconocido",
"unnamed_device": "Dispositivo sin nombre",
"update": "Actualizar"
"update": "Actualizar",
"update_device_error": "Error al actualizar el dispositivo"
},
"entities": {
"caption": "Entidades",
@@ -2206,6 +2222,7 @@
"depends_on_cloud": "Depende de la nube",
"device_unavailable": "Dispositivo no disponible",
"devices": "{count} {count, plural,\n one {dispositivo}\n other {dispositivos}\n}",
"disable_error": "Error al habilitar o deshabilitar la integración",
"disable_restart_confirm": "Reinicia Home Assistant para terminar de deshabilitar esta integración",
"disable": {
"disable_confirm": "¿Estás seguro de que deseas deshabilitar esta entrada de configuración? Sus dispositivos y entidades estarán deshabilitados.",
@@ -2217,6 +2234,7 @@
},
"disabled_cause": "Deshabilitada por {cause}"
},
"disabled_polling": "Sondeo automático para datos actualizados deshabilitado",
"documentation": "Documentación",
"enable_restart_confirm": "Reinicia Home Assistant para terminar de habilitar esta integración",
"entities": "{count} {count, plural,\n one {entidad}\n other {entidades}\n}",
@@ -2713,7 +2731,7 @@
"validation": {
"check_config": "Verificar configuración",
"heading": "Validación de la configuración",
"introduction": "Valida tu configuración si has realizado cambios recientemente y quieres asegurarte de que son correctos",
"introduction": "Valida tu configuración si has realizado cambios recientemente en ella y quieres asegurarte de que son correctos",
"invalid": "Configuración no válida",
"valid": "¡Configuración valida!"
}
@@ -3873,10 +3891,19 @@
"intro": "¿Estás listo para despertar tu casa, reclamar tu privacidad y unirte a una comunidad mundial de pensadores?",
"next": "Siguiente",
"restore": {
"addons": "Complementos",
"confirm_password": "Confirma la contraseña de la instantánea",
"description": "Alternativamente, puedes restaurar desde una copia de seguridad anterior.",
"folders": "Carpetas",
"full_snapshot": "Instantánea completa",
"hide_log": "Ocultar registro completo",
"in_progress": "Restauración en curso",
"show_log": "Mostrar registro completo"
"partial_snapshot": "Instantánea parcial",
"password": "Contraseña de la instantánea",
"password_protection": "Protección con contraseña",
"select_type": "Selecciona qué restaurar",
"show_log": "Mostrar registro completo",
"type": "Tipo de instantánea"
},
"user": {
"create_account": "Crear una cuenta",
+50 -23
View File
@@ -318,6 +318,14 @@
"no_addons": "Ühtegi lisandmoodulit pole veel paigaldatud. Alustamiseks mine lisandmoodulite hoidlasse!"
},
"dialog": {
"hardware": {
"attributes": "Atribuudid",
"device_path": "Seadme asukoht",
"id": "ID",
"search": "Otsi riistvara",
"subsystem": "Alamsüsteem",
"title": "Riistvara"
},
"network": {
"connected_to": "Ühendatud pääsupunktiga {ssid}",
"dhcp": "DHCP",
@@ -358,9 +366,9 @@
"text": "Kas soovid lisandmooduli taaskäivitada rakendades muudatused?"
},
"update": {
"create_snapshot": "Enne {name} värskendamist loo hetktõmmis",
"snapshot": "Hetktõmmis",
"snapshotting": "Üksuse {name} hetktõmmise loomine",
"create_snapshot": "Enne {name} värskendamist loo varukoopia",
"snapshot": "Varukoopia",
"snapshotting": "Üksuse {name} varukoopia loomine",
"updating": "Üksuse {name} värskendamine versioonile {version}"
}
},
@@ -368,6 +376,8 @@
"error": "Viga",
"error_addon_no_ingress": "Valitud lisandmoodul ei toeta ingressi",
"error_addon_not_found": "Lisandmoodulit ei leitud",
"error_addon_not_installed": "Soovitud lisandmoodul pole pigaldatud. Alustamiseks paigalda see",
"error_addon_not_started": "Soovitud lisandmoodul ei tööta. Alustamiseks käivita see",
"faq_link": "KKK viide",
"not_supported": "pole toetatud"
},
@@ -379,17 +389,18 @@
},
"snapshot": {
"addons": "Lisandmoodulid",
"available_snapshots": "Saadaolevad hetktõmmised",
"could_not_create": "Hetktõmmise loomine nurjus",
"available_snapshots": "Saadaolevad varukoopiad",
"confirm_password": "Varukoopia salasõna kinnitamine",
"could_not_create": "Varukoopia loomine nurjus",
"create": "Loo",
"create_blocked_not_running": "Hetktõmmise loomine pole praegu võimalik kuna süsteem on olekus {state}.",
"create_snapshot": "Loo hetktõmmis",
"create_blocked_not_running": "Varukoopia loomine pole praegu võimalik kuna süsteem on olekus {state}.",
"create_snapshot": "Loo varukoopia",
"created": "Loodud",
"delete_selected": "Kustuta valitud hetktõmmised",
"delete_selected": "Kustuta valitud varukoopiad",
"delete_snapshot_confirm": "kustuta",
"delete_snapshot_text": "Kas kustutada {number} {number, plural,\n one {hetktõmmis}\n other {hetktõmmist}\n}?",
"delete_snapshot_title": "Kustuta hetktõmmis",
"description": "Hetktõmmised võimaldavad hõlpsalt varundada ja taastada kõik Home Assistanti andmed.",
"delete_snapshot_text": "Kas kustutada {number} {number, plural,\n one {varukoopia}\n other {varukoopiat}\n}?",
"delete_snapshot_title": "Kustuta varukoopia",
"description": "Varukoopiad võimaldavad hõlpsalt varundada ja taastada kõik Home Assistanti andmed.",
"enter_password": "Sisesta salasõna.",
"failed_to_delete": "Kustutamine nurjus",
"folder": {
@@ -400,18 +411,19 @@
"ssl": "SSL"
},
"folders": "Kaustad",
"full_snapshot": "Täielik hetktõmmis",
"name": "Hetktõmmise nimi",
"no_snapshots": "Sul pole veel ühtegi hetktõmmist.",
"partial_snapshot": "Osaline hetktõmmis",
"password": "Hetktõmmise salasõna",
"full_snapshot": "Täielik varukoopia",
"name": "Varukoopia nimi",
"no_snapshots": "Sul pole veel ühtegi varukoopiat.",
"partial_snapshot": "Osaline varukoopia",
"password": "Varukoopia salasõna",
"password_protected": "salasõnaha kaitstud",
"password_protection": "Salasõnaga kaitstud",
"passwords_not_matching": "Salasõnad ei ühti",
"security": "Turvalisus",
"select_type": "Vali mida taastada",
"selected": "{number} valitud",
"type": "Hetktõmmise tüüp",
"upload_snapshot": "Hetktõmmise üleslaadimine"
"type": "Varukoopia tüüp",
"upload_snapshot": "Varukoopia üleslaadimine"
},
"store": {
"missing_addons": "Ei näe lisandmooduleid? Luba täpsem režiim oma kasutajaprofiili lehel",
@@ -929,8 +941,11 @@
},
"dialogs": {
"config_entry_system_options": {
"enable_new_entities_description": "Kui see on keelatud ei lisata äsja avastatud sidumise {integration} olemeid automaatselt Home Assistant'i.",
"enable_new_entities_description": "Kas lisada äsja avastatud sidumise {integration} olemeid automaatselt Home Assistant'i.",
"enable_new_entities_label": "Luba äsja lisatud olemid.",
"enable_polling_description": "Kas Home Assistant peaks automaatselt küsitlema {integration} üksusi uuenduste saamiseks.",
"enable_polling_label": "Luba värskenduste jaoks küsitlus.",
"restart_home_assistant": "Muudatuste jõustumiseks pead Home Assistanti taaskäivitama.",
"title": "Süsteemi valikud {integration} jaoks",
"update": "Uuenda"
},
@@ -2073,7 +2088,8 @@
"scripts": "Skriptid",
"unknown_error": "Tundmatu viga",
"unnamed_device": "Nimetu seade",
"update": "Uuenda"
"update": "Uuenda",
"update_device_error": "Seadme värskendamine nurjus"
},
"entities": {
"caption": "Olemite register",
@@ -2206,6 +2222,7 @@
"depends_on_cloud": "Sõltub pilveteenusest",
"device_unavailable": "Seade pole saadaval",
"devices": "{count} {count, plural,\n one {seade}\n other {seadet}\n}",
"disable_error": "Sidumise lubamine või keelamine nurjus",
"disable_restart_confirm": "Taaskäivita Home Assistant, et lõpetada selle sidumise keelamine",
"disable": {
"disable_confirm": "Kas soovid selle seadistuskirje kindlasti keelata? Selle seadmed ja üksused keelatakse.",
@@ -2217,6 +2234,7 @@
},
"disabled_cause": "Keelatud {cause} põhjusel"
},
"disabled_polling": "Uuendatud andmete automaatne päring on välja lülitatud",
"documentation": "Vaata dokumentatsiooni",
"enable_restart_confirm": "Selle sidumise lubamise lõpetamiseks taaskäivita Home Assistant",
"entities": "{count} {count, plural,\n one {olem}\n other{olemit}\n}",
@@ -2713,7 +2731,7 @@
"validation": {
"check_config": "Kontrolli seadeid",
"heading": "Seadete kontrollimine",
"introduction": "Kontrolli oma seadeid kui oled neis hiljuti muutusi teinud ja tahad veenduda, et kõik on korrektne",
"introduction": "Kontrolli oma seadeid kui oled neis hiljuti muutusi teinud ja tahad veenduda, et kõik on korrektne.",
"invalid": "Konfiguratsioon on vigane",
"valid": "Konfiguratsioon on korrektne!"
}
@@ -3873,10 +3891,19 @@
"intro": "Kas oled valmis oma kodu ellu äratama, oma privaatsust tagasi võitma ja ühinema ülemaailmse nokitsejate kogukonnaga?",
"next": "Järgmine",
"restore": {
"description": "Teise võimalusena saad taastada eelmise hetktõmmise.",
"addons": "Lisandmoodulid",
"confirm_password": "Kinnita salasõna",
"description": "Teise võimalusena saad taastada eelmise varukoopia.",
"folders": "Kaustad",
"full_snapshot": "Täielik varukoopia",
"hide_log": "Peida täielik logi",
"in_progress": "Toimub taastamine",
"show_log": "Kuva täielik logi"
"partial_snapshot": "Osaline varukoopia",
"password": "Salasõna",
"password_protection": "Salasõnaga kaitstud",
"select_type": "Vali andmestiku tüüp",
"show_log": "Kuva täielik logi",
"type": "Andmestiku tüüp"
},
"user": {
"create_account": "Loo konto",
File diff suppressed because it is too large Load Diff
+31 -4
View File
@@ -318,6 +318,14 @@
"no_addons": "Non hai ancora installato alcun componente aggiuntivo. Vai al negozio di componenti aggiuntivi per iniziare!"
},
"dialog": {
"hardware": {
"attributes": "Attributi",
"device_path": "Percorso del dispositivo",
"id": "ID",
"search": "Cerca hardware",
"subsystem": "Sottosistema",
"title": "Hardware"
},
"network": {
"connected_to": "Connesso a {ssid}",
"dhcp": "DHCP",
@@ -368,6 +376,8 @@
"error": "Si è verificato un errore sconosciuto",
"error_addon_no_ingress": "Il componente aggiuntivo richiesto non supporta l'ingresso",
"error_addon_not_found": "Componente aggiuntivo non trovato",
"error_addon_not_installed": "Il componente aggiuntivo richiesto non è installato. Si prega di installarlo prima di proseguire",
"error_addon_not_started": "Il componente aggiuntivo non è in esecuzione. Si prega di avviarlo prima di proseguire",
"faq_link": "My Home Assistant FAQ",
"not_supported": "Questo reindirizzamento non è supportato dall'istanza di Home Assistant. Controlla il {link} per i reindirizzamenti supportati e la versione in cui sono stati introdotti."
},
@@ -380,6 +390,7 @@
"snapshot": {
"addons": "Componenti aggiuntivi",
"available_snapshots": "Istantanee disponibili",
"confirm_password": "Conferma la password dell'istantanea",
"could_not_create": "Impossibile creare l'istantanea",
"create": "Crea",
"create_blocked_not_running": "La creazione di un'istantanea non è al momento possibile perché il sistema è nello stato {state}.",
@@ -407,11 +418,12 @@
"password": "Password dell'istantanea",
"password_protected": "protetto da password",
"password_protection": "Protezione con password",
"passwords_not_matching": "Le password non corrispondono",
"security": "Sicurezza",
"select_type": "Seleziona cosa ripristinare",
"selected": "{number} selezionato/i",
"type": "Tipo di istantanea",
"upload_snapshot": "Invia istantanea"
"upload_snapshot": "Aggiungi istantanea"
},
"store": {
"missing_addons": "Componenti aggiuntivi mancanti? Abilita la modalità avanzata nella pagina del tuo profilo utente",
@@ -931,6 +943,9 @@
"config_entry_system_options": {
"enable_new_entities_description": "Se disabilitato, le entità appena rilevate per {integration} non verranno automaticamente aggiunte a Home Assistant.",
"enable_new_entities_label": "Abilita nuove entità aggiunte.",
"enable_polling_description": "Se disabilitato, Home Assistant non richiederà più in automatico aggiornamenti alle entità dell'integrazione {integration}.",
"enable_polling_label": "Abilita il polling per gli aggiornamenti.",
"restart_home_assistant": "È necessario riavviare Home Assistant affinché le modifiche abbiano effetto.",
"title": "Opzioni di sistema per {integration}",
"update": "Aggiorna"
},
@@ -2073,7 +2088,8 @@
"scripts": "Script",
"unknown_error": "Errore sconosciuto",
"unnamed_device": "Dispositivo senza nome",
"update": "Aggiorna"
"update": "Aggiorna",
"update_device_error": "Aggiornamento del dispositivo non riuscito"
},
"entities": {
"caption": "Entità",
@@ -2206,6 +2222,7 @@
"depends_on_cloud": "Dipende dal cloud",
"device_unavailable": "Dispositivo non disponibile",
"devices": "{count} {count, plural, \none {dispositivo}\nother {dispositivi}\n}",
"disable_error": "Abilitazione o disabilitazione dell'integrazione non riuscita",
"disable_restart_confirm": "Riavvia Home Assistant per terminare la disabilitazione di questa integrazione",
"disable": {
"disable_confirm": "Sei sicuro di voler disabilitare questa voce di configurazione? I suoi dispositivi ed entità saranno disabilitati.",
@@ -2215,8 +2232,9 @@
"integration": "Integrazione",
"user": "utente"
},
"disabled_cause": "Disabilitato da {causa}"
"disabled_cause": "Disabilitato da {cause}"
},
"disabled_polling": "Polling automatico per i dati aggiornati disabilitato",
"documentation": "Documentazione",
"enable_restart_confirm": "Riavvia Home Assistant per completare l'attivazione di questa integrazione",
"entities": "{count} {count, plural, \none {entità}\nother {entità }\n}",
@@ -3873,10 +3891,19 @@
"intro": "Sei pronto per risvegliare la tua casa, reclamare la tua privacy e far parte di una comunità mondiale di smanettoni?",
"next": "Avanti",
"restore": {
"addons": "Componenti aggiuntivi",
"confirm_password": "Conferma la password dell'istantanea",
"description": "In alternativa è possibile ripristinare da un'istantanea precedente.",
"folders": "Cartelle",
"full_snapshot": "Istantanea completa",
"hide_log": "Nascondi il registro completo",
"in_progress": "Ripristino in corso",
"show_log": "Mostra il registro completo"
"partial_snapshot": "Istantanea parziale",
"password": "Password dell'istantanea",
"password_protection": "Protezione con password",
"select_type": "Seleziona cosa ripristinare",
"show_log": "Mostra il registro completo",
"type": "Tipo di istantanea"
},
"user": {
"create_account": "Crea un Account",
+26 -3
View File
@@ -318,6 +318,14 @@
"no_addons": "아직 설치된 애드온이 없습니다. 시작하려면 애드온 스토어로 이동해보세요!"
},
"dialog": {
"hardware": {
"attributes": "속성",
"device_path": "장치 경로",
"id": "ID",
"search": "하드웨어 검색",
"subsystem": "서브 시스템",
"title": "하드웨어"
},
"network": {
"connected_to": "{ssid}에 연결되었습니다",
"dhcp": "자동 구성",
@@ -368,6 +376,8 @@
"error": "알 수 없는 오류가 발생했습니다",
"error_addon_no_ingress": "해당 애드온은 인그레스를 지원하지 않습니다.",
"error_addon_not_found": "애드온을 찾을 수 없습니다",
"error_addon_not_installed": "해당 애드온은 설치되어있지 않습니다. 애드온을 설치해주세요.",
"error_addon_not_started": "해당 애드온은 실행중이지 않습니다. 애드온을 실행해주세요.",
"faq_link": "내 Home Assistant 자주 묻는 질문",
"not_supported": "이 리디렉션은 Home Assistant 인스턴스에서 지원되지 않습니다. {link}에서 지원되는 리디렉션과 리디렉션이 도입된 버전을 확인해주세요."
},
@@ -380,6 +390,7 @@
"snapshot": {
"addons": "애드온",
"available_snapshots": "사용 가능한 스냅숏",
"confirm_password": "스냅샷 비밀번호 확인",
"could_not_create": "스냅숏을 만들 수 없습니다",
"create": "생성하기",
"create_blocked_not_running": "시스템이 {state} 상태이기 때문에 지금은 스냅숏을 생성할 수 없습니다.",
@@ -387,7 +398,7 @@
"created": "생성됨",
"delete_selected": "선택한 스냅샷 삭제",
"delete_snapshot_confirm": "삭제",
"delete_snapshot_text": "{number} {number, plural,\n one{개의 스냅샷}\n other{개의 스냅샷}\n} 삭제하시겠습니까?",
"delete_snapshot_text": "{number} {number, plural,\n one{개의 스냅샷}\n other{개의 스냅샷}\n} 삭제하시겠습니까?",
"delete_snapshot_title": "스냅샷 삭제",
"description": "스냅숏을 사용하면 Home Assistant 인스턴스의 모든 데이터를 쉽게 백업하고 복원할 수 있습니다.",
"enter_password": "비밀번호를 입력해주세요.",
@@ -407,6 +418,7 @@
"password": "비밀번호",
"password_protected": "비밀번호로 보호됨",
"password_protection": "비밀번호 보호",
"passwords_not_matching": "비밀번호가 일치하지 않습니다",
"security": "보안",
"select_type": "복원 할 항목 선택",
"selected": "{number}개 선택됨",
@@ -2076,7 +2088,8 @@
"scripts": "스크립트",
"unknown_error": "알 수 없는 오류",
"unnamed_device": "이름이 없는 기기",
"update": "업데이트"
"update": "업데이트",
"update_device_error": "장치 업데이트 실패"
},
"entities": {
"caption": "구성요소",
@@ -2209,6 +2222,7 @@
"depends_on_cloud": "클라우드 서비스",
"device_unavailable": "기기 사용불가",
"devices": "{count} {count, plural,\none{개의 기기}\nother{개의 기기}\n}",
"disable_error": "통합구성요소 활성화 혹은 비활성화 실패",
"disable_restart_confirm": "이 통합 구성요소를 비활성화하려면 Home Assistant를 다시 시작해주세요",
"disable": {
"disable_confirm": "이 구성 항목을 비활성화하시겠습니까? 해당 기기 및 구성요소가 비활성화됩니다.",
@@ -3877,10 +3891,19 @@
"intro": "잠들어 있는 집을 깨우고 개인정보를 보호하며 전세계의 공돌이 커뮤니티에 가입 할 준비가 되셨나요?",
"next": "다음",
"restore": {
"addons": "애드온",
"confirm_password": "스냅샷 비밀번호 확인",
"description": "이전 스냅숏에서 복원할 수 있습니다.",
"folders": "폴더",
"full_snapshot": "전체 스냅샷",
"hide_log": "전체 로그 숨기기",
"in_progress": "복원 중",
"show_log": "전체 로그 표시하기"
"partial_snapshot": "부분 스냅샷",
"password": "스냅샷 비밀번호",
"password_protection": "비밀번호 보호",
"select_type": "무엇을 복원할지 선택하세요",
"show_log": "전체 로그 표시하기",
"type": "스냅샷 종류"
},
"user": {
"create_account": "계정 만들기",
+40 -13
View File
@@ -318,6 +318,14 @@
"no_addons": "Du har ikke installert tilleggsprogrammer ennå. Gå over til tilleggsbutikken for å komme i gang!"
},
"dialog": {
"hardware": {
"attributes": "Attributter",
"device_path": "Bane til enhet",
"id": "ID",
"search": "Søk i maskinvare",
"subsystem": "Delsystem",
"title": "Maskinvare"
},
"network": {
"connected_to": "Koblet til {ssid}",
"dhcp": "",
@@ -368,6 +376,8 @@
"error": "En ukjent feil har oppstått",
"error_addon_no_ingress": "Det etterspurte tillegget støtter ikke inngang",
"error_addon_not_found": "Tillegget ble ikke funnet",
"error_addon_not_installed": "Det forespurte tillegget er ikke installert. Vennligst installer den først",
"error_addon_not_started": "Det valgte tillegget kjører ikke. Vennligst start den først",
"faq_link": "Vanlige spørsmål om Min Home Assistant",
"not_supported": "Denne viderekoblingen støttes ikke av Home Assistant-forekomsten. Se på {link} for viderekoblinger som støttes, og hvilken versjon de ble introdusert."
},
@@ -380,6 +390,7 @@
"snapshot": {
"addons": "Tillegg",
"available_snapshots": "Tilgjengelige sikkerhetskopier",
"confirm_password": "Bekreft passord for øyeblikksbilde",
"could_not_create": "Kunne ikke opprette sikkerhetskopi",
"create": "Opprett",
"create_blocked_not_running": "Å lage en sikkerhetskopi er ikke mulig akkurat nå fordi systemet er i {state} status",
@@ -407,6 +418,7 @@
"password": "Passord for øyeblikksbilde",
"password_protected": "Passordbeskyttet",
"password_protection": "Passordbeskyttelse",
"passwords_not_matching": "Passordene samsvarer ikke",
"security": "Sikkerhet",
"select_type": "Velg hva du vil gjenopprette",
"selected": "{number} valgt",
@@ -929,8 +941,11 @@
},
"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_description": "Hvis nylig oppdagede enheter for {integration} skal legges til automatisk.",
"enable_new_entities_label": "Aktiver entiteter som nylig er lagt til.",
"enable_polling_description": "Hvis Home Assistant automatisk skal avstemme {integration} enheter for oppdateringer.",
"enable_polling_label": "Aktiver avstemning for oppdateringer.",
"restart_home_assistant": "Du må starte Home Assistant på nytt for at endringene skal tre i kraft.",
"title": "Systemalternativer for {integration}",
"update": "Oppdater"
},
@@ -1125,7 +1140,7 @@
"zone": "Soner"
},
"reload": {
"automation": "Automatisering",
"automation": "Automasjoner",
"command_line": "Kommandolinjeenheter",
"core": "Plassering og tilpasninger",
"filesize": "Enheter for filstørrelse",
@@ -1433,7 +1448,7 @@
"delete_confirm": "Er du sikker på at du vil slette dette?",
"duplicate": "Dupliser",
"header": "Betingelser",
"introduction": "Betingelsene er valgfrie og vil hindre at automatiseringen kjøres med mindre alle betingelsene er oppfylt.",
"introduction": "Betingelsene er valgfrie og vil hindre at automasjonen kjøres med mindre alle betingelsene er oppfylt.",
"learn_more": "Lær mer om betingelser",
"name": "Betingelse",
"type_select": "Betingelse",
@@ -1514,7 +1529,7 @@
"edit_ui": "Rediger i visuell editor",
"edit_yaml": "Rediger i YAML",
"enable_disable": "Aktivere/deaktivere automasjon",
"introduction": "Bruk automatisering for å bringe hjemmet ditt til live.",
"introduction": "Bruk automasjoner for å bringe hjemmet ditt til live.",
"load_error_not_editable": "Kun automasjoner i automations.yaml kan redigeres",
"load_error_unknown": "Feil ved lasting av automasjon ({err_no}).",
"max": {
@@ -1642,8 +1657,8 @@
"add_automation": "Legg til automasjon",
"delete_automation": "Slett automasjon",
"delete_confirm": "Er du sikker på at du vil slette denne automasjonen?",
"dev_automation": "Feilsøk automatisering",
"dev_only_editable": "Bare automatisering som har en unik ID tildelt, kan feilsøkes.",
"dev_automation": "Feilsøk automasjon",
"dev_only_editable": "Bare automasjoner som har en unik ID tildelt, kan feilsøkes.",
"duplicate": "Dupliser",
"duplicate_automation": "Dupliser automasjon",
"edit_automation": "Rediger automasjon",
@@ -1653,7 +1668,7 @@
},
"introduction": "Automasjonsredigeringen lar deg lage og redigere automasjoner. Følg lenken under for å forsikre deg om at du har konfigurert Home Assistant riktig.",
"learn_more": "Lær mer om automasjoner",
"no_automations": "Vi fant ingen automatiseringer",
"no_automations": "Vi kunne ikke finne noen automasjoner",
"only_editable": "Bare automasjoner definert i automations.yaml kan redigeres.",
"pick_automation": "Velg automasjon for å redigere",
"show_info_automation": "Vis informasjon om automasjon"
@@ -1784,7 +1799,7 @@
"target_browser": "Nettleser"
},
"female": "Kvinne",
"info": "Ta med personlighet hjem ved å få den til å snakke med deg ved å bruke våre tekst-til-tale-tjenester. Du kan bruke dette i automatiseringer og skript ved hjelp av tjenesten {service}.",
"info": "Ta med personlighet hjem ved å få den til å snakke med deg ved å bruke våre tekst-til-tale-tjenester. Du kan bruke dette i automasjoner og skript ved hjelp av tjenesten {service}.",
"male": "Mann",
"title": "Tekst til tale",
"try": "Prøve"
@@ -2073,7 +2088,8 @@
"scripts": "Skript",
"unknown_error": "Ukjent feil",
"unnamed_device": "Enhet uten navn",
"update": "Oppdater"
"update": "Oppdater",
"update_device_error": "Oppdatering av enheten mislyktes"
},
"entities": {
"caption": "Entiteter",
@@ -2136,7 +2152,7 @@
"header": "Konfigurer Home Assistant",
"helpers": {
"caption": "Hjelpere",
"description": "Elementer som hjelper med å bygge automatiseringer",
"description": "Elementer som hjelper med å bygge automasjoner",
"dialog": {
"add_helper": "Legg hjelper",
"add_platform": "Legg til {platform}",
@@ -2206,6 +2222,7 @@
"depends_on_cloud": "Avhenger av skyen",
"device_unavailable": "Enheten er utilgjengelig",
"devices": "{count} {count, plural,\n one {enhet}\n other {enheter}\n}",
"disable_error": "Aktivering eller deaktivering av integrasjonen mislyktes",
"disable_restart_confirm": "Start Home Assistant på nytt for å fullføre deaktiveringen av denne integreringen",
"disable": {
"disable_confirm": "Er du sikker på at du vil deaktivere denne config-oppføringen? Enhetene og enhetene deaktiveres.",
@@ -2217,6 +2234,7 @@
},
"disabled_cause": "Deaktivert av {cause}"
},
"disabled_polling": "Automatisk avstemning for oppdaterte data deaktivert",
"documentation": "Dokumentasjon",
"enable_restart_confirm": "Start Home Assistant på nytt for å fullføre aktiveringen av denne integreringen",
"entities": "{count} {count, plural,\n one {entitet}\n other {entiteter}\n}",
@@ -2668,7 +2686,7 @@
"description": "Start på nytt og stopp Home Assistant-serveren",
"section": {
"reloading": {
"automation": "Automatisering",
"automation": "Automasjoner",
"command_line": "Kommandolinjeenheter",
"core": "Plassering og tilpasninger",
"filesize": "Enheter for filstørrelse",
@@ -2713,7 +2731,7 @@
"validation": {
"check_config": "Sjekk konfigurasjonen",
"heading": "Validering av konfigurasjon",
"introduction": "Valider konfigurasjonen hvis du nylig har gjort endringer i konfigurasjonen og vil forsikre deg om at den er gyldig",
"introduction": "Bekreft konfigurasjonen hvis du nylig har gjort noen endringer i konfigurasjonen og vil være sikker på at den er gyldig.",
"invalid": "Ugyldig konfigurasjon",
"valid": "Gyldig konfigurasjon"
}
@@ -3873,10 +3891,19 @@
"intro": "Er du klar til å ta kontroll over hjemmet ditt, gjenvinne ditt privatliv og bli med i et verdensomspennende samfunn av entusiaster?",
"next": "Neste",
"restore": {
"addons": "Tillegg",
"confirm_password": "Bekreft passord for øyeblikksbilde",
"description": "Alternativt kan du gjenopprette fra et forrige snapshot.",
"folders": "Mapper",
"full_snapshot": "Full sikkerhetskopi",
"hide_log": "Skjul full logg",
"in_progress": "Gjenoppretting pågår",
"show_log": "Vis full logg"
"partial_snapshot": "Delvis sikkerhetskopi",
"password": "Passord for øyeblikksbilde",
"password_protection": "Passordbeskyttelse",
"select_type": "Velg hva du vil gjenopprette",
"show_log": "Vis full logg",
"type": "Type øyeblikksbilde"
},
"user": {
"create_account": "Opprett konto",
+31 -4
View File
@@ -318,6 +318,14 @@
"no_addons": "Je hebt nog geen add-ons geïnstalleerd. Ga naar de add-on store om te beginnen!"
},
"dialog": {
"hardware": {
"attributes": "Attributen",
"device_path": "Apparaatpad",
"id": "ID",
"search": "Zoek hardware",
"subsystem": "Subsysteem",
"title": "Hardware"
},
"network": {
"connected_to": "Verbonden met {ssid}",
"dhcp": "DHCP",
@@ -368,6 +376,8 @@
"error": "Er is een onbekende fout opgetreden",
"error_addon_no_ingress": "De gevraagde add-on ondersteunt geen ingress",
"error_addon_not_found": "Invoegtoepassing niet gevonden",
"error_addon_not_installed": "De gevraagde add-on is niet geïnstalleerd. Gelieve deze eerst te installeren",
"error_addon_not_started": "De gevraagde add-on is niet actief. Start deze a.u.b. eerst",
"faq_link": "My Home Assistant FAQ",
"not_supported": "Deze redirect wordt niet ondersteund door uw Home Assistant instantie. Controleer de {link} voor de ondersteunde redirects en de versie waarin ze zijn geïntroduceerd."
},
@@ -380,6 +390,7 @@
"snapshot": {
"addons": "Invoegtoepassingen",
"available_snapshots": "Beschikbare snapshots",
"confirm_password": "Bevestig Snapshot-wachtwoord",
"could_not_create": "Kon geen snapshot maken",
"create": "Maak",
"create_blocked_not_running": "Het maken van een snapshot is nu niet mogelijk omdat het systeem in {state} staat.",
@@ -407,6 +418,7 @@
"password": "Snapshot wachtwoord",
"password_protected": "beveiligd met wachtwoord",
"password_protection": "Wachtwoord bescherming",
"passwords_not_matching": "De wachtwoorden komen niet overeen",
"security": "Beveiliging",
"select_type": "Selecteer wat u wilt herstellen",
"selected": "{number} geselecteerd",
@@ -929,8 +941,11 @@
},
"dialogs": {
"config_entry_system_options": {
"enable_new_entities_description": "Indien uitgeschakeld, worden nieuwe entiteiten van {integration} niet automatisch aan Home Assistant toegevoegd.",
"enable_new_entities_description": "Of nieuw ontdekte apparaten voor {integratie} automatisch moeten worden toegevoegd.",
"enable_new_entities_label": "Voeg nieuwe entiteiten automatisch toe",
"enable_polling_description": "Of Home Assistant automatisch moet pollen voor updates voor {integration} entiteiten",
"enable_polling_label": "Schakel polling voor updates in.",
"restart_home_assistant": "U moet Home Assistant opnieuw starten om uw wijzigingen door te voeren.",
"title": "Systeeminstellingen voor {integration}",
"update": "Bijwerken"
},
@@ -967,7 +982,7 @@
},
"faq": "documentatie",
"info_customize": "U kunt sommige attributen overschrijven in de {customize_link} sectie.",
"no_unique_id": "Deze entiteit (\" {entity_id} \") heeft geen unieke ID, daarom kunnen de instellingen ervan niet worden beheerd vanuit de gebruikersinterface. Zie de {faq_link} voor meer details.",
"no_unique_id": "Deze entiteit (\"{entity_id}\") heeft geen unieke ID, daarom kunnen de instellingen ervan niet worden beheerd vanuit de gebruikersinterface. Zie de {faq_link} voor meer details.",
"related": "Gerelateerd",
"settings": "instellingen"
},
@@ -2073,7 +2088,8 @@
"scripts": "Scripts",
"unknown_error": "Onbekende fout",
"unnamed_device": "Naamloos apparaat",
"update": "Bijwerken"
"update": "Bijwerken",
"update_device_error": "Updaten van het apparaat mislukt"
},
"entities": {
"caption": "Entiteiten",
@@ -2206,6 +2222,7 @@
"depends_on_cloud": "Cloud afhankelijk",
"device_unavailable": "Apparaat niet beschikbaar",
"devices": "{count} {count, plural,\n one {apparaat}\n other {apparaten}\n}",
"disable_error": "In- of uitschakelen van de integratie mislukt",
"disable_restart_confirm": "Start Home Assistant opnieuw op om het uitzetten van deze integratie te voltooien",
"disable": {
"disable_confirm": "Weet je zeker dat je deze configuratie wilt uitschakelen? Al diens apparaten en entiteiten zullen worden uitgeschakeld.",
@@ -2217,6 +2234,7 @@
},
"disabled_cause": "Uitgeschakeld door {cause}"
},
"disabled_polling": "Automatische polling voor bijgewerkte gegevens uitgeschakeld",
"documentation": "Documentatie",
"enable_restart_confirm": "Start Home Assistant opnieuw op om het opzetten van deze integratie te voltooien",
"entities": "{count} {count, plural,\n one {entiteit}\n other {entiteiten}\n}",
@@ -3873,10 +3891,19 @@
"intro": "Ben je klaar om je huis wakker te maken, je privacy terug te winnen en deel te nemen aan een wereldwijde gemeenschap van knutselaars?",
"next": "Volgende",
"restore": {
"addons": "Invoegtoepassingen",
"confirm_password": "Bevestig Snapshot-wachtwoord",
"description": "Je kunt ook herstellen vanaf een eerdere snapshot.",
"folders": "Mappen",
"full_snapshot": "Volledige snapshot",
"hide_log": "Verberg volledig logboek",
"in_progress": "Herstel in uitvoering",
"show_log": "Volledig logboek weergeven"
"partial_snapshot": "Gedeeltelijke snapshot",
"password": "Snapshot wachtwoord",
"password_protection": "Wachtwoord bescherming",
"select_type": "Selecteer wat u wilt herstellen",
"show_log": "Volledig logboek weergeven",
"type": "Snapshot type"
},
"user": {
"create_account": "Account aanmaken",
+30 -3
View File
@@ -318,6 +318,14 @@
"no_addons": "Nie masz jeszcze zainstalowanych żadnych dodatków. Przejdź do sklepu z dodatkami, aby rozpocząć!"
},
"dialog": {
"hardware": {
"attributes": "Atrybuty",
"device_path": "Ścieżka urządzenia",
"id": "Identyfikator",
"search": "Wyszukaj sprzęt",
"subsystem": "Podsystem",
"title": "Sprzęt"
},
"network": {
"connected_to": "Połączono z {ssid}",
"dhcp": "DHCP",
@@ -368,6 +376,8 @@
"error": "Wystąpił nieznany błąd",
"error_addon_no_ingress": "Żądany dodatek nie obsługuje osadzania",
"error_addon_not_found": "Nie znaleziono dodatku",
"error_addon_not_installed": "Żądany dodatek nie jest zainstalowany. Najpierw go zainstaluj.",
"error_addon_not_started": "Żądany dodatek nie jest uruchomiony. Proszę najpierw go uruchomić.",
"faq_link": "Mój Home Assistant - często zadawane pytania",
"not_supported": "To przekierowanie nie jest obsługiwane przez Twoją instancję Home Assistanta. Sprawdź {link} aby znaleźć obsługiwane przekierowania i wersję, w której zostały wprowadzone."
},
@@ -380,6 +390,7 @@
"snapshot": {
"addons": "Dodatki",
"available_snapshots": "Dostępne snapshoty",
"confirm_password": "Potwierdź hasło snapshota",
"could_not_create": "Nie udało się utworzyć snapshota",
"create": "Utwórz",
"create_blocked_not_running": "Tworzenie snapshota nie jest teraz możliwe, ponieważ system jest w {state}.",
@@ -407,6 +418,7 @@
"password": "Hasło snapshota",
"password_protected": "chroniony hasłem",
"password_protection": "Ochrona hasłem",
"passwords_not_matching": "Hasła nie są takie same",
"security": "Bezpieczeństwo",
"select_type": "Wybierz, co przywrócić",
"selected": "wybrano {number}",
@@ -929,8 +941,11 @@
},
"dialogs": {
"config_entry_system_options": {
"enable_new_entities_description": "Jeśli wyłączone, nowo wykryte encje integracji {integration} nie będą automatycznie dodawane do Home Assistanta.",
"enable_new_entities_description": "Jeśli nowo odkryte urządzenia dla integracji {integration} powinny zostać dodane automatycznie.",
"enable_new_entities_label": "Włącz dodawanie nowych encji.",
"enable_polling_description": "Czy Home Assistant powinien automatycznie odpytywać encje {integration} w poszukiwaniu aktualizacji.",
"enable_polling_label": "Włącz odpytywanie o aktualizacje.",
"restart_home_assistant": "Aby zmiany zostały wprowadzone, musisz ponownie uruchomić Home Assistanta.",
"title": "Opcje systemowe dla {integration}",
"update": "Aktualizuj"
},
@@ -2073,7 +2088,8 @@
"scripts": "Skrypty",
"unknown_error": "Nieznany błąd",
"unnamed_device": "Nienazwane urządzenie",
"update": "Aktualizuj"
"update": "Aktualizuj",
"update_device_error": "Aktualizacja urządzenia nie powiodła się"
},
"entities": {
"caption": "Rejestr encji",
@@ -2206,6 +2222,7 @@
"depends_on_cloud": "Zależny od chmury",
"device_unavailable": "Urządzenie niedostępne",
"devices": "{count} {count, plural,\n one {urządzenie}\n few {urządzenia}\n many {urządzeń}\n other {urządzeń}\n}",
"disable_error": "Nie udało się włączyć lub wyłączyć integracji",
"disable_restart_confirm": "Zrestartuj Home Assistanta, aby zakończyć wyłączanie tej integracji",
"disable": {
"disable_confirm": "Czy na pewno chcesz wyłączyć ten wpis w konfiguracji? Jego urządzenia i instancje zostaną wyłączone.",
@@ -2217,6 +2234,7 @@
},
"disabled_cause": "Wyłączone przez {cause}."
},
"disabled_polling": "Automatyczne odpytywanie o zaktualizowane dane jest wyłączone",
"documentation": "Dokumentacja",
"enable_restart_confirm": "Uruchom ponownie Home Assistanta, aby dokończyć uruchamianie tej integracji",
"entities": "{count} {count, plural,\n one {encja}\n few {encje}\n many {encji}\n other {encji}\n}",
@@ -3873,10 +3891,19 @@
"intro": "Czy jesteś gotowy, aby ożywić swój dom, odzyskać prywatność i dołączyć do światowej społeczności majsterkowiczów?",
"next": "Dalej",
"restore": {
"addons": "Dodatki",
"confirm_password": "Potwierdź hasło snapshota",
"description": "Alternatywnie możesz przywrócić z poprzedniej migawki.",
"folders": "Foldery",
"full_snapshot": "Pełny snapshot",
"hide_log": "Ukryj cały log",
"in_progress": "Trwa przywracanie",
"show_log": "Pokaż cały log"
"partial_snapshot": "Częściowy snapshot",
"password": "Hasło snapshota",
"password_protection": "Ochrona hasłem",
"select_type": "Wybierz, co przywrócić",
"show_log": "Pokaż cały log",
"type": "Typ snapshota"
},
"user": {
"create_account": "Utwórz konto",
+37 -10
View File
@@ -318,6 +318,14 @@
"no_addons": "Нет установленных дополнений. Вы можете установить их из магазина дополнений."
},
"dialog": {
"hardware": {
"attributes": "Атрибуты",
"device_path": "Путь к устройству",
"id": "ID",
"search": "Поиск оборудования",
"subsystem": "Подсистема",
"title": "Оборудование"
},
"network": {
"connected_to": "Подключено к {ssid}",
"dhcp": "DHCP",
@@ -368,6 +376,8 @@
"error": "Произошла неизвестная ошибка",
"error_addon_no_ingress": "Дополнение не поддерживает ingress.",
"error_addon_not_found": "Дополнение не найдено.",
"error_addon_not_installed": "Запрошенное дополнение не установлено. Сначала нужно установить его.",
"error_addon_not_started": "Запрашиваемое дополнение не запущено. Сначала нужно запустить его.",
"faq_link": "часто задаваемыми вопросами по My Home Assistant",
"not_supported": "Это перенаправление не поддерживается Вашим Home Assistant. Ознакомьтесь с {link}, чтобы узнать поддерживаемые перенаправления и версии, в которых они были добавлены."
},
@@ -380,6 +390,7 @@
"snapshot": {
"addons": "Дополнения",
"available_snapshots": "Доступные снимки",
"confirm_password": "Подтверждение пароля",
"could_not_create": "Не удалось создать снимок",
"create": "Создать",
"create_blocked_not_running": "Создание снимка сейчас невозможно, потому что система находится в состоянии {state}.",
@@ -394,24 +405,25 @@
"failed_to_delete": "Не удалось удалить",
"folder": {
"addons/local": "Локальные дополнения",
"homeassistant": "Home Assistant",
"homeassistant": "Home Assistant configuration",
"media": "Media",
"share": "Share",
"ssl": "SSL"
},
"folders": "Папки",
"full_snapshot": "Полный",
"full_snapshot": "Все файлы",
"name": "Название",
"no_snapshots": "Не найдено ни одного снимка",
"partial_snapshot": "Выборочный",
"partial_snapshot": "Выбрать из списка",
"password": "Пароль",
"password_protected": "защищено паролем",
"password_protection": "Защита паролем",
"passwords_not_matching": "Пароли не совпадают",
"security": "Безопасность",
"select_type": "Выберите что нужно восстановить",
"select_type": "Выберите что нужно восстановить из этого снимка файловой системы",
"selected": "Выбрано: {number}",
"type": "Тип снимка",
"upload_snapshot": "Загрузить снимок"
"type": "Выберите что должен включать в себя снимок файловой системы",
"upload_snapshot": "Загрузить снимок на сервер"
},
"store": {
"missing_addons": "Пропали дополнения? Активируйте расширенный режим на странице Вашего профиля пользователя",
@@ -929,8 +941,11 @@
},
"dialogs": {
"config_entry_system_options": {
"enable_new_entities_description": "{integration} будет автоматически добавлять в Home Assistant вновь обнаруженные объекты",
"enable_new_entities_description": "Home Assistant будет автоматически добавлять новые обнаруженные устройства {integration}",
"enable_new_entities_label": "Добавлять новые объекты",
"enable_polling_description": "Home Assistant будет автоматически опрашивать объекты {integration} для получения обновлений",
"enable_polling_label": "Включить опрос для получения обновлений",
"restart_home_assistant": "Чтобы изменения вступили в силу, нужно перезапустить Home Assistant.",
"title": "{integration}",
"update": "Обновить"
},
@@ -2073,7 +2088,8 @@
"scripts": "Скрипты",
"unknown_error": "Неизвестная ошибка.",
"unnamed_device": "Устройство без названия",
"update": "Обновить"
"update": "Обновить",
"update_device_error": "Не удалось обновить устройство"
},
"entities": {
"caption": "Объекты",
@@ -2206,6 +2222,7 @@
"depends_on_cloud": "Зависит от облачных сервисов",
"device_unavailable": "Устройство недоступно",
"devices": "{count} {count, plural,\n one {устройство}\n few {устройства}\n many {устройств}\n other {устройств}\n}",
"disable_error": "Не удалось активировать или деактивировать интеграцию",
"disable_restart_confirm": "Перезапустите Home Assistant, чтобы завершить деактивацию этой интеграции.",
"disable": {
"disable_confirm": "Вы уверены, что хотите деактивировать эту запись конфигурации? Её дочерние устройства и объекты будут также деактивированы.",
@@ -2217,6 +2234,7 @@
},
"disabled_cause": "Деактивировано {cause}"
},
"disabled_polling": "Автоматический опрос для получения обновленных данных отключен",
"documentation": "Документация",
"enable_restart_confirm": "Перезапустите Home Assistant, чтобы завершить подключение этой интеграции.",
"entities": "{count} {count, plural,\n one {объект}\n few {объекта}\n many {объектов}\n other {объектов}\n}",
@@ -2713,7 +2731,7 @@
"validation": {
"check_config": "Начать проверку",
"heading": "Проверка конфигурации",
"introduction": "Проверьте файлы конфигурации, если Вы внесли в них изменения.",
"introduction": "Выполните проверку конфигурации, если в неё были внесены изменения и Вы хотите убедиться в её работоспособности.",
"invalid": "Ошибка в конфигурации",
"valid": "Конфигурация выполнена верно"
}
@@ -3873,10 +3891,19 @@
"intro": "Готовы ли Вы разбудить свой дом, вернуть свою конфиденциальность и присоединиться к всемирному сообществу?",
"next": "Далее",
"restore": {
"addons": "Дополнения",
"confirm_password": "Подтверждение пароля",
"description": "Восстановить предыдущее состояние из снимка файловой системы (snapshot)",
"folders": "Папки",
"full_snapshot": "Все файлы",
"hide_log": "Скрыть журнал",
"in_progress": "Восстановление системы",
"show_log": "Показать журнал"
"partial_snapshot": "Выбрать из списка",
"password": "Пароль",
"password_protection": "Защита паролем",
"select_type": "Выберите что нужно восстановить из этого снимка файловой системы",
"show_log": "Показать журнал",
"type": "Выберите что должен включать в себя снимок файловой системы"
},
"user": {
"create_account": "Создать учётную запись",

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