Allow microphone state to be used in conditions.
This commit is contained in:
@@ -1293,6 +1293,7 @@ All variables listed are under a `conditions:` section.
|
|||||||
| `media_query` | Any valid [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries) string. Media queries must start and end with parentheses. This may be used to alter card configuration based on device/media properties (e.g. viewport width, orientation). Please note that `width` and `height` refer to the entire viewport not just the card. See the [media query example](#media-query-example).|
|
| `media_query` | Any valid [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries) string. Media queries must start and end with parentheses. This may be used to alter card configuration based on device/media properties (e.g. viewport width, orientation). Please note that `width` and `height` refer to the entire viewport not just the card. See the [media query example](#media-query-example).|
|
||||||
| `interacted` | If `true` the condition is satisfied if the card has had human interaction within `view.interaction_seconds` elapsed seconds. If `false` the condition is satisfied if the card has **NOT** had human interaction in that time. |
|
| `interacted` | If `true` the condition is satisfied if the card has had human interaction within `view.interaction_seconds` elapsed seconds. If `false` the condition is satisfied if the card has **NOT** had human interaction in that time. |
|
||||||
| `triggered` | A list of camera IDs which, if triggered in [scan mode](#scan-mode), satisfy the condition.|
|
| `triggered` | A list of camera IDs which, if triggered in [scan mode](#scan-mode), satisfy the condition.|
|
||||||
|
| `microphone` | A object to include microphone state as part of the condition evaluation. See below.|
|
||||||
|
|
||||||
See the [example below](#frigate-card-conditional-example) for a real-world example of how these conditions can be used.
|
See the [example below](#frigate-card-conditional-example) for a real-world example of how these conditions can be used.
|
||||||
|
|
||||||
@@ -1316,6 +1317,18 @@ If multiple entries are provided, the results are `AND`ed.
|
|||||||
|
|
||||||
See the [Menu override example below](#frigate-card-menu-override-example) for an illustration.
|
See the [Menu override example below](#frigate-card-menu-override-example) for an illustration.
|
||||||
|
|
||||||
|
### Microphone Conditions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- conditions:
|
||||||
|
microphone:
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
| - | - |
|
||||||
|
| `muted` | Optional: If `true` or `false` the condition is satisfied if the microphone is muted or unmuted respectively. |
|
||||||
|
| `connected` | Optional: If `true` or `false` the condition is satisfied if the microphone is connected or disconnected respectively. |
|
||||||
|
|
||||||
<a name="frigate-card-elements"></a>
|
<a name="frigate-card-elements"></a>
|
||||||
|
|
||||||
## Picture Elements / Menu Customizations
|
## Picture Elements / Menu Customizations
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export class CardElementManager {
|
|||||||
this._api.getFullscreenManager().initialize();
|
this._api.getFullscreenManager().initialize();
|
||||||
this._api.getExpandManager().initialize();
|
this._api.getExpandManager().initialize();
|
||||||
this._api.getMediaLoadedInfoManager().initialize();
|
this._api.getMediaLoadedInfoManager().initialize();
|
||||||
|
this._api.getMicrophoneManager().initialize();
|
||||||
|
|
||||||
// Whether or not the card is in panel mode on the dashboard.
|
// Whether or not the card is in panel mode on the dashboard.
|
||||||
setOrRemoveAttribute(this._element, isCardInPanel(this._element), 'panel');
|
setOrRemoveAttribute(this._element, isCardInPanel(this._element), 'panel');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { copyConfig } from '../config-mgmt';
|
|||||||
import {
|
import {
|
||||||
FrigateCardCondition,
|
FrigateCardCondition,
|
||||||
frigateConditionalSchema,
|
frigateConditionalSchema,
|
||||||
|
MicrophoneConditionState,
|
||||||
OverrideConfigurationKey,
|
OverrideConfigurationKey,
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
ViewDisplayMode,
|
ViewDisplayMode,
|
||||||
@@ -20,6 +21,7 @@ interface ConditionState {
|
|||||||
displayMode?: ViewDisplayMode;
|
displayMode?: ViewDisplayMode;
|
||||||
triggered?: Set<string>;
|
triggered?: Set<string>;
|
||||||
interaction?: boolean;
|
interaction?: boolean;
|
||||||
|
microphone?: MicrophoneConditionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConditionEvaluateRequestEvent extends Event {
|
export class ConditionEvaluateRequestEvent extends Event {
|
||||||
@@ -254,6 +256,13 @@ export class ConditionsManager {
|
|||||||
result &&=
|
result &&=
|
||||||
state.interaction !== undefined && condition.interaction === state.interaction;
|
state.interaction !== undefined && condition.interaction === state.interaction;
|
||||||
}
|
}
|
||||||
|
if (condition.microphone) {
|
||||||
|
result &&=
|
||||||
|
(condition.microphone.connected === undefined ||
|
||||||
|
state.microphone?.connected === condition.microphone.connected) &&
|
||||||
|
(condition.microphone.muted === undefined ||
|
||||||
|
state.microphone?.muted === condition.microphone.muted);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export class MicrophoneManager implements ReadonlyMicrophoneManager {
|
|||||||
this._api = api;
|
this._api = api;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public initialize(): void {
|
||||||
|
this._setConditionState();
|
||||||
|
}
|
||||||
|
|
||||||
public async connect(): Promise<boolean> {
|
public async connect(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
this._stream = await navigator.mediaDevices.getUserMedia({
|
this._stream = await navigator.mediaDevices.getUserMedia({
|
||||||
@@ -43,13 +47,15 @@ export class MicrophoneManager implements ReadonlyMicrophoneManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this._setMute();
|
this._setMute();
|
||||||
|
this._setConditionState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disconnect(): Promise<void> {
|
public disconnect(): void {
|
||||||
this._stream?.getTracks().forEach((track) => track.stop());
|
this._stream?.getTracks().forEach((track) => track.stop());
|
||||||
|
|
||||||
this._stream = undefined;
|
this._stream = undefined;
|
||||||
|
this._setConditionState();
|
||||||
this._api.getCardElementManager().update();
|
this._api.getCardElementManager().update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +68,7 @@ export class MicrophoneManager implements ReadonlyMicrophoneManager {
|
|||||||
|
|
||||||
this._mute = true;
|
this._mute = true;
|
||||||
this._setMute();
|
this._setMute();
|
||||||
|
this._setConditionState();
|
||||||
|
|
||||||
if (!wasMuted) {
|
if (!wasMuted) {
|
||||||
this._callListeners('muted');
|
this._callListeners('muted');
|
||||||
@@ -89,6 +96,7 @@ export class MicrophoneManager implements ReadonlyMicrophoneManager {
|
|||||||
unmute();
|
unmute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._setConditionState();
|
||||||
if (!wasUnmuted) {
|
if (!wasUnmuted) {
|
||||||
this._callListeners('unmuted');
|
this._callListeners('unmuted');
|
||||||
}
|
}
|
||||||
@@ -144,4 +152,13 @@ export class MicrophoneManager implements ReadonlyMicrophoneManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _setConditionState(): void {
|
||||||
|
this._api.getConditionsManager().setState({
|
||||||
|
microphone: {
|
||||||
|
muted: this.isMuted(),
|
||||||
|
connected: this.isConnected(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export interface CardElementAPI {
|
|||||||
getFullscreenManager(): FullscreenManager;
|
getFullscreenManager(): FullscreenManager;
|
||||||
getInteractionManager(): InteractionManager;
|
getInteractionManager(): InteractionManager;
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
|
getMicrophoneManager(): MicrophoneManager;
|
||||||
getQueryStringManager(): QueryStringManager;
|
getQueryStringManager(): QueryStringManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +179,7 @@ export interface CardMessageAPI {
|
|||||||
|
|
||||||
export interface CardMicrophoneAPI {
|
export interface CardMicrophoneAPI {
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
|
getConditionsManager(): ConditionsManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -479,6 +479,12 @@ export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu | MenuSubmenuSelec
|
|||||||
// Custom Element Configuration: Conditions
|
// Custom Element Configuration: Conditions
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
|
|
||||||
|
const microphoneConditionSchema = z.object({
|
||||||
|
connected: z.boolean().optional(),
|
||||||
|
muted: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
export type MicrophoneConditionState = z.infer<typeof microphoneConditionSchema>;
|
||||||
|
|
||||||
export const frigateCardConditionSchema = z.object({
|
export const frigateCardConditionSchema = z.object({
|
||||||
view: z.string().array().optional(),
|
view: z.string().array().optional(),
|
||||||
fullscreen: z.boolean().optional(),
|
fullscreen: z.boolean().optional(),
|
||||||
@@ -490,6 +496,7 @@ export const frigateCardConditionSchema = z.object({
|
|||||||
display_mode: viewDisplayModeSchema.optional(),
|
display_mode: viewDisplayModeSchema.optional(),
|
||||||
triggered: z.string().array().optional(),
|
triggered: z.string().array().optional(),
|
||||||
interaction: z.boolean().optional(),
|
interaction: z.boolean().optional(),
|
||||||
|
microphone: microphoneConditionSchema.optional(),
|
||||||
});
|
});
|
||||||
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
|
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
|
||||||
|
|
||||||
@@ -736,7 +743,7 @@ export type LiveProvider = (typeof LIVE_PROVIDERS)[number];
|
|||||||
|
|
||||||
const microphoneConfigDefault = {
|
const microphoneConfigDefault = {
|
||||||
always_connected: false,
|
always_connected: false,
|
||||||
disconnect_seconds: 60,
|
disconnect_seconds: 90,
|
||||||
mute_after_microphone_mute_seconds: 60,
|
mute_after_microphone_mute_seconds: 60,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ describe('CardElementManager', () => {
|
|||||||
expect(api.getFullscreenManager().initialize).toBeCalled();
|
expect(api.getFullscreenManager().initialize).toBeCalled();
|
||||||
expect(api.getExpandManager().initialize).toBeCalled();
|
expect(api.getExpandManager().initialize).toBeCalled();
|
||||||
expect(api.getMediaLoadedInfoManager().initialize).toBeCalled();
|
expect(api.getMediaLoadedInfoManager().initialize).toBeCalled();
|
||||||
|
expect(api.getMicrophoneManager().initialize).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disconnect', () => {
|
it('should disconnect', () => {
|
||||||
|
|||||||
@@ -377,9 +377,81 @@ describe('ConditionsManager', () => {
|
|||||||
const manager = new ConditionsManager(createCardAPI());
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
const condition: FrigateCardCondition = { interaction: true };
|
const condition: FrigateCardCondition = { interaction: true };
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
manager.setState({ interaction: true })
|
manager.setState({ interaction: true });
|
||||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||||
manager.setState({ interaction: false })
|
manager.setState({ interaction: false });
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('should evaluate conditions with microphone', () => {
|
||||||
|
it('empty', () => {
|
||||||
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
|
const condition: FrigateCardCondition = { microphone: {} };
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||||
|
manager.setState({ microphone: { connected: true } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||||
|
manager.setState({ microphone: { connected: false } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||||
|
manager.setState({ microphone: { muted: true } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||||
|
manager.setState({ microphone: { muted: false } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('connected is true', () => {
|
||||||
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
|
const condition: FrigateCardCondition = { microphone: { connected: true } };
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
|
manager.setState({ microphone: { connected: true } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||||
|
manager.setState({ microphone: { connected: false } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('connected is false', () => {
|
||||||
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
|
const condition: FrigateCardCondition = { microphone: { connected: false } };
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
|
manager.setState({ microphone: { connected: true } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
|
manager.setState({ microphone: { connected: false } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('muted is true', () => {
|
||||||
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
|
const condition: FrigateCardCondition = { microphone: { muted: true } };
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
|
manager.setState({ microphone: { muted: true } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||||
|
manager.setState({ microphone: { muted: false } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('muted is false', () => {
|
||||||
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
|
const condition: FrigateCardCondition = { microphone: { muted: false } };
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
|
manager.setState({ microphone: { muted: true } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
|
manager.setState({ microphone: { muted: false } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('connected and muted', () => {
|
||||||
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
|
const condition: FrigateCardCondition = {
|
||||||
|
microphone: { muted: false, connected: true },
|
||||||
|
};
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
|
manager.setState({ microphone: { muted: true } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
|
manager.setState({ microphone: { muted: false } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
|
manager.setState({ microphone: { connected: false, muted: false } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
||||||
|
manager.setState({ microphone: { connected: true, muted: false } });
|
||||||
|
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ describe('MicrophoneManager', () => {
|
|||||||
expect(manager.isConnected()).toBeTruthy();
|
expect(manager.isConnected()).toBeTruthy();
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(1);
|
expect(api.getCardElementManager().update).toBeCalledTimes(1);
|
||||||
|
|
||||||
await manager.disconnect();
|
manager.disconnect();
|
||||||
expect(manager.isConnected()).toBeFalsy();
|
expect(manager.isConnected()).toBeFalsy();
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(2);
|
expect(api.getCardElementManager().update).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
@@ -202,7 +202,7 @@ describe('MicrophoneManager', () => {
|
|||||||
await manager.connect();
|
await manager.connect();
|
||||||
expect(listener).not.toHaveBeenCalled();
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
|
||||||
await manager.mute();
|
manager.mute();
|
||||||
expect(listener).not.toHaveBeenCalled();
|
expect(listener).not.toHaveBeenCalled();
|
||||||
|
|
||||||
await manager.unmute();
|
await manager.unmute();
|
||||||
@@ -212,7 +212,7 @@ describe('MicrophoneManager', () => {
|
|||||||
await manager.unmute();
|
await manager.unmute();
|
||||||
expect(listener).toHaveBeenCalledTimes(1);
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
await manager.mute();
|
manager.mute();
|
||||||
expect(listener).toHaveBeenCalledTimes(2);
|
expect(listener).toHaveBeenCalledTimes(2);
|
||||||
expect(listener).toHaveBeenLastCalledWith('muted');
|
expect(listener).toHaveBeenLastCalledWith('muted');
|
||||||
|
|
||||||
@@ -221,4 +221,62 @@ describe('MicrophoneManager', () => {
|
|||||||
await manager.unmute();
|
await manager.unmute();
|
||||||
expect(listener).toHaveBeenCalledTimes(2);
|
expect(listener).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should initialize', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new MicrophoneManager(api);
|
||||||
|
|
||||||
|
manager.initialize();
|
||||||
|
expect(api.getConditionsManager().setState).toBeCalledWith({
|
||||||
|
microphone: { connected: false, muted: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set condition state', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new MicrophoneManager(api);
|
||||||
|
navigatorMock.mediaDevices.getUserMedia.mockReturnValue(createMockStream());
|
||||||
|
|
||||||
|
expect(api.getConditionsManager().setState).not.toBeCalled();
|
||||||
|
|
||||||
|
await manager.connect();
|
||||||
|
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
microphone: {
|
||||||
|
connected: true,
|
||||||
|
muted: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await manager.unmute();
|
||||||
|
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
microphone: {
|
||||||
|
connected: true,
|
||||||
|
muted: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
manager.mute();
|
||||||
|
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
microphone: {
|
||||||
|
connected: true,
|
||||||
|
muted: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
manager.disconnect();
|
||||||
|
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
microphone: {
|
||||||
|
connected: false,
|
||||||
|
muted: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ describe('config defaults', () => {
|
|||||||
lazy_unload: [],
|
lazy_unload: [],
|
||||||
microphone: {
|
microphone: {
|
||||||
always_connected: false,
|
always_connected: false,
|
||||||
disconnect_seconds: 60,
|
disconnect_seconds: 90,
|
||||||
mute_after_microphone_mute_seconds: 60,
|
mute_after_microphone_mute_seconds: 60,
|
||||||
},
|
},
|
||||||
preload: false,
|
preload: false,
|
||||||
|
|||||||
+4
-4
@@ -10,10 +10,10 @@ export default defineConfig({
|
|||||||
// Thresholds will automatically be updated as coverage improves to avoid
|
// Thresholds will automatically be updated as coverage improves to avoid
|
||||||
// back-sliding.
|
// back-sliding.
|
||||||
thresholdAutoUpdate: true,
|
thresholdAutoUpdate: true,
|
||||||
statements: 72.66,
|
statements: 72.72,
|
||||||
branches: 61.9,
|
branches: 61.97,
|
||||||
functions: 73.96,
|
functions: 74.01,
|
||||||
lines: 72.56,
|
lines: 72.62,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user