Repository: dermotduffy/frigate-hass-card (ts)

Issue:
Card wide action not working
Release: v5.2.0
Browser and Operating System: Microsoft Edge / Windows 10 Home v22H2

I have defined a card wide action to trigger the fullscreen mode when the card is tapped based on the example code provide in the README. However, nothing happens when the card is tapped, i.e., clicked on with the mouse pointer.

Here is the yaml:

```
square: false
type: grid
cards:
  - type: custom:frigate-card
    cameras:
      - camera_entity: camera.front_south
        live_provider: image
        dependencies:
          cameras:
            - front_south_sub
      - camera_entity: camera.front_south
        live_provider: go2rtc
        id: front_south_sub
        hide: true
        go2rtc:
          modes:
            - mse
    menu:
      buttons:
        frigate:
          enabled: false
    view:
      actions:
        double_tap_action:
          action: custom:frigate-card-action
          frigate_card_action: fullscreen
    automations:
      - conditions:
          fullscreen: true
        actions:
          - action: custom:frigate-card-action
            frigate_card_action: live_substream_on
        actions_not:
          - action: custom:frigate-card-action
            frigate_card_action: live_substream_off
columns: 3
```

Here's the diagnostic that results if I enable the Frigate button and hold it down. Enabling the Frigate button does not fix the issue.

```
{
  "ha_version": "2024.3.0",
  "card_version": "5.2.0",
  "browser": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0",
  "date": "2024-03-14T13:41:43.882Z",
  "frigate_version": {
    "92f82476c801e2628665f993d5257a32": "5.0.1/0.13.2-6476f8a"
  },
  "lang": "en",
  "timezone": "America/New_York",
  "git": {
    "build_version": "5.2.0-HEAD+g69249b6",
    "build_date": "Fri, 23 Jun 2023 15:26:26 GMT",
    "commit_date": "Thu, 22 Jun 2023 09:21:26 -0600"
  },
  "config": {
    "type": "custom:frigate-card",
    "cameras": [
      {
        "camera_entity": "camera.front_south",
        "live_provider": "image",
        "dependencies": {
          "cameras": [
            "front_south_sub"
          ]
        }
      },
      {
        "camera_entity": "camera.front_south",
        "live_provider": "go2rtc",
        "id": "front_south_sub",
        "hide": true,
        "go2rtc": {
          "modes": [
            "mse"
          ]
        }
      }
    ],
    "menu": {
      "buttons": {
        "frigate": {
          "enabled": true
        }
      }
    },
    "view": {
      "actions": {
        "double_tap_action": {
          "action": "custom:frigate-card-action",
          "frigate_card_action": "fullscreen"
        }
      }
    },
    "automations": [
      {
        "conditions": {
          "fullscreen": true
        },
        "actions": [
          {
            "action": "custom:frigate-card-action",
            "frigate_card_action": "live_substream_on"
          }
        ],
        "actions_not": [
          {
            "action": "custom:frigate-card-action",
            "frigate_card_action": "live_substream_off"
          }
        ]
      }
    ]
  }
}
```

Pull request description:
Fix card-wide actions
* Closes #1402 

