Add initial keyboard shortcut support.
This commit is contained in:
+86
-93
@@ -1,24 +1,28 @@
|
||||
import { ActionConfig, hasAction } from '@dermotduffy/custom-card-helpers';
|
||||
import { ZoomSettingsBase } from '../components-lib/zoom/types.js';
|
||||
import { PTZAction } from '../config/ptz.js';
|
||||
import {
|
||||
ActionConfig,
|
||||
handleActionConfig,
|
||||
hasAction,
|
||||
HomeAssistant,
|
||||
} from '@dermotduffy/custom-card-helpers';
|
||||
import {
|
||||
Actions,
|
||||
ActionPhase,
|
||||
ActionType,
|
||||
Actions,
|
||||
FrigateCardCustomAction,
|
||||
frigateCardCustomActionSchema,
|
||||
FrigateCardGeneralAction,
|
||||
FrigateCardUserSpecifiedView,
|
||||
LogActionConfig,
|
||||
LogActionLevel,
|
||||
PTZActionConfig,
|
||||
PTZDigitialActionConfig,
|
||||
PTZMultiActionConfig,
|
||||
frigateCardCustomActionSchema,
|
||||
} from '../config/types.js';
|
||||
import { arrayify } from './basic.js';
|
||||
|
||||
/**
|
||||
* Convert a generic Action to a FrigateCardCustomAction if it parses correctly.
|
||||
* @param action The generic action configuration.
|
||||
* @returns A FrigateCardCustomAction or null if it cannot be converted.
|
||||
*/
|
||||
export function convertActionToFrigateCardCustomAction(
|
||||
export function convertActionToCardCustomAction(
|
||||
action: unknown,
|
||||
): FrigateCardCustomAction | null {
|
||||
if (!action) {
|
||||
@@ -30,7 +34,7 @@ export function convertActionToFrigateCardCustomAction(
|
||||
return parseResult.success ? parseResult.data : null;
|
||||
}
|
||||
|
||||
export function createFrigateCardSimpleAction(
|
||||
export function createGeneralAction(
|
||||
action: FrigateCardGeneralAction | FrigateCardUserSpecifiedView,
|
||||
options?: {
|
||||
cardID?: string;
|
||||
@@ -43,7 +47,7 @@ export function createFrigateCardSimpleAction(
|
||||
};
|
||||
}
|
||||
|
||||
export function createFrigateCardCameraAction(
|
||||
export function createCameraAction(
|
||||
action: 'camera_select' | 'live_substream_select',
|
||||
camera: string,
|
||||
options?: {
|
||||
@@ -58,7 +62,7 @@ export function createFrigateCardCameraAction(
|
||||
};
|
||||
}
|
||||
|
||||
export function createFrigateCardMediaPlayerAction(
|
||||
export function createMediaPlayerAction(
|
||||
mediaPlayer: string,
|
||||
mediaPlayerAction: 'play' | 'stop',
|
||||
options?: {
|
||||
@@ -74,7 +78,7 @@ export function createFrigateCardMediaPlayerAction(
|
||||
};
|
||||
}
|
||||
|
||||
export function createFrigateCardDisplayModeAction(
|
||||
export function createDisplayModeAction(
|
||||
displayMode: 'single' | 'grid',
|
||||
options?: {
|
||||
cardID?: string;
|
||||
@@ -88,38 +92,87 @@ export function createFrigateCardDisplayModeAction(
|
||||
};
|
||||
}
|
||||
|
||||
export function createFrigateCardShowPTZAction(
|
||||
showPTZ: boolean,
|
||||
export function createPTZControlsAction(
|
||||
enabled: boolean,
|
||||
options?: {
|
||||
cardID?: string;
|
||||
},
|
||||
): FrigateCardCustomAction {
|
||||
return {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'show_ptz',
|
||||
show_ptz: showPTZ,
|
||||
frigate_card_action: 'ptz_controls',
|
||||
enabled: enabled,
|
||||
...(options?.cardID && { card_id: options.cardID }),
|
||||
};
|
||||
}
|
||||
|
||||
export function createFrigateCardChangeZoomAction(
|
||||
targetID: string,
|
||||
options?: {
|
||||
cardID?: string;
|
||||
pan?: {
|
||||
x?: number;
|
||||
y?: number;
|
||||
};
|
||||
zoom?: number;
|
||||
},
|
||||
): FrigateCardCustomAction {
|
||||
export function createPTZAction(options?: {
|
||||
cardID?: string;
|
||||
ptzAction?: PTZAction;
|
||||
ptzPhase?: ActionPhase;
|
||||
ptzPreset?: string;
|
||||
cameraID?: string;
|
||||
}): PTZActionConfig {
|
||||
return {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'change_zoom',
|
||||
target_id: targetID,
|
||||
frigate_card_action: 'ptz',
|
||||
...(options?.cardID && { card_id: options.cardID }),
|
||||
...(options?.ptzAction && { ptz_action: options.ptzAction }),
|
||||
...(options?.ptzPhase && { ptz_phase: options.ptzPhase }),
|
||||
...(options?.ptzPreset && { ptz_preset: options.ptzPreset }),
|
||||
...(options?.cameraID && { camera: options.cameraID }),
|
||||
};
|
||||
}
|
||||
|
||||
export function createPTZDigitalAction(options?: {
|
||||
cardID?: string;
|
||||
ptzPhase?: ActionPhase;
|
||||
ptzAction?: PTZAction;
|
||||
absolute?: ZoomSettingsBase;
|
||||
targetID?: string;
|
||||
}): PTZDigitialActionConfig {
|
||||
return {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz_digital',
|
||||
...(options?.cardID && { card_id: options.cardID }),
|
||||
...(options?.ptzAction && { ptz_action: options.ptzAction }),
|
||||
...(options?.ptzPhase && { ptz_phase: options.ptzPhase }),
|
||||
...(options?.absolute && { absolute: options.absolute }),
|
||||
...(options?.targetID && { target_id: options.targetID }),
|
||||
};
|
||||
}
|
||||
|
||||
export function createPTZMultiAction(options?: {
|
||||
cardID?: string;
|
||||
ptzAction?: PTZAction;
|
||||
ptzPhase?: ActionPhase;
|
||||
ptzPreset?: string;
|
||||
targetID?: string;
|
||||
}): PTZMultiActionConfig {
|
||||
return {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz_multi',
|
||||
...(options?.cardID && { card_id: options.cardID }),
|
||||
...(options?.ptzAction && { ptz_action: options.ptzAction }),
|
||||
...(options?.ptzPhase && { ptz_phase: options.ptzPhase }),
|
||||
...(options?.ptzPreset && { ptz_preset: options.ptzPreset }),
|
||||
...(options?.targetID && { target_id: options.targetID }),
|
||||
};
|
||||
}
|
||||
|
||||
export function createLogAction(
|
||||
message: string,
|
||||
options?: {
|
||||
cardID?: string;
|
||||
level?: LogActionLevel;
|
||||
},
|
||||
): LogActionConfig {
|
||||
return {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'log',
|
||||
message: message,
|
||||
level: options?.level ?? 'info',
|
||||
...(options?.cardID && { card_id: options.cardID }),
|
||||
...(options?.pan && { pan: options.pan }),
|
||||
...(options?.zoom && { zoom: options.zoom }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,63 +203,6 @@ export function getActionConfigGivenAction(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frigate card custom version of handleAction
|
||||
* (https://github.com/custom-cards/custom-card-helpers/blob/master/src/handle-action.ts)
|
||||
* that handles the custom action events the card supports.
|
||||
* @param node The node that fired the event.
|
||||
* @param hass The Home Assistant object.
|
||||
* @param actionConfig A single action config, array of action configs or
|
||||
* undefined for the default action config for 'tap'.
|
||||
* @param action The action string (e.g. 'hold')
|
||||
* @returns Whether or not an action was executed.
|
||||
*/
|
||||
export const frigateCardHandleActionConfig = (
|
||||
node: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
config: {
|
||||
camera_image?: string;
|
||||
entity?: string;
|
||||
},
|
||||
action: string,
|
||||
actionConfig?: ActionType | ActionType[] | null,
|
||||
): boolean => {
|
||||
// Only allow a tap action to use a default non-config (the more-info config).
|
||||
if (actionConfig || action == 'tap') {
|
||||
frigateCardHandleAction(node, hass, config, actionConfig);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const frigateCardHandleAction = (
|
||||
node: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
config: {
|
||||
camera_image?: string;
|
||||
entity?: string;
|
||||
},
|
||||
actionConfig?: ActionType | ActionType[] | null,
|
||||
): void => {
|
||||
// ActionConfig vs ActionType:
|
||||
// * There is a slight typing (but not functional) difference between
|
||||
// ActionType in this card and ActionConfig in `custom-card-helpers`. See
|
||||
// `ExtendedConfirmationRestrictionConfig` in `types.ts` for the source and
|
||||
// reason behind this difference.
|
||||
if (Array.isArray(actionConfig)) {
|
||||
actionConfig.forEach((action) =>
|
||||
handleActionConfig(node, hass, config, action as ActionConfig | undefined),
|
||||
);
|
||||
} else {
|
||||
handleActionConfig(
|
||||
node,
|
||||
hass,
|
||||
config,
|
||||
(actionConfig ?? undefined) as ActionConfig | undefined,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if an action config has a real action. A modified version of
|
||||
* custom-card-helpers hasAction to also work with arrays of action configs.
|
||||
@@ -216,10 +212,7 @@ export const frigateCardHandleAction = (
|
||||
export const frigateCardHasAction = (config?: ActionType | ActionType[]): boolean => {
|
||||
// See note above on 'ActionConfig vs ActionType' for why this cast is
|
||||
// necessary and harmless.
|
||||
if (Array.isArray(config)) {
|
||||
return !!config.find((item) => hasAction(item as ActionConfig | undefined));
|
||||
}
|
||||
return hasAction(config as ActionConfig | undefined);
|
||||
return arrayify(config).some((item) => hasAction(item as ActionConfig | undefined));
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+12
-1
@@ -5,9 +5,10 @@ import {
|
||||
format,
|
||||
} from 'date-fns';
|
||||
import { StyleInfo } from 'lit/directives/style-map';
|
||||
import { round } from 'lodash-es';
|
||||
import isEqualWith from 'lodash-es/isEqualWith';
|
||||
import mergeWith from 'lodash-es/mergeWith';
|
||||
import round from 'lodash-es/round';
|
||||
import uniq from 'lodash-es/uniq';
|
||||
import { FrigateCardError } from '../types';
|
||||
|
||||
export type ModifyInterface<T, R> = Omit<T, keyof R> & R;
|
||||
@@ -254,6 +255,16 @@ export const recursivelyMergeObjectsNotArrays = <T>(target: T, src1: T, src2: T)
|
||||
return mergeWith(target, src1, src2, (_a, b) => (Array.isArray(b) ? b : undefined));
|
||||
};
|
||||
|
||||
export const recursivelyMergeObjectsConcatenatingArraysUniquely = <T>(
|
||||
target: T,
|
||||
src1: T,
|
||||
src2: T,
|
||||
): T => {
|
||||
return mergeWith(target, src1, src2, (a, b) =>
|
||||
Array.isArray(a) ? uniq(a.concat(b)) : undefined,
|
||||
);
|
||||
};
|
||||
|
||||
export const aspectRatioToString = (options?: {
|
||||
ratio?: number[];
|
||||
defaultStatic?: boolean;
|
||||
|
||||
+11
-10
@@ -301,17 +301,18 @@ export function getEntityIcon(
|
||||
*/
|
||||
export const sideLoadHomeAssistantElements = async (): Promise<boolean> => {
|
||||
const neededElements = [
|
||||
'ha-selector',
|
||||
'ha-menu-button',
|
||||
'ha-camera-stream',
|
||||
'ha-hls-player',
|
||||
'ha-web-rtc-player',
|
||||
'ha-icon',
|
||||
'ha-circular-progress',
|
||||
'ha-icon-button',
|
||||
'ha-card',
|
||||
'ha-svg-icon',
|
||||
'ha-button-menu',
|
||||
'ha-button',
|
||||
'ha-camera-stream',
|
||||
'ha-card',
|
||||
'ha-circular-progress',
|
||||
'ha-hls-player',
|
||||
'ha-icon-button',
|
||||
'ha-icon',
|
||||
'ha-menu-button',
|
||||
'ha-selector',
|
||||
'ha-svg-icon',
|
||||
'ha-web-rtc-player',
|
||||
];
|
||||
|
||||
if (neededElements.every((element) => customElements.get(element))) {
|
||||
|
||||
+71
-15
@@ -1,19 +1,75 @@
|
||||
import { Capabilities } from '../camera-manager/capabilities';
|
||||
import { FrigateCardPTZConfig, PTZ_CONTROL_ACTIONS } from '../config/types';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { PTZAction } from '../config/ptz';
|
||||
import { PTZCapabilities } from '../types';
|
||||
import { View } from '../view/view';
|
||||
import { getStreamCameraID } from './substream';
|
||||
|
||||
export const hasUsablePTZ = (
|
||||
capabilities: Capabilities | null,
|
||||
config: FrigateCardPTZConfig,
|
||||
): boolean => {
|
||||
for (const actionName of PTZ_CONTROL_ACTIONS) {
|
||||
if ('actions_' + actionName in config) {
|
||||
return true;
|
||||
export type PTZType = 'digital' | 'ptz';
|
||||
interface PTZTarget {
|
||||
targetID: string;
|
||||
type: PTZType;
|
||||
}
|
||||
|
||||
export const getPTZTarget = (
|
||||
view: View,
|
||||
options?: {
|
||||
type?: PTZType;
|
||||
cameraManager?: CameraManager;
|
||||
},
|
||||
): PTZTarget | null => {
|
||||
if (view.isViewerView()) {
|
||||
const targetID = view.queryResults?.getSelectedResult()?.getID() ?? null;
|
||||
return options?.type === 'ptz' || !targetID
|
||||
? null
|
||||
: {
|
||||
targetID: targetID,
|
||||
type: 'digital',
|
||||
};
|
||||
} else if (view.is('live')) {
|
||||
const substreamAwareCameraID = getStreamCameraID(view);
|
||||
let type: PTZType = 'digital';
|
||||
|
||||
if (options?.type !== 'digital' && options?.cameraManager) {
|
||||
if (hasCameraTruePTZ(options.cameraManager, substreamAwareCameraID)) {
|
||||
type = 'ptz';
|
||||
}
|
||||
if (type !== 'ptz' && options?.type === 'ptz') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
targetID: substreamAwareCameraID,
|
||||
type: type,
|
||||
};
|
||||
}
|
||||
const ptzCapabilities = capabilities?.getPTZCapabilities();
|
||||
return (
|
||||
!!ptzCapabilities?.panTilt?.length ||
|
||||
!!ptzCapabilities?.zoom?.length ||
|
||||
!!ptzCapabilities?.presets?.length
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const hasCameraTruePTZ = (
|
||||
cameraManager: CameraManager,
|
||||
cameraID: string,
|
||||
): boolean => {
|
||||
return !!cameraManager
|
||||
.getStore()
|
||||
.getCamera(cameraID)
|
||||
?.getCapabilities()
|
||||
?.hasPTZCapability();
|
||||
};
|
||||
|
||||
export const ptzActionToCapabilityKey = (
|
||||
action: PTZAction,
|
||||
): keyof PTZCapabilities | null => {
|
||||
switch (action) {
|
||||
case 'left':
|
||||
case 'right':
|
||||
case 'up':
|
||||
case 'down':
|
||||
return action;
|
||||
case 'zoom_in':
|
||||
return 'zoomIn';
|
||||
case 'zoom_out':
|
||||
return 'zoomOut';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { View } from '../view/view';
|
||||
|
||||
export const getStreamCameraID = (view: View): string => {
|
||||
return view?.context?.live?.overrides?.get(view.camera) ?? view.camera;
|
||||
export const getStreamCameraID = (view: View, cameraID?: string): string => {
|
||||
return view.context?.live?.overrides?.get(cameraID ?? view.camera) ?? view.camera;
|
||||
};
|
||||
|
||||
export const hasSubstream = (view: View): boolean => {
|
||||
|
||||
Reference in New Issue
Block a user