diff --git a/pyproject.toml b/pyproject.toml index bd5d34eab8..14b67e7416 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20221003.0" +version = "20221004.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/components/ha-web-rtc-player.ts b/src/components/ha-web-rtc-player.ts index 3987ff918c..0e7d31bc3a 100644 --- a/src/components/ha-web-rtc-player.ts +++ b/src/components/ha-web-rtc-player.ts @@ -7,7 +7,9 @@ import { TemplateResult, } from "lit"; import { customElement, property, state, query } from "lit/decorators"; +import { isComponentLoaded } from "../common/config/is_component_loaded"; import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera"; +import { fetchWebRtcSettings } from "../data/rtsp_to_webrtc"; import type { HomeAssistant } from "../types"; import "./ha-alert"; @@ -86,7 +88,8 @@ class HaWebRtcPlayer extends LitElement { private async _startWebRtc(): Promise { 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 // however, not used by any integrations. peerConnection.createDataChannel("dataSendChannel"); @@ -102,12 +105,25 @@ class HaWebRtcPlayer extends LitElement { ); await peerConnection.setLocalDescription(offer); + let candidates = ""; // Build an Offer SDP string with ice candidates + const iceResolver = new Promise((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; try { webRtcAnswer = await handleWebRtcOffer( this.hass, this.entityid, - offer.sdp! + offer_sdp ); } catch (err: any) { this._error = "Failed to start WebRTC stream: " + err.message; @@ -138,6 +154,23 @@ class HaWebRtcPlayer extends LitElement { this._peerConnection = peerConnection; } + private async _fetchPeerConfiguration(): Promise { + 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() { if (this._remoteStream) { this._remoteStream.getTracks().forEach((track) => { diff --git a/src/data/rtsp_to_webrtc.ts b/src/data/rtsp_to_webrtc.ts new file mode 100644 index 0000000000..777d7147e4 --- /dev/null +++ b/src/data/rtsp_to_webrtc.ts @@ -0,0 +1,10 @@ +import { HomeAssistant } from "../types"; + +export interface WebRtcSettings { + stun_server?: string; +} + +export const fetchWebRtcSettings = async (hass: HomeAssistant) => + hass.callWS({ + type: "rtsp_to_webrtc/get_settings", + }); diff --git a/src/data/zwave_js.ts b/src/data/zwave_js.ts index 7863c3de84..336c9824b6 100644 --- a/src/data/zwave_js.ts +++ b/src/data/zwave_js.ts @@ -306,14 +306,19 @@ export interface ZWaveJSNodeStatusUpdatedMessage { export interface ZWaveJSNodeFirmwareUpdateProgressMessage { event: "firmware update progress"; + current_file: number; + total_files: number; sent_fragments: number; total_fragments: number; + progress: number; } export interface ZWaveJSNodeFirmwareUpdateFinishedMessage { event: "firmware update finished"; status: FirmwareUpdateStatus; - wait_time: number; + success: boolean; + wait_time?: number; + reinterview: boolean; } export type ZWaveJSNodeFirmwareUpdateCapabilities = diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index 00c0f99a4d..bf3cbacac8 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -639,21 +639,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { return [ haStyle, 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 { margin: 0; padding: 0 16px; diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 66cb64e64a..2c8d9d3afa 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -14,6 +14,7 @@ import "@polymer/paper-tooltip/paper-tooltip"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { differenceInDays } from "date-fns/esm"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { formatShortDateTime } from "../../../common/datetime/format_date_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 { showNewAutomationDialog } from "./show-dialog-new-automation"; -const DAY_IN_MILLISECONDS = 86400000; - @customElement("ha-automation-picker") class HaAutomationPicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -109,16 +108,13 @@ class HaAutomationPicker extends LitElement { ? (name, automation: any) => { const date = new Date(automation.attributes.last_triggered); const now = new Date(); - - const diff = now.getTime() - date.getTime(); - const dayDiff = diff / DAY_IN_MILLISECONDS; - + const dayDifference = differenceInDays(now, date); return html` ${name}
${this.hass.localize("ui.card.automation.last_triggered")}: ${automation.attributes.last_triggered - ? dayDiff > 3 + ? dayDifference > 3 ? formatShortDateTime(date, this.hass.locale) : relativeTime(date, this.hass.locale) : this.hass.localize("ui.components.relative_time.never")} @@ -136,13 +132,10 @@ class HaAutomationPicker extends LitElement { template: (last_triggered) => { const date = new Date(last_triggered); const now = new Date(); - - const diff = now.getTime() - date.getTime(); - const dayDiff = diff / DAY_IN_MILLISECONDS; - + const dayDifference = differenceInDays(now, date); return html` ${last_triggered - ? dayDiff > 3 + ? dayDifference > 3 ? formatShortDateTime(date, this.hass.locale) : relativeTime(date, this.hass.locale) : this.hass.localize("ui.components.relative_time.never")} diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts index c2b6837169..a775b05506 100644 --- a/src/panels/config/integrations/dialog-add-integration.ts +++ b/src/panels/config/integrations/dialog-add-integration.ts @@ -187,12 +187,7 @@ class AddIntegrationDialog extends LitElement { for (const [domain, domainBrands] of Object.entries(sb)) { const integration = this._findIntegration(domain); - if ( - !integration || - (!integration.config_flow && - !integration.iot_standards && - !integration.integrations) - ) { + if (!integration) { continue; } for (const [slug, name] of Object.entries(domainBrands)) { diff --git a/src/panels/config/integrations/ha-domain-integrations.ts b/src/panels/config/integrations/ha-domain-integrations.ts index 610414d77f..45a2716ac0 100644 --- a/src/panels/config/integrations/ha-domain-integrations.ts +++ b/src/panels/config/integrations/ha-domain-integrations.ts @@ -36,7 +36,9 @@ class HaDomainIntegrations extends LitElement { protected render() { return html` ${this.flowsInProgress?.length - ? html`

We discovered the following:

+ ? html`

+ ${this.hass.localize("ui.panel.config.integrations.discovered")} +

${this.flowsInProgress.map( (flow) => html` ` - )}` + )} +
  • + ${this.integration?.integrations + ? html`

    + ${this.hass.localize( + "ui.panel.config.integrations.available_integrations" + )} +

    ` + : ""}` : ""} ${this.integration?.iot_standards - ? this.integration.iot_standards.map((standard) => { - const domain: string = standardToDomain[standard] || standard; - return html` - - ${this.hass.localize( - `ui.panel.config.integrations.add_${domain}_device` - )} standard in standardToDomain) + .map((standard) => { + const domain: string = standardToDomain[standard]; + return html` - - `; - }) + + ${this.hass.localize( + `ui.panel.config.integrations.add_${domain}_device` + )} + + `; + }) : ""} ${this.integration?.integrations ? Object.entries(this.integration.integrations) @@ -155,9 +167,11 @@ class HaDomainIntegrations extends LitElement { @click=${this._integrationPicked} hasMeta > - Setup another instance of - ${this.integration.name || - domainToName(this.hass.localize, this.domain)} + ${this.hass.localize("ui.panel.config.integrations.new_flow", { + integration: + this.integration.name || + domainToName(this.hass.localize, this.domain), + })} ` : html` html` - ${last_activated && !UNAVAILABLE_STATES.includes(last_activated) - ? formatDateTime(new Date(last_activated), this.hass.locale) - : this.hass.localize("ui.components.relative_time.never")} - `, + template: (last_activated) => { + const date = new Date(last_activated); + const now = new Date(); + 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 = { diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 97ef655d18..73a7eaa762 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -11,7 +11,9 @@ import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; 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 { computeStateName } from "../../../common/entity/compute_state_name"; import { navigate } from "../../../common/navigate"; @@ -99,18 +101,22 @@ class HaScriptPicker extends LitElement { direction: "asc", grows: true, template: narrow - ? (name, script: any) => html` - ${name} -
    - ${this.hass.localize("ui.card.automation.last_triggered")}: - ${script.attributes.last_triggered - ? formatDateTime( - new Date(script.attributes.last_triggered), - this.hass.locale - ) - : this.hass.localize("ui.components.relative_time.never")} -
    - ` + ? (name, script: any) => { + const date = new Date(script.attributes.last_triggered); + const now = new Date(); + const dayDifference = differenceInDays(now, date); + return html` + ${name} +
    + ${this.hass.localize("ui.card.automation.last_triggered")}: + ${script.attributes.last_triggered + ? dayDifference > 3 + ? formatShortDateTime(date, this.hass.locale) + : relativeTime(date, this.hass.locale) + : this.hass.localize("ui.components.relative_time.never")} +
    + `; + } : undefined, }, }; @@ -119,11 +125,18 @@ class HaScriptPicker extends LitElement { sortable: true, width: "40%", title: this.hass.localize("ui.card.automation.last_triggered"), - template: (last_triggered) => html` - ${last_triggered - ? formatDateTime(new Date(last_triggered), this.hass.locale) - : this.hass.localize("ui.components.relative_time.never")} - `, + template: (last_triggered) => { + const date = new Date(last_triggered); + const now = new Date(); + 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")} + `; + }, }; } diff --git a/src/translations/en.json b/src/translations/en.json index 4bc1820a41..4494ffae8a 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2850,6 +2850,8 @@ "description": "Manage integrations with services or devices", "integration": "integration", "discovered": "Discovered", + "available_integrations": "Available integrations", + "new_flow": "Setup another instance of {integration}", "attention": "Attention required", "configured": "Configured", "new": "Select brand", @@ -2973,12 +2975,8 @@ "description": "This step requires you to visit an external website to be completed.", "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_flow": "Please wait while {integration} is being setup", + "loading_flow": "Please wait, starting configuration wizard for {integration}", "loading_step": "Loading next step for {integration}", "fallback_title": "the integration" }, diff --git a/src/translations/translationMetadata.json b/src/translations/translationMetadata.json index 285d9265d8..2368f41fa3 100644 --- a/src/translations/translationMetadata.json +++ b/src/translations/translationMetadata.json @@ -116,6 +116,9 @@ "lv": { "nativeName": "Latviešu" }, + "ml": { + "nativeName": "മലയാളം" + }, "nl": { "nativeName": "Nederlands" },