20221004.0 (#13987)

This commit is contained in:
Bram Kragten 2022-10-04 17:09:56 +02:00 committed by GitHub
commit 4dce9404a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 168 additions and 101 deletions

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20221003.0" version = "20221004.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -7,7 +7,9 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state, query } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera"; import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera";
import { fetchWebRtcSettings } from "../data/rtsp_to_webrtc";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-alert"; import "./ha-alert";
@ -86,7 +88,8 @@ class HaWebRtcPlayer extends LitElement {
private async _startWebRtc(): Promise<void> { private async _startWebRtc(): Promise<void> {
this._error = undefined; this._error = undefined;
const peerConnection = new RTCPeerConnection(); const configuration = await this._fetchPeerConfiguration();
const peerConnection = new RTCPeerConnection(configuration);
// Some cameras (such as nest) require a data channel to establish a stream // Some cameras (such as nest) require a data channel to establish a stream
// however, not used by any integrations. // however, not used by any integrations.
peerConnection.createDataChannel("dataSendChannel"); peerConnection.createDataChannel("dataSendChannel");
@ -102,12 +105,25 @@ class HaWebRtcPlayer extends LitElement {
); );
await peerConnection.setLocalDescription(offer); await peerConnection.setLocalDescription(offer);
let candidates = ""; // Build an Offer SDP string with ice candidates
const iceResolver = new Promise<void>((resolve) => {
peerConnection.addEventListener("icecandidate", async (event) => {
if (!event.candidate) {
resolve(); // Gathering complete
return;
}
candidates += `a=${event.candidate.candidate}\r\n`;
});
});
await iceResolver;
const offer_sdp = offer.sdp! + candidates;
let webRtcAnswer: WebRtcAnswer; let webRtcAnswer: WebRtcAnswer;
try { try {
webRtcAnswer = await handleWebRtcOffer( webRtcAnswer = await handleWebRtcOffer(
this.hass, this.hass,
this.entityid, this.entityid,
offer.sdp! offer_sdp
); );
} catch (err: any) { } catch (err: any) {
this._error = "Failed to start WebRTC stream: " + err.message; this._error = "Failed to start WebRTC stream: " + err.message;
@ -138,6 +154,23 @@ class HaWebRtcPlayer extends LitElement {
this._peerConnection = peerConnection; this._peerConnection = peerConnection;
} }
private async _fetchPeerConfiguration(): Promise<RTCConfiguration> {
if (!isComponentLoaded(this.hass!, "rtsp_to_webrtc")) {
return {};
}
const settings = await fetchWebRtcSettings(this.hass!);
if (!settings || !settings.stun_server) {
return {};
}
return {
iceServers: [
{
urls: [`stun:${settings.stun_server!}`],
},
],
};
}
private _cleanUp() { private _cleanUp() {
if (this._remoteStream) { if (this._remoteStream) {
this._remoteStream.getTracks().forEach((track) => { this._remoteStream.getTracks().forEach((track) => {

View File

@ -0,0 +1,10 @@
import { HomeAssistant } from "../types";
export interface WebRtcSettings {
stun_server?: string;
}
export const fetchWebRtcSettings = async (hass: HomeAssistant) =>
hass.callWS<WebRtcSettings>({
type: "rtsp_to_webrtc/get_settings",
});

View File

@ -306,14 +306,19 @@ export interface ZWaveJSNodeStatusUpdatedMessage {
export interface ZWaveJSNodeFirmwareUpdateProgressMessage { export interface ZWaveJSNodeFirmwareUpdateProgressMessage {
event: "firmware update progress"; event: "firmware update progress";
current_file: number;
total_files: number;
sent_fragments: number; sent_fragments: number;
total_fragments: number; total_fragments: number;
progress: number;
} }
export interface ZWaveJSNodeFirmwareUpdateFinishedMessage { export interface ZWaveJSNodeFirmwareUpdateFinishedMessage {
event: "firmware update finished"; event: "firmware update finished";
status: FirmwareUpdateStatus; status: FirmwareUpdateStatus;
wait_time: number; success: boolean;
wait_time?: number;
reinterview: boolean;
} }
export type ZWaveJSNodeFirmwareUpdateCapabilities = export type ZWaveJSNodeFirmwareUpdateCapabilities =

View File

@ -639,21 +639,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
return [ return [
haStyle, haStyle,
css` css`
h1 {
margin: 0;
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
opacity: var(--dark-primary-opacity);
display: flex;
align-items: center;
}
h3 { h3 {
margin: 0; margin: 0;
padding: 0 16px; padding: 0 16px;

View File

@ -14,6 +14,7 @@ import "@polymer/paper-tooltip/paper-tooltip";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { differenceInDays } from "date-fns/esm";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatShortDateTime } from "../../../common/datetime/format_date_time"; import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time"; import { relativeTime } from "../../../common/datetime/relative_time";
@ -50,8 +51,6 @@ import { documentationUrl } from "../../../util/documentation-url";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import { showNewAutomationDialog } from "./show-dialog-new-automation"; import { showNewAutomationDialog } from "./show-dialog-new-automation";
const DAY_IN_MILLISECONDS = 86400000;
@customElement("ha-automation-picker") @customElement("ha-automation-picker")
class HaAutomationPicker extends LitElement { class HaAutomationPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -109,16 +108,13 @@ class HaAutomationPicker extends LitElement {
? (name, automation: any) => { ? (name, automation: any) => {
const date = new Date(automation.attributes.last_triggered); const date = new Date(automation.attributes.last_triggered);
const now = new Date(); const now = new Date();
const dayDifference = differenceInDays(now, date);
const diff = now.getTime() - date.getTime();
const dayDiff = diff / DAY_IN_MILLISECONDS;
return html` return html`
${name} ${name}
<div class="secondary"> <div class="secondary">
${this.hass.localize("ui.card.automation.last_triggered")}: ${this.hass.localize("ui.card.automation.last_triggered")}:
${automation.attributes.last_triggered ${automation.attributes.last_triggered
? dayDiff > 3 ? dayDifference > 3
? formatShortDateTime(date, this.hass.locale) ? formatShortDateTime(date, this.hass.locale)
: relativeTime(date, this.hass.locale) : relativeTime(date, this.hass.locale)
: this.hass.localize("ui.components.relative_time.never")} : this.hass.localize("ui.components.relative_time.never")}
@ -136,13 +132,10 @@ class HaAutomationPicker extends LitElement {
template: (last_triggered) => { template: (last_triggered) => {
const date = new Date(last_triggered); const date = new Date(last_triggered);
const now = new Date(); const now = new Date();
const dayDifference = differenceInDays(now, date);
const diff = now.getTime() - date.getTime();
const dayDiff = diff / DAY_IN_MILLISECONDS;
return html` return html`
${last_triggered ${last_triggered
? dayDiff > 3 ? dayDifference > 3
? formatShortDateTime(date, this.hass.locale) ? formatShortDateTime(date, this.hass.locale)
: relativeTime(date, this.hass.locale) : relativeTime(date, this.hass.locale)
: this.hass.localize("ui.components.relative_time.never")} : this.hass.localize("ui.components.relative_time.never")}

View File

@ -187,12 +187,7 @@ class AddIntegrationDialog extends LitElement {
for (const [domain, domainBrands] of Object.entries(sb)) { for (const [domain, domainBrands] of Object.entries(sb)) {
const integration = this._findIntegration(domain); const integration = this._findIntegration(domain);
if ( if (!integration) {
!integration ||
(!integration.config_flow &&
!integration.iot_standards &&
!integration.integrations)
) {
continue; continue;
} }
for (const [slug, name] of Object.entries(domainBrands)) { for (const [slug, name] of Object.entries(domainBrands)) {

View File

@ -36,7 +36,9 @@ class HaDomainIntegrations extends LitElement {
protected render() { protected render() {
return html`<mwc-list> return html`<mwc-list>
${this.flowsInProgress?.length ${this.flowsInProgress?.length
? html`<h3>We discovered the following:</h3> ? html`<h3>
${this.hass.localize("ui.panel.config.integrations.discovered")}
</h3>
${this.flowsInProgress.map( ${this.flowsInProgress.map(
(flow) => html`<mwc-list-item (flow) => html`<mwc-list-item
graphic="medium" graphic="medium"
@ -60,36 +62,46 @@ class HaDomainIntegrations extends LitElement {
> >
<ha-icon-next slot="meta"></ha-icon-next> <ha-icon-next slot="meta"></ha-icon-next>
</mwc-list-item>` </mwc-list-item>`
)}` )}
<li divider role="separator"></li>
${this.integration?.integrations
? html`<h3>
${this.hass.localize(
"ui.panel.config.integrations.available_integrations"
)}
</h3>`
: ""}`
: ""} : ""}
${this.integration?.iot_standards ${this.integration?.iot_standards
? this.integration.iot_standards.map((standard) => { ? this.integration.iot_standards
const domain: string = standardToDomain[standard] || standard; .filter((standard) => standard in standardToDomain)
return html`<mwc-list-item .map((standard) => {
graphic="medium" const domain: string = standardToDomain[standard];
.domain=${domain} return html`<mwc-list-item
@click=${this._standardPicked} graphic="medium"
hasMeta .domain=${domain}
> @click=${this._standardPicked}
<img hasMeta
slot="graphic"
loading="lazy"
src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>
<span
>${this.hass.localize(
`ui.panel.config.integrations.add_${domain}_device`
)}</span
> >
<ha-icon-next slot="meta"></ha-icon-next> <img
</mwc-list-item>`; slot="graphic"
}) loading="lazy"
src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>
<span
>${this.hass.localize(
`ui.panel.config.integrations.add_${domain}_device`
)}</span
>
<ha-icon-next slot="meta"></ha-icon-next>
</mwc-list-item>`;
})
: ""} : ""}
${this.integration?.integrations ${this.integration?.integrations
? Object.entries(this.integration.integrations) ? Object.entries(this.integration.integrations)
@ -155,9 +167,11 @@ class HaDomainIntegrations extends LitElement {
@click=${this._integrationPicked} @click=${this._integrationPicked}
hasMeta hasMeta
> >
Setup another instance of ${this.hass.localize("ui.panel.config.integrations.new_flow", {
${this.integration.name || integration:
domainToName(this.hass.localize, this.domain)} this.integration.name ||
domainToName(this.hass.localize, this.domain),
})}
<ha-icon-next slot="meta"></ha-icon-next> <ha-icon-next slot="meta"></ha-icon-next>
</mwc-list-item>` </mwc-list-item>`
: html`<ha-integration-list-item : html`<ha-integration-list-item
@ -272,16 +286,25 @@ class HaDomainIntegrations extends LitElement {
css` css`
:host { :host {
display: block; display: block;
--mdc-list-item-graphic-size: 40px;
--mdc-list-side-padding: 24px;
} }
h3 { h3 {
margin: 0 24px; margin: 8px 24px 0;
color: var(--primary-text-color); color: var(--secondary-text-color);
font-size: 14px; font-size: 14px;
font-weight: 500;
}
h3:first-of-type {
margin-top: 0;
} }
img { img {
width: 40px; width: 40px;
height: 40px; height: 40px;
} }
li[divider] {
margin-top: 8px;
}
`, `,
]; ];
} }

View File

@ -104,8 +104,8 @@ export class HaIntegrationListItem extends ListItemBase {
styles, styles,
css` css`
:host { :host {
padding-left: var(--mdc-list-side-padding, 20px); --mdc-list-side-padding: 24px;
padding-right: var(--mdc-list-side-padding, 20px); --mdc-list-item-graphic-size: 40px;
} }
:host([graphic="avatar"]:not([twoLine])), :host([graphic="avatar"]:not([twoLine])),
:host([graphic="icon"]:not([twoLine])) { :host([graphic="icon"]:not([twoLine])) {

View File

@ -11,6 +11,7 @@ import "@polymer/paper-tooltip/paper-tooltip";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { differenceInDays } from "date-fns/esm";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
@ -42,7 +43,8 @@ import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import { formatDateTime } from "../../../common/datetime/format_date_time"; import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
import { UNAVAILABLE_STATES } from "../../../data/entity"; import { UNAVAILABLE_STATES } from "../../../data/entity";
@customElement("ha-scene-dashboard") @customElement("ha-scene-dashboard")
@ -109,11 +111,18 @@ class HaSceneDashboard extends LitElement {
), ),
sortable: true, sortable: true,
width: "30%", width: "30%",
template: (last_activated) => html` template: (last_activated) => {
${last_activated && !UNAVAILABLE_STATES.includes(last_activated) const date = new Date(last_activated);
? formatDateTime(new Date(last_activated), this.hass.locale) const now = new Date();
: this.hass.localize("ui.components.relative_time.never")} const dayDifference = differenceInDays(now, date);
`, return html`
${last_activated && !UNAVAILABLE_STATES.includes(last_activated)
? dayDifference > 3
? formatShortDateTime(date, this.hass.locale)
: relativeTime(date, this.hass.locale)
: this.hass.localize("ui.components.relative_time.never")}
`;
},
}; };
} }
columns.only_editable = { columns.only_editable = {

View File

@ -11,7 +11,9 @@ import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { formatDateTime } from "../../../common/datetime/format_date_time"; import { differenceInDays } from "date-fns/esm";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
@ -99,18 +101,22 @@ class HaScriptPicker extends LitElement {
direction: "asc", direction: "asc",
grows: true, grows: true,
template: narrow template: narrow
? (name, script: any) => html` ? (name, script: any) => {
${name} const date = new Date(script.attributes.last_triggered);
<div class="secondary"> const now = new Date();
${this.hass.localize("ui.card.automation.last_triggered")}: const dayDifference = differenceInDays(now, date);
${script.attributes.last_triggered return html`
? formatDateTime( ${name}
new Date(script.attributes.last_triggered), <div class="secondary">
this.hass.locale ${this.hass.localize("ui.card.automation.last_triggered")}:
) ${script.attributes.last_triggered
: this.hass.localize("ui.components.relative_time.never")} ? dayDifference > 3
</div> ? formatShortDateTime(date, this.hass.locale)
` : relativeTime(date, this.hass.locale)
: this.hass.localize("ui.components.relative_time.never")}
</div>
`;
}
: undefined, : undefined,
}, },
}; };
@ -119,11 +125,18 @@ class HaScriptPicker extends LitElement {
sortable: true, sortable: true,
width: "40%", width: "40%",
title: this.hass.localize("ui.card.automation.last_triggered"), title: this.hass.localize("ui.card.automation.last_triggered"),
template: (last_triggered) => html` template: (last_triggered) => {
${last_triggered const date = new Date(last_triggered);
? formatDateTime(new Date(last_triggered), this.hass.locale) const now = new Date();
: this.hass.localize("ui.components.relative_time.never")} const dayDifference = differenceInDays(now, date);
`, return html`
${last_triggered
? dayDifference > 3
? formatShortDateTime(date, this.hass.locale)
: relativeTime(date, this.hass.locale)
: this.hass.localize("ui.components.relative_time.never")}
`;
},
}; };
} }

View File

@ -2850,6 +2850,8 @@
"description": "Manage integrations with services or devices", "description": "Manage integrations with services or devices",
"integration": "integration", "integration": "integration",
"discovered": "Discovered", "discovered": "Discovered",
"available_integrations": "Available integrations",
"new_flow": "Setup another instance of {integration}",
"attention": "Attention required", "attention": "Attention required",
"configured": "Configured", "configured": "Configured",
"new": "Select brand", "new": "Select brand",
@ -2973,12 +2975,8 @@
"description": "This step requires you to visit an external website to be completed.", "description": "This step requires you to visit an external website to be completed.",
"open_site": "Open website" "open_site": "Open website"
}, },
"pick_flow_step": {
"title": "We discovered these, want to set them up?",
"new_flow": "No, set up an other instance of {integration}"
},
"loading": { "loading": {
"loading_flow": "Please wait while {integration} is being setup", "loading_flow": "Please wait, starting configuration wizard for {integration}",
"loading_step": "Loading next step for {integration}", "loading_step": "Loading next step for {integration}",
"fallback_title": "the integration" "fallback_title": "the integration"
}, },

View File

@ -116,6 +116,9 @@
"lv": { "lv": {
"nativeName": "Latviešu" "nativeName": "Latviešu"
}, },
"ml": {
"nativeName": "മലയാളം"
},
"nl": { "nl": {
"nativeName": "Nederlands" "nativeName": "Nederlands"
}, },