Merge pull request #3098 from home-assistant/dev

20190417.0
This commit is contained in:
Paulus Schoutsen 2019-04-17 09:39:05 -07:00 committed by GitHub
commit 9f0b20634a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 738 additions and 306 deletions

View File

@ -17,49 +17,49 @@ import "../components/hassio-card-content";
const PERMIS_DESC = {
rating: {
title: "Addon Security Rating",
title: "Add-on Security Rating",
description:
"Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an addon requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
"Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
},
host_network: {
title: "Host Network",
description:
"Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the addon full access to the network capabilities of the host machine. This gives the addon more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the addon.",
"Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.",
},
homeassistant_api: {
title: "Home Assistant API Access",
description:
"This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the addon as well, which enables an addon to interact with Home Assistant without the need for additional authentication tokens.",
"This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.",
},
full_access: {
title: "Full Hardware Access",
description:
"This addon is given full access to the hardware of your system, by request of the addon author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the addon security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the addon manually. Only disable the protection mode if you know, need AND trust the source of this addon.",
"This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
hassio_api: {
title: "Hass.io API Access",
description:
"The addon was given access to the Hass.io API, by request of the addon author. By default, the addon can access general version information of your system. When the addon requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
"The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
},
docker_api: {
title: "Full Docker Access",
description:
"The addon author has requested the addon to have management access to the Docker instance running on your system. This mode gives the addon full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the addon security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the addon manually. Only disable the protection mode if you know, need AND trust the source of this addon.",
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
host_pid: {
title: "Host Processes Namespace",
description:
"Usually, the processes the addon runs, are isolated from all other system processes. The addon author has requested the addon to have access to the system processes running on the host system instance, and allow the addon to spawn processes on the host system as well. This mode gives the addon full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the addon security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the addon manually. Only disable the protection mode if you know, need AND trust the source of this addon.",
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
apparmor: {
title: "AppArmor",
description:
"AppArmor ('Application Armor') is a Linux kernel security module that restricts addons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAddon authors can provide their security profiles, optimized for the addon, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the addon.",
"AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.",
},
auth_api: {
title: "Home Assistant Authentication",
description:
"An addon can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
"An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
},
ingress: {
title: "Ingress",
@ -229,7 +229,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
<template is="dom-if" if="[[!addon.protected]]">
<paper-card heading="Warning: Protection mode is disabled!" class="warning">
<div class="card-content">
Protection mode on this addon is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this addon.
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
</div>
<div class="card-actions">
<mwc-button on-click="protectionToggled">Enable Protection mode</mwc-button>
@ -238,9 +238,9 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
</paper-card>
</template>
<div class="security">
<h3>Addon Security Rating</h3>
<h3>Add-on Security Rating</h3>
<div class="description light-color">
Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an addon requires on your system, the lower the score, thus raising the possible security risks.
Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.
</div>
<ha-label-badge
class$="[[computeSecurityClassName(addon.rating)]]"
@ -416,7 +416,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
</template>
<template is="dom-if" if="[[!addon.version]]">
<template is="dom-if" if="[[!addon.available]]">
<p class="warning">This addon is not available on your system.</p>
<p class="warning">This add-on is not available on your system.</p>
</template>
<ha-call-api-button
disabled="[[!addon.available]]"

View File

@ -44,6 +44,7 @@ class HassioAddonNetwork extends EventsMixin(PolymerElement) {
<td>[[item.container]]</td>
<td>
<paper-input
placeholder="disabled"
value="{{item.host}}"
no-label-float=""
></paper-input>

View File

@ -17,7 +17,7 @@ class HassioCardContent extends LitElement {
@property() public hass!: HomeAssistant;
@property() public title!: string;
@property() public description?: string;
@property({ type: Boolean }) public available: boolean = true;
@property({ type: Boolean }) public available?: boolean;
@property() public datetime?: string;
@property() public iconTitle?: string;
@property() public iconClass?: string;
@ -33,7 +33,9 @@ class HassioCardContent extends LitElement {
<div>
<div class="title">${this.title}</div>
<div class="addition">
${this.description} ${this.available ? undefined : " (Not available"}
${this.description}
${/* treat as available when undefined */
this.available === false ? " (Not available)" : ""}
${this.datetime
? html`
<ha-relative-time

View File

@ -26,26 +26,13 @@ class HassioHassUpdate extends PolymerElement {
<template is="dom-if" if="[[computeUpdateAvailable(hassInfo)]]">
<div class="content">
<div class="card-group">
<div class="title">Update available! 🎉</div>
<paper-card>
<paper-card heading="Update available! 🎉">
<div class="card-content">
<hassio-card-content
hass="[[hass]]"
title="Home Assistant [[hassInfo.last_version]] is available"
description="You are currently running version [[hassInfo.version]]"
icon="hassio:home-assistant"
icon-class="hassupdate"
></hassio-card-content>
Home Assistant [[hassInfo.last_version]] is available and you
are currently running Home Assistant [[hassInfo.version]].
<template is="dom-if" if="[[error]]">
<div class="error">Error: [[error]]</div>
</template>
<p>
<a
href="https://www.home-assistant.io/latest-release-notes/"
target="_blank"
>Read the release notes</a
>
</p>
</div>
<div class="card-actions">
<ha-call-api-button
@ -54,7 +41,7 @@ class HassioHassUpdate extends PolymerElement {
>Update</ha-call-api-button
>
<a
href="https://github.com/home-assistant/home-assistant/releases"
href="https://www.home-assistant.io/latest-release-notes/"
target="_blank"
><mwc-button>Release notes</mwc-button></a
>

View File

@ -17,6 +17,9 @@ import {
HassioSupervisorInfo,
HassioHostInfo,
HassioHomeAssistantInfo,
fetchHassioAddonInfo,
createHassioSession,
HassioPanelInfo,
} from "../../src/data/hassio";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
@ -32,6 +35,7 @@ customElements.get("paper-icon-button").prototype._keyBindings = {};
@customElement("hassio-main")
class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
@property() public hass!: HomeAssistant;
@property() public panel!: HassioPanelInfo;
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
@ -117,6 +121,11 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
}
private async _fetchData() {
if (this.panel.config && this.panel.config.ingress) {
await this._redirectIngress(this.panel.config.ingress);
return;
}
const [supervisorInfo, hostInfo, hassInfo] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
@ -127,6 +136,28 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
this._hassInfo = hassInfo;
}
private async _redirectIngress(addonSlug: string) {
try {
const [addon] = await Promise.all([
fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {
throw new Error("Failed to fetch add-on info");
}),
createHassioSession(this.hass).catch(() => {
throw new Error("Failed to create an ingress session");
}),
]);
if (!addon.ingress_url) {
throw new Error("Add-on does not support Ingress");
}
location.assign(addon.ingress_url);
// await a promise that doesn't resolve, so we show the loading screen
// while we load the next page.
await new Promise(() => undefined);
} catch (err) {
alert(`Unable to open ingress connection `);
}
}
private _apiCalled(ev) {
if (!ev.detail.success) {
return;

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20190410.0",
version="20190417.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
@ -16,7 +16,7 @@ setup(
"hass_frontend_es5.*",
]
),
install_requires=["user-agents==1.1.0"],
install_requires=["user-agents==2.0.0"],
include_package_data=True,
zip_safe=False,
)

View File

@ -57,6 +57,10 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
padding-top: 100%;
}
.banner.content-type-game:before {
padding-top: 100%;
}
.banner.no-cover:before {
padding-top: 88px;
}
@ -310,6 +314,8 @@ class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
cls += " no-cover";
} else if (playerObj.stateObj.attributes.media_content_type === "music") {
cls += " content-type-music";
} else if (playerObj.stateObj.attributes.media_content_type === "game") {
cls += " content-type-game";
}
return cls;
}

View File

@ -0,0 +1,211 @@
import {
property,
PropertyValues,
LitElement,
TemplateResult,
html,
CSSResult,
css,
customElement,
} from "lit-element";
import computeStateName from "../common/entity/compute_state_name";
import { HomeAssistant, CameraEntity } from "../types";
import { fireEvent } from "../common/dom/fire_event";
import {
CAMERA_SUPPORT_STREAM,
fetchStreamUrl,
computeMJPEGStreamUrl,
} from "../data/camera";
import { supportsFeature } from "../common/entity/supports-feature";
type HLSModule = typeof import("hls.js");
@customElement("ha-camera-stream")
class HaCameraStream extends LitElement {
@property() public hass?: HomeAssistant;
@property() public stateObj?: CameraEntity;
@property({ type: Boolean }) public showControls = false;
@property() private _attached = false;
// We keep track if we should force MJPEG with a string
// that way it automatically resets if we change entity.
@property() private _forceMJPEG: string | undefined = undefined;
private _hlsPolyfillInstance?: Hls;
public connectedCallback() {
super.connectedCallback();
this._attached = true;
}
public disconnectedCallback() {
super.disconnectedCallback();
this._attached = false;
}
protected render(): TemplateResult | void {
if (!this.stateObj || !this._attached) {
return html``;
}
return html`
${this._shouldRenderMJPEG
? html`
<img
@load=${this._elementResized}
.src=${__DEMO__
? "/demo/webcamp.jpg"
: computeMJPEGStreamUrl(this.stateObj)}
.alt=${computeStateName(this.stateObj)}
/>
`
: html`
<video
autoplay
muted
playsinline
?controls=${this.showControls}
@loadeddata=${this._elementResized}
></video>
`}
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
const stateObjChanged = changedProps.has("stateObj");
const attachedChanged = changedProps.has("_attached");
const oldState = changedProps.get("stateObj") as this["stateObj"];
const oldEntityId = oldState ? oldState.entity_id : undefined;
const curEntityId = this.stateObj ? this.stateObj.entity_id : undefined;
if (
(!stateObjChanged && !attachedChanged) ||
(stateObjChanged && oldEntityId === curEntityId)
) {
return;
}
// If we are no longer attached, destroy polyfill.
if (attachedChanged && !this._attached) {
this._destroyPolyfill();
return;
}
// Nothing to do if we are render MJPEG.
if (this._shouldRenderMJPEG) {
return;
}
// Tear down existing polyfill, if available
this._destroyPolyfill();
if (curEntityId) {
this._startHls();
}
}
private get _shouldRenderMJPEG() {
return (
this._forceMJPEG === this.stateObj!.entity_id ||
!this.hass!.config.components.includes("stream") ||
!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)
);
}
private get _videoEl(): HTMLVideoElement {
return this.shadowRoot!.querySelector("video")!;
}
private async _startHls(): Promise<void> {
// tslint:disable-next-line
const Hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
.default as HLSModule;
let hlsSupported = Hls.isSupported();
const videoEl = this._videoEl;
if (!hlsSupported) {
hlsSupported =
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
}
if (!hlsSupported) {
this._forceMJPEG = this.stateObj!.entity_id;
return;
}
try {
const { url } = await fetchStreamUrl(
this.hass!,
this.stateObj!.entity_id
);
if (Hls.isSupported()) {
this._renderHLSPolyfill(videoEl, Hls, url);
} else {
this._renderHLSNative(videoEl, url);
}
return;
} catch (err) {
// Fails if we were unable to get a stream
// tslint:disable-next-line
console.error(err);
this._forceMJPEG = this.stateObj!.entity_id;
}
}
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
videoEl.src = url;
await new Promise((resolve) =>
videoEl.addEventListener("loadedmetadata", resolve)
);
videoEl.play();
}
private async _renderHLSPolyfill(
videoEl: HTMLVideoElement,
// tslint:disable-next-line
Hls: HLSModule,
url: string
) {
const hls = new Hls();
this._hlsPolyfillInstance = hls;
hls.attachMedia(videoEl);
hls.on(Hls.Events.MEDIA_ATTACHED, () => {
hls.loadSource(url);
});
}
private _elementResized() {
fireEvent(this, "iron-resize");
}
private _destroyPolyfill(): void {
if (this._hlsPolyfillInstance) {
this._hlsPolyfillInstance.destroy();
this._hlsPolyfillInstance = undefined;
}
}
static get styles(): CSSResult {
return css`
:host,
img,
video {
display: block;
}
img,
video {
width: 100%;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-camera-stream": HaCameraStream;
}
}

View File

@ -15,7 +15,7 @@ import "./ha-icon";
import "../components/user/ha-user-badge";
import isComponentLoaded from "../common/config/is_component_loaded";
import { HomeAssistant, Panel } from "../types";
import { HomeAssistant, PanelInfo } from "../types";
import { fireEvent } from "../common/dom/fire_event";
import { DEFAULT_PANEL } from "../common/const";
@ -32,7 +32,9 @@ const computePanels = (hass: HomeAssistant) => {
logbook: 2,
history: 3,
};
const result: Panel[] = Object.values(panels).filter((panel) => panel.title);
const result: PanelInfo[] = Object.values(panels).filter(
(panel) => panel.title
);
result.sort((a, b) => {
const aBuiltIn = a.component_name in sortValue;

View File

@ -1,4 +1,11 @@
import { HomeAssistant } from "../types";
import { HomeAssistant, PanelInfo } from "../types";
export type HassioPanelInfo = PanelInfo<
| undefined
| {
ingress?: string;
}
>;
interface HassioResponse<T> {
data: T;

View File

@ -1,3 +1,5 @@
import { PanelInfo } from "../types";
export interface CustomPanelConfig {
name: string;
embed_iframe: boolean;
@ -6,3 +8,7 @@ export interface CustomPanelConfig {
module_url?: string;
html_url?: string;
}
export type CustomPanelInfo<T = {}> = PanelInfo<
T & { _panel_custom: CustomPanelConfig }
>;

View File

@ -8,39 +8,36 @@ import {
css,
} from "lit-element";
import computeStateName from "../../../common/entity/compute_state_name";
import { HomeAssistant, CameraEntity } from "../../../types";
import { fireEvent } from "../../../common/dom/fire_event";
import {
fetchStreamUrl,
computeMJPEGStreamUrl,
CAMERA_SUPPORT_STREAM,
CameraPreferences,
fetchCameraPrefs,
updateCameraPrefs,
} from "../../../data/camera";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-camera-stream";
import "@polymer/paper-checkbox/paper-checkbox";
// Not duplicate import, it's for typing
// tslint:disable-next-line
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
type HLSModule = typeof import("hls.js");
class MoreInfoCamera extends LitElement {
@property() public hass?: HomeAssistant;
@property() public stateObj?: CameraEntity;
@property() private _cameraPrefs?: CameraPreferences;
private _hlsPolyfillInstance?: Hls;
public disconnectedCallback() {
super.disconnectedCallback();
this._teardownPlayback();
}
protected render(): TemplateResult | void {
if (!this.hass || !this.stateObj) {
return html``;
}
return html`
<div id="root"></div>
<ha-camera-stream
.hass="${this.hass}"
.stateObj="${this.stateObj}"
showcontrols
></ha-camera-stream>
${this._cameraPrefs
? html`
<paper-checkbox
@ -68,122 +65,14 @@ class MoreInfoCamera extends LitElement {
return;
}
// Tear down if we have something and we need to build it up
if (oldEntityId) {
this._teardownPlayback();
}
if (curEntityId) {
this._startPlayback();
}
}
private async _startPlayback(): Promise<void> {
if (!this.stateObj) {
return;
}
if (
!this.hass!.config.components.includes("stream") ||
!supportsFeature(this.stateObj, CAMERA_SUPPORT_STREAM)
curEntityId &&
this.hass!.config.components.includes("stream") &&
supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)
) {
this._renderMJPEG();
return;
// Fetch in background while we set up the video.
this._fetchCameraPrefs();
}
const videoEl = document.createElement("video");
videoEl.style.width = "100%";
videoEl.autoplay = true;
videoEl.controls = true;
videoEl.muted = true;
// tslint:disable-next-line
const Hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any)
.default as HLSModule;
let hlsSupported = Hls.isSupported();
if (!hlsSupported) {
hlsSupported =
videoEl.canPlayType("application/vnd.apple.mpegurl") !== "";
}
if (hlsSupported) {
try {
const { url } = await fetchStreamUrl(
this.hass!,
this.stateObj.entity_id
);
// Fetch in background while we set up the video.
this._fetchCameraPrefs();
if (Hls.isSupported()) {
this._renderHLSPolyfill(videoEl, Hls, url);
} else {
this._renderHLSNative(videoEl, url);
}
return;
} catch (err) {
// When an error happens, we will do nothing so we render mjpeg.
}
}
this._renderMJPEG();
}
private get _videoRoot(): HTMLDivElement {
return this.shadowRoot!.getElementById("root")! as HTMLDivElement;
}
private async _renderHLSNative(videoEl: HTMLVideoElement, url: string) {
videoEl.src = url;
this._videoRoot.appendChild(videoEl);
await new Promise((resolve) =>
videoEl.addEventListener("loadedmetadata", resolve)
);
videoEl.play();
}
private async _renderHLSPolyfill(
videoEl: HTMLVideoElement,
// tslint:disable-next-line
Hls: HLSModule,
url: string
) {
const hls = new Hls();
this._hlsPolyfillInstance = hls;
await new Promise((resolve) => {
hls.on(Hls.Events.MEDIA_ATTACHED, resolve);
hls.attachMedia(videoEl);
});
hls.loadSource(url);
this._videoRoot.appendChild(videoEl);
videoEl.addEventListener("loadeddata", () =>
fireEvent(this, "iron-resize")
);
}
private _renderMJPEG() {
const img = document.createElement("img");
img.style.width = "100%";
img.addEventListener("load", () => fireEvent(this, "iron-resize"));
img.src = __DEMO__
? "/demo/webcamp.jpg"
: computeMJPEGStreamUrl(this.stateObj!);
img.alt = computeStateName(this.stateObj!);
this._videoRoot.appendChild(img);
}
private _teardownPlayback(): any {
if (this._hlsPolyfillInstance) {
this._hlsPolyfillInstance.destroy();
this._hlsPolyfillInstance = undefined;
}
const root = this._videoRoot;
while (root.lastChild) {
root.removeChild(root.lastChild);
}
this.stateObj = undefined;
this._cameraPrefs = undefined;
}
private async _fetchCameraPrefs() {

View File

@ -4,8 +4,7 @@ import { createCustomPanelElement } from "../util/custom-panel/create-custom-pan
import { setCustomPanelProperties } from "../util/custom-panel/set-custom-panel-properties";
import { fireEvent } from "../common/dom/fire_event";
import { PolymerElement } from "@polymer/polymer";
import { Panel } from "../types";
import { CustomPanelConfig } from "../data/panel_custom";
import { CustomPanelInfo } from "../data/panel_custom";
declare global {
interface Window {
@ -39,12 +38,12 @@ function setProperties(properties) {
setCustomPanelProperties(panelEl, properties);
}
function initialize(panel: Panel, properties: {}) {
function initialize(panel: CustomPanelInfo, properties: {}) {
const style = document.createElement("style");
style.innerHTML = "body{margin:0}";
document.head.appendChild(style);
const config = panel.config!._panel_custom as CustomPanelConfig;
const config = panel.config._panel_custom;
let start: Promise<unknown> = Promise.resolve();
if (!webComponentsSupported) {

View File

@ -2,8 +2,8 @@ import { property, PropertyValues, UpdatingElement } from "lit-element";
import { loadCustomPanel } from "../../util/custom-panel/load-custom-panel";
import { createCustomPanelElement } from "../../util/custom-panel/create-custom-panel-element";
import { setCustomPanelProperties } from "../../util/custom-panel/set-custom-panel-properties";
import { HomeAssistant, Route, Panel } from "../../types";
import { CustomPanelConfig } from "../../data/panel_custom";
import { HomeAssistant, Route } from "../../types";
import { CustomPanelInfo } from "../../data/panel_custom";
import { navigate } from "../../common/navigate";
declare global {
@ -16,7 +16,7 @@ export class HaPanelCustom extends UpdatingElement {
@property() public hass!: HomeAssistant;
@property() public narrow!: boolean;
@property() public route!: Route;
@property() public panel!: Panel;
@property() public panel!: CustomPanelInfo;
private _setProperties?: (props: {}) => void | undefined;
// Since navigate fires events on `window`, we need to expose this as a function
@ -66,8 +66,8 @@ export class HaPanelCustom extends UpdatingElement {
}
}
private _createPanel(panel: Panel) {
const config = panel.config!._panel_custom as CustomPanelConfig;
private _createPanel(panel: CustomPanelInfo) {
const config = panel.config!._panel_custom;
const tempA = document.createElement("a");
tempA.href = config.html_url || config.js_url || config.module_url || "";

View File

@ -60,6 +60,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
.image="${this._config.image}"
.stateImage="${this._config.state_image}"
.cameraImage="${this._config.camera_image}"
.cameraView="${this._config.camera_view}"
.entity="${this._config.entity}"
.aspectRatio="${this._config.aspect_ratio}"
></hui-image>

View File

@ -108,6 +108,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
.cameraImage="${computeDomain(this._config.entity) === "camera"
? this._config.entity
: this._config.camera_image}"
.cameraView="${this._config.camera_view}"
.entity="${this._config.entity}"
.aspectRatio="${this._config.aspect_ratio}"
@ha-click="${this._handleTap}"

View File

@ -131,6 +131,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
.image="${this._config.image}"
.stateImage="${this._config.state_image}"
.cameraImage="${this._config.camera_image}"
.cameraView="${this._config.camera_view}"
.entity="${this._config.entity}"
.aspectRatio="${this._config.aspect_ratio}"
></hui-image>

View File

@ -2,6 +2,7 @@ import { LovelaceCardConfig, ActionConfig } from "../../../data/lovelace";
import { Condition } from "../common/validate-condition";
import { EntityConfig } from "../entity-rows/types";
import { LovelaceElementConfig } from "../elements/types";
import { HuiImage } from "../components/hui-image";
export interface AlarmPanelCardConfig extends LovelaceCardConfig {
entity: string;
@ -129,6 +130,7 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
title?: string;
image?: string;
camera_image?: string;
camera_view?: HuiImage["cameraView"];
state_image?: {};
aspect_ratio?: string;
entity?: string;
@ -140,6 +142,7 @@ export interface PictureEntityCardConfig extends LovelaceCardConfig {
name?: string;
image?: string;
camera_image?: string;
camera_view?: HuiImage["cameraView"];
state_image?: {};
aspect_ratio?: string;
tap_action?: ActionConfig;
@ -153,6 +156,7 @@ export interface PictureGlanceCardConfig extends LovelaceCardConfig {
title?: string;
image?: string;
camera_image?: string;
camera_view?: HuiImage["cameraView"];
state_image?: {};
aspect_ratio?: string;
entity?: string;

View File

@ -14,7 +14,7 @@ import {
query,
customElement,
} from "lit-element";
import { HomeAssistant } from "../../../types";
import { HomeAssistant, CameraEntity } from "../../../types";
import { styleMap } from "lit-html/directives/style-map";
import { classMap } from "lit-html/directives/class-map";
import { b64toBlob } from "../../../common/file/b64-to-blob";
@ -28,7 +28,7 @@ export interface StateSpecificConfig {
}
@customElement("hui-image")
class HuiImage extends LitElement {
export class HuiImage extends LitElement {
@property() public hass?: HomeAssistant;
@property() public entity?: string;
@ -39,6 +39,8 @@ class HuiImage extends LitElement {
@property() public cameraImage?: string;
@property() public cameraView?: "live" | "auto";
@property() public aspectRatio?: string;
@property() public filter?: string;
@ -60,7 +62,9 @@ class HuiImage extends LitElement {
public connectedCallback(): void {
super.connectedCallback();
this._attached = true;
this._startUpdateCameraInterval();
if (this.cameraImage && this.cameraView !== "live") {
this._startUpdateCameraInterval();
}
}
public disconnectedCallback(): void {
@ -77,11 +81,17 @@ class HuiImage extends LitElement {
// Figure out image source to use
let imageSrc: string | undefined;
let cameraObj: CameraEntity | undefined;
// Track if we are we using a fallback image, used for filter.
let imageFallback = !this.stateImage;
if (this.cameraImage) {
imageSrc = this._cameraImageSrc;
if (this.cameraView === "live") {
cameraObj =
this.hass && (this.hass.states[this.cameraImage] as CameraEntity);
} else {
imageSrc = this._cameraImageSrc;
}
} else if (this.stateImage) {
const stateImage = this.stateImage[state];
@ -119,16 +129,25 @@ class HuiImage extends LitElement {
ratio: Boolean(ratio && ratio.w > 0 && ratio.h > 0),
})}
>
<img
id="image"
src=${imageSrc}
@error=${this._onImageError}
@load=${this._onImageLoad}
style=${styleMap({
filter,
display: this._loadError ? "none" : "block",
})}
/>
${this.cameraImage && this.cameraView === "live"
? html`
<ha-camera-stream
.hass="${this.hass}"
.stateObj="${cameraObj}"
></ha-camera-stream>
`
: html`
<img
id="image"
src=${imageSrc}
@error=${this._onImageError}
@load=${this._onImageLoad}
style=${styleMap({
filter,
display: this._loadError ? "none" : "block",
})}
/>
`}
<div
id="brokenImage"
style=${styleMap({
@ -141,7 +160,7 @@ class HuiImage extends LitElement {
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("cameraImage")) {
if (changedProps.has("cameraImage") && this.cameraView !== "live") {
this._updateCameraImageSrc();
this._startUpdateCameraInterval();
return;

View File

@ -78,16 +78,16 @@ export interface Themes {
themes: { [key: string]: Theme };
}
export interface Panel {
export interface PanelInfo<T = {} | null> {
component_name: string;
config: { [key: string]: any } | null;
config: T;
icon: string | null;
title: string | null;
url_path: string;
}
export interface Panels {
[name: string]: Panel;
[name: string]: PanelInfo;
}
export interface Translation {
@ -212,14 +212,6 @@ export type CameraEntity = HassEntityBase & {
};
};
export interface PanelInfo<T = unknown> {
component_name: string;
icon?: string;
title?: string;
url_path: string;
config: T;
}
export interface Route {
prefix: string;
path: string;
@ -229,7 +221,7 @@ export interface PanelElement extends HTMLElement {
hass?: HomeAssistant;
narrow?: boolean;
route?: Route | null;
panel?: Panel;
panel?: PanelInfo;
}
export interface LocalizeMixin {

View File

@ -26,7 +26,17 @@ hassAttributeUtil.DOMAIN_DEVICE_CLASS = {
"vibration",
"window",
],
cover: ["garage"],
cover: [
"awning",
"blind",
"curtain",
"damper",
"door",
"garage",
"shade",
"shutter",
"window",
],
sensor: [
"battery",
"humidity",

View File

@ -1,6 +1,6 @@
{
"panel": {
"config": "Konfigurasie",
"config": "Opstellings",
"states": "Oorsig",
"map": "Kaart",
"logbook": "Logboek",
@ -13,7 +13,8 @@
"dev-templates": "Template",
"dev-mqtt": "MQTT",
"dev-info": "Info",
"calendar": "Kalender"
"calendar": "Kalender",
"profile": "Profiel"
},
"state": {
"default": {
@ -91,19 +92,28 @@
"off": "OK",
"on": "Probleem"
},
"connectivity": {
"off": "Ontkoppel",
"on": "Gekoppel"
},
"cold": {
"off": "Normaal",
"on": "Koud"
},
"door": {
"off": "Toe",
"on": "Oop"
},
"garage_door": {
"off": "Toe",
"on": "Oop"
},
"heat": {
"off": "Normaal",
"on": "Warm"
},
"window": {
"off": "Toe",
"on": "Oop"
},
"lock": {
@ -138,8 +148,8 @@
"manual": "Handmatig"
},
"configurator": {
"configure": "Konfigureer",
"configured": "Gekonfigureer"
"configure": "Stel op",
"configured": "Opgestel"
},
"cover": {
"open": "Oop",
@ -248,9 +258,13 @@
},
"vacuum": {
"cleaning": "Skoonmaak",
"docked": "Vasgemeer",
"docked": "Vasgemeer by hawe",
"error": "Fout",
"idle": "Ledig",
"off": "Af",
"on": "Aan",
"paused": "Onderbreek"
"paused": "Onderbreek",
"returning": "Oppad terug hawe toe"
},
"timer": {
"active": "aktief",
@ -265,7 +279,9 @@
"state_badge": {
"default": {
"unknown": "?",
"unavailable": "Onbeskik"
"unavailable": "Onbeskik",
"error": "Fout",
"entity_not_found": "Entiteit nie gevind nie"
},
"alarm_control_panel": {
"armed": "Gewapen",
@ -295,24 +311,88 @@
"add_item": "Voeg item",
"microphone_tip": "Druk die mikrofoon regs bo en sê \"Add candy to my shopping list\""
},
"history": {
"showing_entries": "Wys inskrywings vir",
"period": "Tydperk"
},
"logbook": {
"showing_entries": "Wys inskrywings vir",
"period": "Tydperk"
},
"mailbox": {
"empty": "U het geen boodskappe nie",
"playback_title": "Boodskap terugspeel",
"delete_prompt": "Skrap hierdie boodskap?",
"delete_button": "Skrap"
},
"config": {
"header": "Stel Home Assistant op",
"introduction": "Hier is dit moontlik om u komponente en Home Assistant op te stel. Tans kan alles in verband met die gebruikerskoppelvlak nog nie hier opgestel word nie, maar ons werk daaraan.",
"core": {
"caption": "Algemeen",
"description": "Bevestig u opstellingslêer en beheer die bediener",
"section": {
"core": {
"header": "Opstellings en bedienerbeheer",
"introduction": "As u opstellings verander, kan dit 'n vermoeiende proses wees. Ons weet. Hierdie afdeling sal probeer om u lewe 'n bietjie makliker te maak.",
"validation": {
"heading": "Opstellings validering",
"introduction": "Verifieer u opstellings as u onlangs 'n paar veranderinge aan u opstellings gemaak het en wil seker maak dat dit alles geldig is",
"check_config": "Verifieer opstellings",
"valid": "Opstellings geldig!",
"invalid": "Opstellings ongeldig!"
},
"reloading": {
"heading": "Opstellings herlaai tans",
"introduction": "Sommige dele van Home Assistant kan herlaai sonder om te herbegin. Deur om herlaai te kliek, sal die huidige opstelling ontlaai en die nuwe een laai.",
"core": "Herlaai kern",
"group": "Herlaai groepe",
"automation": "Herlaai outomatisasies",
"script": "Herlaai skripte"
},
"server_management": {
"heading": "Bediener bestuur",
"introduction": "Beheer u Home Assistant-bediener ... met Home Assistant.",
"restart": "Herbegin",
"stop": "Staak"
}
}
}
},
"customize": {
"caption": "Pasgemaakte Instellings",
"description": "Pas u entiteite aan",
"picker": {
"header": "Pasgemaakte Instellings",
"introduction": "Verfyn per-entiteit eienskappe. Bygevoegde \/ gewysigde aanpassings sal onmiddellik in werking tree. Verwyderde aanpassings sal in werkin tree wanneer die entiteit opgedateer word."
}
},
"automation": {
"caption": "Outomatisering",
"description": "Skep en wysig outomatisasies",
"picker": {
"introduction": "Die outomatiseringsredakteur stel jou in staat om outomatisasies te skep en te wysig. Volg die onderstaande skakel om die instruksies te lees om seker te maak dat u Home Assistant korrek opgestel het.",
"header": "Outomatiseringsredakteur",
"introduction": "Die outomatiseringsredakteur stel u in staat om outomatisasies te skep en te wysig. Volg die onderstaande skakel om die instruksies te lees om seker te maak dat u Home Assistant korrek opgestel het.",
"pick_automation": "Kies outomatisasie om te redigeer",
"no_automations": "Ons kon nie redigeerbare outomatisasies vind nie",
"add_automation": "Voeg outomatisering by",
"learn_more": "Kom meer te wete oor outomatisasies"
},
"editor": {
"introduction": "Gebruik outomatisasies om jou huis lewend te maak",
"default_name": "Nuwe outomatisering",
"save": "Stoor",
"unsaved_confirm": "U het ongestoorde veranderinge. Is u seker u wil die blad verlaat?",
"alias": "Naam",
"triggers": {
"header": "Snellers",
"introduction": "Snellers is wat die prosessering van 'n outomatiseringsreël afskop. Dit is moontlik om verskeie snellers vir dieselfde reël te spesifiseer. Sodra 'n sneller begin, sal Home Assistant die voorwaardes, indien enige, bevestig en die aksie roep.",
"add": "Voeg sneller by",
"duplicate": "Dupliseer",
"delete": "Skrap",
"delete_confirm": "Is jy seker jy wil dit skrap?",
"delete_confirm": "Is u seker u wil dit skrap?",
"unsupported_platform": "Ongesteunde platform: {platform}",
"type_select": "Sneller tipe",
"type": {
"event": {
"label": "Gebeurtenis",
@ -320,10 +400,21 @@
"event_data": "Gebeurtenis data"
},
"state": {
"label": "Staat"
"label": "Staat",
"from": "Vanaf",
"to": "Tot en met",
"for": "Vir"
},
"homeassistant": {
"event": "Gebeurtenis:"
"label": "Home Assistant",
"event": "Gebeurtenis:",
"start": "Begin",
"shutdown": "Staak"
},
"mqtt": {
"label": "MQTT",
"topic": "Onderwerp",
"payload": "Loonvrag (opsioneel)"
},
"numeric_state": {
"label": "Numeriese toestand",
@ -343,13 +434,16 @@
"value_template": "Waarde templaat"
},
"time": {
"label": "Tyd"
"label": "Tyd",
"at": "Om"
},
"zone": {
"label": "Sone",
"entity": "Entiteit met plek",
"zone": "Sone",
"event": "Gebeurtenis:"
"event": "Gebeurtenis:",
"enter": "Betree",
"leave": "Verlaat"
},
"webhook": {
"label": "Webhook",
@ -374,10 +468,11 @@
},
"conditions": {
"header": "Voorwaardes",
"introduction": "Voorwaardes is 'n opsionele deel van 'n outomatiseringsreël en kan gebruik word om te verhoed dat 'n aksie plaasvind wanneer dit geaktiveer word. Voorwaardes lyk baie soos snellers maar is baie anders. 'n Sneller sal kyk na gebeure wat in die stelsel gebeur terwyl 'n voorwaarde net kyk hoe die stelsel lyk. Byvoorbeeld: 'n Sneller kan sien dat 'n skakelaar aangeskakel word. 'n Voorwaarde kan net sien of 'n skakelaar tans aan of af is.",
"add": "Voeg voorwaarde by",
"duplicate": "Dupliseer",
"delete": "Skrap",
"delete_confirm": "Is jy seker jy wil dit skrap?",
"delete_confirm": "Is u seker u wil dit skrap?",
"unsupported_condition": "Ongesteunde voorwaarde: {condition}",
"type_select": "Voorwaarde tipe",
"type": {
@ -423,7 +518,7 @@
"add": "Voeg aksie by",
"duplicate": "Dupliseer",
"delete": "Skrap",
"delete_confirm": "Is jy seker jy wil dit skrap?",
"delete_confirm": "Is u seker u wil dit skrap?",
"unsupported_action": "Ongesteunde aksie: {action}",
"type_select": "Aksie tipe",
"type": {
@ -454,8 +549,13 @@
}
},
"script": {
"caption": "Skrip",
"description": "Skep en wysig skripte"
},
"zwave": {
"caption": "Z-Wave",
"description": "Bestuur u Z-Wave netwerk"
},
"users": {
"caption": "Gebruikers",
"description": "Bestuur gebruikers",
@ -478,9 +578,24 @@
"create": "Skep"
}
},
"cloud": {
"caption": "Home Assistant Cloud",
"description_login": "Aangemeld as {email}",
"description_not_login": "Nie aangemeld nie",
"description_features": "Beheer terwyl weg van die huis af is, integreer met Alexa en Google Assistant."
},
"integrations": {
"caption": "Integrasies",
"description": "Bestuur gekoppelde toestelle en dienste",
"discovered": "Ontdek",
"configured": "Opgestel",
"new": "Stel 'n nuwe integrasie op",
"configure": "Stel op",
"none": "Nog niks is opgestel nie",
"config_entry": {
"delete_confirm": "Is jy seker jy wil hierdie integrasie skrap?",
"no_devices": "Hierdie integrasie het geen toestelle nie.",
"no_device": "Entiteite sonder toestelle",
"delete_confirm": "Is u seker u wil hierdie integrasie skrap?",
"restart_confirm": "Herbegin Home Assistant om hierdie integrasie te voltooi",
"manuf": "deur {manufacturer}",
"hub": "Gekonnekteer via",
@ -494,22 +609,33 @@
"caption": "ZHA",
"description": "Zigbee Home Automation netwerk bestuur",
"services": {
"reconfigure": "Herkonfigureer ZHA-toestel (heal device). Gebruik dit as jy probleme ondervind met die toestel. As die betrokke toestel 'n battery aangedrewe toestel is, maak asseblief seker dat dit wakker is en bevele aanvaar wanneer u hierdie diens gebruik.",
"updateDeviceName": "Stel 'n persoonlike naam vir hierdie toestel in die toestelregister"
"reconfigure": "Stel ZHA-toestel weer op (heal device). Gebruik dit as u probleme ondervind met die toestel. As die betrokke toestel 'n battery aangedrewe toestel is, maak asseblief seker dat dit wakker is en bevele aanvaar wanneer u hierdie diens gebruik.",
"updateDeviceName": "Stel 'n persoonlike naam vir hierdie toestel in die toestelregister",
"remove": "Verwyder 'n toestel van die ZigBee-netwerk."
},
"device_card": {
"device_name_placeholder": "Gebruiker se naam",
"area_picker_label": "Gebied",
"update_name_button": "Verander Naam"
},
"add_device_page": {
"header": "Zigbee Home Automation - Voeg Toestelle By",
"spinner": "Opsoek na ZHA Zigbee-toestelle...",
"discovery_text": "Ontdekde toestelle sal hier verskyn. Volg die instruksies vir u toestel(e) en plaas die toestel(e) in die paringsmodus."
}
},
"area_registry": {
"caption": "Gebiedsregister",
"description": "Oorsig van alle gebiede in jou huis.",
"description": "Oorsig van alle gebiede in u huis.",
"picker": {
"header": "Gebiedsregister",
"introduction": "Gebiede word gebruik om toestelle te organiseer gebaseer op waar hulle is. Hierdie inligting sal deur die Home Assistant gebruik word om u te help om u koppelvlak, toestemmings en integrasies met ander stelsels te organiseer.",
"introduction2": "Om toestelle in 'n gebied te plaas, gebruik die skakel hieronder om na die integrasies bladsy toe te gaan en klik dan op 'n gekonfigureerde integrasie om na die toestelkaarte toe te gaan.",
"introduction2": "Om toestelle in 'n gebied te plaas, gebruik die skakel hieronder om na die integrasies bladsy toe te gaan en klik dan op 'n opgestelde integrasie om na die toestelkaarte toe te gaan.",
"integrations_page": "Integrasies bladsy",
"no_areas": "Dit lyk asof jy nog geen gebiede het nie!",
"no_areas": "Dit lyk asof u nog geen gebiede het nie!",
"create_area": "SKEP GEBIED"
},
"no_areas": "Dit lyk asof jy nog geen gebiede het nie!",
"no_areas": "Dit lyk asof u nog geen gebiede het nie!",
"create_area": "SKEP GEBIED",
"editor": {
"default_name": "Nuwe Gebied",
@ -535,12 +661,6 @@
"update": "OPDATEER"
}
},
"customize": {
"picker": {
"header": "Pasgemaakte Instellings ",
"introduction": "Verfyn per-entiteit eienskappe. Bygevoegde \/ gewysigde aanpassings sal onmiddellik in werking tree. Verwyderde aanpassings sal in werkin tree wanneer die entiteit opgedateer word."
}
},
"person": {
"caption": "Persone",
"description": "Bestuur die persone wat Home Assistant op spoor.",
@ -552,9 +672,86 @@
}
}
},
"profile": {
"push_notifications": {
"header": "\"Push\" kennisgewings",
"description": "Stuur kennisgewings na hierdie toestel.",
"error_load_platform": "Stel notify.html5 op.",
"error_use_https": "Vereis dat SSL gedeaktiveer is vir gebruikerskoopelvlak.",
"push_notifications": "\"Push\" kennisgewings",
"link_promo": "Kom meer te wete"
},
"language": {
"header": "Taal",
"link_promo": "Help om te vertaal",
"dropdown_label": "Taal"
},
"themes": {
"header": "Tema",
"error_no_theme": "Geen temas beskikbaar nie.",
"link_promo": "Kom meer te wete oor temas",
"dropdown_label": "Tema"
},
"refresh_tokens": {
"header": "Verfris-tekseenhede",
"description": "Elke verfris-tekseenheid verteenwoordig 'n aanmeldingssessie. Verfris-tekseenhede sal outomaties verwyder word wanneer u op meld af klik. Die volgende verfris-tekseenhede is tans aktief vir u rekening.",
"token_title": "Verfris-tekseenheid vir {clientId}",
"created_at": "Geskep op {date}",
"confirm_delete": "Is u seker u wil die verfris-tekseenheid vir {name} skrap?",
"delete_failed": "Het misluk om die verfris-tekseenheid te skrap.",
"last_used": "Laas gebruik op {date} vanaf {location}",
"not_used": "Is nog nooit gebruik nie",
"current_token_tooltip": "Nie in staat om huidige verfris-tekseenheid te skrap nie"
},
"long_lived_access_tokens": {
"header": "Langlewende-toegangs-tekseenhede",
"description": "Skep langlewende-toegangs-tekseenhede om u skripte in staat te stel om met u Home Assistant-instansie te kommunikeer. Elke tekseenheid sal geldig wees vir 10 jaar vanaf die skepping. Die volgende langlewende-toegangs-tekseenheid is tans aktief.",
"learn_auth_requests": "Leer hoe om geverifieerde versoeke te maak.",
"created_at": "Geskep op {date}",
"confirm_delete": "Is u seker u wil die toegangs-tekseenheid vir {name} skrap?",
"delete_failed": "Het misluk om die toegangs-tekseenheid te skrap.",
"create": "Skep Tekseenheid",
"create_failed": "Het misluk om die toegangs-tekseenheid te maak.",
"prompt_name": "Naam?",
"prompt_copy_token": "Kopieer u toegangs-tekseenheid. Dit sal nie weer gewys word nie.",
"empty_state": "U het nog geen langlewende-toegangs-tekseenhede nie.",
"last_used": "Laas gebruik op {date} vanaf {location}",
"not_used": "Is nog nooit gebruik nie"
},
"current_user": "U is tans aangemeld as {fullName} .",
"is_owner": "U is 'n eienaar.",
"logout": "Meld af",
"change_password": {
"header": "Verander Wagwoord",
"current_password": "Huidige Wagwoord",
"new_password": "Nuwe Wagwoord",
"confirm_new_password": "Bevestig Nuwe Wagwoord",
"error_required": "Vereis",
"submit": "Dien in"
},
"mfa": {
"header": "Multi-faktor Verifikasie Modules",
"disable": "Deaktiveer",
"enable": "Aktiveer",
"confirm_disable": "Is u seker u wil {name} deaktiveer?"
},
"mfa_setup": {
"title_aborted": "Gestaak",
"title_success": "Sukses!",
"step_done": "Opstelling gedoen vir {step}",
"close": "Toe",
"submit": "Dien in"
}
},
"page-authorize": {
"initializing": "Inisialiseer",
"authorizing_client": "U is op die punt om {clientId} toegang te gee tot u Home Assistant instansie.",
"logging_in_with": "Meld tans aan met **{authProviderName}**.",
"pick_auth_provider": "Of meld aan met",
"abort_intro": "Aanmelding gestaak",
"form": {
"working": "Wag asseblief",
"unknown_error": "Iets het skeef geloop",
"providers": {
"homeassistant": {
"step": {
@ -568,7 +765,7 @@
"data": {
"code": "Twee-faktor-Verifikasiekode"
},
"description": "Maak die ** {mfa_module_name} ** op jou toestel oop om jou twee-faktor-verifikasiekode te sien en jou identiteit te verifieer:"
"description": "Maak die ** {mfa_module_name} ** op u toestel oop om u twee-faktor-verifikasiekode te sien en u identiteit te verifieer:"
}
},
"error": {
@ -581,20 +778,41 @@
},
"legacy_api_password": {
"step": {
"init": {
"data": {
"password": "API wagwoord"
},
"description": "Voer asseblief die API-wagwoord in u http-config in:"
},
"mfa": {
"data": {
"code": "Twee-faktor-Verifikasiekode"
},
"description": "Maak die ** {mfa_module_name} ** op jou toestel oop om jou twee-faktor-verifikasiekode te sien en jou identiteit te verifieer:"
"description": "Maak die ** {mfa_module_name} ** op u toestel oop om u twee-faktor-verifikasiekode te sien en u identiteit te verifieer:"
}
},
"error": {
"invalid_auth": "Ongeldige API wagwoord",
"invalid_code": "Ongeldige verifikasiekode"
},
"abort": {
"no_api_password_set": "U het nie 'n API-wagwoord opgestel nie.",
"login_expired": "Sessie verstryk, teken asseblief weer aan."
}
},
"trusted_networks": {
"step": {
"init": {
"data": {
"user": "Gebruiker"
},
"description": "Kies asseblief die gebruiker as wie U will aanmeld as:"
}
},
"abort": {
"not_whitelisted": "U rekenaar is nie op die witlys nie."
}
},
"command_line": {
"step": {
"init": {
@ -607,7 +825,7 @@
"data": {
"code": "Twee-faktor-Verifikasiekode"
},
"description": "Maak die ** {mfa_module_name} ** op jou toestel oop om jou twee-faktor-verifikasiekode te sien en jou identiteit te verifieer:"
"description": "Maak die ** {mfa_module_name} ** op u toestel oop om u twee-faktor-verifikasiekode te sien en u identiteit te verifieer:"
}
},
"error": {
@ -622,7 +840,10 @@
}
},
"page-onboarding": {
"intro": "Is u gereed om u huis te ontwaak, u privaatheid te herwin en by 'n wêreldwye gemeenskap van peuters aan te sluit?",
"user": {
"intro": "Kom ons begin deur 'n gebruikers rekening te skep.",
"required_field": "Vereis",
"data": {
"name": "Naam",
"username": "Gebruikersnaam",
@ -631,46 +852,11 @@
},
"create_account": "Skep Rekening",
"error": {
"required_fields": "Vul al die vereiste velde in",
"password_not_match": "Wagwoorde stem nie ooreen nie"
}
}
},
"profile": {
"refresh_tokens": {
"header": "Verfris-tekseenhede",
"description": "Elke verfris-tekseenheid verteenwoordig 'n aanmeldingssessie. Verfris-tekseenhede sal outomaties verwyder word wanneer u op meld af klik. Die volgende verfris-tekseenhede is tans aktief vir u rekening.",
"token_title": "Verfris-tekseenheid vir {clientId}",
"created_at": "Geskep op {date}",
"confirm_delete": "Is jy seker jy wil die verfris-tekseenheid vir {name} skrap?",
"delete_failed": "Het misluk om die verfris-tekseenheid te skrap.",
"last_used": "Laas gebruik op {date} vanaf {location}",
"not_used": "Is nog nooit gebruik nie",
"current_token_tooltip": "Nie in staat om huidige verfris-tekseenheid te skrap nie"
},
"long_lived_access_tokens": {
"header": "Langlewende-toegangs-tekseenhede",
"description": "Skep langlewende-toegangs-tekseenhede om jou skripte in staat te stel om met jou Home Assistant-instansie te kommunikeer. Elke tekseenheid sal geldig wees vir 10 jaar vanaf die skepping. Die volgende langlewende-toegangs-tekseenheid is tans aktief.",
"learn_auth_requests": "Leer hoe om geverifieerde versoeke te maak.",
"created_at": "Geskep op {date}",
"confirm_delete": "Is jy seker jy wil die toegangs-tekseenheid vir {name} skrap?",
"delete_failed": "Het misluk om die toegangs-tekseenheid te skrap.",
"create": "Skep Tekseenheid",
"create_failed": "Het misluk om die toegangs-tekseenheid te maak.",
"prompt_name": "Naam?",
"prompt_copy_token": "Kopieer jou toegangs-tekseenheid. Dit sal nie weer gewys word nie.",
"empty_state": "Jy het nog geen langlewende-toegangs-tekseenhede nie.",
"last_used": "Laas gebruik op {date} vanaf {location}",
"not_used": "Is nog nooit gebruik nie"
},
"logout": "Meld af",
"change_password": {
"header": "Verander Wagwoord",
"current_password": "Huidige Wagwoord",
"new_password": "Nuwe Wagwoord",
"confirm_new_password": "Bevestig Nuwe Wagwoord",
"submit": "Dien in"
}
},
"lovelace": {
"cards": {
"shopping-list": {
@ -680,53 +866,53 @@
},
"empty_state": {
"title": "Welkom tuis",
"no_devices": "Hierdie bladsy laat jou toe om jou toestelle te beheer, maar dit lyk asof jy nog nie toestelle opgestel het nie. Gaan na die integrasies bladsy om te begin.",
"no_devices": "Hierdie bladsy laat u toe om u toestelle te beheer, maar dit lyk asof u nog nie toestelle opgestel het nie. Gaan na die integrasies bladsy om te begin.",
"go_to_integrations_page": "Gaan na die integrasies bladsy."
}
},
"editor": {
"edit_card": {
"header": "Kaartkonfigurasie",
"header": "Kaart opstelling",
"save": "Stoor",
"toggle_editor": "Wissel redigeerder",
"pick_card": "Kies die kaart wat jy wil byvoeg.",
"pick_card": "Kies die kaart wat u wil byvoeg.",
"add": "Voeg Kaart by",
"edit": "Wysig",
"delete": "Skrap",
"move": "Skuif"
},
"migrate": {
"header": "Konfigurasie Onversoenbaar",
"header": "Opstellings Onversoenbaar",
"para_no_id": "Hierdie element het nie 'n ID nie. Voeg asseblief 'n ID by vir hierdie element in 'ui-lovelace.yaml'.",
"para_migrate": "Druk die 'Migreer konfigurasie' knoppie as jy wil hê Home Assistant moet vir jou ID's by al jou kaarte en aansigte outomaties byvoeg.",
"migrate": "Migreer konfigurasie"
"para_migrate": "Druk die 'Migreer opstellings' knoppie as u wil hê Home Assistant moet vir u ID's by al u kaarte en aansigte outomaties byvoeg.",
"migrate": "Migreer opstellings"
},
"header": "Wysig gebruikerskoppelvlak",
"edit_view": {
"header": "Bekyk konfigurasie",
"header": "Bekyk Opstellings",
"add": "Voeg aansig by",
"edit": "Wysig aansig",
"delete": "Skrap aansig"
},
"save_config": {
"header": "Neem beheer van jou Lovelace gebruikerskoppelvlak",
"header": "Neem beheer van u Lovelace gebruikerskoppelvlak",
"para": "Home Assistant sal outomaties u gebruikerskoppelvlak handhaaf, dit opdateer wanneer nuwe entiteite of Lovelace-komponente beskikbaar raak. Alhoewel, as u beheer neem, sal Home Assistant nie meer outomaties veranderings vir u doen nie.",
"para_sure": "Is jy seker jy wil beheer neem oor jou gebruikerskoppelvlak?",
"para_sure": "Is u seker u wil beheer neem oor u gebruikerskoppelvlak?",
"cancel": "Toemaar",
"save": "Neem beheer"
},
"menu": {
"raw_editor": "Plat konfigurasie-redigeerder"
"raw_editor": "Plat opstellings-redigeerder"
},
"raw_editor": {
"header": "Wysig Konfigurasie",
"header": "Wysig Opstellings",
"save": "Stoor",
"unsaved_changes": "Ongestoorde veranderinge",
"saved": "Gestoor"
}
},
"menu": {
"configure_ui": "Konfigureer gebruikerskoppelvlak",
"configure_ui": "Stel gebruikerskoppelvlak op",
"unused_entities": "Ongebruikte entiteite",
"help": "Help",
"refresh": "Verfris"
@ -735,15 +921,17 @@
"entity_not_found": "Entiteit nie beskikbaar nie: {entity}",
"entity_non_numeric": "Entiteit is nie-numeriese: {entity}"
}
},
"logbook": {
"period": "Tydperk"
}
},
"sidebar": {
"log_out": "Meld af",
"developer_tools": "Ontwikkelaar gereedskap"
},
"common": {
"loading": "Laai tans",
"cancel": "Kanselleer",
"save": "Stoor"
},
"duration": {
"day": "{count} {count, plural,\n one {dag}\n other {dae}\n}",
"week": "{count} {count, plural,\n one {week}\n other {weke}\n}",
@ -752,9 +940,17 @@
"hour": "{count} {count, plural,\n one {uur}\n other {ure}\n}"
},
"login-form": {
"password": "Wagwoord"
"password": "Wagwoord",
"remember": "Onthou",
"log_in": "Meld aan"
},
"card": {
"camera": {
"not_available": "Beeld nie beskikbaar nie"
},
"persistent_notification": {
"dismiss": "Ontslaan"
},
"scene": {
"activate": "Aktiveer"
},
@ -839,6 +1035,15 @@
"lock": "Sluit toe",
"unlock": "Sluit oop"
},
"vacuum": {
"actions": {
"resume_cleaning": "Hervat stofsuig",
"return_to_base": "Keer terug na die hawe",
"start_cleaning": "Begin stofsuig",
"turn_on": "Skakel aan",
"turn_off": "Skakel af"
}
},
"water_heater": {
"currently": "Tans",
"on_off": "Aan \/ af",
@ -848,6 +1053,14 @@
}
},
"components": {
"entity": {
"entity-picker": {
"entity": "Entiteit"
}
},
"service-picker": {
"service": "Diens"
},
"relative_time": {
"past": "{time} gelede",
"future": "In {time}",
@ -892,8 +1105,15 @@
}
}
},
"common": {
"save": "Stoor"
"auth_store": {
"ask": "Wil u hierdie aanmelding stoor?",
"decline": "Nee dankie",
"confirm": "Stoor aanmelding"
},
"notification_drawer": {
"click_to_configure": "Klik knoppie om {entity} op te stel",
"empty": "Geen kennisgewings",
"title": "Kennisgewings"
}
},
"domain": {
@ -919,6 +1139,12 @@
"light": "Lig",
"lock": "Slot",
"mailbox": "Posbus",
"media_player": "Media-speler",
"notify": "Stel in kennis",
"plant": "Plant",
"proximity": "Nabyheid",
"remote": "Afgeleë",
"scene": "Toneel",
"script": "Skrip",
"sensor": "Sensor",
"sun": "Son",
@ -934,6 +1160,13 @@
"system_health": "Stelsel Gesondheid",
"person": "Persoon"
},
"attribute": {
"weather": {
"humidity": "Humiditeit",
"visibility": "Sigbaarheid",
"wind_speed": "Wind spoed"
}
},
"state_attributes": {
"climate": {
"fan_mode": {

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Home Assistant Cloud",
"description_login": "Sessió iniciada com a {email}",
"description_not_login": "No has iniciat sessió"
"description_not_login": "No has iniciat sessió",
"description_features": "Controla la casa des de fora, pots integrar Alexa i Google Assistant."
},
"integrations": {
"caption": "Integracions",
@ -849,7 +850,7 @@
"password": "Contrasenya",
"password_confirm": "Confirma la contrasenya"
},
"create_account": "Crear un compte",
"create_account": "Crear compte",
"error": {
"required_fields": "Omple tots els camps obligatoris",
"password_not_match": "Les contrasenyes no coincideixen"
@ -865,7 +866,7 @@
},
"empty_state": {
"title": "Benvingut\/da a casa",
"no_devices": "Aquesta pàgina et permet controlar els teus dispositius, però sembla que encara no en tens cap configurat. Vés a la pàgina d'integracions per començar.",
"no_devices": "Aquesta pàgina et permet controlar els teus dispositius, però sembla que encara no en tens cap configurat. Vés a la pàgina d'integracions per a començar.",
"go_to_integrations_page": "Vés a la pàgina d'integracions."
}
},

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Home Assistant Cloud",
"description_login": "Angemeldet als {email}",
"description_not_login": "Nicht angemeldet"
"description_not_login": "Nicht angemeldet",
"description_features": "Fernsteuerung und Integration mit Alexa und Google Assistant."
},
"integrations": {
"caption": "Integrationen",

View File

@ -237,7 +237,7 @@
},
"query_stage": {
"initializing": "Αρχικοποίηση ({query_stage})",
"dead": "νεκρός ({query_stage})"
"dead": "Νεκρός ({query_stage})"
}
},
"weather": {
@ -279,7 +279,9 @@
"state_badge": {
"default": {
"unknown": "Άγν",
"unavailable": "Μη Διαθ"
"unavailable": "Μη Διαθ",
"error": "Σφάλμα",
"entity_not_found": "Η οντότητα δεν βρέθηκε"
},
"alarm_control_panel": {
"armed": "Οπλισμένο",
@ -579,7 +581,8 @@
"cloud": {
"caption": "Σύννεφο Home Assistant",
"description_login": "Συνδεδεμένος ως {e-mail}",
"description_not_login": "Μη συνδεδεμένος"
"description_not_login": "Μη συνδεδεμένος",
"description_features": "Έλεγχος εκτός σπιτιού, ενσωμάτωση με τα Alexa και Google Assistant"
},
"integrations": {
"caption": "Ενσωματώσεις",
@ -607,7 +610,18 @@
"description": "Διαχείριση του δικτύου ZigBee Home Automation",
"services": {
"reconfigure": "Ρυθμίστε ξανά τη συσκευή ZHA (heal συσκευή). Χρησιμοποιήστε αυτήν την επιλογή εάν αντιμετωπίζετε ζητήματα με τη συσκευή. Εάν η συγκεκριμένη συσκευή τροφοδοτείται απο μπαταρία βεβαιωθείτε ότι είναι ενεργοποιημένη και δέχεται εντολές όταν χρησιμοποιείτε αυτή την υπηρεσία.",
"updateDeviceName": "Ορίστε ένα προσαρμοσμένο όνομα γι αυτήν τη συσκευή στο μητρώο συσκευών."
"updateDeviceName": "Ορίστε ένα προσαρμοσμένο όνομα γι αυτήν τη συσκευή στο μητρώο συσκευών.",
"remove": "Καταργήστε μια συσκευή από το δίκτυο ZigBee."
},
"device_card": {
"device_name_placeholder": "Όνομα χρήστη",
"area_picker_label": "Περιοχή",
"update_name_button": "Ενημέρωση ονόματος"
},
"add_device_page": {
"header": "Zigbee Home Automation - Προσθήκη Συσκευών",
"spinner": "Αναζήτηση συσκευών ZHA Zigbee ...",
"discovery_text": "Οι ανακαλυφθείσες συσκευές θα εμφανιστούν εδώ. Ακολουθήστε τις οδηγίες για τις συσκευές σας και τοποθετήστε τις συσκευές στη λειτουργία αντιστοίχισης."
}
},
"area_registry": {

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Home Assistant Cloud",
"description_login": "Logged in as {email}",
"description_not_login": "Not logged in"
"description_not_login": "Not logged in",
"description_features": "Control away from home, integrate with Alexa and Google Assistant."
},
"integrations": {
"caption": "Integrations",

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Nube Home Assistant",
"description_login": "Ha iniciado sesión como {email}",
"description_not_login": "No ha iniciado sesión"
"description_not_login": "No ha iniciado sesión",
"description_features": "Control fuera de casa, integre con Alexa y con Google Assistant."
},
"integrations": {
"caption": "Integraciones",

View File

@ -506,7 +506,7 @@
},
"zone": {
"label": "Zona",
"entity": "Entidad con la ubicación",
"entity": "Entidad con la ubicación",
"zone": "Zona"
}
},
@ -581,7 +581,8 @@
"cloud": {
"caption": "Nube Home Assistant",
"description_login": "Ha iniciado sesión como {email}",
"description_not_login": "No ha iniciado sesión"
"description_not_login": "No ha iniciado sesión",
"description_features": "Control fuera de casa, integración con Alexa y Google Assistant."
},
"integrations": {
"caption": "Integraciones",

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Home Assistant Cloud",
"description_login": "Connesso come {email}",
"description_not_login": "Accesso non effettuato"
"description_not_login": "Accesso non effettuato",
"description_features": "Controllo fuori casa, integrazione con Alexa e Google Assistant."
},
"integrations": {
"caption": "Integrazioni",

View File

@ -324,6 +324,9 @@
"deactivate_user": "ユーザーを無効化",
"delete_user": "ユーザーを削除"
}
},
"cloud": {
"description_features": "自宅の外からコントロールするために、AlexaとGoogleアシスタントに統合します。"
}
}
},

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Home Assistant Cloud",
"description_login": "{email} 로 로그인 되어있습니다",
"description_not_login": "로그인이 되어있지 않습니다"
"description_not_login": "로그인이 되어있지 않습니다",
"description_features": "Alexa 및 Google Assistant 를 통해 집 밖에서도 집을 관리합니다."
},
"integrations": {
"caption": "통합 구성요소",

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Home Assistant Cloud",
"description_login": "Ageloggt als {email}",
"description_not_login": "Net ageloggt"
"description_not_login": "Net ageloggt",
"description_features": "Steiert vun ënnerwee aus, integréiert mam Alexa an Google Assistant."
},
"integrations": {
"caption": "Integratiounen",

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Home Assistant Cloud",
"description_login": "Logget inn som {email}",
"description_not_login": "Ikke pålogget"
"description_not_login": "Ikke pålogget",
"description_features": "Kontroller bortefra hjemmet, integrere med Alexa og Google Assistant."
},
"integrations": {
"caption": "Integrasjoner",

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Home Assistent Cloud",
"description_login": "Ingelogd als {email}",
"description_not_login": "Niet ingelogd"
"description_not_login": "Niet ingelogd",
"description_features": "Bestuur weg van huis, verbind met Alexa en Google Assistant."
},
"integrations": {
"caption": "Integraties",

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Chmura Home Assistant",
"description_login": "Zalogowany jako {email}",
"description_not_login": "Nie zalogowany"
"description_not_login": "Nie zalogowany",
"description_features": "Sterowanie z spoza domu, integracja z Alexą i Google Assistant."
},
"integrations": {
"caption": "Integracje",

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Home Assistant Cloud",
"description_login": "Выполнен вход с учетной записью {email}",
"description_not_login": "Вход не выполнен"
"description_not_login": "Вход не выполнен",
"description_features": "Управление вдали от дома, интеграция с Alexa и Google Assistant."
},
"integrations": {
"caption": "Интеграции",

View File

@ -581,7 +581,8 @@
"cloud": {
"caption": "Home Assistant Cloud",
"description_login": "登入帳號:{email}",
"description_not_login": "未登入"
"description_not_login": "未登入",
"description_features": "整合 Alexa 及 Google 助理,遠端控制智能家居。"
},
"integrations": {
"caption": "整合",