@@ -265,10 +265,10 @@ cameras:
|
|||||||
|
|
||||||
Configures whether and how the content is proxied via
|
Configures whether and how the content is proxied via
|
||||||
[hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration)
|
[hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration)
|
||||||
(this must be installed separately). This allows fetching media **through** the
|
(this must be installed separately). This allows fetching media or live streams
|
||||||
Home Assistant process itself, allowing the card to access resources it
|
**through** the Home Assistant process itself, allowing the card to access
|
||||||
otherwise would not be able to directly access. There are [security and
|
resources it otherwise would not be able to directly access. There are [security
|
||||||
performance
|
and performance
|
||||||
implications](https://github.com/dermotduffy/hass-web-proxy-integration?tab=readme-ov-file#considerations)
|
implications](https://github.com/dermotduffy/hass-web-proxy-integration?tab=readme-ov-file#considerations)
|
||||||
to consider before installing
|
to consider before installing
|
||||||
[hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration)
|
[hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration)
|
||||||
@@ -283,7 +283,9 @@ cameras:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Not all [engines](./engine.md) benefit from proxying:
|
For live streams, only the `go2rtc` [live provider](./live-provider.md) currently supports live stream proxying.
|
||||||
|
|
||||||
|
For media, not all [engines](./engine.md) benefit from proxying:
|
||||||
|
|
||||||
| Engine | Purpose of proxying |
|
| Engine | Purpose of proxying |
|
||||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
@@ -300,6 +302,7 @@ Proxying parameters:
|
|||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| ------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `live` | `auto` | Whether or not to proxy live streams. `true` to proxy, `false` to not proxy, or `auto` to allow the camera engine to decide whether to proxy or not. Not all [live providers](./live-provider.md) support proxying live streams. |
|
||||||
| `media` | `auto` | Whether or not to proxy media items. `true` to proxy, `false` to not proxy, or `auto` to allow the camera engine to decide whether to proxy or not. |
|
| `media` | `auto` | Whether or not to proxy media items. `true` to proxy, `false` to not proxy, or `auto` to allow the camera engine to decide whether to proxy or not. |
|
||||||
| `dynamic` | `true` | Whether to dynamically (at the time) request proxying of the required media item, or rely on statically user-configured pre-existing proxying. See the [hass-web-proxy-integration documentation](https://github.com/dermotduffy/hass-web-proxy-integration). |
|
| `dynamic` | `true` | Whether to dynamically (at the time) request proxying of the required media item, or rely on statically user-configured pre-existing proxying. See the [hass-web-proxy-integration documentation](https://github.com/dermotduffy/hass-web-proxy-integration). |
|
||||||
| `ssl_verification` | `auto` | Whether to verify the validity of SSL certificates. If `true` always verifies, if `false` never verifies and if `auto` the [engine](./engine.md) decides the best setting for that camera ecosystem. |
|
| `ssl_verification` | `auto` | Whether to verify the validity of SSL certificates. If `true` always verifies, if `false` never verifies and if `auto` the [engine](./engine.md) decides the best setting for that camera ecosystem. |
|
||||||
@@ -431,6 +434,7 @@ cameras:
|
|||||||
preset: window
|
preset: window
|
||||||
- camera_entity: camera.needs_proxy
|
- camera_entity: camera.needs_proxy
|
||||||
proxy:
|
proxy:
|
||||||
|
live: auto
|
||||||
media: auto
|
media: auto
|
||||||
dynamic: true
|
dynamic: true
|
||||||
ssl_verification: auto
|
ssl_verification: auto
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
|
|
||||||
The `live_provider` parameter determines what provides the live stream for a camera. Each provider offers different capabilities:
|
The `live_provider` parameter determines what provides the live stream for a camera. Each provider offers different capabilities:
|
||||||
|
|
||||||
| Live Provider | Latency | Frame Rate | Loading Time | Installation | Description |
|
| Live Provider | Latency | Frame Rate | Loading Time | Installation | Supports [Proxying](./README.md?id=proxy) | Description |
|
||||||
| ---------------------------------- | ------- | ---------- | ------------ | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------------------------------- | ------- | ---------- | ------------ | ------------------------------ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `go2rtc` | Best | High | Better | Builtin | Uses [go2rtc](https://github.com/AlexxIT/go2rtc) to stream live feeds. This is supported by Frigate >= `0.12`. |
|
| `go2rtc` | Best | High | Better | Builtin | :white_check_mark: | Uses [go2rtc](https://github.com/AlexxIT/go2rtc) to stream live feeds. This is supported by Frigate >= `0.12`. |
|
||||||
| `ha` (default HA configuration) | Poor | High | Better | Builtin | Use the built-in Home Assistant camera stream. |
|
| `ha` (default HA configuration) | Poor | High | Better | Builtin | :heavy_multiplication_x: | Use the built-in Home Assistant camera stream. |
|
||||||
| `ha` (Native WebRTC) | Best | High | Better | Builtin | Use the built-in Home Assistant camera streams -- can be configured to use [native WebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) offering a very low-latency feed direct to your browser. |
|
| `ha` (Native WebRTC) | Best | High | Better | Builtin | :heavy_multiplication_x: | Use the built-in Home Assistant camera streams -- can be configured to use [native WebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) offering a very low-latency feed direct to your browser. |
|
||||||
| `ha` (when configured with LL-HLS) | Better | High | Better | Builtin | Use the built-in Home Assistant camera streams -- can be configured to use an [LL-HLS](https://www.home-assistant.io/integrations/stream/#ll-hls) feed for lower latency. |
|
| `ha` (when configured with LL-HLS) | Better | High | Better | Builtin | :heavy_multiplication_x: | Use the built-in Home Assistant camera streams -- can be configured to use an [LL-HLS](https://www.home-assistant.io/integrations/stream/#ll-hls) feed for lower latency. |
|
||||||
| `image` | Poor | Poor | Best | Builtin | Use refreshing snapshots of the built-in Home Assistant camera streams. |
|
| `image` | Poor | Poor | Best | Builtin | :heavy_multiplication_x: | Use refreshing snapshots of the built-in Home Assistant camera streams. |
|
||||||
| `jsmpeg` | Better | Low | Poor | Builtin | Use a the JSMPEG stream. |
|
| `jsmpeg` | Better | Low | Poor | Builtin | :heavy_multiplication_x: | Use a the JSMPEG stream. |
|
||||||
| `webrtc-card` | Best | High | Better | Separate installation required | Embed's [AlexxIT's WebRTC Card](https://github.com/AlexxIT/WebRTC) to stream live feed, requires manual extra setup. See below. Not to be confused with native Home Assistant WebRTC (use the `ha` provider). |
|
| `webrtc-card` | Best | High | Better | Separate installation required | :heavy_multiplication_x: | Embed's [AlexxIT's WebRTC Card](https://github.com/AlexxIT/WebRTC) to stream live feed, requires manual extra setup. See below. Not to be confused with native Home Assistant WebRTC (use the `ha` provider). |
|
||||||
|
|
||||||
## `go2rtc`
|
## `go2rtc`
|
||||||
|
|
||||||
@@ -32,6 +32,8 @@ cameras:
|
|||||||
| `stream` | Determined by camera engine (e.g. `frigate` camera name). | A valid `go2rtc` stream name. |
|
| `stream` | Determined by camera engine (e.g. `frigate` camera name). | A valid `go2rtc` stream name. |
|
||||||
| `url` | Determined by camera engine (e.g. the `frigate` engine will automatically generate a URL for the go2rtc backend that runs in the Frigate container). | The root `go2rtc` URL the card should stream the video from. This is only needed for non-Frigate usecases, or advanced Frigate usecases. Example: `http://my-custom-go2rtc:1984` |
|
| `url` | Determined by camera engine (e.g. the `frigate` engine will automatically generate a URL for the go2rtc backend that runs in the Frigate container). | The root `go2rtc` URL the card should stream the video from. This is only needed for non-Frigate usecases, or advanced Frigate usecases. Example: `http://my-custom-go2rtc:1984` |
|
||||||
|
|
||||||
|
?> If `url` is manually set and `proxy.live` is set to `auto` on the camera (the default), the video stream will automatically be proxied via the Home Assistant process if the [hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration) is detected. See [proxying](./README.md?id=proxy).
|
||||||
|
|
||||||
## `image`
|
## `image`
|
||||||
|
|
||||||
All configuration is under:
|
All configuration is under:
|
||||||
|
|||||||
@@ -488,6 +488,27 @@ folders:
|
|||||||
regexp: 'Person.*'
|
regexp: 'Person.*'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `go2rtc`
|
||||||
|
|
||||||
|
This example will use a custom `go2rtc` server, automatically proxying the video
|
||||||
|
stream via the Home Assistant process if
|
||||||
|
[hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration)
|
||||||
|
is detected. See [proxying](./configuration/cameras/README.md?id=proxy).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: custom:advanced-camera-card
|
||||||
|
cameras:
|
||||||
|
- camera_entity: camera.office
|
||||||
|
live_provider: go2rtc
|
||||||
|
go2rtc:
|
||||||
|
# This would normally risk mixed content issues with browsers (accessing
|
||||||
|
# `http` content over a `https` connection to Home Assistant is often
|
||||||
|
# forbidden by browsers). In this scenario, since hass-web-proxy-integration
|
||||||
|
# has been installed separately by the user, the video will be automatically
|
||||||
|
# proxied.
|
||||||
|
url: http://my-custom-go2rtc
|
||||||
|
```
|
||||||
|
|
||||||
## Human interaction
|
## Human interaction
|
||||||
|
|
||||||
This example will automatically use a HD live substream when
|
This example will automatically use a HD live substream when
|
||||||
|
|||||||
@@ -217,6 +217,22 @@ providers](./configuration/cameras/live-provider.md) such as `image` or `jsmpeg`
|
|||||||
The card will only show the fullscreen menu button when fullscreen can usefully
|
The card will only show the fullscreen menu button when fullscreen can usefully
|
||||||
be activated, which means for certain views on the iPhone it will be absent.
|
be activated, which means for certain views on the iPhone it will be absent.
|
||||||
|
|
||||||
|
### Custom `go2rtc` server only works on Home Network
|
||||||
|
|
||||||
|
This card runs in your browser, and (if configured) attempts to opens a direct
|
||||||
|
connection to a `go2rtc` server. If you are manually specifying a custom server
|
||||||
|
using the [`url`](./configuration/cameras/live-provider.md?id=go2rtc) option,
|
||||||
|
your browser may only be able to access that server when you're on the same
|
||||||
|
network, and/or when both Home Assistant and the `go2rtc` server are both
|
||||||
|
accessed over `http` or both over `https`.
|
||||||
|
|
||||||
|
To automatically proxy the connection via the Home Assistant process instead
|
||||||
|
(ensuring that if you can access the card, you'll always have access to the
|
||||||
|
`go2rtc` server), optionally install
|
||||||
|
[hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration)
|
||||||
|
and your connection will be automatically proxied. See
|
||||||
|
[proxying](./configuration/cameras/README.md?id=proxy).
|
||||||
|
|
||||||
### iOS App not updating after card version change
|
### iOS App not updating after card version change
|
||||||
|
|
||||||
Try resetting the app frontend cache:
|
Try resetting the app frontend cache:
|
||||||
|
|||||||
@@ -77,8 +77,15 @@ export class Camera {
|
|||||||
|
|
||||||
public getProxyConfig(): CameraProxyConfig {
|
public getProxyConfig(): CameraProxyConfig {
|
||||||
return {
|
return {
|
||||||
dynamic: this._config.proxy.dynamic,
|
live:
|
||||||
|
this._config.proxy.live === 'auto'
|
||||||
|
? // Live is proxied if the live provider is go2rtc and if a go2rtc
|
||||||
|
// URL is manually set.
|
||||||
|
this._config.live_provider === 'go2rtc' && !!this._config.go2rtc?.url
|
||||||
|
: this._config.proxy.live,
|
||||||
media: this._config.proxy.media === 'auto' ? false : this._config.proxy.media,
|
media: this._config.proxy.media === 'auto' ? false : this._config.proxy.media,
|
||||||
|
|
||||||
|
dynamic: this._config.proxy.dynamic,
|
||||||
ssl_verification: this._config.proxy.ssl_verification !== false,
|
ssl_verification: this._config.proxy.ssl_verification !== false,
|
||||||
ssl_ciphers:
|
ssl_ciphers:
|
||||||
this._config.proxy.ssl_ciphers === 'auto'
|
this._config.proxy.ssl_ciphers === 'auto'
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export interface CameraEndpoints {
|
|||||||
|
|
||||||
export interface CameraProxyConfig {
|
export interface CameraProxyConfig {
|
||||||
dynamic: boolean;
|
dynamic: boolean;
|
||||||
|
live: boolean;
|
||||||
media: boolean;
|
media: boolean;
|
||||||
ssl_verification: boolean;
|
ssl_verification: boolean;
|
||||||
ssl_ciphers: SSLCiphers;
|
ssl_ciphers: SSLCiphers;
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { MediaActionsController } from '../../components-lib/media-actions-contr
|
|||||||
import { MediaHeightController } from '../../components-lib/media-height-controller.js';
|
import { MediaHeightController } from '../../components-lib/media-height-controller.js';
|
||||||
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
||||||
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
||||||
import { CameraConfig } from '../../config/schema/cameras.js';
|
|
||||||
import { TransitionEffect } from '../../config/schema/common/transition-effect.js';
|
import { TransitionEffect } from '../../config/schema/common/transition-effect.js';
|
||||||
import { LiveConfig } from '../../config/schema/live.js';
|
import { LiveConfig } from '../../config/schema/live.js';
|
||||||
import { CardWideConfig, configDefaults } from '../../config/schema/types.js';
|
import { CardWideConfig, configDefaults } from '../../config/schema/types.js';
|
||||||
@@ -170,18 +169,8 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
const slides: TemplateResult[] = [];
|
const slides: TemplateResult[] = [];
|
||||||
const cameraToSlide: Record<string, number> = {};
|
const cameraToSlide: Record<string, number> = {};
|
||||||
|
|
||||||
for (const [cameraID, cameraConfig] of this.cameraManager
|
for (const cameraID of cameraIDs ?? []) {
|
||||||
.getStore()
|
const slide = this._renderLive(this._getSubstreamCameraID(cameraID, view));
|
||||||
.getCameraConfigEntries(cameraIDs)) {
|
|
||||||
const liveCameraID = this._getSubstreamCameraID(cameraID, view);
|
|
||||||
const liveCameraConfig =
|
|
||||||
cameraID === liveCameraID
|
|
||||||
? cameraConfig
|
|
||||||
: this.cameraManager?.getStore().getCameraConfig(liveCameraID);
|
|
||||||
|
|
||||||
const slide = liveCameraConfig
|
|
||||||
? this._renderLive(liveCameraID, liveCameraConfig)
|
|
||||||
: null;
|
|
||||||
if (slide) {
|
if (slide) {
|
||||||
cameraToSlide[cameraID] = slides.length;
|
cameraToSlide[cameraID] = slides.length;
|
||||||
slides.push(slide);
|
slides.push(slide);
|
||||||
@@ -207,11 +196,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _renderLive(
|
protected _renderLive(cameraID: string): TemplateResult | void {
|
||||||
cameraID: string,
|
const camera = this.cameraManager?.getStore().getCamera(cameraID);
|
||||||
cameraConfig: CameraConfig,
|
if (!this.liveConfig || !this.hass || !this.cameraManager || !camera) {
|
||||||
): TemplateResult | void {
|
|
||||||
if (!this.liveConfig || !this.hass || !this.cameraManager) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +211,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
.microphoneState=${view?.camera === cameraID
|
.microphoneState=${view?.camera === cameraID
|
||||||
? this.microphoneState
|
? this.microphoneState
|
||||||
: undefined}
|
: undefined}
|
||||||
.cameraConfig=${cameraConfig}
|
.camera=${camera}
|
||||||
.cameraEndpoints=${guard(
|
.cameraEndpoints=${guard(
|
||||||
[this.cameraManager, cameraID],
|
[this.cameraManager, cameraID],
|
||||||
() => this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined,
|
() => this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined,
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { guard } from 'lit/directives/guard.js';
|
import { guard } from 'lit/directives/guard.js';
|
||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
|
import { Camera } from '../../camera-manager/camera.js';
|
||||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||||
import { MicrophoneState } from '../../card-controller/types.js';
|
import { MicrophoneState } from '../../card-controller/types.js';
|
||||||
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
|
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
|
||||||
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
|
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
import { PartialZoomSettings } from '../../components-lib/zoom/types.js';
|
import { PartialZoomSettings } from '../../components-lib/zoom/types.js';
|
||||||
import { CameraConfig, LiveProvider } from '../../config/schema/cameras.js';
|
import { LiveProvider } from '../../config/schema/cameras.js';
|
||||||
import { LiveConfig } from '../../config/schema/live.js';
|
import { LiveConfig } from '../../config/schema/live.js';
|
||||||
import { CardWideConfig, configDefaults } from '../../config/schema/types.js';
|
import { CardWideConfig, configDefaults } from '../../config/schema/types.js';
|
||||||
import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
|
import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
|
||||||
@@ -39,7 +40,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraConfig?: CameraConfig;
|
public camera?: Camera;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraEndpoints?: CameraEndpoints;
|
public cameraEndpoints?: CameraEndpoints;
|
||||||
@@ -105,20 +106,18 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
* @returns A live provider (that is not 'auto').
|
* @returns A live provider (that is not 'auto').
|
||||||
*/
|
*/
|
||||||
protected _getResolvedProvider(): Omit<LiveProvider, 'auto'> {
|
protected _getResolvedProvider(): Omit<LiveProvider, 'auto'> {
|
||||||
if (this.cameraConfig?.live_provider === 'auto') {
|
const config = this.camera?.getConfig();
|
||||||
if (
|
if (config?.live_provider === 'auto') {
|
||||||
this.cameraConfig?.webrtc_card?.entity ||
|
if (config?.webrtc_card?.entity || config?.webrtc_card?.url) {
|
||||||
this.cameraConfig?.webrtc_card?.url
|
|
||||||
) {
|
|
||||||
return 'webrtc-card';
|
return 'webrtc-card';
|
||||||
} else if (this.cameraConfig?.camera_entity) {
|
} else if (config?.camera_entity) {
|
||||||
return 'ha';
|
return 'ha';
|
||||||
} else if (this.cameraConfig?.frigate.camera_name) {
|
} else if (config?.frigate.camera_name) {
|
||||||
return 'jsmpeg';
|
return 'jsmpeg';
|
||||||
}
|
}
|
||||||
return configDefaults.cameras.live_provider;
|
return configDefaults.cameras.live_provider;
|
||||||
}
|
}
|
||||||
return this.cameraConfig?.live_provider || 'image';
|
return config?.live_provider || 'image';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,7 +128,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
protected _shouldShowImageDuringLoading(): boolean {
|
protected _shouldShowImageDuringLoading(): boolean {
|
||||||
return (
|
return (
|
||||||
!this._isVideoMediaLoaded &&
|
!this._isVideoMediaLoaded &&
|
||||||
!!this.cameraConfig?.camera_entity &&
|
!!this.camera?.getConfig()?.camera_entity &&
|
||||||
!!this.hass &&
|
!!this.hass &&
|
||||||
!!this.liveConfig?.show_image_during_load &&
|
!!this.liveConfig?.show_image_during_load &&
|
||||||
!this._showStreamTroubleshooting &&
|
!this._showStreamTroubleshooting &&
|
||||||
@@ -172,7 +171,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has('cameraConfig')) {
|
if (changedProps.has('camera')) {
|
||||||
const provider = this._getResolvedProvider();
|
const provider = this._getResolvedProvider();
|
||||||
if (provider === 'jsmpeg') {
|
if (provider === 'jsmpeg') {
|
||||||
this._importPromises.push(import('./providers/jsmpeg.js'));
|
this._importPromises.push(import('./providers/jsmpeg.js'));
|
||||||
@@ -198,8 +197,9 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _renderContainer(template: TemplateResult): TemplateResult {
|
protected _renderContainer(template: TemplateResult): TemplateResult {
|
||||||
|
const config = this.camera?.getConfig();
|
||||||
const intermediateTemplate = html` <advanced-camera-card-media-dimensions-container
|
const intermediateTemplate = html` <advanced-camera-card-media-dimensions-container
|
||||||
.dimensionsConfig=${this.cameraConfig?.dimensions}
|
.dimensionsConfig=${config?.dimensions}
|
||||||
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||||
if (ev.detail.placeholder) {
|
if (ev.detail.placeholder) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
@@ -213,11 +213,11 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
|
|
||||||
return html` ${this.liveConfig?.zoomable
|
return html` ${this.liveConfig?.zoomable
|
||||||
? html` <advanced-camera-card-zoomer
|
? html` <advanced-camera-card-zoomer
|
||||||
.defaultSettings=${guard([this.cameraConfig?.dimensions?.layout], () =>
|
.defaultSettings=${guard([config?.dimensions?.layout], () =>
|
||||||
this.cameraConfig?.dimensions?.layout
|
config?.dimensions?.layout
|
||||||
? {
|
? {
|
||||||
pan: this.cameraConfig.dimensions.layout.pan,
|
pan: config.dimensions.layout.pan,
|
||||||
zoom: this.cameraConfig.dimensions.layout.zoom,
|
zoom: config.dimensions.layout.zoom,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
)}
|
)}
|
||||||
@@ -233,11 +233,13 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
|
const cameraConfig = this.camera?.getConfig();
|
||||||
if (
|
if (
|
||||||
!this._lazyLoadController?.isLoaded() ||
|
!this._lazyLoadController?.isLoaded() ||
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
!this.liveConfig ||
|
!this.liveConfig ||
|
||||||
!this.cameraConfig
|
!this.camera ||
|
||||||
|
!cameraConfig
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -251,27 +253,26 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
if (
|
if (
|
||||||
provider === 'ha' ||
|
provider === 'ha' ||
|
||||||
provider === 'image' ||
|
provider === 'image' ||
|
||||||
(this.cameraConfig?.camera_entity &&
|
(cameraConfig?.camera_entity && cameraConfig.always_error_if_entity_unavailable)
|
||||||
this.cameraConfig.always_error_if_entity_unavailable)
|
|
||||||
) {
|
) {
|
||||||
if (!this.cameraConfig?.camera_entity) {
|
if (!cameraConfig?.camera_entity) {
|
||||||
dispatchLiveErrorEvent(this);
|
dispatchLiveErrorEvent(this);
|
||||||
return renderMessage({
|
return renderMessage({
|
||||||
message: localize('error.no_live_camera'),
|
message: localize('error.no_live_camera'),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
icon: 'mdi:camera',
|
icon: 'mdi:camera',
|
||||||
context: this.cameraConfig,
|
context: cameraConfig,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateObj = this.hass.states[this.cameraConfig.camera_entity];
|
const stateObj = this.hass.states[cameraConfig.camera_entity];
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
dispatchLiveErrorEvent(this);
|
dispatchLiveErrorEvent(this);
|
||||||
return renderMessage({
|
return renderMessage({
|
||||||
message: localize('error.live_camera_not_found'),
|
message: localize('error.live_camera_not_found'),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
icon: 'mdi:camera',
|
icon: 'mdi:camera',
|
||||||
context: this.cameraConfig,
|
context: cameraConfig,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +302,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
? html` <advanced-camera-card-live-image
|
? html` <advanced-camera-card-live-image
|
||||||
${ref(this._refProvider)}
|
${ref(this._refProvider)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${cameraConfig}
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
...classes,
|
...classes,
|
||||||
// The image provider is providing the temporary loading image,
|
// The image provider is providing the temporary loading image,
|
||||||
@@ -320,7 +321,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
${ref(this._refProvider)}
|
${ref(this._refProvider)}
|
||||||
class=${classMap(classes)}
|
class=${classMap(classes)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${cameraConfig}
|
||||||
?controls=${this.liveConfig.controls.builtin}
|
?controls=${this.liveConfig.controls.builtin}
|
||||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||||
>
|
>
|
||||||
@@ -330,7 +331,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
${ref(this._refProvider)}
|
${ref(this._refProvider)}
|
||||||
class=${classMap(classes)}
|
class=${classMap(classes)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.camera=${this.camera}
|
||||||
.cameraEndpoints=${this.cameraEndpoints}
|
.cameraEndpoints=${this.cameraEndpoints}
|
||||||
.microphoneState=${this.microphoneState}
|
.microphoneState=${this.microphoneState}
|
||||||
.microphoneConfig=${this.liveConfig.microphone}
|
.microphoneConfig=${this.liveConfig.microphone}
|
||||||
@@ -343,7 +344,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
${ref(this._refProvider)}
|
${ref(this._refProvider)}
|
||||||
class=${classMap(classes)}
|
class=${classMap(classes)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${cameraConfig}
|
||||||
.cameraEndpoints=${this.cameraEndpoints}
|
.cameraEndpoints=${this.cameraEndpoints}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
?controls=${this.liveConfig.controls.builtin}
|
?controls=${this.liveConfig.controls.builtin}
|
||||||
@@ -355,7 +356,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
|||||||
${ref(this._refProvider)}
|
${ref(this._refProvider)}
|
||||||
class=${classMap(classes)}
|
class=${classMap(classes)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${cameraConfig}
|
||||||
.cameraEndpoints=${this.cameraEndpoints}
|
.cameraEndpoints=${this.cameraEndpoints}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||||
|
|||||||
@@ -7,17 +7,23 @@ import {
|
|||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
|
import { Camera } from '../../../../camera-manager/camera.js';
|
||||||
import { CameraEndpoints } from '../../../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../../../camera-manager/types.js';
|
||||||
import { MicrophoneState } from '../../../../card-controller/types.js';
|
import { MicrophoneState } from '../../../../card-controller/types.js';
|
||||||
import { dispatchLiveErrorEvent } from '../../../../components-lib/live/utils/dispatch-live-error.js';
|
import { dispatchLiveErrorEvent } from '../../../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
import { VideoMediaPlayerController } from '../../../../components-lib/media-player/video.js';
|
import { VideoMediaPlayerController } from '../../../../components-lib/media-player/video.js';
|
||||||
import { CameraConfig } from '../../../../config/schema/cameras.js';
|
|
||||||
import { MicrophoneConfig } from '../../../../config/schema/live.js';
|
import { MicrophoneConfig } from '../../../../config/schema/live.js';
|
||||||
|
import { homeAssistantSignPath } from '../../../../ha/sign-path.js';
|
||||||
import { HomeAssistant } from '../../../../ha/types.js';
|
import { HomeAssistant } from '../../../../ha/types.js';
|
||||||
|
import {
|
||||||
|
addDynamicProxyURL,
|
||||||
|
getWebProxiedURL,
|
||||||
|
shouldUseWebProxy,
|
||||||
|
} from '../../../../ha/web-proxy.js';
|
||||||
import { localize } from '../../../../localize/localize.js';
|
import { localize } from '../../../../localize/localize.js';
|
||||||
import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
|
import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
|
||||||
import { MediaPlayer, MediaPlayerController, Message } from '../../../../types.js';
|
import { MediaPlayer, MediaPlayerController, Message } from '../../../../types.js';
|
||||||
import { convertEndpointAddressToSignedWebsocket } from '../../../../utils/endpoint.js';
|
import { errorToConsole } from '../../../../utils/basic.js';
|
||||||
import { renderMessage } from '../../../message.js';
|
import { renderMessage } from '../../../message.js';
|
||||||
import { VideoRTC } from './video-rtc.js';
|
import { VideoRTC } from './video-rtc.js';
|
||||||
|
|
||||||
@@ -36,7 +42,7 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer
|
|||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraConfig?: CameraConfig;
|
public camera?: Camera;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraEndpoints?: CameraEndpoints;
|
public cameraEndpoints?: CameraEndpoints;
|
||||||
@@ -79,46 +85,102 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _createPlayer(): Promise<void> {
|
protected _handleError(message: Message, e?: Error): void {
|
||||||
if (!this.hass) {
|
if (e) {
|
||||||
return;
|
errorToConsole(e as Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._message = {
|
||||||
|
type: 'error',
|
||||||
|
...message,
|
||||||
|
};
|
||||||
|
dispatchLiveErrorEvent(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _getPlayerSource(): Promise<string | null> {
|
||||||
|
const cameraConfig = this.camera?.getConfig();
|
||||||
|
if (!this.hass || !cameraConfig) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const endpoint = this.cameraEndpoints?.go2rtc;
|
const endpoint = this.cameraEndpoints?.go2rtc;
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
this._message = {
|
this._handleError({
|
||||||
type: 'error',
|
|
||||||
message: localize('error.live_camera_no_endpoint'),
|
message: localize('error.live_camera_no_endpoint'),
|
||||||
context: this.cameraConfig,
|
context: cameraConfig,
|
||||||
};
|
});
|
||||||
dispatchLiveErrorEvent(this);
|
return null;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const address = await convertEndpointAddressToSignedWebsocket(
|
const proxyConfig = this.camera?.getProxyConfig();
|
||||||
this.hass,
|
let src: string | null = endpoint.endpoint;
|
||||||
endpoint,
|
let sign: boolean = endpoint.sign ?? false;
|
||||||
GO2RTC_URL_SIGN_EXPIRY_SECONDS,
|
|
||||||
);
|
if (proxyConfig && shouldUseWebProxy(this.hass, proxyConfig, 'live')) {
|
||||||
if (!address) {
|
if (proxyConfig.dynamic) {
|
||||||
this._message = {
|
try {
|
||||||
type: 'error',
|
await addDynamicProxyURL(this.hass, endpoint.endpoint, {
|
||||||
message: localize('error.failed_sign'),
|
proxyConfig,
|
||||||
context: this.cameraConfig,
|
ttl: GO2RTC_URL_SIGN_EXPIRY_SECONDS,
|
||||||
};
|
|
||||||
dispatchLiveErrorEvent(this);
|
// The link may need to be opened multiple times.
|
||||||
|
openLimit: 0,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this._handleError(
|
||||||
|
{
|
||||||
|
message: localize('error.failed_proxy'),
|
||||||
|
context: cameraConfig,
|
||||||
|
},
|
||||||
|
e as Error,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
src = getWebProxiedURL(endpoint.endpoint, { websocket: true });
|
||||||
|
sign = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (src && sign) {
|
||||||
|
try {
|
||||||
|
src = await homeAssistantSignPath(
|
||||||
|
this.hass,
|
||||||
|
src,
|
||||||
|
GO2RTC_URL_SIGN_EXPIRY_SECONDS,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this._handleError(
|
||||||
|
{
|
||||||
|
message: localize('error.failed_sign'),
|
||||||
|
context: cameraConfig,
|
||||||
|
},
|
||||||
|
e as Error,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _createPlayer(): Promise<void> {
|
||||||
|
const src = await this._getPlayerSource();
|
||||||
|
if (!src) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._player = new VideoRTC();
|
this._player = new VideoRTC();
|
||||||
this._player.mediaPlayerController = this._mediaPlayerController;
|
this._player.mediaPlayerController = this._mediaPlayerController;
|
||||||
this._player.microphoneStream = this.microphoneState?.stream ?? null;
|
this._player.microphoneStream = this.microphoneState?.stream ?? null;
|
||||||
this._player.src = address;
|
this._player.src = src;
|
||||||
this._player.visibilityCheck = false;
|
this._player.visibilityCheck = false;
|
||||||
this._player.setControls(this.controls);
|
this._player.setControls(this.controls);
|
||||||
|
|
||||||
if (this.cameraConfig?.go2rtc?.modes && this.cameraConfig.go2rtc.modes.length) {
|
const cameraConfig = this.camera?.getConfig();
|
||||||
this._player.mode = this.cameraConfig.go2rtc.modes.join(',');
|
if (cameraConfig?.go2rtc?.modes && cameraConfig.go2rtc.modes.length) {
|
||||||
|
this._player.mode = cameraConfig.go2rtc.modes.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispa
|
|||||||
import { JSMPEGMediaPlayerController } from '../../../components-lib/media-player/jsmpeg.js';
|
import { JSMPEGMediaPlayerController } from '../../../components-lib/media-player/jsmpeg.js';
|
||||||
import { CameraConfig } from '../../../config/schema/cameras.js';
|
import { CameraConfig } from '../../../config/schema/cameras.js';
|
||||||
import { CardWideConfig } from '../../../config/schema/types.js';
|
import { CardWideConfig } from '../../../config/schema/types.js';
|
||||||
|
import { homeAssistantSignPath } from '../../../ha/sign-path.js';
|
||||||
import { HomeAssistant } from '../../../ha/types.js';
|
import { HomeAssistant } from '../../../ha/types.js';
|
||||||
import { localize } from '../../../localize/localize.js';
|
import { localize } from '../../../localize/localize.js';
|
||||||
import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss';
|
import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss';
|
||||||
import { MediaPlayer, MediaPlayerController, Message } from '../../../types.js';
|
import { MediaPlayer, MediaPlayerController, Message } from '../../../types.js';
|
||||||
import { convertEndpointAddressToSignedWebsocket } from '../../../utils/endpoint.js';
|
import { convertHTTPAdressToWebsocket, errorToConsole } from '../../../utils/basic.js';
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
dispatchMediaPauseEvent,
|
dispatchMediaPauseEvent,
|
||||||
@@ -183,11 +184,18 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const address = await convertEndpointAddressToSignedWebsocket(
|
let response: string | null | undefined;
|
||||||
this.hass,
|
try {
|
||||||
endpoint,
|
response = await homeAssistantSignPath(
|
||||||
JSMPEG_URL_SIGN_EXPIRY_SECONDS,
|
this.hass,
|
||||||
);
|
endpoint.endpoint,
|
||||||
|
JSMPEG_URL_SIGN_EXPIRY_SECONDS,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorToConsole(e as Error);
|
||||||
|
}
|
||||||
|
const address = response ? convertHTTPAdressToWebsocket(response) : null;
|
||||||
|
|
||||||
if (!address) {
|
if (!address) {
|
||||||
this._message = {
|
this._message = {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|||||||
@@ -158,13 +158,17 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
|||||||
// Don't use URL() parsing, since that will strip the port number if
|
// Don't use URL() parsing, since that will strip the port number if
|
||||||
// it's the default, just need to strip any hash part of the URL.
|
// it's the default, just need to strip any hash part of the URL.
|
||||||
const urlWithoutQSorHash = unsignedURL.split(/#/)[0];
|
const urlWithoutQSorHash = unsignedURL.split(/#/)[0];
|
||||||
await addDynamicProxyURL(this.hass, urlWithoutQSorHash, {
|
|
||||||
sslVerification: proxyConfig.ssl_verification,
|
|
||||||
sslCiphers: proxyConfig.ssl_ciphers,
|
|
||||||
|
|
||||||
// The link may need to be opened multiple times.
|
try {
|
||||||
openLimit: 0,
|
await addDynamicProxyURL(this.hass, urlWithoutQSorHash, {
|
||||||
});
|
proxyConfig,
|
||||||
|
|
||||||
|
// The link may need to be opened multiple times.
|
||||||
|
openLimit: 0,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
errorToConsole(e as Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -121,10 +121,11 @@ export const cameraConfigDefault = {
|
|||||||
entities: [],
|
entities: [],
|
||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
media: 'auto' as const,
|
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
ssl_verification: 'auto' as const,
|
live: 'auto' as const,
|
||||||
|
media: 'auto' as const,
|
||||||
ssl_ciphers: 'auto' as const,
|
ssl_ciphers: 'auto' as const,
|
||||||
|
ssl_verification: 'auto' as const,
|
||||||
},
|
},
|
||||||
always_error_if_entity_unavailable: false,
|
always_error_if_entity_unavailable: false,
|
||||||
};
|
};
|
||||||
@@ -133,6 +134,7 @@ const SSL_CIPHERS = ['default', 'insecure', 'intermediate', 'modern'] as const;
|
|||||||
export type SSLCiphers = (typeof SSL_CIPHERS)[number];
|
export type SSLCiphers = (typeof SSL_CIPHERS)[number];
|
||||||
|
|
||||||
const proxyConfigSchema = z.object({
|
const proxyConfigSchema = z.object({
|
||||||
|
live: z.boolean().or(z.literal('auto')).default(cameraConfigDefault.proxy.live),
|
||||||
media: z.boolean().or(z.literal('auto')).default(cameraConfigDefault.proxy.media),
|
media: z.boolean().or(z.literal('auto')).default(cameraConfigDefault.proxy.media),
|
||||||
dynamic: z.boolean().default(cameraConfigDefault.proxy.dynamic),
|
dynamic: z.boolean().default(cameraConfigDefault.proxy.dynamic),
|
||||||
ssl_verification: z
|
ssl_verification: z
|
||||||
@@ -144,7 +146,6 @@ const proxyConfigSchema = z.object({
|
|||||||
.or(z.literal('auto'))
|
.or(z.literal('auto'))
|
||||||
.default(cameraConfigDefault.proxy.ssl_ciphers),
|
.default(cameraConfigDefault.proxy.ssl_ciphers),
|
||||||
});
|
});
|
||||||
export type ProxyConfig = z.infer<typeof proxyConfigSchema>;
|
|
||||||
|
|
||||||
const rotationSchema = z
|
const rotationSchema = z
|
||||||
.literal(0)
|
.literal(0)
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export const CONF_CAMERAS_ARRAY_DIMENSIONS_LAYOUT_ZOOM_FACTOR =
|
|||||||
`${CONF_CAMERAS}.#.dimensions.layout.zoom` as const;
|
`${CONF_CAMERAS}.#.dimensions.layout.zoom` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_PROXY_DYNAMIC =
|
export const CONF_CAMERAS_ARRAY_PROXY_DYNAMIC =
|
||||||
`${CONF_CAMERAS}.#.proxy.dynamic` as const;
|
`${CONF_CAMERAS}.#.proxy.dynamic` as const;
|
||||||
|
export const CONF_CAMERAS_ARRAY_PROXY_LIVE = `${CONF_CAMERAS}.#.proxy.live` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_PROXY_MEDIA = `${CONF_CAMERAS}.#.proxy.media` as const;
|
export const CONF_CAMERAS_ARRAY_PROXY_MEDIA = `${CONF_CAMERAS}.#.proxy.media` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_PROXY_SSL_CIPHERS =
|
export const CONF_CAMERAS_ARRAY_PROXY_SSL_CIPHERS =
|
||||||
`${CONF_CAMERAS}.#.proxy.ssl_ciphers` as const;
|
`${CONF_CAMERAS}.#.proxy.ssl_ciphers` as const;
|
||||||
|
|||||||
+15
-7
@@ -77,6 +77,7 @@ import {
|
|||||||
CONF_CAMERAS_ARRAY_MOTIONEYE_MOVIES_FILE_PATTERN,
|
CONF_CAMERAS_ARRAY_MOTIONEYE_MOVIES_FILE_PATTERN,
|
||||||
CONF_CAMERAS_ARRAY_MOTIONEYE_URL,
|
CONF_CAMERAS_ARRAY_MOTIONEYE_URL,
|
||||||
CONF_CAMERAS_ARRAY_PROXY_DYNAMIC,
|
CONF_CAMERAS_ARRAY_PROXY_DYNAMIC,
|
||||||
|
CONF_CAMERAS_ARRAY_PROXY_LIVE,
|
||||||
CONF_CAMERAS_ARRAY_PROXY_MEDIA,
|
CONF_CAMERAS_ARRAY_PROXY_MEDIA,
|
||||||
CONF_CAMERAS_ARRAY_PROXY_SSL_CIPHERS,
|
CONF_CAMERAS_ARRAY_PROXY_SSL_CIPHERS,
|
||||||
CONF_CAMERAS_ARRAY_PROXY_SSL_VERIFICATION,
|
CONF_CAMERAS_ARRAY_PROXY_SSL_VERIFICATION,
|
||||||
@@ -871,19 +872,19 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _proxyMedia: EditorSelectOption[] = [
|
protected _proxyModes: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{
|
{
|
||||||
value: 'auto',
|
value: 'auto',
|
||||||
label: localize('config.cameras.proxy.media.auto'),
|
label: localize('config.cameras.proxy.modes.auto'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: true,
|
value: true,
|
||||||
label: localize('config.cameras.proxy.media.true'),
|
label: localize('config.cameras.proxy.modes.true'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: false,
|
value: false,
|
||||||
label: localize('config.cameras.proxy.media.false'),
|
label: localize('config.cameras.proxy.modes.false'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -2563,10 +2564,17 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
|||||||
'mdi:arrow-decision',
|
'mdi:arrow-decision',
|
||||||
html`
|
html`
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(
|
||||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_MEDIA, cameraIndex),
|
getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_LIVE, cameraIndex),
|
||||||
this._proxyMedia,
|
this._proxyModes,
|
||||||
{
|
{
|
||||||
label: localize('config.cameras.proxy.media.editor_label'),
|
label: localize('config.cameras.proxy.live'),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
${this._renderOptionSelector(
|
||||||
|
getArrayConfigPath(CONF_CAMERAS_ARRAY_PROXY_MEDIA, cameraIndex),
|
||||||
|
this._proxyModes,
|
||||||
|
{
|
||||||
|
label: localize('config.cameras.proxy.media'),
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
${this._renderSwitch(
|
${this._renderSwitch(
|
||||||
|
|||||||
+21
-13
@@ -7,30 +7,37 @@ const hasWebProxyAvailable = (hass: HomeAssistant): boolean => {
|
|||||||
return hass.config.components.includes(HASS_WEB_PROXY_DOMAIN);
|
return hass.config.components.includes(HASS_WEB_PROXY_DOMAIN);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getWebProxiedURL = (url: string, v?: number): string => {
|
interface ProxiedURLOptions {
|
||||||
return `/api/${HASS_WEB_PROXY_DOMAIN}/v${v ?? 0}/?url=${encodeURIComponent(url)}`;
|
version?: number;
|
||||||
|
websocket?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a proxied URL for the given URL.
|
||||||
|
* @param url The URL to proxy.
|
||||||
|
* @param options Options for the proxied URL.
|
||||||
|
* @returns The proxied URL.
|
||||||
|
*/
|
||||||
|
export const getWebProxiedURL = (url: string, options?: ProxiedURLOptions): string => {
|
||||||
|
return (
|
||||||
|
`/api/${HASS_WEB_PROXY_DOMAIN}/v${options?.version ?? 0}/` +
|
||||||
|
`${options?.websocket ? 'ws' : ''}?url=${encodeURIComponent(url)}`
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shouldUseWebProxy = (
|
export const shouldUseWebProxy = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
proxyConfig: CameraProxyConfig,
|
proxyConfig: CameraProxyConfig,
|
||||||
context: 'media' = 'media',
|
context: 'media' | 'live' = 'media',
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return hasWebProxyAvailable(hass) && !!proxyConfig[context];
|
return hasWebProxyAvailable(hass) && !!proxyConfig[context];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Request that HA sign a path. May throw.
|
|
||||||
* @param hass The HomeAssistant object used to request the signature.
|
|
||||||
* @param path The path to sign.
|
|
||||||
* @param expires An optional number of seconds to sign the path for (by default
|
|
||||||
* HA will sign for 30 seconds).
|
|
||||||
* @returns The signed URL, or null if the response was malformed.
|
|
||||||
*/
|
|
||||||
export async function addDynamicProxyURL(
|
export async function addDynamicProxyURL(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
url_pattern: string,
|
url_pattern: string,
|
||||||
options?: {
|
options?: {
|
||||||
|
proxyConfig?: CameraProxyConfig;
|
||||||
urlID?: string;
|
urlID?: string;
|
||||||
sslVerification?: boolean;
|
sslVerification?: boolean;
|
||||||
sslCiphers?: string;
|
sslCiphers?: string;
|
||||||
@@ -43,8 +50,9 @@ export async function addDynamicProxyURL(
|
|||||||
url_pattern: url_pattern,
|
url_pattern: url_pattern,
|
||||||
...(options && {
|
...(options && {
|
||||||
url_id: options.urlID,
|
url_id: options.urlID,
|
||||||
ssl_verification: options.sslVerification,
|
ssl_verification:
|
||||||
ssl_ciphers: options.sslCiphers,
|
options.sslVerification ?? options?.proxyConfig?.ssl_verification,
|
||||||
|
ssl_ciphers: options.sslCiphers ?? options?.proxyConfig?.ssl_ciphers,
|
||||||
open_limit: options.openLimit,
|
open_limit: options.openLimit,
|
||||||
ttl: options.ttl,
|
ttl: options.ttl,
|
||||||
allow_unauthenticated: options.allowUnauthenticated,
|
allow_unauthenticated: options.allowUnauthenticated,
|
||||||
|
|||||||
@@ -83,9 +83,9 @@
|
|||||||
"rotation": "",
|
"rotation": "",
|
||||||
"rotations": {
|
"rotations": {
|
||||||
"0": "",
|
"0": "",
|
||||||
"90": "",
|
|
||||||
"180": "",
|
"180": "",
|
||||||
"270": ""
|
"270": "",
|
||||||
|
"90": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -145,9 +145,10 @@
|
|||||||
"proxy": {
|
"proxy": {
|
||||||
"dynamic": "",
|
"dynamic": "",
|
||||||
"editor_label": "",
|
"editor_label": "",
|
||||||
"media": {
|
"live": "",
|
||||||
|
"media": "",
|
||||||
|
"modes": {
|
||||||
"auto": "",
|
"auto": "",
|
||||||
"editor_label": "",
|
|
||||||
"false": "",
|
"false": "",
|
||||||
"true": ""
|
"true": ""
|
||||||
},
|
},
|
||||||
@@ -679,6 +680,7 @@
|
|||||||
"duplicate_camera_id": "Duplica l'identificador de la càmera Frigate per a la següent càmera, utilitza el paràmetre 'id' per identificar les càmeres de manera única",
|
"duplicate_camera_id": "Duplica l'identificador de la càmera Frigate per a la següent càmera, utilitza el paràmetre 'id' per identificar les càmeres de manera única",
|
||||||
"duplicate_folder_id": "",
|
"duplicate_folder_id": "",
|
||||||
"empty_response": "S'ha rebut una resposta buida de Home Assistant per a la sol·licitud",
|
"empty_response": "S'ha rebut una resposta buida de Home Assistant per a la sol·licitud",
|
||||||
|
"failed_proxy": "",
|
||||||
"failed_response": "No s'ha pogut rebre la resposta de Home Assistant per a la sol·licitud",
|
"failed_response": "No s'ha pogut rebre la resposta de Home Assistant per a la sol·licitud",
|
||||||
"failed_retain": "No s'ha pogut retenir l'esdeveniment",
|
"failed_retain": "No s'ha pogut retenir l'esdeveniment",
|
||||||
"failed_sign": "No s'ha pogut signar l'URL de Home Assistant",
|
"failed_sign": "No s'ha pogut signar l'URL de Home Assistant",
|
||||||
|
|||||||
@@ -83,9 +83,9 @@
|
|||||||
"rotation": "Rotation",
|
"rotation": "Rotation",
|
||||||
"rotations": {
|
"rotations": {
|
||||||
"0": "No rotation",
|
"0": "No rotation",
|
||||||
"90": "90 degrees clockwise",
|
|
||||||
"180": "180 degrees clockwise",
|
"180": "180 degrees clockwise",
|
||||||
"270": "270 degrees clockwise"
|
"270": "270 degrees clockwise",
|
||||||
|
"90": "90 degrees clockwise"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -145,11 +145,12 @@
|
|||||||
"proxy": {
|
"proxy": {
|
||||||
"dynamic": "Dynamic proxying",
|
"dynamic": "Dynamic proxying",
|
||||||
"editor_label": "Camera proxying",
|
"editor_label": "Camera proxying",
|
||||||
"media": {
|
"live": "Live proxying",
|
||||||
"auto": "Media proxying automatically configured",
|
"media": "Media proxying",
|
||||||
"editor_label": "Media proxying",
|
"modes": {
|
||||||
"false": "Media proxying disabled",
|
"auto": "Proxying automatically configured",
|
||||||
"true": "Media proxying enabled"
|
"false": "Proxying disabled",
|
||||||
|
"true": "Proxying enabled"
|
||||||
},
|
},
|
||||||
"ssl_ciphers": {
|
"ssl_ciphers": {
|
||||||
"auto": "SSL ciphers automatically configured",
|
"auto": "SSL ciphers automatically configured",
|
||||||
@@ -330,8 +331,8 @@
|
|||||||
"folders": {
|
"folders": {
|
||||||
"ha": {
|
"ha": {
|
||||||
"editor_label": "Home Assistant Media Folder Options",
|
"editor_label": "Home Assistant Media Folder Options",
|
||||||
"url": "URL of media folder",
|
"path_info": "Additional path matching functionality is available in the text editor",
|
||||||
"path_info": "Additional path matching functionality is available in the text editor"
|
"url": "URL of media folder"
|
||||||
},
|
},
|
||||||
"icon": "Icon for this folder",
|
"icon": "Icon for this folder",
|
||||||
"id": "Unique ID for this folder in this card",
|
"id": "Unique ID for this folder in this card",
|
||||||
@@ -665,8 +666,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"awaiting_live": "Waiting for live stream to load ...",
|
|
||||||
"awaiting_folder": "Waiting for folder to load",
|
"awaiting_folder": "Waiting for folder to load",
|
||||||
|
"awaiting_live": "Waiting for live stream to load ...",
|
||||||
"awaiting_media": "Waiting for media to load",
|
"awaiting_media": "Waiting for media to load",
|
||||||
"camera_initialization": "Camera initialization failed",
|
"camera_initialization": "Camera initialization failed",
|
||||||
"camera_initialization_reolink": "Could not initialize Reolink camera",
|
"camera_initialization_reolink": "Could not initialize Reolink camera",
|
||||||
@@ -679,6 +680,7 @@
|
|||||||
"duplicate_camera_id": "Duplicate camera id for the following camera, use the 'id' parameter to uniquely identify cameras",
|
"duplicate_camera_id": "Duplicate camera id for the following camera, use the 'id' parameter to uniquely identify cameras",
|
||||||
"duplicate_folder_id": "Duplicate folder id for the following folder, use the 'id' parameter to uniquely identify folders",
|
"duplicate_folder_id": "Duplicate folder id for the following folder, use the 'id' parameter to uniquely identify folders",
|
||||||
"empty_response": "Received empty response from Home Assistant for request",
|
"empty_response": "Received empty response from Home Assistant for request",
|
||||||
|
"failed_proxy": "Could not proxy via Home Assistant",
|
||||||
"failed_response": "Failed to receive response from Home Assistant for request",
|
"failed_response": "Failed to receive response from Home Assistant for request",
|
||||||
"failed_retain": "Could not retain event",
|
"failed_retain": "Could not retain event",
|
||||||
"failed_sign": "Could not sign Home Assistant URL",
|
"failed_sign": "Could not sign Home Assistant URL",
|
||||||
|
|||||||
@@ -83,9 +83,9 @@
|
|||||||
"rotation": "",
|
"rotation": "",
|
||||||
"rotations": {
|
"rotations": {
|
||||||
"0": "",
|
"0": "",
|
||||||
"90": "",
|
|
||||||
"180": "",
|
"180": "",
|
||||||
"270": ""
|
"270": "",
|
||||||
|
"90": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -145,11 +145,12 @@
|
|||||||
"proxy": {
|
"proxy": {
|
||||||
"dynamic": "Proxy dynamique",
|
"dynamic": "Proxy dynamique",
|
||||||
"editor_label": "Proxy de la caméra",
|
"editor_label": "Proxy de la caméra",
|
||||||
"media": {
|
"live": "",
|
||||||
"auto": "Proxy média automatique",
|
"media": "",
|
||||||
"editor_label": "Proxy média",
|
"modes": {
|
||||||
"false": "Proxy média désactivé",
|
"auto": "Proxy automatique",
|
||||||
"true": "Proxy média activé"
|
"false": "Proxy désactivé",
|
||||||
|
"true": "Proxy activé"
|
||||||
},
|
},
|
||||||
"ssl_ciphers": {
|
"ssl_ciphers": {
|
||||||
"auto": "Chiffrement SSL automatique",
|
"auto": "Chiffrement SSL automatique",
|
||||||
@@ -679,6 +680,7 @@
|
|||||||
"duplicate_camera_id": "ID de caméra Frigate en double pour la caméra suivante, utilisez le paramètre « ID » pour identifier de manière unique les caméras",
|
"duplicate_camera_id": "ID de caméra Frigate en double pour la caméra suivante, utilisez le paramètre « ID » pour identifier de manière unique les caméras",
|
||||||
"duplicate_folder_id": "",
|
"duplicate_folder_id": "",
|
||||||
"empty_response": "Réponse vide reçue de Home Assistant pour la demande",
|
"empty_response": "Réponse vide reçue de Home Assistant pour la demande",
|
||||||
|
"failed_proxy": "",
|
||||||
"failed_response": "Échec de la réponse de Home Assistant à la demande",
|
"failed_response": "Échec de la réponse de Home Assistant à la demande",
|
||||||
"failed_retain": "Impossible de conserver l'événement",
|
"failed_retain": "Impossible de conserver l'événement",
|
||||||
"failed_sign": "Impossible de signer l'URL de Home Assistant",
|
"failed_sign": "Impossible de signer l'URL de Home Assistant",
|
||||||
|
|||||||
@@ -83,9 +83,9 @@
|
|||||||
"rotation": "",
|
"rotation": "",
|
||||||
"rotations": {
|
"rotations": {
|
||||||
"0": "",
|
"0": "",
|
||||||
"90": "",
|
|
||||||
"180": "",
|
"180": "",
|
||||||
"270": ""
|
"270": "",
|
||||||
|
"90": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -145,9 +145,10 @@
|
|||||||
"proxy": {
|
"proxy": {
|
||||||
"dynamic": "",
|
"dynamic": "",
|
||||||
"editor_label": "",
|
"editor_label": "",
|
||||||
"media": {
|
"live": "",
|
||||||
|
"media": "",
|
||||||
|
"modes": {
|
||||||
"auto": "",
|
"auto": "",
|
||||||
"editor_label": "",
|
|
||||||
"false": "",
|
"false": "",
|
||||||
"true": ""
|
"true": ""
|
||||||
},
|
},
|
||||||
@@ -679,6 +680,7 @@
|
|||||||
"duplicate_camera_id": "Duplicato ID dellla telecamera Frigate, utilizzare il parametro 'ID' per identificare in modo univoco le telecamere",
|
"duplicate_camera_id": "Duplicato ID dellla telecamera Frigate, utilizzare il parametro 'ID' per identificare in modo univoco le telecamere",
|
||||||
"duplicate_folder_id": "",
|
"duplicate_folder_id": "",
|
||||||
"empty_response": "Ricevuto risposta vuota da Home Assistant per la richiesta",
|
"empty_response": "Ricevuto risposta vuota da Home Assistant per la richiesta",
|
||||||
|
"failed_proxy": "",
|
||||||
"failed_response": "Impossibile ricevere risposta da Home Assistant per la richiesta",
|
"failed_response": "Impossibile ricevere risposta da Home Assistant per la richiesta",
|
||||||
"failed_retain": "Impossibile conservare l'evento",
|
"failed_retain": "Impossibile conservare l'evento",
|
||||||
"failed_sign": "Impossibile firmare l'URL ad Home Assistant",
|
"failed_sign": "Impossibile firmare l'URL ad Home Assistant",
|
||||||
|
|||||||
@@ -83,9 +83,9 @@
|
|||||||
"rotation": "",
|
"rotation": "",
|
||||||
"rotations": {
|
"rotations": {
|
||||||
"0": "",
|
"0": "",
|
||||||
"90": "",
|
|
||||||
"180": "",
|
"180": "",
|
||||||
"270": ""
|
"270": "",
|
||||||
|
"90": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -145,9 +145,10 @@
|
|||||||
"proxy": {
|
"proxy": {
|
||||||
"dynamic": "",
|
"dynamic": "",
|
||||||
"editor_label": "",
|
"editor_label": "",
|
||||||
"media": {
|
"live": "",
|
||||||
|
"media": "",
|
||||||
|
"modes": {
|
||||||
"auto": "",
|
"auto": "",
|
||||||
"editor_label": "",
|
|
||||||
"false": "",
|
"false": "",
|
||||||
"true": ""
|
"true": ""
|
||||||
},
|
},
|
||||||
@@ -679,6 +680,7 @@
|
|||||||
"duplicate_camera_id": "Duplique o ID da câmera Frigate para a câmera a seguir, use o parâmetro 'id' para identificar exclusivamente as câmeras",
|
"duplicate_camera_id": "Duplique o ID da câmera Frigate para a câmera a seguir, use o parâmetro 'id' para identificar exclusivamente as câmeras",
|
||||||
"duplicate_folder_id": "",
|
"duplicate_folder_id": "",
|
||||||
"empty_response": "Sem resposta do Home Assistant para a solicitação",
|
"empty_response": "Sem resposta do Home Assistant para a solicitação",
|
||||||
|
"failed_proxy": "",
|
||||||
"failed_response": "Falha ao receber resposta do Home Assistant para solicitação",
|
"failed_response": "Falha ao receber resposta do Home Assistant para solicitação",
|
||||||
"failed_retain": "Não foi possível reter o evento",
|
"failed_retain": "Não foi possível reter o evento",
|
||||||
"failed_sign": "Não foi possível assinar a URL do Home Assistant",
|
"failed_sign": "Não foi possível assinar a URL do Home Assistant",
|
||||||
|
|||||||
@@ -83,9 +83,9 @@
|
|||||||
"rotation": "",
|
"rotation": "",
|
||||||
"rotations": {
|
"rotations": {
|
||||||
"0": "",
|
"0": "",
|
||||||
"90": "",
|
|
||||||
"180": "",
|
"180": "",
|
||||||
"270": ""
|
"270": "",
|
||||||
|
"90": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -145,9 +145,10 @@
|
|||||||
"proxy": {
|
"proxy": {
|
||||||
"dynamic": "",
|
"dynamic": "",
|
||||||
"editor_label": "",
|
"editor_label": "",
|
||||||
"media": {
|
"live": "",
|
||||||
|
"media": "",
|
||||||
|
"modes": {
|
||||||
"auto": "",
|
"auto": "",
|
||||||
"editor_label": "",
|
|
||||||
"false": "",
|
"false": "",
|
||||||
"true": ""
|
"true": ""
|
||||||
},
|
},
|
||||||
@@ -679,6 +680,7 @@
|
|||||||
"duplicate_camera_id": "Duplique o ID da câmera Frigate para a câmera a seguir, use o parâmetro 'id' para identificar exclusivamente as câmeras",
|
"duplicate_camera_id": "Duplique o ID da câmera Frigate para a câmera a seguir, use o parâmetro 'id' para identificar exclusivamente as câmeras",
|
||||||
"duplicate_folder_id": "",
|
"duplicate_folder_id": "",
|
||||||
"empty_response": "Sem resposta do Home Assistant para a solicitação",
|
"empty_response": "Sem resposta do Home Assistant para a solicitação",
|
||||||
|
"failed_proxy": "",
|
||||||
"failed_response": "Falha ao receber resposta do Home Assistant para solicitação",
|
"failed_response": "Falha ao receber resposta do Home Assistant para solicitação",
|
||||||
"failed_retain": "Não foi possível reter o evento",
|
"failed_retain": "Não foi possível reter o evento",
|
||||||
"failed_sign": "Não foi possível assinar a URL do Home Assistant",
|
"failed_sign": "Não foi possível assinar a URL do Home Assistant",
|
||||||
|
|||||||
@@ -323,3 +323,7 @@ export const generateFloatApproximatelyEqualsCustomizer = (
|
|||||||
: undefined;
|
: undefined;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const convertHTTPAdressToWebsocket = (url: string): string => {
|
||||||
|
return url.replace(/^http/i, 'ws');
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { homeAssistantSignPath } from '../ha/sign-path';
|
|
||||||
import { HomeAssistant } from '../ha/types';
|
|
||||||
import { Endpoint } from '../types';
|
|
||||||
import { errorToConsole } from './basic';
|
|
||||||
|
|
||||||
export const convertEndpointAddressToSignedWebsocket = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
endpoint: Endpoint,
|
|
||||||
expires?: number,
|
|
||||||
): Promise<string | null> => {
|
|
||||||
if (!endpoint.sign) {
|
|
||||||
return endpoint.endpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: string | null | undefined;
|
|
||||||
try {
|
|
||||||
response = await homeAssistantSignPath(hass, endpoint.endpoint, expires);
|
|
||||||
} catch (e) {
|
|
||||||
errorToConsole(e as Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response ? response.replace(/^http/i, 'ws') : null;
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,6 @@ import { Camera } from '../../src/camera-manager/camera.js';
|
|||||||
import { GenericCameraManagerEngine } from '../../src/camera-manager/generic/engine-generic.js';
|
import { GenericCameraManagerEngine } from '../../src/camera-manager/generic/engine-generic.js';
|
||||||
import { CameraProxyConfig } from '../../src/camera-manager/types.js';
|
import { CameraProxyConfig } from '../../src/camera-manager/types.js';
|
||||||
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
||||||
import { ProxyConfig } from '../../src/config/schema/cameras.js';
|
|
||||||
import {
|
import {
|
||||||
callStateWatcherCallback,
|
callStateWatcherCallback,
|
||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
@@ -179,6 +178,7 @@ describe('Camera', () => {
|
|||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: false,
|
media: false,
|
||||||
ssl_verification: true,
|
ssl_verification: true,
|
||||||
ssl_ciphers: 'default' as const,
|
ssl_ciphers: 'default' as const,
|
||||||
@@ -186,9 +186,10 @@ describe('Camera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when media set to true',
|
'when media set to true',
|
||||||
{ media: true },
|
{ proxy: { media: true } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: true,
|
media: true,
|
||||||
ssl_verification: true,
|
ssl_verification: true,
|
||||||
ssl_ciphers: 'default' as const,
|
ssl_ciphers: 'default' as const,
|
||||||
@@ -196,9 +197,10 @@ describe('Camera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when media set to false',
|
'when media set to false',
|
||||||
{ media: false as const },
|
{ proxy: { media: false as const } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: false,
|
media: false,
|
||||||
ssl_verification: true,
|
ssl_verification: true,
|
||||||
ssl_ciphers: 'default' as const,
|
ssl_ciphers: 'default' as const,
|
||||||
@@ -206,9 +208,10 @@ describe('Camera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when media set to auto',
|
'when media set to auto',
|
||||||
{ media: 'auto' as const },
|
{ proxy: { media: 'auto' as const } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: false,
|
media: false,
|
||||||
ssl_verification: true,
|
ssl_verification: true,
|
||||||
ssl_ciphers: 'default' as const,
|
ssl_ciphers: 'default' as const,
|
||||||
@@ -216,9 +219,10 @@ describe('Camera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when ssl_verification is set to auto',
|
'when ssl_verification is set to auto',
|
||||||
{ ssl_verification: 'auto' as const },
|
{ proxy: { ssl_verification: 'auto' as const } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: false,
|
media: false,
|
||||||
ssl_verification: true,
|
ssl_verification: true,
|
||||||
ssl_ciphers: 'default' as const,
|
ssl_ciphers: 'default' as const,
|
||||||
@@ -226,9 +230,10 @@ describe('Camera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when ssl_verification is set to true',
|
'when ssl_verification is set to true',
|
||||||
{ ssl_verification: true },
|
{ proxy: { ssl_verification: true } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: false,
|
media: false,
|
||||||
ssl_verification: true,
|
ssl_verification: true,
|
||||||
ssl_ciphers: 'default' as const,
|
ssl_ciphers: 'default' as const,
|
||||||
@@ -236,9 +241,10 @@ describe('Camera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when ssl_verification is set to false',
|
'when ssl_verification is set to false',
|
||||||
{ ssl_verification: false },
|
{ proxy: { ssl_verification: false } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: false,
|
media: false,
|
||||||
ssl_verification: false,
|
ssl_verification: false,
|
||||||
ssl_ciphers: 'default' as const,
|
ssl_ciphers: 'default' as const,
|
||||||
@@ -246,9 +252,10 @@ describe('Camera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when ssl_ciphers is set to auto',
|
'when ssl_ciphers is set to auto',
|
||||||
{ ssl_ciphers: 'auto' as const },
|
{ proxy: { ssl_ciphers: 'auto' as const } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: false,
|
media: false,
|
||||||
ssl_verification: true,
|
ssl_verification: true,
|
||||||
ssl_ciphers: 'default' as const,
|
ssl_ciphers: 'default' as const,
|
||||||
@@ -256,9 +263,10 @@ describe('Camera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when ssl_ciphers is set to modern',
|
'when ssl_ciphers is set to modern',
|
||||||
{ ssl_ciphers: 'modern' as const },
|
{ proxy: { ssl_ciphers: 'modern' as const } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: false,
|
media: false,
|
||||||
ssl_verification: true,
|
ssl_verification: true,
|
||||||
ssl_ciphers: 'modern' as const,
|
ssl_ciphers: 'modern' as const,
|
||||||
@@ -266,9 +274,47 @@ describe('Camera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when dynamic is set to false',
|
'when dynamic is set to false',
|
||||||
{ dynamic: false },
|
{ proxy: { dynamic: false } },
|
||||||
{
|
{
|
||||||
dynamic: false,
|
dynamic: false,
|
||||||
|
live: false,
|
||||||
|
media: false,
|
||||||
|
ssl_verification: true,
|
||||||
|
ssl_ciphers: 'default' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'when go2rtc has no url',
|
||||||
|
{ live_provider: 'go2rtc' },
|
||||||
|
{
|
||||||
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
|
media: false,
|
||||||
|
ssl_verification: true,
|
||||||
|
ssl_ciphers: 'default' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'when go2rtc has a url',
|
||||||
|
{ live_provider: 'go2rtc', go2rtc: { url: 'http://localhost:1984' } },
|
||||||
|
{
|
||||||
|
dynamic: true,
|
||||||
|
live: true,
|
||||||
|
media: false,
|
||||||
|
ssl_verification: true,
|
||||||
|
ssl_ciphers: 'default' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'when go2rtc has a url but live is set to false',
|
||||||
|
{
|
||||||
|
live_provider: 'go2rtc',
|
||||||
|
go2rtc: { url: 'http://localhost:1984' },
|
||||||
|
proxy: { live: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: false,
|
media: false,
|
||||||
ssl_verification: true,
|
ssl_verification: true,
|
||||||
ssl_ciphers: 'default' as const,
|
ssl_ciphers: 'default' as const,
|
||||||
@@ -276,15 +322,9 @@ describe('Camera', () => {
|
|||||||
],
|
],
|
||||||
])(
|
])(
|
||||||
'%s',
|
'%s',
|
||||||
(
|
(_name: string, cameraConfig: unknown, expectedResult: CameraProxyConfig) => {
|
||||||
_name: string,
|
|
||||||
proxyConfig: Partial<ProxyConfig>,
|
|
||||||
expectedResult: CameraProxyConfig,
|
|
||||||
) => {
|
|
||||||
const camera = new Camera(
|
const camera = new Camera(
|
||||||
createCameraConfig({
|
createCameraConfig(cameraConfig),
|
||||||
proxy: proxyConfig,
|
|
||||||
}),
|
|
||||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||||
);
|
);
|
||||||
expect(camera.getProxyConfig()).toEqual(expectedResult);
|
expect(camera.getProxyConfig()).toEqual(expectedResult);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { ReolinkCamera } from '../../../src/camera-manager/reolink/camera';
|
|||||||
import { CameraProxyConfig } from '../../../src/camera-manager/types';
|
import { CameraProxyConfig } from '../../../src/camera-manager/types';
|
||||||
import { ActionsExecutor } from '../../../src/card-controller/actions/types';
|
import { ActionsExecutor } from '../../../src/card-controller/actions/types';
|
||||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||||
import { ProxyConfig } from '../../../src/config/schema/cameras';
|
|
||||||
import { EntityRegistryManagerLive } from '../../../src/ha/registry/entity';
|
import { EntityRegistryManagerLive } from '../../../src/ha/registry/entity';
|
||||||
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
||||||
import {
|
import {
|
||||||
@@ -341,6 +340,7 @@ describe('ReolinkCamera', () => {
|
|||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: true,
|
media: true,
|
||||||
ssl_verification: false,
|
ssl_verification: false,
|
||||||
ssl_ciphers: 'intermediate' as const,
|
ssl_ciphers: 'intermediate' as const,
|
||||||
@@ -348,9 +348,10 @@ describe('ReolinkCamera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when media set to on',
|
'when media set to on',
|
||||||
{ media: true },
|
{ proxy: { media: true } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: true,
|
media: true,
|
||||||
ssl_verification: false,
|
ssl_verification: false,
|
||||||
ssl_ciphers: 'intermediate' as const,
|
ssl_ciphers: 'intermediate' as const,
|
||||||
@@ -358,9 +359,10 @@ describe('ReolinkCamera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when media set to off',
|
'when media set to off',
|
||||||
{ media: false },
|
{ proxy: { media: false } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: false,
|
media: false,
|
||||||
ssl_verification: false,
|
ssl_verification: false,
|
||||||
ssl_ciphers: 'intermediate' as const,
|
ssl_ciphers: 'intermediate' as const,
|
||||||
@@ -368,9 +370,10 @@ describe('ReolinkCamera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when media set to auto',
|
'when media set to auto',
|
||||||
{ media: 'auto' as const },
|
{ proxy: { media: 'auto' as const } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: true,
|
media: true,
|
||||||
ssl_verification: false,
|
ssl_verification: false,
|
||||||
ssl_ciphers: 'intermediate' as const,
|
ssl_ciphers: 'intermediate' as const,
|
||||||
@@ -378,9 +381,10 @@ describe('ReolinkCamera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when ssl_verification is set to auto',
|
'when ssl_verification is set to auto',
|
||||||
{ ssl_verification: 'auto' as const },
|
{ proxy: { ssl_verification: 'auto' as const } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: true,
|
media: true,
|
||||||
ssl_verification: false,
|
ssl_verification: false,
|
||||||
ssl_ciphers: 'intermediate' as const,
|
ssl_ciphers: 'intermediate' as const,
|
||||||
@@ -388,9 +392,10 @@ describe('ReolinkCamera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when ssl_verification is set to true',
|
'when ssl_verification is set to true',
|
||||||
{ ssl_verification: true },
|
{ proxy: { ssl_verification: true } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: true,
|
media: true,
|
||||||
ssl_verification: true,
|
ssl_verification: true,
|
||||||
ssl_ciphers: 'intermediate' as const,
|
ssl_ciphers: 'intermediate' as const,
|
||||||
@@ -398,9 +403,10 @@ describe('ReolinkCamera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when ssl_verification is set to false',
|
'when ssl_verification is set to false',
|
||||||
{ ssl_verification: false },
|
{ proxy: { ssl_verification: false } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: true,
|
media: true,
|
||||||
ssl_verification: false,
|
ssl_verification: false,
|
||||||
ssl_ciphers: 'intermediate' as const,
|
ssl_ciphers: 'intermediate' as const,
|
||||||
@@ -408,9 +414,10 @@ describe('ReolinkCamera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when ssl_ciphers is set to auto',
|
'when ssl_ciphers is set to auto',
|
||||||
{ ssl_ciphers: 'auto' as const },
|
{ proxy: { ssl_ciphers: 'auto' as const } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: true,
|
media: true,
|
||||||
ssl_verification: false,
|
ssl_verification: false,
|
||||||
ssl_ciphers: 'intermediate' as const,
|
ssl_ciphers: 'intermediate' as const,
|
||||||
@@ -418,9 +425,10 @@ describe('ReolinkCamera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when ssl_ciphers is set to modern',
|
'when ssl_ciphers is set to modern',
|
||||||
{ ssl_ciphers: 'modern' as const },
|
{ proxy: { ssl_ciphers: 'modern' as const } },
|
||||||
{
|
{
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: true,
|
media: true,
|
||||||
ssl_verification: false,
|
ssl_verification: false,
|
||||||
ssl_ciphers: 'modern' as const,
|
ssl_ciphers: 'modern' as const,
|
||||||
@@ -428,9 +436,47 @@ describe('ReolinkCamera', () => {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'when dynamic is set to false',
|
'when dynamic is set to false',
|
||||||
{ dynamic: false },
|
{ proxy: { dynamic: false } },
|
||||||
{
|
{
|
||||||
dynamic: false,
|
dynamic: false,
|
||||||
|
live: false,
|
||||||
|
media: true,
|
||||||
|
ssl_verification: false,
|
||||||
|
ssl_ciphers: 'intermediate' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'when go2rtc has no url',
|
||||||
|
{ live_provider: 'go2rtc' },
|
||||||
|
{
|
||||||
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
|
media: true,
|
||||||
|
ssl_verification: false,
|
||||||
|
ssl_ciphers: 'intermediate' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'when go2rtc has a url',
|
||||||
|
{ live_provider: 'go2rtc', go2rtc: { url: 'http://localhost:1984' } },
|
||||||
|
{
|
||||||
|
dynamic: true,
|
||||||
|
live: true,
|
||||||
|
media: true,
|
||||||
|
ssl_verification: false,
|
||||||
|
ssl_ciphers: 'intermediate' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'when go2rtc has a url but live is set to false',
|
||||||
|
{
|
||||||
|
live_provider: 'go2rtc',
|
||||||
|
go2rtc: { url: 'http://localhost:1984' },
|
||||||
|
proxy: { live: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dynamic: true,
|
||||||
|
live: false,
|
||||||
media: true,
|
media: true,
|
||||||
ssl_verification: false,
|
ssl_verification: false,
|
||||||
ssl_ciphers: 'intermediate' as const,
|
ssl_ciphers: 'intermediate' as const,
|
||||||
@@ -438,15 +484,9 @@ describe('ReolinkCamera', () => {
|
|||||||
],
|
],
|
||||||
])(
|
])(
|
||||||
'%s',
|
'%s',
|
||||||
(
|
(_name: string, cameraConfig: unknown, expectedResult: CameraProxyConfig) => {
|
||||||
_name: string,
|
|
||||||
proxyConfig: Partial<ProxyConfig>,
|
|
||||||
expectedResult: CameraProxyConfig,
|
|
||||||
) => {
|
|
||||||
const camera = new ReolinkCamera(
|
const camera = new ReolinkCamera(
|
||||||
createCameraConfig({
|
createCameraConfig(cameraConfig),
|
||||||
proxy: proxyConfig,
|
|
||||||
}),
|
|
||||||
mock<CameraManagerEngine>(),
|
mock<CameraManagerEngine>(),
|
||||||
);
|
);
|
||||||
expect(camera.getProxyConfig()).toEqual(expectedResult);
|
expect(camera.getProxyConfig()).toEqual(expectedResult);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ describe('config defaults', () => {
|
|||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
|
live: 'auto',
|
||||||
media: 'auto',
|
media: 'auto',
|
||||||
ssl_verification: 'auto',
|
ssl_verification: 'auto',
|
||||||
ssl_ciphers: 'auto',
|
ssl_ciphers: 'auto',
|
||||||
|
|||||||
@@ -8,15 +8,21 @@ import {
|
|||||||
import { createHASS } from '../test-utils.js';
|
import { createHASS } from '../test-utils.js';
|
||||||
|
|
||||||
describe('getWebProxiedURL', () => {
|
describe('getWebProxiedURL', () => {
|
||||||
it('should return proxied URL with v != 0', () => {
|
it('should return proxied URL with default version', () => {
|
||||||
expect(getWebProxiedURL('http://example.com', 2)).toBe(
|
expect(getWebProxiedURL('http://example.com')).toBe(
|
||||||
|
'/api/hass_web_proxy/v0/?url=http%3A%2F%2Fexample.com',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return proxied URL with non-default version', () => {
|
||||||
|
expect(getWebProxiedURL('http://example.com', { version: 2 })).toBe(
|
||||||
'/api/hass_web_proxy/v2/?url=http%3A%2F%2Fexample.com',
|
'/api/hass_web_proxy/v2/?url=http%3A%2F%2Fexample.com',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return proxied URL with default v', () => {
|
it('should return proxied URL with websocket', () => {
|
||||||
expect(getWebProxiedURL('http://example.com')).toBe(
|
expect(getWebProxiedURL('http://example.com', { websocket: true })).toBe(
|
||||||
'/api/hass_web_proxy/v0/?url=http%3A%2F%2Fexample.com',
|
'/api/hass_web_proxy/v0/ws?url=http%3A%2F%2Fexample.com',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -26,6 +32,7 @@ describe('shouldUseWebProxy', () => {
|
|||||||
config: Partial<CameraProxyConfig> = {},
|
config: Partial<CameraProxyConfig> = {},
|
||||||
): CameraProxyConfig => ({
|
): CameraProxyConfig => ({
|
||||||
media: true,
|
media: true,
|
||||||
|
live: true,
|
||||||
ssl_verification: true,
|
ssl_verification: true,
|
||||||
ssl_ciphers: 'default',
|
ssl_ciphers: 'default',
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
@@ -83,4 +90,28 @@ describe('addDynamicProxyURL', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add dynamic proxy URL using config defaults', async () => {
|
||||||
|
const hass = createHASS();
|
||||||
|
const proxyConfig: CameraProxyConfig = {
|
||||||
|
media: true,
|
||||||
|
live: true,
|
||||||
|
ssl_verification: false,
|
||||||
|
ssl_ciphers: 'insecure',
|
||||||
|
dynamic: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await addDynamicProxyURL(hass, 'http://example.com', {
|
||||||
|
proxyConfig: proxyConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(hass.callService).toHaveBeenCalledWith(
|
||||||
|
'hass_web_proxy',
|
||||||
|
'create_proxied_url',
|
||||||
|
expect.objectContaining({
|
||||||
|
ssl_verification: false,
|
||||||
|
ssl_ciphers: 'insecure',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
arrayMove,
|
arrayMove,
|
||||||
aspectRatioToStyle,
|
aspectRatioToStyle,
|
||||||
contentsChanged,
|
contentsChanged,
|
||||||
|
convertHTTPAdressToWebsocket,
|
||||||
dayToDate,
|
dayToDate,
|
||||||
desparsifyArrays,
|
desparsifyArrays,
|
||||||
errorToConsole,
|
errorToConsole,
|
||||||
@@ -514,3 +515,23 @@ describe('generateFloatApproximatelyEqualsCustomizer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('convertHTTPAdressToWebsocket', () => {
|
||||||
|
it('should convert http to ws', () => {
|
||||||
|
expect(convertHTTPAdressToWebsocket('http://example.com')).toBe('ws://example.com');
|
||||||
|
});
|
||||||
|
it('should convert https to ws', () => {
|
||||||
|
expect(convertHTTPAdressToWebsocket('https://example.com')).toBe(
|
||||||
|
'wss://example.com',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should not change ws url', () => {
|
||||||
|
expect(convertHTTPAdressToWebsocket('ws://example.com')).toBe('ws://example.com');
|
||||||
|
});
|
||||||
|
it('should not change wss url', () => {
|
||||||
|
expect(convertHTTPAdressToWebsocket('wss://example.com')).toBe('wss://example.com');
|
||||||
|
});
|
||||||
|
it('should handle mixed case', () => {
|
||||||
|
expect(convertHTTPAdressToWebsocket('HtTp://example.com')).toBe('ws://example.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { homeAssistantSignPath } from '../../src/ha/sign-path.js';
|
|
||||||
import { convertEndpointAddressToSignedWebsocket } from '../../src/utils/endpoint';
|
|
||||||
import { createHASS } from '../test-utils';
|
|
||||||
|
|
||||||
vi.mock('../../src/ha/sign-path.js');
|
|
||||||
|
|
||||||
describe('convertEndpointAddressToSignedWebsocket', () => {
|
|
||||||
it('without signing', async () => {
|
|
||||||
expect(
|
|
||||||
await convertEndpointAddressToSignedWebsocket(createHASS(), {
|
|
||||||
endpoint: 'http://example.com',
|
|
||||||
sign: false,
|
|
||||||
}),
|
|
||||||
).toBe('http://example.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with signing', () => {
|
|
||||||
afterEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('successful', async () => {
|
|
||||||
vi.mocked(homeAssistantSignPath).mockResolvedValue('http://signed.com');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await convertEndpointAddressToSignedWebsocket(createHASS(), {
|
|
||||||
endpoint: 'http://example.com',
|
|
||||||
sign: true,
|
|
||||||
}),
|
|
||||||
).toBe('ws://signed.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with null response', async () => {
|
|
||||||
vi.mocked(homeAssistantSignPath).mockResolvedValue(null);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await convertEndpointAddressToSignedWebsocket(createHASS(), {
|
|
||||||
endpoint: 'http://example.com',
|
|
||||||
sign: true,
|
|
||||||
}),
|
|
||||||
).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with exception on signing', async () => {
|
|
||||||
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
|
||||||
|
|
||||||
vi.mocked(homeAssistantSignPath).mockRejectedValue(new Error());
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await convertEndpointAddressToSignedWebsocket(createHASS(), {
|
|
||||||
endpoint: 'http://example.com',
|
|
||||||
sign: true,
|
|
||||||
}),
|
|
||||||
).toBeNull();
|
|
||||||
|
|
||||||
expect(consoleSpy).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user