Skip to content

Commit

Permalink
20241105.0 (#22679)
Browse files Browse the repository at this point in the history
  • Loading branch information
piitaya authored Nov 5, 2024
2 parents 452cfee + 9cdae4f commit a6971d6
Show file tree
Hide file tree
Showing 18 changed files with 330 additions and 164 deletions.
9 changes: 9 additions & 0 deletions build-scripts/gulp/gather-static.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ function copyMapPanel(staticDir) {
);
}

function copyZXingWasm(staticDir) {
const staticPath = genStaticPath(staticDir);
copyFileDir(
npmPath("zxing-wasm/dist/reader/zxing_reader.wasm"),
staticPath("js")
);
}

gulp.task("copy-locale-data", async () => {
const staticDir = paths.app_output_static;
copyLocaleData(staticDir);
Expand Down Expand Up @@ -143,6 +151,7 @@ gulp.task("copy-static-app", async () => {
copyMapPanel(staticDir);

// Qr Scanner assets
copyZXingWasm(staticDir);
copyQrScannerWorker(staticDir);
});

Expand Down
16 changes: 0 additions & 16 deletions demo/src/configs/sections/description.ts

This file was deleted.

2 changes: 0 additions & 2 deletions demo/src/configs/sections/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import type { DemoConfig } from "../types";
import { demoLovelaceDescription } from "./description";
import { demoEntitiesSections } from "./entities";
import { demoLovelaceSections } from "./lovelace";

export const demoSections: DemoConfig = {
authorName: "Home Assistant",
authorUrl: "https://github.com/home-assistant/frontend/",
name: "Home Demo",
description: demoLovelaceDescription,
lovelace: demoLovelaceSections,
entities: demoEntitiesSections,
theme: () => ({}),
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "2.2.11",
"chart.js": "4.4.6",
"color-name": "2.0.0",
"comlink": "4.4.1",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "home-assistant-frontend"
version = "20241104.0"
version = "20241105.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"
Expand Down
1 change: 1 addition & 0 deletions src/components/data-table/ha-data-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,7 @@ export class HaDataTable extends LitElement {
.group-header {
padding-top: 12px;
height: var(--data-table-row-height, 52px);
padding-left: 12px;
padding-inline-start: 12px;
padding-inline-end: initial;
Expand Down
212 changes: 133 additions & 79 deletions src/components/ha-camera-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
type PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature";
import {
Expand All @@ -24,6 +26,13 @@ import type { HomeAssistant } from "../types";
import "./ha-hls-player";
import "./ha-web-rtc-player";

const MJPEG_STREAM = "mjpeg";

type Stream = {
type: StreamType | typeof MJPEG_STREAM;
visible: boolean;
};

@customElement("ha-camera-stream")
export class HaCameraStream extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
Expand All @@ -46,16 +55,13 @@ export class HaCameraStream extends LitElement {

@state() private _capabilities?: CameraCapabilities;

@state() private _streamType?: StreamType;

@state() private _hlsStreams?: { hasAudio: boolean; hasVideo: boolean };

@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };

public willUpdate(changedProps: PropertyValues): void {
if (
changedProps.has("stateObj") &&
!this._shouldRenderMJPEG &&
this.stateObj &&
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
this.stateObj.entity_id
Expand All @@ -79,88 +85,77 @@ export class HaCameraStream extends LitElement {
if (!this.stateObj) {
return nothing;
}
if (__DEMO__ || this._shouldRenderMJPEG) {
const streams = this._streams(
this._capabilities?.frontend_stream_types,
this._hlsStreams,
this._webRtcStreams
);
return html`${repeat(
streams,
(stream) => stream.type + this.stateObj!.entity_id,
(stream) => this._renderStream(stream)
)}`;
}

private _renderStream(stream: Stream) {
if (!this.stateObj) {
return nothing;
}
if (stream.type === MJPEG_STREAM) {
return html`<img
.src=${__DEMO__
? this.stateObj.attributes.entity_picture!
: this._connected
? computeMJPEGStreamUrl(this.stateObj)
: ""}
: this._posterUrl || ""}
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
/>`;
}
return html`${this._streamType === STREAM_TYPE_HLS ||
(!this._streamType &&
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_HLS))
? html`<ha-hls-player
autoplay
playsinline
.allowExoPlayer=${this.allowExoPlayer}
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleHlsStreams}
class=${!this._streamType && this._webRtcStreams?.hasVideo
? "hidden"
: ""}
></ha-hls-player>`
: nothing}
${this._streamType === STREAM_TYPE_WEB_RTC ||
(!this._streamType &&
this._capabilities?.frontend_stream_types.includes(STREAM_TYPE_WEB_RTC))
? html`<ha-web-rtc-player
autoplay
playsinline
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleWebRtcStreams}
class=${this._streamType !== STREAM_TYPE_WEB_RTC &&
!this._webRtcStreams
? "hidden"
: ""}
></ha-web-rtc-player>`
: nothing}`;

if (stream.type === STREAM_TYPE_HLS) {
return html`<ha-hls-player
autoplay
playsinline
.allowExoPlayer=${this.allowExoPlayer}
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleHlsStreams}
class=${stream.visible ? "" : "hidden"}
></ha-hls-player>`;
}

if (stream.type === STREAM_TYPE_WEB_RTC) {
return html`<ha-web-rtc-player
autoplay
playsinline
.muted=${this.muted}
.controls=${this.controls}
.hass=${this.hass}
.entityid=${this.stateObj.entity_id}
.posterUrl=${this._posterUrl}
@streams=${this._handleWebRtcStreams}
class=${stream.visible ? "" : "hidden"}
></ha-web-rtc-player>`;
}

return nothing;
}

private async _getCapabilities() {
this._capabilities = undefined;
this._hlsStreams = undefined;
this._webRtcStreams = undefined;
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
this._capabilities = { frontend_stream_types: [] };
return;
}
this._capabilities = await fetchCameraCapabilities(
this.hass!,
this.stateObj!.entity_id
);
if (this._capabilities.frontend_stream_types.length === 1) {
this._streamType = this._capabilities.frontend_stream_types[0];
}
}

private get _shouldRenderMJPEG() {
if (!supportsFeature(this.stateObj!, CAMERA_SUPPORT_STREAM)) {
// Steaming is not supported by the camera so fallback to MJPEG stream
return true;
}
if (
this._capabilities &&
(!this._capabilities.frontend_stream_types.includes(STREAM_TYPE_HLS) ||
this._hlsStreams?.hasVideo === false) &&
(!this._capabilities.frontend_stream_types.includes(
STREAM_TYPE_WEB_RTC
) ||
this._webRtcStreams?.hasVideo === false)
) {
// No video in HLS stream and no video in WebRTC stream
return true;
}
return false;
}

private async _getPosterUrl(): Promise<void> {
Expand All @@ -179,28 +174,87 @@ export class HaCameraStream extends LitElement {

private _handleHlsStreams(ev: CustomEvent) {
this._hlsStreams = ev.detail;
this._pickStreamType();
}

private _handleWebRtcStreams(ev: CustomEvent) {
this._webRtcStreams = ev.detail;
this._pickStreamType();
}

private _pickStreamType() {
if (!this._hlsStreams || !this._webRtcStreams) {
return;
}
if (
this._hlsStreams.hasVideo &&
this._hlsStreams.hasAudio &&
!this._webRtcStreams.hasAudio
) {
this._streamType = STREAM_TYPE_HLS;
} else if (this._webRtcStreams.hasVideo) {
this._streamType = STREAM_TYPE_WEB_RTC;
private _streams = memoizeOne(
(
supportedTypes?: StreamType[],
hlsStreams?: { hasAudio: boolean; hasVideo: boolean },
webRtcStreams?: { hasAudio: boolean; hasVideo: boolean }
): Stream[] => {
if (__DEMO__) {
return [{ type: MJPEG_STREAM, visible: true }];
}
if (!supportedTypes) {
return [];
}
if (supportedTypes.length === 0) {
// doesn't support any stream type, fallback to mjpeg
return [{ type: MJPEG_STREAM, visible: true }];
}
if (supportedTypes.length === 1) {
// only 1 stream type, no need to choose
if (
(supportedTypes[0] === STREAM_TYPE_HLS &&
hlsStreams?.hasVideo === false) ||
(supportedTypes[0] === STREAM_TYPE_WEB_RTC &&
webRtcStreams?.hasVideo === false)
) {
// stream failed to load, fallback to mjpeg
return [{ type: MJPEG_STREAM, visible: true }];
}
return [{ type: supportedTypes[0], visible: true }];
}
if (hlsStreams && webRtcStreams) {
// fully loaded
if (
hlsStreams.hasVideo &&
hlsStreams.hasAudio &&
!webRtcStreams.hasAudio
) {
// webRTC stream is missing audio, use HLS
return [{ type: STREAM_TYPE_HLS, visible: true }];
}
if (webRtcStreams.hasVideo) {
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
}
// both streams failed to load, fallback to mjpeg
return [{ type: MJPEG_STREAM, visible: true }];
}

if (hlsStreams?.hasVideo !== webRtcStreams?.hasVideo) {
// one of the two streams is loaded, or errored
// choose the one that has video or is still loading
if (hlsStreams?.hasVideo) {
return [
{ type: STREAM_TYPE_HLS, visible: true },
{ type: STREAM_TYPE_WEB_RTC, visible: false },
];
}
if (hlsStreams?.hasVideo === false) {
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
}
if (webRtcStreams?.hasVideo) {
return [
{ type: STREAM_TYPE_WEB_RTC, visible: true },
{ type: STREAM_TYPE_HLS, visible: false },
];
}
if (webRtcStreams?.hasVideo === false) {
return [{ type: STREAM_TYPE_HLS, visible: true }];
}
}

return [
{ type: STREAM_TYPE_HLS, visible: true },
{ type: STREAM_TYPE_WEB_RTC, visible: false },
];
}
}
);

static get styles(): CSSResultGroup {
return css`
Expand Down
4 changes: 4 additions & 0 deletions src/components/ha-md-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ export class HaMdDialog extends MdDialog {
display: contents;
}
.scroller {
overflow: var(--dialog-content-overflow, auto);
}
slot[name="content"]::slotted(*) {
padding: var(--dialog-content-padding, 24px);
}
Expand Down
26 changes: 26 additions & 0 deletions src/components/ha-md-select-option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { MdSelectOption } from "@material/web/select/select-option";
import { css } from "lit";
import { customElement } from "lit/decorators";

@customElement("ha-md-select-option")
export class HaMdSelectOption extends MdSelectOption {
static override styles = [
...super.styles,
css`
:host {
--ha-icon-display: block;
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-secondary: var(--secondary-text-color);
--md-sys-color-surface: var(--card-background-color);
--md-sys-color-on-surface: var(--primary-text-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
}
`,
];
}

declare global {
interface HTMLElementTagNameMap {
"ha-md-select-option": HaMdSelectOption;
}
}
Loading

0 comments on commit a6971d6

Please sign in to comment.