diff --git a/src/card-controller/media-player-manager.ts b/src/card-controller/media-player-manager.ts index 76095eab..023415c7 100644 --- a/src/card-controller/media-player-manager.ts +++ b/src/card-controller/media-player-manager.ts @@ -1,5 +1,9 @@ import { CameraConfig, FrigateCardConfig } from '../config/types'; -import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../const'; +import { + MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA, + MEDIA_PLAYER_SUPPORT_STOP, + MEDIA_PLAYER_SUPPORT_TURN_OFF, +} from '../const'; import { localize } from '../localize/localize'; import { errorToConsole } from '../utils/basic'; import { Entity } from '../utils/ha/registry/entity/types'; @@ -85,12 +89,26 @@ export class MediaPlayerManager { } public async stop(mediaPlayer: string): Promise { - await this._api - .getHASSManager() - .getHASS() - ?.callService('media_player', 'media_stop', { - entity_id: mediaPlayer, - }); + const hass = this._api.getHASSManager().getHASS(); + const stateObj = hass?.states[mediaPlayer]; + if (!stateObj) { + return; + } + + let service: string; + if (supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_STOP)) { + service = 'media_stop'; + } else if (supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_TURN_OFF)) { + // Google Cast devices don't support media_stop, but turning off has the + // same effect. + service = 'turn_off'; + } else { + return; + } + + await hass.callService('media_player', service, { + entity_id: mediaPlayer, + }); } public async playLive(mediaPlayer: string, cameraID: string): Promise { diff --git a/src/const.ts b/src/const.ts index e37be8a9..cf504b42 100644 --- a/src/const.ts +++ b/src/const.ts @@ -363,7 +363,9 @@ export const CONF_PERFORMANCE_STYLE_BORDER_RADIUS = `${CONF_PERFORMANCE}.style.b export const CONF_PROFILES = 'profiles' as const; -// Taken from https://github.dev/home-assistant/frontend/blob/b5861869e39290fd2e15737e89571dfc543b3ad3/src/data/media-player.ts#L93 +// Taken from https://github.com/home-assistant/frontend/blob/a759767d794f02527d127802831e68d3caf0cb7a/src/data/media-player.ts#L82 +export const MEDIA_PLAYER_SUPPORT_TURN_OFF = 256; +export const MEDIA_PLAYER_SUPPORT_STOP = 4096; export const MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA = 131072; // The number of media items to fetch at a time (for clips/snapshot views, and diff --git a/tests/card-controller/media-player-manager.test.ts b/tests/card-controller/media-player-manager.test.ts index 633f0539..7140a283 100644 --- a/tests/card-controller/media-player-manager.test.ts +++ b/tests/card-controller/media-player-manager.test.ts @@ -1,7 +1,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { MediaPlayerManager } from '../../src/card-controller/media-player-manager'; -import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../../src/const'; +import { + MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA, + MEDIA_PLAYER_SUPPORT_STOP, + MEDIA_PLAYER_SUPPORT_TURN_OFF, +} from '../../src/const'; import { ExtendedHomeAssistant } from '../../src/types'; import { EntityRegistryManager } from '../../src/utils/ha/registry/entity'; import { @@ -179,21 +183,84 @@ describe('MediaPlayerManager', () => { }); }); - it('should stop', async () => { - const api = createCardAPI(); - vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); + describe('should stop', () => { + it('should call media_stop when device supports it', async () => { + const api = createCardAPI(); + vi.mocked(api.getHASSManager().getHASS).mockReturnValue( + createHASS({ + 'media_player.foo': createStateEntity({ + entity_id: 'media_player.foo', + attributes: { + supported_features: MEDIA_PLAYER_SUPPORT_STOP, + }, + }), + }), + ); - const manager = new MediaPlayerManager(api); + const manager = new MediaPlayerManager(api); - await manager.stop('media_player.foo'); + await manager.stop('media_player.foo'); - expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( - 'media_player', - 'media_stop', - { - entity_id: 'media_player.foo', - }, - ); + expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( + 'media_player', + 'media_stop', + { + entity_id: 'media_player.foo', + }, + ); + }); + + it('should call turn_off when device does supports it', async () => { + const api = createCardAPI(); + vi.mocked(api.getHASSManager().getHASS).mockReturnValue( + createHASS({ + 'media_player.foo': createStateEntity({ + entity_id: 'media_player.foo', + attributes: { + supported_features: MEDIA_PLAYER_SUPPORT_TURN_OFF, + }, + }), + }), + ); + + const manager = new MediaPlayerManager(api); + + await manager.stop('media_player.foo'); + + expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith( + 'media_player', + 'turn_off', + { + entity_id: 'media_player.foo', + }, + ); + }); + + it('should do nothing without some supported feature', async () => { + const api = createCardAPI(); + vi.mocked(api.getHASSManager().getHASS).mockReturnValue( + createHASS({ + 'media_player.foo': createStateEntity({ + entity_id: 'media_player.foo', + }), + }), + ); + const manager = new MediaPlayerManager(api); + + await manager.stop('media_player.foo'); + + expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled(); + }); + + it('should do nothing without hass state', async () => { + const api = createCardAPI(); + vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); + const manager = new MediaPlayerManager(api); + + await manager.stop('media_player.foo'); + + expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled(); + }); }); describe('should play', () => {