Patch:
diff --git a/src/card-controller/actions-manager.ts b/src/card-controller/actions-manager.ts
index 89060699..69c54ab4 100644
--- a/src/card-controller/actions-manager.ts
+++ b/src/card-controller/actions-manager.ts
@@ -1,3 +1,4 @@
+import { z } from 'zod';
 import {
   Actions,
   ActionsConfig,
@@ -13,6 +14,15 @@ import {
 import { getStreamCameraID } from '../utils/substream.js';
 import { CardActionsManagerAPI } from './types.js';
 
+const interactionSchema = z.object({
+  action: z.enum(['tap', 'double_tap', 'hold', 'start_tap', 'end_tap']),
+});
+export type Interaction = z.infer<typeof interactionSchema>;
+
+const interactionEventSchema = z.object({
+  detail: interactionSchema,
+});
+
 export class ActionsManager {
   protected _api: CardActionsManagerAPI;
 
@@ -49,7 +59,12 @@ export class ActionsManager {
   /**
    * Handle an human interaction called on an element (e.g. 'tap').
    */
-  public handleInteraction(interaction: string): void {
+  public handleInteractionEvent = (ev: Event): void => {
+    const result = interactionEventSchema.safeParse(ev);
+    if (!result.success) {
+      return;
+    }
+    const interaction = result.data.detail.action;
     const hass = this._api.getHASSManager().getHASS();
     const config = this.getMergedActions();
     const actionConfig = getActionConfigGivenAction(interaction, config);
@@ -70,12 +85,11 @@ export class ActionsManager {
         actionConfig,
       );
     }
-  }
+  };
 
   public handleActionEvent = (ev: Event): void => {
     if (!('detail' in ev)) {
-      // The event may not actually be a CustomEvent object, but may still have a
-      // detail field. See:
+      // The event may not be a CustomEvent object, see:
       // https://github.com/custom-cards/custom-card-helpers/blob/master/src/fire-event.ts#L70
       return;
     }
diff --git a/src/card-controller/card-element-manager.ts b/src/card-controller/card-element-manager.ts
index 38599fd2..1b442f1e 100644
--- a/src/card-controller/card-element-manager.ts
+++ b/src/card-controller/card-element-manager.ts
@@ -77,7 +77,11 @@ export class CardElementManager {
       this._api.getActionsManager().handleActionEvent,
     );
     this._element.addEventListener(
-      '@action',
+      'action',
+      this._api.getActionsManager().handleInteractionEvent,
+    );
+    this._element.addEventListener(
+      'action',
       this._api.getInteractionManager().reportInteraction,
     );
 
@@ -123,7 +127,11 @@ export class CardElementManager {
       this._api.getActionsManager().handleActionEvent,
     );
     this._element.removeEventListener(
-      '@action',
+      'action',
+      this._api.getActionsManager().handleInteractionEvent,
+    );
+    this._element.removeEventListener(
+      'action',
       this._api.getInteractionManager().reportInteraction,
     );
 


Test patch:
diff --git a/tests/card-controller/actions-manager.test.ts b/tests/card-controller/actions-manager.test.ts
index 02742759..8b11b640 100644
--- a/tests/card-controller/actions-manager.test.ts
+++ b/tests/card-controller/actions-manager.test.ts
@@ -14,7 +14,7 @@ import {
   frigateCardHandleActionConfig,
   getActionConfigGivenAction,
 } from '../../src/utils/action.js';
-import { ActionsManager } from '../../src/card-controller/actions-manager';
+import { ActionsManager, Interaction } from '../../src/card-controller/actions-manager';
 import {
   createCardAPI,
   createConfig,
@@ -165,7 +165,9 @@ describe('ActionsManager.handleInteraction', () => {
     };
     vi.mocked(getActionConfigGivenAction).mockReturnValue(actionForThisInteraction);
 
-    manager.handleInteraction('tap');
+    manager.handleInteractionEvent(
+      new CustomEvent<Interaction>('event', { detail: { action: 'tap' } }),
+    );
 
     expect(frigateCardHandleActionConfig).toBeCalledWith(
       element,
@@ -176,13 +178,29 @@ describe('ActionsManager.handleInteraction', () => {
     );
   });
 
-  it('should not handle interaction', () => {
+  it('should not handle interaction without hass', () => {
     const api = createCardAPI();
     const manager = new ActionsManager(api);
     vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
 
     // No values of hass.
-    manager.handleInteraction('tap');
+    manager.handleInteractionEvent(
+      new CustomEvent<Interaction>('event', { detail: { action: 'tap' } }),
+    );
+    expect(frigateCardHandleActionConfig).not.toBeCalledWith();
+  });
+
+  it('should not handle malformed interaction', () => {
+    const api = createCardAPI();
+    const manager = new ActionsManager(api);
+
+    manager.handleInteractionEvent(
+      new CustomEvent<Interaction>('event', {
+
+        // Malformed interaction type.
+        detail: { action: 'double_finger_snap' } as unknown as Interaction,
+      }),
+    );
     expect(frigateCardHandleActionConfig).not.toBeCalledWith();
   });
 });
diff --git a/tests/card-controller/card-element-manager.test.ts b/tests/card-controller/card-element-manager.test.ts
index c378ab10..82f9392b 100644
--- a/tests/card-controller/card-element-manager.test.ts
+++ b/tests/card-controller/card-element-manager.test.ts
@@ -122,7 +122,11 @@ describe('CardElementManager', () => {
       api.getActionsManager().handleActionEvent,
     );
     expect(addEventListener).toBeCalledWith(
-      '@action',
+      'action',
+      api.getActionsManager().handleInteractionEvent,
+    );
+    expect(addEventListener).toBeCalledWith(
+      'action',
       api.getInteractionManager().reportInteraction,
     );
     expect(windowAddEventListener).toBeCalledWith('location-changed', expect.anything());
@@ -169,7 +173,11 @@ describe('CardElementManager', () => {
       api.getActionsManager().handleActionEvent,
     );
     expect(removeEventListener).toBeCalledWith(
-      '@action',
+      'action',
+      api.getActionsManager().handleInteractionEvent,
+    );
+    expect(removeEventListener).toBeCalledWith(
+      'action',
       api.getInteractionManager().reportInteraction,
     );
     expect(windowRemoveEventListener).toBeCalledWith(