diff --git a/package.json b/package.json index 2b3798c9..34efe135 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,9 @@ "@graphiteds/core": "^1.9.21", "@lit-labs/scoped-registry-mixin": "^1.0.3", "@lit-labs/task": "^1.1.3", - "@types/bluebird": "^3.5.42", "any-date-parser": "^2.2.0", "component-emitter": "^1.3.1", "compute-scroll-into-view": "^3.1.1", - "crypto": "^1.0.1", "date-fns": "^3.6.0", "date-fns-tz": "^3.1.3", "embla-carousel": "^8.6.0", diff --git a/src/camera-manager/frigate/util.ts b/src/camera-manager/frigate/util.ts index d7a01c07..89547f40 100644 --- a/src/camera-manager/frigate/util.ts +++ b/src/camera-manager/frigate/util.ts @@ -155,11 +155,9 @@ export const getReviewThumbnailURL = ( * Get generic review severity. */ export const getReviewSeverity = (severity: FrigateReviewSeverity): Severity => { + // Frigate severities: 'alert' -> 'high', 'detection' -> 'medium'. if (severity === 'alert') { return 'high'; } - if (severity === 'detection') { - return 'medium'; - } - return 'low'; + return 'medium'; }; diff --git a/src/card-controller/config/config-manager.ts b/src/card-controller/config/config-manager.ts index b17849d6..4477adc3 100644 --- a/src/card-controller/config/config-manager.ts +++ b/src/card-controller/config/config-manager.ts @@ -73,18 +73,14 @@ export class ConfigManager { (hint ?? localize('error.invalid_configuration_no_hint')), ); } - const config = advancedCameraCardConfigSchema.parse( - setProfiles( - inputConfig, - - // The config is cloned here because Zod 4 returns shared constant - // defaults by reference. Since setProfiles() mutates the configuration - // in-place, those mutations would "pollute" the global defaults and break - // test isolation if we didn't use a fresh clone here. - copyConfig(parseResult.data), - - parseResult.data.profiles, - ), + // The config is cloned here because Zod 4 returns shared constant + // defaults by reference. Since setProfiles() mutates the configuration + // in-place, those mutations would "pollute" the global defaults and break + // test isolation if we didn't use a fresh clone here. + const config = setProfiles( + inputConfig, + copyConfig(parseResult.data), + parseResult.data.profiles, ); this._rawConfig = inputConfig; diff --git a/src/components-lib/gallery/gallery-core-controller.ts b/src/components-lib/gallery/gallery-core-controller.ts index ddf6b799..8bc00741 100644 --- a/src/components-lib/gallery/gallery-core-controller.ts +++ b/src/components-lib/gallery/gallery-core-controller.ts @@ -23,6 +23,7 @@ export class GalleryCoreController implements ReactiveController { private _options: GalleryCoreOptions | null = null; private _touchScrollYPosition: number | null = null; + private _observedSentinel: HTMLElement | null = null; // Wheel / touch events may be voluminous, throttle extension calls. private _throttledExtendUp = throttle( @@ -99,14 +100,21 @@ export class GalleryCoreController implements ReactiveController { this._host.removeEventListener('touchend', this._touchEndHandler); this._resizeObserver.disconnect(); this._intersectionObserver.disconnect(); + this._observedSentinel = null; } public hostUpdated(): void { const sentinel = this._getSentintelBottom(); - this._intersectionObserver.disconnect(); - if (sentinel) { - this._intersectionObserver.observe(sentinel); + // Avoid redundant observer disconnect/reconnect on every Lit update cycle + // when the sentinel element hasn't changed. + if (sentinel !== this._observedSentinel) { + this._intersectionObserver.disconnect(); + this._observedSentinel = sentinel; + + if (sentinel) { + this._intersectionObserver.observe(sentinel); + } } } diff --git a/src/components/live/providers/webrtc-card.ts b/src/components/live/providers/webrtc-card.ts index 9db970ec..a0e550bd 100644 --- a/src/components/live/providers/webrtc-card.ts +++ b/src/components/live/providers/webrtc-card.ts @@ -63,6 +63,8 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi private hass?: HomeAssistant; + private _videoRTC: VideoRTC | null = null; + private _mediaPlayerController = new VideoMediaPlayerController( this, () => this._getVideo(), @@ -85,6 +87,7 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi } disconnectedCallback(): void { + this._videoRTC = null; this._message = null; super.disconnectedCallback(); } @@ -97,16 +100,12 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi } } - private _getVideoRTC(): VideoRTC | null { - return (this.renderRoot?.querySelector('#webrtc') ?? null) as VideoRTC | null; - } - /** * Get the underlying video player. * @returns The player or `null` if not found. */ private _getVideo(): HTMLVideoElement | null { - return this._getVideoRTC()?.video ?? null; + return this._videoRTC?.video ?? null; } private async _getWebRTCCardElement(): Promise { @@ -196,7 +195,7 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi // Extract the video component after it has been rendered and generate the // media load event. this.updateComplete.then(() => { - const videoRTC = this._getVideoRTC(); + this._videoRTC = this.renderRoot?.querySelector('#webrtc') ?? null; const video = this._getVideo(); if (video) { setControlsOnVideo(video, this.controls); @@ -210,7 +209,9 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi supportsPause: true, hasAudio: mayHaveAudio(video), }, - ...(videoRTC && { technology: getTechnologyForVideoRTC(videoRTC) }), + ...(this._videoRTC && { + technology: getTechnologyForVideoRTC(this._videoRTC), + }), }); }; video.onplay = () => dispatchMediaPlayEvent(this); diff --git a/tests/camera-manager/frigate/util.test.ts b/tests/camera-manager/frigate/util.test.ts index 8e13474d..e4b24ecd 100644 --- a/tests/camera-manager/frigate/util.test.ts +++ b/tests/camera-manager/frigate/util.test.ts @@ -258,7 +258,4 @@ describe('getReviewSeverity', () => { it('should get detection severity', () => { expect(getReviewSeverity('detection')).toBe('medium'); }); - it('should get significant_motion severity', () => { - expect(getReviewSeverity('significant_motion')).toBe('low'); - }); }); diff --git a/tests/card-controller/overlay-message-manager.test.ts b/tests/card-controller/overlay-message-manager.test.ts index e5eb28c5..8b9bbcb7 100644 --- a/tests/card-controller/overlay-message-manager.test.ts +++ b/tests/card-controller/overlay-message-manager.test.ts @@ -22,7 +22,7 @@ describe('OverlayMessageManager', () => { it('should set and get message', () => { const manager = new OverlayMessageManager(api); - const message = { message: 'foo' }; + const message = { text: 'foo' }; manager.setMessage(message); expect(manager.getMessage()).toBe(message); @@ -32,7 +32,7 @@ describe('OverlayMessageManager', () => { it('should reset message', () => { const manager = new OverlayMessageManager(api); - manager.setMessage({ message: 'foo' }); + manager.setMessage({ text: 'foo' }); vi.clearAllMocks(); manager.reset(); diff --git a/tests/components-lib/gallery/gallery-core-controller.test.ts b/tests/components-lib/gallery/gallery-core-controller.test.ts index 0739503a..df4711a7 100644 --- a/tests/components-lib/gallery/gallery-core-controller.test.ts +++ b/tests/components-lib/gallery/gallery-core-controller.test.ts @@ -105,7 +105,54 @@ describe('GalleryCoreController', () => { ).toBeCalledWith(sentinel); }); - it('should not observe when sentinel is null', () => { + it('should disconnect when sentinel changes to null', () => { + const sentinel = document.createElement('div'); + let currentSentinel: HTMLElement | null = sentinel; + const getSentinelBottom = vi.fn(() => currentSentinel); + const host = createLitElement(); + + const controller = createController({ + host, + getSentinelBottom, + }); + + // First call with a real sentinel. + controller.hostUpdated(); + + currentSentinel = null; + controller.hostUpdated(); + + expect( + vi.mocked(IntersectionObserver).mock.results[0].value.disconnect, + ).toBeCalledTimes(2); + expect( + vi.mocked(IntersectionObserver).mock.results[0].value.observe, + ).toBeCalledTimes(1); + }); + + it('should skip disconnect/observe when sentinel is unchanged', () => { + const sentinel = document.createElement('div'); + const getSentinelBottom = vi.fn(() => sentinel); + const host = createLitElement(); + + const controller = createController({ + host, + getSentinelBottom, + }); + + controller.hostUpdated(); + controller.hostUpdated(); + + // Only called once despite two hostUpdated() calls. + expect( + vi.mocked(IntersectionObserver).mock.results[0].value.disconnect, + ).toBeCalledTimes(1); + expect( + vi.mocked(IntersectionObserver).mock.results[0].value.observe, + ).toBeCalledTimes(1); + }); + + it('should not observe when sentinel is null from the start', () => { const getSentinelBottom = vi.fn(() => null); const host = createLitElement(); @@ -118,7 +165,7 @@ describe('GalleryCoreController', () => { expect( vi.mocked(IntersectionObserver).mock.results[0].value.disconnect, - ).toBeCalled(); + ).not.toBeCalled(); expect( vi.mocked(IntersectionObserver).mock.results[0].value.observe, ).not.toBeCalled(); diff --git a/tests/utils/basic.test.ts b/tests/utils/basic.test.ts index 2f78fa9e..073fd011 100644 --- a/tests/utils/basic.test.ts +++ b/tests/utils/basic.test.ts @@ -311,7 +311,7 @@ describe('getChildrenFromElement', () => { describe('recursivelyMergeObjectsNotArrays', () => { it('should recursively merge objects but replace arrays', () => { expect( - recursivelyMergeObjectsNotArrays( + recursivelyMergeObjectsNotArrays>( {}, { a: { @@ -358,7 +358,7 @@ describe('recursivelyMergeObjectsNotArrays', () => { describe('recursivelyMergeObjectsConcatenatingArraysUniquely', () => { it('should recursively merge objects but uniquely concat arrays', () => { expect( - recursivelyMergeObjectsConcatenatingArraysUniquely( + recursivelyMergeObjectsConcatenatingArraysUniquely>( {}, { a: { diff --git a/yarn.lock b/yarn.lock index 5a02454d..4c9e8218 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2044,13 +2044,6 @@ __metadata: languageName: node linkType: hard -"@types/bluebird@npm:^3.5.42": - version: 3.5.42 - resolution: "@types/bluebird@npm:3.5.42" - checksum: 10c0/ce752a5e277bbc0cdee3dee9c875ac093d1331905a8a5175665ccb8cb76b7b95a84836f92bc46076c2fd1e384a107866893eeeb20f6fb33427482663879faf93 - languageName: node - linkType: hard - "@types/estree@npm:*, @types/estree@npm:1.0.5, @types/estree@npm:^1.0.0": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" @@ -2493,7 +2486,6 @@ __metadata: "@rollup/plugin-terser": "npm:^0.4.4" "@rollup/plugin-typescript": "npm:^11.1.6" "@semantic-release/github": "npm:^10.3.3" - "@types/bluebird": "npm:^3.5.42" "@types/js-yaml": "npm:^4" "@types/lodash-es": "npm:^4.17.12" "@types/masonry-layout": "npm:^4.2.8" @@ -2504,7 +2496,6 @@ __metadata: component-emitter: "npm:^1.3.1" compute-scroll-into-view: "npm:^3.1.1" conventional-changelog-conventionalcommits: "npm:^8.0.0" - crypto: "npm:^1.0.1" date-fns: "npm:^3.6.0" date-fns-tz: "npm:^3.1.3" docsify-cli: "npm:^4.4.4" @@ -3802,13 +3793,6 @@ __metadata: languageName: node linkType: hard -"crypto@npm:^1.0.1": - version: 1.0.1 - resolution: "crypto@npm:1.0.1" - checksum: 10c0/fcf7dbd68ac5415b7fde7d7208fe203038e92e83e8a8fcf6e86ab4771ce3dd026d6967a990ba56b9d1c771378210814d5c90d907d3739fbd1723d552ad6c8ab8 - languageName: node - linkType: hard - "css-declaration-sorter@npm:^7.2.0": version: 7.2.0 resolution: "css-declaration-sorter@npm:7.2.0"