Fix view-related querystring action race condition
This commit is contained in:
+46
-13
@@ -48,12 +48,12 @@ import {
|
|||||||
CameraConfig,
|
CameraConfig,
|
||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FRIGATE_CARD_VIEW_DEFAULT,
|
|
||||||
FrigateCardConfig,
|
FrigateCardConfig,
|
||||||
frigateCardConfigSchema,
|
frigateCardConfigSchema,
|
||||||
FrigateCardCustomAction,
|
FrigateCardCustomAction,
|
||||||
FrigateCardError,
|
FrigateCardError,
|
||||||
FrigateCardView,
|
FrigateCardView,
|
||||||
|
FRIGATE_CARD_VIEW_DEFAULT,
|
||||||
MediaLoadedInfo,
|
MediaLoadedInfo,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
Message,
|
Message,
|
||||||
@@ -65,6 +65,7 @@ import {
|
|||||||
frigateCardHandleActionConfig,
|
frigateCardHandleActionConfig,
|
||||||
frigateCardHasAction,
|
frigateCardHasAction,
|
||||||
getActionConfigGivenAction,
|
getActionConfigGivenAction,
|
||||||
|
isViewAction,
|
||||||
} from './utils/action.js';
|
} from './utils/action.js';
|
||||||
import { errorToConsole } from './utils/basic.js';
|
import { errorToConsole } from './utils/basic.js';
|
||||||
import { log } from './utils/debug.js';
|
import { log } from './utils/debug.js';
|
||||||
@@ -440,8 +441,17 @@ class FrigateCard extends LitElement {
|
|||||||
return this._overriddenConfig || this._config;
|
return this._overriddenConfig || this._config;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _changeView(args?: { view?: View; resetMessage?: boolean }): void {
|
protected _changeView(args?: {
|
||||||
log(this._cardWideConfig, `Frigate Card view change: `, args?.view ?? '[default]');
|
view?: View;
|
||||||
|
viewName?: FrigateCardView;
|
||||||
|
cameraID?: string;
|
||||||
|
resetMessage?: boolean;
|
||||||
|
}): void {
|
||||||
|
log(
|
||||||
|
this._cardWideConfig,
|
||||||
|
`Frigate Card view change: `,
|
||||||
|
args?.view ?? args?.viewName ?? '[default]',
|
||||||
|
);
|
||||||
const changeView = (view: View): void => {
|
const changeView = (view: View): void => {
|
||||||
if (View.isMajorMediaChange(this._view, view)) {
|
if (View.isMajorMediaChange(this._view, view)) {
|
||||||
this._mediaLoadedInfoController.clear();
|
this._mediaLoadedInfoController.clear();
|
||||||
@@ -464,12 +474,13 @@ class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!args?.view) {
|
if (!args?.view) {
|
||||||
// Load the default view.
|
|
||||||
let cameraID: string | null = null;
|
let cameraID: string | null = null;
|
||||||
if (this._cameraManager) {
|
if (this._cameraManager) {
|
||||||
const cameras = this._cameraManager.getStore().getVisibleCameras();
|
const cameras = this._cameraManager.getStore().getVisibleCameras();
|
||||||
if (cameras) {
|
if (cameras) {
|
||||||
if (this._view?.camera && this._getConfig().view.update_cycle_camera) {
|
if (args?.cameraID && cameras.has(args.cameraID)) {
|
||||||
|
cameraID = args.cameraID;
|
||||||
|
} else if (this._view?.camera && this._getConfig().view.update_cycle_camera) {
|
||||||
const keys = Array.from(cameras.keys());
|
const keys = Array.from(cameras.keys());
|
||||||
const currentIndex = keys.indexOf(this._view.camera);
|
const currentIndex = keys.indexOf(this._view.camera);
|
||||||
const targetIndex = currentIndex + 1 >= keys.length ? 0 : currentIndex + 1;
|
const targetIndex = currentIndex + 1 >= keys.length ? 0 : currentIndex + 1;
|
||||||
@@ -484,7 +495,7 @@ class FrigateCard extends LitElement {
|
|||||||
if (cameraID) {
|
if (cameraID) {
|
||||||
changeView(
|
changeView(
|
||||||
new View({
|
new View({
|
||||||
view: this._getConfig().view.default,
|
view: args?.viewName ?? this._getConfig().view.default,
|
||||||
camera: cameraID,
|
camera: cameraID,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -719,11 +730,26 @@ class FrigateCard extends LitElement {
|
|||||||
this._handleThrownError(e);
|
this._handleThrownError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's no view set yet, set one. This will be the case on initial camera load.
|
// Set a view on initial load. However,if the query string contains an
|
||||||
|
// action that needs to render content (e.g. a view action or diagnostics),
|
||||||
|
// we don't set any view here and allow that content to be triggered by the
|
||||||
|
// firstUpdated() call. To do otherwise may cause a race condition between
|
||||||
|
// the default view and the querystring view, see:
|
||||||
|
// https://github.com/dermotduffy/frigate-hass-card/issues/1200
|
||||||
if (!this._view) {
|
if (!this._view) {
|
||||||
|
const querystringActions = getActionsFromQueryString();
|
||||||
|
if (
|
||||||
|
!querystringActions.find(
|
||||||
|
(action) =>
|
||||||
|
isViewAction(action) || action.frigate_card_action === 'diagnostics',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this._changeView({
|
||||||
// Don't reset the message which may be set to an error above. This sets the
|
// Don't reset the message which may be set to an error above. This sets the
|
||||||
// first view using the newly loaded cameras.
|
// first view using the newly loaded cameras.
|
||||||
this._changeView({ resetMessage: false });
|
resetMessage: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1024,7 +1050,10 @@ class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _cardActionHandler(frigateCardAction: FrigateCardCustomAction): void {
|
protected _cardActionHandler(frigateCardAction: FrigateCardCustomAction): void {
|
||||||
if (!this._view || !this._cameraManager) {
|
// Note: This function needs to process (view-related) commands even when
|
||||||
|
// _view has not yet been initialized (since it may be used to set a view
|
||||||
|
// via the querystring).
|
||||||
|
if (!this._cameraManager) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1052,10 +1081,8 @@ class FrigateCard extends LitElement {
|
|||||||
case 'snapshots':
|
case 'snapshots':
|
||||||
case 'timeline':
|
case 'timeline':
|
||||||
this._changeView({
|
this._changeView({
|
||||||
view: new View({
|
viewName: action,
|
||||||
view: action,
|
cameraID: this._view?.camera,
|
||||||
camera: this._view.camera,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'download':
|
case 'download':
|
||||||
@@ -1097,21 +1124,27 @@ class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'live_substream_select': {
|
case 'live_substream_select': {
|
||||||
|
if (this._view) {
|
||||||
const view = createViewWithSelectedSubstream(
|
const view = createViewWithSelectedSubstream(
|
||||||
this._view,
|
this._view,
|
||||||
frigateCardAction.camera,
|
frigateCardAction.camera,
|
||||||
);
|
);
|
||||||
view && this._changeView({ view: view });
|
view && this._changeView({ view: view });
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'live_substream_off': {
|
case 'live_substream_off': {
|
||||||
|
if (this._view) {
|
||||||
const view = createViewWithoutSubstream(this._view);
|
const view = createViewWithoutSubstream(this._view);
|
||||||
view && this._changeView({ view: view });
|
view && this._changeView({ view: view });
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'live_substream_on': {
|
case 'live_substream_on': {
|
||||||
|
if (this._view) {
|
||||||
const view = createViewWithNextStream(this._cameraManager, this._view);
|
const view = createViewWithNextStream(this._cameraManager, this._view);
|
||||||
view && this._changeView({ view: view });
|
view && this._changeView({ view: view });
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'media_player':
|
case 'media_player':
|
||||||
|
|||||||
+7
-9
@@ -209,15 +209,11 @@ const frigateCardCustomActionsBaseSchema = customActionSchema.extend({
|
|||||||
|
|
||||||
const FRIGATE_CARD_GENERAL_ACTIONS = [
|
const FRIGATE_CARD_GENERAL_ACTIONS = [
|
||||||
'camera_ui',
|
'camera_ui',
|
||||||
'clip',
|
|
||||||
'clips',
|
|
||||||
'default',
|
'default',
|
||||||
'diagnostics',
|
'diagnostics',
|
||||||
'expand',
|
'expand',
|
||||||
'download',
|
'download',
|
||||||
'fullscreen',
|
'fullscreen',
|
||||||
'image',
|
|
||||||
'live',
|
|
||||||
'menu_toggle',
|
'menu_toggle',
|
||||||
'mute',
|
'mute',
|
||||||
'live_substream_on',
|
'live_substream_on',
|
||||||
@@ -226,15 +222,11 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
|
|||||||
'microphone_unmute',
|
'microphone_unmute',
|
||||||
'play',
|
'play',
|
||||||
'pause',
|
'pause',
|
||||||
'recording',
|
|
||||||
'recordings',
|
|
||||||
'screenshot',
|
'screenshot',
|
||||||
'snapshot',
|
|
||||||
'snapshots',
|
|
||||||
'timeline',
|
|
||||||
'unmute',
|
'unmute',
|
||||||
] as const;
|
] as const;
|
||||||
const FRIGATE_CARD_ACTIONS = [
|
const FRIGATE_CARD_ACTIONS = [
|
||||||
|
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
||||||
...FRIGATE_CARD_GENERAL_ACTIONS,
|
...FRIGATE_CARD_GENERAL_ACTIONS,
|
||||||
'camera_select',
|
'camera_select',
|
||||||
'live_substream_select',
|
'live_substream_select',
|
||||||
@@ -242,6 +234,11 @@ const FRIGATE_CARD_ACTIONS = [
|
|||||||
] as const;
|
] as const;
|
||||||
export type FrigateCardAction = (typeof FRIGATE_CARD_ACTIONS)[number];
|
export type FrigateCardAction = (typeof FRIGATE_CARD_ACTIONS)[number];
|
||||||
|
|
||||||
|
const frigateCardViewActionSchema = frigateCardCustomActionsBaseSchema.extend({
|
||||||
|
frigate_card_action: z.enum(FRIGATE_CARD_VIEWS_USER_SPECIFIED),
|
||||||
|
});
|
||||||
|
export type FrigateCardViewAction = z.infer<typeof frigateCardViewActionSchema>;
|
||||||
|
|
||||||
const frigateCardGeneralActionSchema = frigateCardCustomActionsBaseSchema.extend({
|
const frigateCardGeneralActionSchema = frigateCardCustomActionsBaseSchema.extend({
|
||||||
frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS),
|
frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS),
|
||||||
});
|
});
|
||||||
@@ -261,6 +258,7 @@ const frigateCardMediaPlayerActionSchema = frigateCardCustomActionsBaseSchema.ex
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const frigateCardCustomActionSchema = z.union([
|
export const frigateCardCustomActionSchema = z.union([
|
||||||
|
frigateCardViewActionSchema,
|
||||||
frigateCardGeneralActionSchema,
|
frigateCardGeneralActionSchema,
|
||||||
frigateCardCameraSelectActionSchema,
|
frigateCardCameraSelectActionSchema,
|
||||||
frigateCardLiveDependencySelectActionSchema,
|
frigateCardLiveDependencySelectActionSchema,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
FrigateCardAction,
|
FrigateCardAction,
|
||||||
FrigateCardCustomAction,
|
FrigateCardCustomAction,
|
||||||
frigateCardCustomActionSchema,
|
frigateCardCustomActionSchema,
|
||||||
|
FrigateCardViewAction,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -173,3 +174,21 @@ export const frigateCardHasAction = (config?: ActionType | ActionType[]): boolea
|
|||||||
export const stopEventFromActivatingCardWideActions = (ev: Event): void => {
|
export const stopEventFromActivatingCardWideActions = (ev: Event): void => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isViewAction = (
|
||||||
|
action: FrigateCardCustomAction,
|
||||||
|
): action is FrigateCardViewAction => {
|
||||||
|
switch (action.frigate_card_action) {
|
||||||
|
case 'clip':
|
||||||
|
case 'clips':
|
||||||
|
case 'image':
|
||||||
|
case 'live':
|
||||||
|
case 'recording':
|
||||||
|
case 'recordings':
|
||||||
|
case 'snapshot':
|
||||||
|
case 'snapshots':
|
||||||
|
case 'timeline':
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { handleActionConfig, hasAction } from 'custom-card-helpers';
|
import { handleActionConfig, hasAction } from 'custom-card-helpers';
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { actionSchema } from '../../src/types';
|
import {
|
||||||
|
actionSchema,
|
||||||
|
FrigateCardAction,
|
||||||
|
FrigateCardCustomAction,
|
||||||
|
frigateCardCustomActionSchema,
|
||||||
|
} from '../../src/types';
|
||||||
import {
|
import {
|
||||||
convertActionToFrigateCardCustomAction,
|
convertActionToFrigateCardCustomAction,
|
||||||
createFrigateCardCustomAction,
|
createFrigateCardCustomAction,
|
||||||
|
frigateCardHandleAction,
|
||||||
frigateCardHandleActionConfig,
|
frigateCardHandleActionConfig,
|
||||||
frigateCardHasAction,
|
frigateCardHasAction,
|
||||||
getActionConfigGivenAction,
|
getActionConfigGivenAction,
|
||||||
|
isViewAction,
|
||||||
stopEventFromActivatingCardWideActions,
|
stopEventFromActivatingCardWideActions,
|
||||||
} from '../../src/utils/action';
|
} from '../../src/utils/action';
|
||||||
import { createHASS } from '../test-utils';
|
import { createHASS } from '../test-utils';
|
||||||
@@ -173,6 +180,23 @@ describe('frigateCardHandleActionConfig', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('frigateCardHandleAction', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const action = actionSchema.parse({
|
||||||
|
action: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call action handler', () => {
|
||||||
|
frigateCardHandleAction(element, createHASS(), {}, action);
|
||||||
|
expect(handleActionConfig).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('frigateCardHasAction', () => {
|
describe('frigateCardHasAction', () => {
|
||||||
const action = actionSchema.parse({
|
const action = actionSchema.parse({
|
||||||
action: 'toggle',
|
action: 'toggle',
|
||||||
@@ -200,3 +224,45 @@ describe('stopEventFromActivatingCardWideActions', () => {
|
|||||||
expect(event.stopPropagation).toBeCalled();
|
expect(event.stopPropagation).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isViewAction', () => {
|
||||||
|
const createAction = (action: FrigateCardAction): FrigateCardCustomAction => {
|
||||||
|
return frigateCardCustomActionSchema.parse({
|
||||||
|
action: 'fire-dom-event' as const,
|
||||||
|
frigate_card_action: action,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
it('should return true for clip view ', () => {
|
||||||
|
expect(isViewAction(createAction('clip'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for clips view ', () => {
|
||||||
|
expect(isViewAction(createAction('clips'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for image view ', () => {
|
||||||
|
expect(isViewAction(createAction('image'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for live view ', () => {
|
||||||
|
expect(isViewAction(createAction('live'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for recording view ', () => {
|
||||||
|
expect(isViewAction(createAction('recording'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for live view ', () => {
|
||||||
|
expect(isViewAction(createAction('live'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for recordings view ', () => {
|
||||||
|
expect(isViewAction(createAction('recordings'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for snapshot view ', () => {
|
||||||
|
expect(isViewAction(createAction('snapshot'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for snapshots view ', () => {
|
||||||
|
expect(isViewAction(createAction('snapshots'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return true for timeline view ', () => {
|
||||||
|
expect(isViewAction(createAction('timeline'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should return false for anything else', () => {
|
||||||
|
expect(isViewAction(createAction('diagnostics'))).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user