Add native PTZ element.
This commit is contained in:
@@ -803,6 +803,7 @@ elements to add special Frigate card functionality.
|
|||||||
| `custom:frigate-card-menu-submenu` | Add a configurable submenu dropdown. See [configuration below](#frigate-card-menu-submenu).|
|
| `custom:frigate-card-menu-submenu` | Add a configurable submenu dropdown. See [configuration below](#frigate-card-menu-submenu).|
|
||||||
| `custom:frigate-card-menu-submenu-select` | Add a submenu based on a `select` or `input_select`. See [configuration below](#frigate-card-submenu-select).|
|
| `custom:frigate-card-menu-submenu-select` | Add a submenu based on a `select` or `input_select`. See [configuration below](#frigate-card-submenu-select).|
|
||||||
| `custom:frigate-card-conditional` | Restrict a set of elements to only render when the card is showing particular a particular [view](#views). See [configuration below](#frigate-card-conditional).|
|
| `custom:frigate-card-conditional` | Restrict a set of elements to only render when the card is showing particular a particular [view](#views). See [configuration below](#frigate-card-conditional).|
|
||||||
|
| `custom:frigate-card-ptz` | Add a PTZ (Pan Tilt Zoom) controller overlay. See [configuration below](#frigate-card-ptz).|
|
||||||
|
|
||||||
|
|
||||||
**Note**: ℹ️ Manual positioning of custom menu icons or submenus via the `style`
|
**Note**: ℹ️ Manual positioning of custom menu icons or submenus via the `style`
|
||||||
@@ -864,6 +865,19 @@ Parameters for the `custom:frigate-card-conditional` element:
|
|||||||
`elements` | The elements to render. Can be any supported element, include additional condition or custom elements. |
|
`elements` | The elements to render. Can be any supported element, include additional condition or custom elements. |
|
||||||
| `conditions` | A set of conditions that must evaluate to true in order for the elements to be rendered. See [Frigate Card Conditions](#frigate-card-conditions). |
|
| `conditions` | A set of conditions that must evaluate to true in order for the elements to be rendered. See [Frigate Card Conditions](#frigate-card-conditions). |
|
||||||
|
|
||||||
|
#### `custom:frigate-card-ptz`
|
||||||
|
|
||||||
|
Parameters for the `custom:frigate-card-ptz` element:
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
| ------------- | - | -------------------------------------------- |
|
||||||
|
| `type` | | Must be `custom:frigate-card-ptz`. |
|
||||||
|
| `style` | `translate(-50%, -50%)` | Position and style the element using CSS. See [Picture Element styling](https://www.home-assistant.io/dashboards/picture-elements/#how-to-use-the-style-object). |
|
||||||
|
| `mode` | `vertical` | Whether to show a `vertical` or `horizontal` PTZ control. |
|
||||||
|
| `actions_left`, `actions_right`, `actions_up`, `actions_down`, `actions_zoom_in`, `actions_zoom_out`, `actions_home` | The [Home Assistant actions](https://www.home-assistant.io/dashboards/actions/) to call when this icon is interacted with. |
|
||||||
|
| `data_left`, `data_right`, `data_up`, `data_down`, `data_zoom_in`, `data_zoom_out`, `data_home` | Shorthand for a `tap_action` that calls the `service` with the data provided in this argument. Internally, this is just translated into the longer-form `actions_[button]`. If both `actions_X` and `data_X` are specified, `actions_X` takes priority. This is compatible with [AlexxIT's WebRTC Card PTZ configuration](https://github.com/AlexxIT/WebRTC/wiki/PTZ-Config-Examples). |
|
||||||
|
| `service` | | An optional Home Assistant service to call when the `data_` parameters are used. |
|
||||||
|
|
||||||
### Special Actions
|
### Special Actions
|
||||||
|
|
||||||
#### `custom:frigate-card-action`
|
#### `custom:frigate-card-action`
|
||||||
@@ -1663,6 +1677,31 @@ elements:
|
|||||||
state: on
|
state: on
|
||||||
state_not: off
|
state_not: off
|
||||||
mediaLoaded: true
|
mediaLoaded: true
|
||||||
|
# Full form PTZ actions (only left button shown).
|
||||||
|
- type: custom:frigate-card-ptz
|
||||||
|
mode: vertical
|
||||||
|
style:
|
||||||
|
transform: none
|
||||||
|
right: 20px
|
||||||
|
top: 180px
|
||||||
|
actions_left:
|
||||||
|
tap_action:
|
||||||
|
action: call-service
|
||||||
|
service: sonoff.send_command
|
||||||
|
service_data:
|
||||||
|
device: '048123'
|
||||||
|
cmd: left
|
||||||
|
# Equivalent short form PTZ actions (only left button shown)
|
||||||
|
- type: custom:frigate-card-ptz
|
||||||
|
mode: vertical
|
||||||
|
style:
|
||||||
|
transform: none
|
||||||
|
right: 20px
|
||||||
|
top: 180px
|
||||||
|
service: sonoff.send_command
|
||||||
|
data_left:
|
||||||
|
device: '048123'
|
||||||
|
cmd: left
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -2147,7 +2186,7 @@ You can restrict icons to only show for certain [views](#views) using a
|
|||||||
`custom:frigate-card-conditional` element (e.g. PTZ controls)
|
`custom:frigate-card-conditional` element (e.g. PTZ controls)
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Expand: View-based conditions (e.g. PTZ controls)</summary>
|
<summary>Expand: View-based conditions (e.g. custom PTZ controls)</summary>
|
||||||
|
|
||||||
This example shows PTZ icons that call a PTZ service, but only in the `live` view.
|
This example shows PTZ icons that call a PTZ service, but only in the `live` view.
|
||||||
|
|
||||||
@@ -2491,7 +2530,6 @@ overrides:
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
||||||
### Refreshing a static image
|
### Refreshing a static image
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -2532,6 +2570,47 @@ elements:
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
### Using a PTZ picture element
|
||||||
|
|
||||||
|
The card supports a custom PTZ element (`custom:frigate-card-ptz`) to conveniently control pan, tilt and zoom for cameras.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand: Using the native PTZ picture element</summary>
|
||||||
|
|
||||||
|
This example shows the native PTZ element when the `live` or `image` view is displayed and the stream (media) has loaded.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
[...]
|
||||||
|
elements:
|
||||||
|
- type: custom:frigate-card-conditional
|
||||||
|
conditions:
|
||||||
|
mediaLoaded: true
|
||||||
|
view:
|
||||||
|
- live
|
||||||
|
- image
|
||||||
|
elements:
|
||||||
|
- type: custom:frigate-card-ptz
|
||||||
|
mode: horizontal
|
||||||
|
style:
|
||||||
|
transform: none
|
||||||
|
right: 20px
|
||||||
|
top: 180px
|
||||||
|
service: sonoff.send_command
|
||||||
|
data_left:
|
||||||
|
device: '048123'
|
||||||
|
cmd: left
|
||||||
|
data_right:
|
||||||
|
device: '048123'
|
||||||
|
cmd: right
|
||||||
|
data_up:
|
||||||
|
device: '048123'
|
||||||
|
cmd: up
|
||||||
|
data_down:
|
||||||
|
device: '048123'
|
||||||
|
cmd: down
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
<a name="card-updates"></a>
|
<a name="card-updates"></a>
|
||||||
|
|
||||||
### Using `card-mod` to style the card
|
### Using `card-mod` to style the card
|
||||||
|
|||||||
+132
-12
@@ -1,28 +1,39 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers';
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { ConditionState, fetchStateAndEvaluateCondition } from '../card-condition.js';
|
import { ConditionState, fetchStateAndEvaluateCondition } from '../card-condition.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import elementsStyle from '../scss/elements.scss';
|
import elementsStyle from '../scss/elements.scss';
|
||||||
|
import ptzStyle from '../scss/elements-ptz.scss';
|
||||||
import {
|
import {
|
||||||
|
Actions,
|
||||||
|
ActionsConfig,
|
||||||
FrigateCardError,
|
FrigateCardError,
|
||||||
|
FrigateCardPTZConfig,
|
||||||
FrigateConditional,
|
FrigateConditional,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
MenuIcon,
|
MenuIcon,
|
||||||
MenuStateIcon,
|
MenuStateIcon,
|
||||||
MenuSubmenu,
|
MenuSubmenu,
|
||||||
MenuSubmenuSelect,
|
MenuSubmenuSelect,
|
||||||
PictureElements
|
PictureElements,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
import { dispatchFrigateCardErrorEvent } from './message.js';
|
import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||||
|
import { actionHandler } from '../action-handler-directive.js';
|
||||||
|
import {
|
||||||
|
frigateCardHandleActionConfig,
|
||||||
|
frigateCardHasAction,
|
||||||
|
getActionConfigGivenAction,
|
||||||
|
} from '../utils/action.js';
|
||||||
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
|
|
||||||
/* A note on picture element rendering:
|
/* A note on picture element rendering:
|
||||||
*
|
*
|
||||||
@@ -343,14 +354,123 @@ export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuI
|
|||||||
@customElement('frigate-card-menu-submenu-select')
|
@customElement('frigate-card-menu-submenu-select')
|
||||||
export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuIcon<MenuSubmenuSelect> {}
|
export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuIcon<MenuSubmenuSelect> {}
|
||||||
|
|
||||||
|
@customElement('frigate-card-ptz')
|
||||||
|
export class FrigateCardPTZ extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _config: FrigateCardPTZConfig | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the card config.
|
||||||
|
* @param config The configuration.
|
||||||
|
*/
|
||||||
|
public setConfig(config: FrigateCardPTZConfig): void {
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called before each update.
|
||||||
|
*/
|
||||||
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
|
if (changedProps.has('_config')) {
|
||||||
|
this.setAttribute('data-orientation', this._config?.orientation ?? 'vertical');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a PTZ action.
|
||||||
|
* @param ev The actionHandler event.
|
||||||
|
* @param config The action configuration.
|
||||||
|
*/
|
||||||
|
protected _actionHandler(
|
||||||
|
ev: HASSDomEvent<{ action: string }>,
|
||||||
|
config?: ActionsConfig,
|
||||||
|
): void {
|
||||||
|
// Nothing else has the configuration for this action, so don't let it
|
||||||
|
// propagate further.
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
const interaction: string = ev.detail.action;
|
||||||
|
const action = getActionConfigGivenAction(interaction, config);
|
||||||
|
if (config && action && this.hass) {
|
||||||
|
frigateCardHandleActionConfig(this, this.hass, config, interaction, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the elements.
|
||||||
|
* @returns A rendered template or void.
|
||||||
|
*/
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this._config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const renderIcon = (
|
||||||
|
name: string,
|
||||||
|
icon: string,
|
||||||
|
actions?: Actions,
|
||||||
|
): TemplateResult => {
|
||||||
|
const hasHold = frigateCardHasAction(actions?.hold_action);
|
||||||
|
const hasDoubleClick = frigateCardHasAction(actions?.double_tap_action);
|
||||||
|
const classes = {
|
||||||
|
[name]: true,
|
||||||
|
disabled: !actions,
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`<ha-icon
|
||||||
|
class=${classMap(classes)}
|
||||||
|
icon=${icon}
|
||||||
|
.actionHandler=${actionHandler({
|
||||||
|
hasHold: hasHold,
|
||||||
|
hasDoubleClick: hasDoubleClick,
|
||||||
|
})}
|
||||||
|
.title=${localize(`elements.ptz.${name}`)}
|
||||||
|
@action=${(ev) => this._actionHandler(ev, actions)}
|
||||||
|
></ha-icon>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return html` <div class="ptz">
|
||||||
|
<div class="ptz-move">
|
||||||
|
${renderIcon('right', 'mdi:arrow-right', this._config.actions_right)}
|
||||||
|
${renderIcon('left', 'mdi:arrow-left', this._config.actions_left)}
|
||||||
|
${renderIcon('up', 'mdi:arrow-up', this._config.actions_up)}
|
||||||
|
${renderIcon('down', 'mdi:arrow-down', this._config.actions_down)}
|
||||||
|
</div>
|
||||||
|
${this._config.actions_zoom_in || this._config.actions_zoom_out
|
||||||
|
? html` <div class="ptz-zoom">
|
||||||
|
${renderIcon('zoom_in', 'mdi:plus', this._config.actions_zoom_in)}
|
||||||
|
${renderIcon('zoom_out', 'mdi:minus', this._config.actions_zoom_out)}
|
||||||
|
</div>`
|
||||||
|
: html``}
|
||||||
|
${this._config.actions_home
|
||||||
|
? html`
|
||||||
|
<div class="ptz-home">
|
||||||
|
${renderIcon('home', 'mdi:home', this._config.actions_home)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html``}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return compiled CSS styles.
|
||||||
|
*/
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(ptzStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"frigate-card-conditional": FrigateCardElementsConditional
|
'frigate-card-conditional': FrigateCardElementsConditional;
|
||||||
"frigate-card-elements": FrigateCardElements
|
'frigate-card-elements': FrigateCardElements;
|
||||||
"frigate-card-menu-submenu-select": FrigateCardElementsMenuSubmenuSelect
|
'frigate-card-menu-submenu-select': FrigateCardElementsMenuSubmenuSelect;
|
||||||
"frigate-card-menu-submenu": FrigateCardElementsMenuSubmenu
|
'frigate-card-menu-submenu': FrigateCardElementsMenuSubmenu;
|
||||||
"frigate-card-menu-state-icon": FrigateCardElementsMenuStateIcon
|
'frigate-card-menu-state-icon': FrigateCardElementsMenuStateIcon;
|
||||||
"frigate-card-menu-icon": FrigateCardElementsMenuIcon
|
'frigate-card-menu-icon': FrigateCardElementsMenuIcon;
|
||||||
"frigate-card-elements-core": FrigateCardElementsCore
|
'frigate-card-elements-core': FrigateCardElementsCore;
|
||||||
}
|
'frigate-card-ptz': FrigateCardPTZ;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,5 +372,16 @@
|
|||||||
"no_thumbnail": "No thumbnail available",
|
"no_thumbnail": "No thumbnail available",
|
||||||
"retain_indefinitely": "Event will be indefinitely retained",
|
"retain_indefinitely": "Event will be indefinitely retained",
|
||||||
"timeline": "See event in timeline"
|
"timeline": "See event in timeline"
|
||||||
|
},
|
||||||
|
"elements": {
|
||||||
|
"ptz": {
|
||||||
|
"up": "Up",
|
||||||
|
"down": "Down",
|
||||||
|
"left": "Left",
|
||||||
|
"right": "Right",
|
||||||
|
"zoom_in": "Zoom In",
|
||||||
|
"zoom_out": "Zoom Out",
|
||||||
|
"home": "Home"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// Modified from / inspired by:
|
||||||
|
// https://github.com/AlexxIT/WebRTC/blob/master/custom_components/webrtc/www/webrtc-camera.js
|
||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
--frigate-card-ptz-icon-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************
|
||||||
|
* Main Containers
|
||||||
|
*****************/
|
||||||
|
.ptz {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
color: var(--light-primary-color);
|
||||||
|
opacity: 0.4;
|
||||||
|
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([data-orientation='vertical']) .ptz {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
:host([data-orientation='horizontal']) .ptz {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ptz:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([data-orientation='vertical']) .ptz div {
|
||||||
|
width: calc(var(--frigate-card-ptz-icon-size) * 3);
|
||||||
|
}
|
||||||
|
:host([data-orientation='horizontal']) .ptz div {
|
||||||
|
height: calc(var(--frigate-card-ptz-icon-size) * 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ptz-move,
|
||||||
|
.ptz-zoom,
|
||||||
|
.ptz-home {
|
||||||
|
position: relative;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ptz-move {
|
||||||
|
height: calc(var(--frigate-card-ptz-icon-size) * 3);
|
||||||
|
width: calc(var(--frigate-card-ptz-icon-size) * 3);
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([data-orientation='horizontal']) .ptz .ptz-zoom,
|
||||||
|
:host([data-orientation='horizontal']) .ptz .ptz-home {
|
||||||
|
width: calc(var(--frigate-card-ptz-icon-size) * 1.5);
|
||||||
|
}
|
||||||
|
:host([data-orientation='vertical']) .ptz .ptz-zoom,
|
||||||
|
:host([data-orientation='vertical']) .ptz .ptz-home {
|
||||||
|
height: calc(var(--frigate-card-ptz-icon-size) * 1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ptz-zoom,
|
||||||
|
.ptz-home {
|
||||||
|
border-radius: var(--ha-card-border-radius, 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/***********
|
||||||
|
* PTZ Icons
|
||||||
|
***********/
|
||||||
|
ha-icon {
|
||||||
|
position: absolute;
|
||||||
|
--mdc-icon-size: var(--frigate-card-ptz-icon-size);
|
||||||
|
}
|
||||||
|
ha-icon:not(.disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.disabled {
|
||||||
|
color: var(--disabled-text-color);
|
||||||
|
}
|
||||||
|
.up {
|
||||||
|
top: 5px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.down {
|
||||||
|
bottom: 5px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.left {
|
||||||
|
left: 5px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
right: 5px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([data-orientation='vertical']) .zoom_in {
|
||||||
|
right: 5px;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
:host([data-orientation='vertical']) .zoom_out {
|
||||||
|
left: 5px;
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
:host([data-orientation='horizontal']) .zoom_in {
|
||||||
|
left: 50%;
|
||||||
|
top: 5px;
|
||||||
|
}
|
||||||
|
:host([data-orientation='horizontal']) .zoom_out {
|
||||||
|
left: 50%;
|
||||||
|
bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([data-orientation='vertical']) .zoom_in,
|
||||||
|
:host([data-orientation='vertical']) .zoom_out {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
:host([data-orientation='horizontal']) .zoom_in,
|
||||||
|
:host([data-orientation='horizontal']) .zoom_out {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home {
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-50%);
|
||||||
|
}
|
||||||
@@ -499,6 +499,47 @@ const frigateConditionalSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type FrigateConditional = z.infer<typeof frigateConditionalSchema>;
|
export type FrigateConditional = z.infer<typeof frigateConditionalSchema>;
|
||||||
|
|
||||||
|
const frigateCardPTZSchema = z.preprocess(
|
||||||
|
// To avoid lots of YAML duplication, provide an easy way to just specify the
|
||||||
|
// service data as actions for each PTZ icon, and it will be preprocessed into
|
||||||
|
// the full form. This also provides compatability with the AlexIT/WebRTC PTZ
|
||||||
|
// configuration.
|
||||||
|
(data) => {
|
||||||
|
if (!data || typeof data !== 'object' || !data['service']) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
const out = { ...data };
|
||||||
|
['left', 'right', 'up', 'down', 'zoom_in', 'zoom_out', 'home'].forEach((name) => {
|
||||||
|
if (`data_${name}` in data && !(`actions_${name}` in data)) {
|
||||||
|
out[`actions_${name}`] = {
|
||||||
|
tap_action: {
|
||||||
|
action: 'call-service',
|
||||||
|
service: data['service'],
|
||||||
|
service_data: data[`data_${name}`],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
delete out[`data_${name}`];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
z.object({
|
||||||
|
type: z.literal('custom:frigate-card-ptz'),
|
||||||
|
style: z.object({}).passthrough().optional(),
|
||||||
|
orientation: z.enum(['vertical', 'horizontal']).default('vertical').optional(),
|
||||||
|
service: z.string().optional(),
|
||||||
|
actions_left: actionsBaseSchema.optional(),
|
||||||
|
actions_right: actionsBaseSchema.optional(),
|
||||||
|
actions_up: actionsBaseSchema.optional(),
|
||||||
|
actions_down: actionsBaseSchema.optional(),
|
||||||
|
actions_zoom_in: actionsBaseSchema.optional(),
|
||||||
|
actions_zoom_out: actionsBaseSchema.optional(),
|
||||||
|
actions_home: actionsBaseSchema.optional(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export type FrigateCardPTZConfig = z.infer<typeof frigateCardPTZSchema>;
|
||||||
|
|
||||||
// Cannot use discriminatedUnion since customSchema uses a superRefine, which
|
// Cannot use discriminatedUnion since customSchema uses a superRefine, which
|
||||||
// causes false rejections.
|
// causes false rejections.
|
||||||
const pictureElementSchema = z.union([
|
const pictureElementSchema = z.union([
|
||||||
@@ -507,6 +548,7 @@ const pictureElementSchema = z.union([
|
|||||||
menuSubmenuSchema,
|
menuSubmenuSchema,
|
||||||
menuSubmenuSelectSchema,
|
menuSubmenuSelectSchema,
|
||||||
frigateConditionalSchema,
|
frigateConditionalSchema,
|
||||||
|
frigateCardPTZSchema,
|
||||||
stateBadgeIconSchema,
|
stateBadgeIconSchema,
|
||||||
stateIconSchema,
|
stateIconSchema,
|
||||||
stateLabelSchema,
|
stateLabelSchema,
|
||||||
|
|||||||
Reference in New Issue
Block a user