fix: Use HA style and coloring for all icons (#1790)
This commit is contained in:
@@ -334,6 +334,7 @@ cameras:
|
||||
position:
|
||||
x: 50
|
||||
y: 50
|
||||
always_error_if_entity_unavailable: false
|
||||
- camera_entity: camera.entrance
|
||||
icon: 'mdi:car'
|
||||
title: 'Front entrance'
|
||||
|
||||
@@ -58,7 +58,6 @@ import {
|
||||
RecordingSegmentsQueryResultsMap,
|
||||
} from '../types';
|
||||
import { getDefaultGo2RTCEndpoint } from '../utils/go2rtc-endpoint';
|
||||
import frigateLogo from './assets/frigate.svg';
|
||||
import { FrigateCamera, isBirdseye } from './camera';
|
||||
import { FrigateEventWatcher } from './event-watcher';
|
||||
import { FrigateViewMediaFactory } from './media';
|
||||
@@ -912,8 +911,8 @@ export class FrigateCameraManagerEngine
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
): CameraManagerCameraMetadata {
|
||||
const metadata = super.getCameraMetadata(hass, cameraConfig);
|
||||
return {
|
||||
...super.getCameraMetadata(hass, cameraConfig),
|
||||
title:
|
||||
cameraConfig.title ??
|
||||
getEntityTitle(hass, cameraConfig.camera_entity) ??
|
||||
@@ -921,8 +920,7 @@ export class FrigateCameraManagerEngine
|
||||
prettifyTitle(cameraConfig.frigate?.camera_name) ??
|
||||
cameraConfig.id ??
|
||||
'',
|
||||
icon: metadata.icon,
|
||||
engineLogo: frigateLogo,
|
||||
engineIcon: 'frigate',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/st
|
||||
import { PTZAction } from '../../config/ptz';
|
||||
import { ActionPhase, CameraConfig } from '../../config/types';
|
||||
import { ExtendedHomeAssistant } from '../../types';
|
||||
import { getEntityIcon, getEntityTitle } from '../../utils/ha';
|
||||
import { getEntityTitle } from '../../utils/ha';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { Camera } from '../camera';
|
||||
import { Capabilities } from '../capabilities';
|
||||
@@ -202,9 +202,11 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
getEntityTitle(hass, cameraConfig.webrtc_card?.entity) ??
|
||||
cameraConfig.id ??
|
||||
'',
|
||||
icon:
|
||||
cameraConfig?.icon ??
|
||||
(cameraEntity ? getEntityIcon(hass, cameraEntity, 'mdi:video') : 'mdi:video'),
|
||||
icon: {
|
||||
entity: cameraEntity ?? undefined,
|
||||
icon: cameraConfig.icon,
|
||||
fallback: 'mdi:video',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
QueryReturnType,
|
||||
} from '../types';
|
||||
import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
|
||||
import motioneyeLogo from './assets/motioneye.svg';
|
||||
import { MotionEyeCamera } from './camera';
|
||||
import { MotionEyeEventQueryResults } from './types';
|
||||
|
||||
@@ -420,10 +419,9 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
): CameraManagerCameraMetadata {
|
||||
const metadata = super.getCameraMetadata(hass, cameraConfig);
|
||||
return {
|
||||
...metadata,
|
||||
engineLogo: motioneyeLogo,
|
||||
...super.getCameraMetadata(hass, cameraConfig),
|
||||
engineIcon: 'motioneye',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
QueryReturnType,
|
||||
} from '../types';
|
||||
import { getPTZCapabilitiesFromCameraConfig } from '../utils/ptz';
|
||||
import reolinkLogo from './assets/reolink.svg';
|
||||
import { ReolinkCamera } from './camera';
|
||||
import { BrowseMediaReolinkCameraMetadata, ReolinkEventQueryResults } from './types';
|
||||
|
||||
@@ -395,10 +394,9 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
): CameraManagerCameraMetadata {
|
||||
const metadata = super.getCameraMetadata(hass, cameraConfig);
|
||||
return {
|
||||
...metadata,
|
||||
engineLogo: reolinkLogo,
|
||||
...super.getCameraMetadata(hass, cameraConfig),
|
||||
engineIcon: 'reolink',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CapabilityKey } from '../types';
|
||||
import { CapabilityKey, Icon } from '../types';
|
||||
import { FrigateCardView, SSLCiphers } from '../config/types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
|
||||
@@ -106,8 +106,10 @@ export interface CameraManagerMediaCapabilities {
|
||||
|
||||
export interface CameraManagerCameraMetadata {
|
||||
title: string;
|
||||
icon: string;
|
||||
engineLogo?: string;
|
||||
icon: Icon;
|
||||
|
||||
// Engine icon is just a string since it will never be entity-derived.
|
||||
engineIcon?: string;
|
||||
}
|
||||
|
||||
export interface CameraEndpointsContext {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { localize } from '../localize/localize';
|
||||
import { errorToConsole } from '../utils/basic';
|
||||
import { Entity } from '../utils/ha/registry/entity/types';
|
||||
import { supportsFeature } from '../utils/ha/update';
|
||||
import { supportsFeature } from '../utils/ha';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { ViewMediaClassifier } from '../view/media-classifier';
|
||||
import { CardMediaPlayerAPI } from './types';
|
||||
|
||||
@@ -45,7 +45,7 @@ export class StatusBarItemManager {
|
||||
const cameraMetadata = options?.view
|
||||
? options?.cameraManager?.getCameraMetadata(options?.view?.camera)
|
||||
: null;
|
||||
const engineLogoIcon = cameraMetadata?.engineLogo ?? null;
|
||||
const engineIcon = cameraMetadata?.engineIcon ?? null;
|
||||
const title = options?.view?.is('live')
|
||||
? cameraMetadata?.title ?? null
|
||||
: options?.view?.isViewerView()
|
||||
@@ -99,11 +99,11 @@ export class StatusBarItemManager {
|
||||
]
|
||||
: []),
|
||||
|
||||
...(engineLogoIcon
|
||||
...(engineIcon
|
||||
? [
|
||||
{
|
||||
type: 'custom:frigate-card-status-bar-image' as const,
|
||||
image: engineLogoIcon,
|
||||
type: 'custom:frigate-card-status-bar-icon' as const,
|
||||
icon: engineIcon,
|
||||
...options?.statusConfig?.items.engine,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import frigateSVG from '../camera-manager/frigate/assets/frigate.svg';
|
||||
import motioneyeSVG from '../camera-manager/motioneye/assets/motioneye.svg';
|
||||
import reolinkSVG from '../camera-manager/reolink/assets/reolink.svg';
|
||||
import { Icon } from '../types';
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
|
||||
export class IconController {
|
||||
public getCustomIcon(icon?: Icon): string | null {
|
||||
switch (icon?.icon) {
|
||||
case 'frigate':
|
||||
return frigateSVG;
|
||||
case 'motioneye':
|
||||
return motioneyeSVG;
|
||||
case 'reolink':
|
||||
return reolinkSVG;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public createStateObjectForStateBadge(
|
||||
hass: HomeAssistant,
|
||||
entityID: string,
|
||||
): HassEntity | null {
|
||||
if (!hass.states[entityID]) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...hass.states[entityID],
|
||||
attributes: {
|
||||
...hass.states[entityID].attributes,
|
||||
|
||||
// State badge is the only available component that will allow the
|
||||
// Home Assistant frontend to correctly color based on the state, but
|
||||
// it also will render an image (instead of an icon) if one is present
|
||||
// in the attributes. By overriding the below attributes, we avoid
|
||||
// that behavior.
|
||||
entity_picture: undefined,
|
||||
entity_picture_local: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from '../utils/action';
|
||||
import { isTruthy } from '../utils/basic';
|
||||
import { isBeingCasted } from '../utils/casting';
|
||||
import { getEntityIcon, getEntityTitle } from '../utils/ha';
|
||||
import { getEntityTitle } from '../utils/ha';
|
||||
import { getPTZTarget } from '../utils/ptz';
|
||||
import { getStreamCameraID, hasSubstream } from '../utils/substream';
|
||||
import { View } from '../view/view';
|
||||
@@ -134,30 +134,27 @@ export class MenuButtonController {
|
||||
// current view for a less surprising UX.
|
||||
const menuCameraIDs = cameraManager.getStore().getCameraIDsWithCapability('menu');
|
||||
if (menuCameraIDs.size > 1) {
|
||||
const menuItems = Array.from(
|
||||
cameraManager.getStore().getCameraConfigEntries(menuCameraIDs),
|
||||
([cameraID, config]) => {
|
||||
const action = createCameraAction('camera_select', cameraID);
|
||||
const metadata = cameraManager.getCameraMetadata(cameraID);
|
||||
const submenuItems = Array.from(menuCameraIDs, (cameraID) => {
|
||||
const action = createCameraAction('camera_select', cameraID);
|
||||
const metadata = cameraManager.getCameraMetadata(cameraID);
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
icon: metadata?.icon,
|
||||
entity: config.camera_entity,
|
||||
state_color: true,
|
||||
title: metadata?.title,
|
||||
selected: view?.camera === cameraID,
|
||||
...(action && { tap_action: action }),
|
||||
};
|
||||
},
|
||||
);
|
||||
return {
|
||||
enabled: true,
|
||||
icon: metadata?.icon.icon,
|
||||
entity: metadata?.icon.entity,
|
||||
state_color: true,
|
||||
title: metadata?.title,
|
||||
selected: view?.camera === cameraID,
|
||||
...(action && { tap_action: action }),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
icon: 'mdi:video-switch',
|
||||
...config.menu.buttons.cameras,
|
||||
type: 'custom:frigate-card-menu-submenu',
|
||||
title: localize('config.menu.buttons.cameras'),
|
||||
items: menuItems,
|
||||
items: submenuItems,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -201,11 +198,10 @@ export class MenuButtonController {
|
||||
const menuItems = Array.from(streams, (streamID) => {
|
||||
const action = createCameraAction('live_substream_select', streamID);
|
||||
const metadata = cameraManager.getCameraMetadata(streamID) ?? undefined;
|
||||
const cameraConfig = cameraManager.getStore().getCameraConfig(streamID);
|
||||
return {
|
||||
enabled: true,
|
||||
icon: metadata?.icon,
|
||||
entity: cameraConfig?.camera_entity,
|
||||
icon: metadata?.icon.icon,
|
||||
entity: metadata?.icon.entity,
|
||||
state_color: true,
|
||||
title: metadata?.title,
|
||||
selected: substreamAwareCameraID === streamID,
|
||||
@@ -465,7 +461,6 @@ export class MenuButtonController {
|
||||
return {
|
||||
enabled: true,
|
||||
selected: false,
|
||||
icon: getEntityIcon(hass, playerEntityID),
|
||||
entity: playerEntityID,
|
||||
state_color: false,
|
||||
title: title,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HASSDomEvent, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { HASSDomEvent } from '@dermotduffy/custom-card-helpers';
|
||||
import { LitElement } from 'lit';
|
||||
import { orderBy } from 'lodash-es';
|
||||
import { dispatchActionExecutionRequest } from '../card-controller/actions/utils/execution-request.js';
|
||||
@@ -9,13 +9,11 @@ import {
|
||||
type MenuConfig,
|
||||
type MenuItem,
|
||||
} from '../config/types.js';
|
||||
import { StateParameters } from '../types.js';
|
||||
import {
|
||||
convertActionToCardCustomAction,
|
||||
getActionConfigGivenAction,
|
||||
} from '../utils/action';
|
||||
import { arrayify, isTruthy, setOrRemoveAttribute } from '../utils/basic.js';
|
||||
import { refreshDynamicStateParameters } from '../utils/ha/index.js';
|
||||
|
||||
export class MenuController {
|
||||
protected _host: LitElement;
|
||||
@@ -158,13 +156,6 @@ export class MenuController {
|
||||
}
|
||||
}
|
||||
|
||||
public getFreshButtonState(hass: HomeAssistant, button: MenuItem): StateParameters {
|
||||
const stateParameters = { ...button } as StateParameters;
|
||||
return hass && button.type === 'custom:frigate-card-menu-state-icon'
|
||||
? refreshDynamicStateParameters(hass, stateParameters)
|
||||
: stateParameters;
|
||||
}
|
||||
|
||||
protected _sortButtons(): void {
|
||||
this._buttons = orderBy(
|
||||
this._buttons,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { localize } from '../localize/localize';
|
||||
import datePickerStyle from '../scss/date-picker.scss';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action';
|
||||
import { dispatchFrigateCardEvent } from '../utils/basic';
|
||||
import './icon';
|
||||
|
||||
export interface DatePickerEvent {
|
||||
date: Date | null;
|
||||
@@ -44,16 +45,16 @@ export class FrigateCardDatePicker extends LitElement {
|
||||
@input=${() => changed()}
|
||||
@change=${() => changed()}
|
||||
/>
|
||||
<ha-icon
|
||||
<frigate-card-icon
|
||||
aria-label="${localize('timeline.select_date')}"
|
||||
title="${localize('timeline.select_date')}"
|
||||
.icon=${this.icon ?? `mdi:calendar-search`}
|
||||
.icon=${{ icon: this.icon ?? `mdi:calendar-search` }}
|
||||
@click=${(ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
this._refInput.value?.showPicker();
|
||||
}}
|
||||
>
|
||||
</ha-icon>`;
|
||||
</frigate-card-icon>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -14,6 +14,7 @@ import drawerInjectStyle from '../scss/drawer-inject.scss';
|
||||
import drawerStyle from '../scss/drawer.scss';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action';
|
||||
import { getChildrenFromElement, isHoverableDevice } from '../utils/basic';
|
||||
import './icon';
|
||||
|
||||
export interface DrawerIcons {
|
||||
open?: string;
|
||||
@@ -121,11 +122,13 @@ export class FrigateCardDrawer extends LitElement {
|
||||
this.open = !this.open;
|
||||
}}
|
||||
>
|
||||
<ha-icon
|
||||
<frigate-card-icon
|
||||
class="control"
|
||||
icon="${this.open
|
||||
? this.icons?.open ?? 'mdi:menu-open'
|
||||
: this.icons?.closed ?? 'mdi:menu'}"
|
||||
.icon="${{
|
||||
icon: this.open
|
||||
? this.icons?.open ?? 'mdi:menu-open'
|
||||
: this.icons?.closed ?? 'mdi:menu',
|
||||
}}"
|
||||
@mouseenter=${() => {
|
||||
// Only open the drawer on mousenter when the device
|
||||
// supports hover (otherwise iOS may end up passing on
|
||||
@@ -136,7 +139,7 @@ export class FrigateCardDrawer extends LitElement {
|
||||
}
|
||||
}}
|
||||
>
|
||||
</ha-icon>
|
||||
</frigate-card-icon>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { IconController } from '../components-lib/icon-controller';
|
||||
import iconStyle from '../scss/icon.scss';
|
||||
import { Icon } from '../types';
|
||||
|
||||
@customElement('frigate-card-icon')
|
||||
export class FrigateCardIcon extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public icon?: Icon;
|
||||
|
||||
private _controller = new IconController();
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const customIconURL = this._controller.getCustomIcon(this.icon);
|
||||
if (customIconURL) {
|
||||
return html`<img src="${customIconURL}" />`;
|
||||
}
|
||||
if (this.icon?.icon) {
|
||||
return html`<ha-icon icon="${this.icon.icon}"></ha-icon>`;
|
||||
}
|
||||
if (this.hass && this.icon?.entity) {
|
||||
const stateObj = this._controller.createStateObjectForStateBadge(
|
||||
this.hass,
|
||||
this.icon.entity,
|
||||
);
|
||||
if (stateObj) {
|
||||
return html`<state-badge
|
||||
.stateColor=${this.icon.stateColor ?? true}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></state-badge>`;
|
||||
}
|
||||
}
|
||||
if (this.icon?.fallback) {
|
||||
return html`<ha-icon icon="${this.icon.fallback}"></ha-icon>`;
|
||||
}
|
||||
return html``;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(iconStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-icon': FrigateCardIcon;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { KeyAssignerController } from '../components-lib/key-assigner-controller
|
||||
import { KeyboardShortcut } from '../config/keyboard-shortcuts';
|
||||
import keyAssignerStyle from '../scss/key-assigner.scss';
|
||||
import { localize } from '../localize/localize';
|
||||
import './icon';
|
||||
|
||||
@customElement('frigate-card-key-assigner')
|
||||
export class FrigateCardKeyAssigner extends LitElement {
|
||||
@@ -48,15 +49,11 @@ export class FrigateCardKeyAssigner extends LitElement {
|
||||
this._controller.toggleAssigning();
|
||||
}}
|
||||
>
|
||||
<ha-icon icon="mdi:keyboard-settings"></ha-icon>
|
||||
<frigate-card-icon .icon=${{ icon: 'mdi:keyboard-settings' }}></frigate-card-icon>
|
||||
<span class="${classMap({
|
||||
dotdotdot: this._controller.isAssigning(),
|
||||
})}">
|
||||
${
|
||||
this._controller.isAssigning()
|
||||
? localize('key_assigner.assigning')
|
||||
: localize('key_assigner.assign')
|
||||
}
|
||||
${this._controller.isAssigning() ? '' : localize('key_assigner.assign')}
|
||||
</span>
|
||||
</ha-button>
|
||||
${
|
||||
@@ -66,7 +63,9 @@ export class FrigateCardKeyAssigner extends LitElement {
|
||||
this._controller.setValue(null);
|
||||
}}
|
||||
>
|
||||
<ha-icon icon="mdi:keyboard-off"></ha-icon>
|
||||
<frigate-card-icon
|
||||
.icon=${{ icon: 'mdi:keyboard-off' }}
|
||||
></frigate-card-icon>
|
||||
<span> ${localize('key_assigner.unassign')} </span>
|
||||
</ha-button>`
|
||||
: ''
|
||||
|
||||
@@ -11,6 +11,7 @@ import { classMap } from 'lit/directives/class-map.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
|
||||
import { PartialZoomSettings } from '../../components-lib/zoom/types.js';
|
||||
import {
|
||||
CameraConfig,
|
||||
@@ -26,11 +27,11 @@ import { aspectRatioToString } from '../../utils/basic.js';
|
||||
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||
import { playMediaMutingIfNecessary } from '../../utils/media.js';
|
||||
import '../icon.js';
|
||||
import { renderMessage } from '../message.js';
|
||||
import '../next-prev-control.js';
|
||||
import '../ptz.js';
|
||||
import '../surround.js';
|
||||
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
|
||||
|
||||
@customElement('frigate-card-live-provider')
|
||||
export class FrigateCardLiveProvider
|
||||
@@ -385,10 +386,10 @@ export class FrigateCardLiveProvider
|
||||
: html``}
|
||||
`)}
|
||||
${showImageDuringLoading && !this._isVideoMediaLoaded
|
||||
? html`<ha-icon
|
||||
? html`<frigate-card-icon
|
||||
title=${localize('error.awaiting_live')}
|
||||
icon="mdi:progress-helper"
|
||||
></ha-icon>`
|
||||
.icon=${{ icon: 'mdi:progress-helper' }}
|
||||
></frigate-card-icon>`
|
||||
: ''} `;
|
||||
}
|
||||
|
||||
|
||||
+17
-25
@@ -1,15 +1,15 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { actionHandler } from '../action-handler-directive.js';
|
||||
import { MenuController } from '../components-lib/menu-controller.js';
|
||||
import type { MenuConfig, MenuItem } from '../config/types.js';
|
||||
import menuStyle from '../scss/menu.scss';
|
||||
import { frigateCardHasAction } from '../utils/action.js';
|
||||
import { getCustomIconURL } from '../utils/custom-icons.js';
|
||||
import { getEntityTitle } from '../utils/ha/index.js';
|
||||
import { EntityRegistryManager } from '../utils/ha/registry/entity/index.js';
|
||||
import './icon.js';
|
||||
import './submenu.js';
|
||||
|
||||
@customElement('frigate-card-menu')
|
||||
@@ -60,38 +60,30 @@ export class FrigateCardMenu extends LitElement {
|
||||
</frigate-card-submenu-select>`;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// For `data-domain` and `data-state`, see: See
|
||||
// https://github.com/home-assistant/frontend/blob/dev/src/components/entity/state-badge.ts#L54
|
||||
// =====================================================================================
|
||||
// Buttons are styled in a few ways (in order of precedence):
|
||||
//
|
||||
// - User provided style
|
||||
// - Color/Brightness styling for the `light` domain (calculated in
|
||||
// `refreshDynamicStateParameters`)
|
||||
// - Static styling based on domain (`data-domain`) and state
|
||||
// (`data-state`). This looks up a CSS style in `menu.scss`.
|
||||
|
||||
const buttonState = this._controller.getFreshButtonState(this.hass, button);
|
||||
const customImageURL = getCustomIconURL(buttonState.icon);
|
||||
const title =
|
||||
this.hass && button.type === 'custom:frigate-card-menu-state-icon' && !button.title
|
||||
? getEntityTitle(this.hass, button.entity)
|
||||
: button.title;
|
||||
|
||||
return html` <ha-icon-button
|
||||
data-domain=${ifDefined(buttonState.data_domain)}
|
||||
data-state=${ifDefined(buttonState.data_state)}
|
||||
class="button"
|
||||
style="${styleMap(buttonState.style || {})}"
|
||||
style="${styleMap(button.style || {})}"
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: frigateCardHasAction(button.hold_action),
|
||||
hasDoubleClick: frigateCardHasAction(button.double_tap_action),
|
||||
})}
|
||||
.label=${buttonState.title || ''}
|
||||
.label=${title ?? ''}
|
||||
@action=${(ev) => this._controller.actionHandler(ev, button)}
|
||||
>
|
||||
${customImageURL
|
||||
? html`<img src="${customImageURL}" />`
|
||||
: html`<ha-icon
|
||||
icon="${buttonState.icon || 'mdi:gesture-tap-button'}"
|
||||
></ha-icon>`}
|
||||
<frigate-card-icon
|
||||
.hass=${this.hass}
|
||||
.icon=${{
|
||||
icon: button.icon,
|
||||
entity: button.entity,
|
||||
stateColor: button.state_color,
|
||||
fallback: 'mdi:gesture-tap-button',
|
||||
}}
|
||||
></frigate-card-icon>
|
||||
</ha-icon-button>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { localize } from '../localize/localize.js';
|
||||
import messageStyle from '../scss/message.scss';
|
||||
import { FrigateCardError, Message, MessageType } from '../types.js';
|
||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||
import './icon.js';
|
||||
|
||||
@customElement('frigate-card-message')
|
||||
export class FrigateCardMessage extends LitElement {
|
||||
@@ -38,7 +39,7 @@ export class FrigateCardMessage extends LitElement {
|
||||
return html` <div class="wrapper">
|
||||
<div class="message padded">
|
||||
<div class="icon">
|
||||
<ha-icon icon="${icon}"> </ha-icon>
|
||||
<frigate-card-icon .icon="${{ icon: icon }}"></frigate-card-icon>
|
||||
</div>
|
||||
<div class="contents">
|
||||
<span class="${classMap(classes)}">
|
||||
@@ -103,9 +104,11 @@ export class FrigateCardProgressIndicator extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html` <div class="message vertical">
|
||||
${this.animated
|
||||
? html`<ha-circular-progress active="true" size="${this.size}">
|
||||
? html`<ha-circular-progress indeterminate size="${this.size}">
|
||||
</ha-circular-progress>`
|
||||
: html`<ha-icon icon="mdi:timer-sand"></ha-icon>`}
|
||||
: html`<frigate-card-icon
|
||||
.icon=${{ icon: 'mdi:timer-sand' }}
|
||||
></frigate-card-icon>`}
|
||||
${this.message ? html`<span>${this.message}</span>` : html``}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { NextPreviousControlConfig } from '../config/types.js';
|
||||
import controlStyle from '../scss/next-previous-control.scss';
|
||||
import { Icon } from '../types.js';
|
||||
import { renderTask } from '../utils/task.js';
|
||||
import { createFetchThumbnailTask } from '../utils/thumbnail.js';
|
||||
|
||||
@@ -29,7 +30,7 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
||||
public thumbnail?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public icon?: string;
|
||||
public icon?: Icon;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public disabled = false;
|
||||
@@ -67,17 +68,14 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
||||
|
||||
if (renderIcon) {
|
||||
const icon =
|
||||
!this.thumbnail ||
|
||||
!this.icon ||
|
||||
this._controlConfig.style === 'chevrons' ||
|
||||
this._thumbnailError
|
||||
? this.side === 'left'
|
||||
? 'mdi:chevron-left'
|
||||
: 'mdi:chevron-right'
|
||||
: this.icon;
|
||||
this.icon && !this._thumbnailError && this._controlConfig.style !== 'chevrons'
|
||||
? this.icon
|
||||
: this.side === 'left'
|
||||
? { icon: 'mdi:chevron-left' }
|
||||
: { icon: 'mdi:chevron-right' };
|
||||
|
||||
return html` <ha-icon-button class="${classMap(classes)}" .label=${this.label}>
|
||||
<ha-icon icon=${icon}></ha-icon>
|
||||
<frigate-card-icon .hass=${this.hass} .icon=${icon}></frigate-card-icon>
|
||||
</ha-icon-button>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Actions, PTZControlsConfig } from '../config/types.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import ptzStyle from '../scss/ptz.scss';
|
||||
import { frigateCardHasAction } from '../utils/action.js';
|
||||
import './icon.js';
|
||||
|
||||
@customElement('frigate-card-ptz')
|
||||
export class FrigateCardPTZ extends LitElement {
|
||||
@@ -67,9 +68,9 @@ export class FrigateCardPTZ extends LitElement {
|
||||
};
|
||||
|
||||
return actions
|
||||
? html`<ha-icon
|
||||
? html`<frigate-card-icon
|
||||
class=${classMap(classes)}
|
||||
icon=${icon}
|
||||
.icon=${{ icon: icon }}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: frigateCardHasAction(actions?.hold_action),
|
||||
hasDoubleClick: frigateCardHasAction(actions?.double_tap_action),
|
||||
@@ -77,7 +78,7 @@ export class FrigateCardPTZ extends LitElement {
|
||||
.title=${localize(`elements.ptz.${name}`)}
|
||||
@action=${(ev: HASSDomEvent<{ action: string }>) =>
|
||||
this._controller.handleAction(ev, actions)}
|
||||
></ha-icon>`
|
||||
></frigate-card-icon>`
|
||||
: html``;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { StatusBarController } from '../components-lib/status-bar-controller';
|
||||
import { StatusBarConfig, StatusBarItem } from '../config/types';
|
||||
import statusStyle from '../scss/status.scss';
|
||||
import { frigateCardHasAction } from '../utils/action';
|
||||
import { getCustomIconURL } from '../utils/custom-icons.js';
|
||||
import './icon.js';
|
||||
|
||||
@customElement('frigate-card-status-bar')
|
||||
export class FrigateCardStatusBar extends LitElement {
|
||||
@@ -68,20 +68,12 @@ export class FrigateCardStatusBar extends LitElement {
|
||||
${item.string}
|
||||
</div>`;
|
||||
} else if (item.type === 'custom:frigate-card-status-bar-icon') {
|
||||
const customIconURL = getCustomIconURL(item.icon);
|
||||
return customIconURL
|
||||
? html`<img
|
||||
.actionHandler=${handler}
|
||||
class="${classes}"
|
||||
src="${customIconURL}"
|
||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
||||
/>`
|
||||
: html`<ha-icon
|
||||
.actionHandler=${handler}
|
||||
class="${classes}"
|
||||
icon="${item.icon}"
|
||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
||||
></ha-icon>`;
|
||||
return html`<frigate-card-icon
|
||||
.actionHandler=${handler}
|
||||
.icon=${{ icon: item.icon }}
|
||||
class="${classes}"
|
||||
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
|
||||
></frigate-card-icon>`;
|
||||
} else if (item.type === 'custom:frigate-card-status-bar-image') {
|
||||
return html`<img
|
||||
.actionHandler=${handler}
|
||||
|
||||
+40
-43
@@ -13,16 +13,21 @@ import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||
import { actionHandler } from '../action-handler-directive.js';
|
||||
import { MenuSubmenu, MenuSubmenuItem, MenuSubmenuSelect } from '../config/types.js';
|
||||
import submenuStyle from '../scss/submenu.scss';
|
||||
import { StateParameters } from '../types.js';
|
||||
import {
|
||||
frigateCardHasAction,
|
||||
stopEventFromActivatingCardWideActions,
|
||||
} from '../utils/action.js';
|
||||
import { getCustomIconURL } from '../utils/custom-icons.js';
|
||||
import { isHassDifferent, refreshDynamicStateParameters } from '../utils/ha';
|
||||
import { getEntityTitle, isHassDifferent } from '../utils/ha';
|
||||
import { getEntityStateTranslation } from '../utils/ha/entity-state-translation.js';
|
||||
import { EntityRegistryManager } from '../utils/ha/registry/entity/index.js';
|
||||
import { domainIcon } from '../utils/icons/domain-icon.js';
|
||||
import './icon.js';
|
||||
import { Icon } from '../types.js';
|
||||
|
||||
interface ExtendedMenuSubmenu extends MenuSubmenu {
|
||||
// An internal version of a submenu that allows entity-based submenus (for
|
||||
// FrigateCardSubmenuSelect).
|
||||
icon: string | Icon;
|
||||
}
|
||||
|
||||
@customElement('frigate-card-submenu')
|
||||
export class FrigateCardSubmenu extends LitElement {
|
||||
@@ -30,47 +35,23 @@ export class FrigateCardSubmenu extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public submenu?: MenuSubmenu;
|
||||
public submenu?: ExtendedMenuSubmenu;
|
||||
|
||||
protected _renderItem(item: MenuSubmenuItem): TemplateResult | void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
const stateParameters = refreshDynamicStateParameters(this.hass, {
|
||||
...item,
|
||||
} as StateParameters);
|
||||
const getIcon = (stateParameters: StateParameters): TemplateResult => {
|
||||
if (stateParameters.icon) {
|
||||
const url = getCustomIconURL(stateParameters.icon);
|
||||
return url
|
||||
? html`<img
|
||||
style="${styleMap(stateParameters.style || {})}"
|
||||
data-domain=${ifDefined(stateParameters.data_domain)}
|
||||
data-state=${ifDefined(stateParameters.data_state)}
|
||||
slot="graphic"
|
||||
src=${url}
|
||||
/>`
|
||||
: html` <ha-icon
|
||||
style="${styleMap(stateParameters.style || {})}"
|
||||
data-domain=${ifDefined(stateParameters.data_domain)}
|
||||
data-state=${ifDefined(stateParameters.data_state)}
|
||||
slot="graphic"
|
||||
icon="${stateParameters.icon || ''}"
|
||||
>
|
||||
</ha-icon>`;
|
||||
}
|
||||
return html``;
|
||||
};
|
||||
|
||||
const title = item.title ?? getEntityTitle(this.hass, item.entity);
|
||||
return html`
|
||||
<mwc-list-item
|
||||
style="${styleMap(stateParameters.style || {})}"
|
||||
graphic=${ifDefined(stateParameters.icon ? 'icon' : undefined)}
|
||||
style="${styleMap(item.style || {})}"
|
||||
graphic=${ifDefined(item.icon || item.entity ? 'icon' : undefined)}
|
||||
?twoline=${!!item.subtitle}
|
||||
?selected=${item.selected}
|
||||
?activated=${item.selected}
|
||||
?disabled=${item.enabled === false}
|
||||
aria-label="${stateParameters.title || ''}"
|
||||
aria-label="${title ?? ''}"
|
||||
@action=${(ev) => {
|
||||
// Attach the action config so ascendants have access to it.
|
||||
ev.detail.config = item;
|
||||
@@ -80,9 +61,17 @@ export class FrigateCardSubmenu extends LitElement {
|
||||
hasDoubleClick: frigateCardHasAction(item.double_tap_action),
|
||||
})}
|
||||
>
|
||||
<span>${stateParameters.title || ''}</span>
|
||||
<span>${title ?? ''}</span>
|
||||
${item.subtitle ? html`<span slot="secondary">${item.subtitle}</span>` : ''}
|
||||
${getIcon(stateParameters)}
|
||||
<frigate-card-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.icon=${{
|
||||
icon: item.icon,
|
||||
entity: item.entity,
|
||||
}}
|
||||
style="${styleMap(item.style || {})}"
|
||||
></frigate-card-icon>
|
||||
</mwc-list-item>
|
||||
`;
|
||||
}
|
||||
@@ -118,7 +107,14 @@ export class FrigateCardSubmenu extends LitElement {
|
||||
hasDoubleClick: frigateCardHasAction(this.submenu.double_tap_action),
|
||||
})}
|
||||
>
|
||||
<ha-icon icon="${this.submenu.icon}"></ha-icon>
|
||||
<frigate-card-icon
|
||||
.hass=${this.hass}
|
||||
.icon=${typeof this.submenu.icon === 'string'
|
||||
? {
|
||||
icon: this.submenu.icon,
|
||||
}
|
||||
: this.submenu.icon}
|
||||
></frigate-card-icon>
|
||||
</ha-icon-button>
|
||||
${items.map(this._renderItem.bind(this))}
|
||||
</ha-button-menu>
|
||||
@@ -207,18 +203,19 @@ export class FrigateCardSubmenuSelect extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = getEntityTitle(this.hass, entityID);
|
||||
const submenu: MenuSubmenu = {
|
||||
// Default icon. It should be impossible for this to be used, since
|
||||
// this.submenuSelect will always have an entity, which means
|
||||
// refreshDynamicStateParameters will always return an icon.
|
||||
icon: domainIcon('select'),
|
||||
|
||||
// Pull out the dynamic properties (like icon, and title) from the state.
|
||||
...refreshDynamicStateParameters(this.hass, this.submenuSelect as StateParameters),
|
||||
...(title && { title }),
|
||||
|
||||
// Override it with anything explicitly set in the submenuSelect.
|
||||
...this.submenuSelect,
|
||||
|
||||
icon: {
|
||||
icon: this.submenuSelect.icon,
|
||||
entity: entityID,
|
||||
fallback: 'mdi:format-list-bulleted',
|
||||
},
|
||||
|
||||
type: 'custom:frigate-card-menu-submenu',
|
||||
items: [],
|
||||
};
|
||||
|
||||
+49
-40
@@ -105,10 +105,10 @@ export class FrigateCardThumbnailFeatureThumbnail extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const imageOff = html`<ha-icon
|
||||
icon="mdi:image-off"
|
||||
const imageOff = html`<frigate-card-icon
|
||||
.icon=${{ icon: 'mdi:image-off' }}
|
||||
title=${localize('thumbnail.no_thumbnail')}
|
||||
></ha-icon> `;
|
||||
></frigate-card-icon> `;
|
||||
|
||||
if (!this._embedThumbnailTask || this._thumbnailError) {
|
||||
return imageOff;
|
||||
@@ -150,8 +150,11 @@ export class FrigateCardThumbnailFeatureText extends LitElement {
|
||||
return;
|
||||
}
|
||||
return html`
|
||||
${this.cameraMetadata?.engineLogo
|
||||
? html`<img class="background" src="${this.cameraMetadata.engineLogo}" />`
|
||||
${this.cameraMetadata?.engineIcon
|
||||
? html`<frigate-card-icon
|
||||
class="background"
|
||||
.icon=${{ icon: this.cameraMetadata.engineIcon }}
|
||||
></frigate-card-icon>`
|
||||
: ''}
|
||||
<div class="content">
|
||||
<div class="title">${format(this.date, 'HH:mm')}</div>
|
||||
@@ -211,18 +214,18 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement {
|
||||
<div class="details">
|
||||
${startTime
|
||||
? html` <div>
|
||||
<ha-icon
|
||||
<frigate-card-icon
|
||||
title=${localize('event.start')}
|
||||
.icon=${'mdi:calendar-clock-outline'}
|
||||
></ha-icon>
|
||||
.icon=${{ icon: 'mdi:calendar-clock-outline' }}
|
||||
></frigate-card-icon>
|
||||
<span title="${startTime}">${startTime}</span>
|
||||
</div>
|
||||
${duration || inProgress
|
||||
? html` <div>
|
||||
<ha-icon
|
||||
<frigate-card-icon
|
||||
title=${localize('event.duration')}
|
||||
.icon=${'mdi:clock-outline'}
|
||||
></ha-icon>
|
||||
.icon=${{ icon: 'mdi:clock-outline' }}
|
||||
></frigate-card-icon>
|
||||
${duration ? html`<span title="${duration}">${duration}</span>` : ''}
|
||||
${inProgress
|
||||
? html`<span title="${inProgress}">${inProgress}</span>`
|
||||
@@ -232,31 +235,37 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement {
|
||||
: ''}
|
||||
${this.cameraTitle
|
||||
? html` <div>
|
||||
<ha-icon title=${localize('event.camera')} .icon=${'mdi:cctv'}></ha-icon>
|
||||
<frigate-card-icon
|
||||
title=${localize('event.camera')}
|
||||
.icon=${{ icon: 'mdi:cctv' }}
|
||||
></frigate-card-icon>
|
||||
<span title="${this.cameraTitle}">${this.cameraTitle}</span>
|
||||
</div>`
|
||||
: ''}
|
||||
${where
|
||||
? html` <div>
|
||||
<ha-icon
|
||||
<frigate-card-icon
|
||||
title=${localize('event.where')}
|
||||
.icon=${'mdi:map-marker-outline'}
|
||||
></ha-icon>
|
||||
.icon=${{ icon: 'mdi:map-marker-outline' }}
|
||||
></frigate-card-icon>
|
||||
<span title="${where}">${where}</span>
|
||||
</div>`
|
||||
: html``}
|
||||
${tags
|
||||
? html` <div>
|
||||
<ha-icon title=${localize('event.tag')} .icon=${'mdi:tag'}></ha-icon>
|
||||
<frigate-card-icon
|
||||
title=${localize('event.tag')}
|
||||
.icon=${{ icon: 'mdi:tag' }}
|
||||
></frigate-card-icon>
|
||||
<span title="${tags}">${tags}</span>
|
||||
</div>`
|
||||
: html``}
|
||||
${seek
|
||||
? html` <div>
|
||||
<ha-icon
|
||||
<frigate-card-icon
|
||||
title=${localize('event.seek')}
|
||||
.icon=${'mdi:clock-fast'}
|
||||
></ha-icon>
|
||||
.icon=${{ icon: 'mdi:clock-fast' }}
|
||||
></frigate-card-icon>
|
||||
<span title="${seek}">${seek}</span>
|
||||
</div>`
|
||||
: html``}
|
||||
@@ -306,18 +315,18 @@ export class FrigateCardThumbnailDetailsRecording extends LitElement {
|
||||
<div class="details">
|
||||
${startTime
|
||||
? html` <div>
|
||||
<ha-icon
|
||||
<frigate-card-icon
|
||||
title=${localize('recording.start')}
|
||||
.icon=${'mdi:calendar-clock-outline'}
|
||||
></ha-icon>
|
||||
.icon=${{ icon: 'mdi:calendar-clock-outline' }}
|
||||
></frigate-card-icon>
|
||||
<span title="${startTime}">${startTime}</span>
|
||||
</div>
|
||||
${duration || inProgress
|
||||
? html` <div>
|
||||
<ha-icon
|
||||
<frigate-card-icon
|
||||
title=${localize('recording.duration')}
|
||||
.icon=${'mdi:clock-outline'}
|
||||
></ha-icon>
|
||||
.icon=${{ icon: 'mdi:clock-outline' }}
|
||||
></frigate-card-icon>
|
||||
${duration ? html`<span title="${duration}">${duration}</span>` : ''}
|
||||
${inProgress
|
||||
? html`<span title="${inProgress}">${inProgress}</span>`
|
||||
@@ -327,19 +336,19 @@ export class FrigateCardThumbnailDetailsRecording extends LitElement {
|
||||
: ''}
|
||||
${seek
|
||||
? html` <div>
|
||||
<ha-icon
|
||||
<frigate-card-icon
|
||||
title=${localize('event.seek')}
|
||||
.icon=${'mdi:clock-fast'}
|
||||
></ha-icon>
|
||||
.icon=${{ icon: 'mdi:clock-fast' }}
|
||||
></frigate-card-icon>
|
||||
<span title="${seek}">${seek}</span>
|
||||
</div>`
|
||||
: html``}
|
||||
${eventCount !== null
|
||||
? html`<div>
|
||||
<ha-icon
|
||||
<frigate-card-icon
|
||||
title=${localize('recording.events')}
|
||||
.icon=${'mdi:shield-alert'}
|
||||
></ha-icon>
|
||||
.icon=${{ icon: 'mdi:shield-alert' }}
|
||||
></frigate-card-icon>
|
||||
<span title="${eventCount}">${eventCount}</span>
|
||||
</div>`
|
||||
: ``}
|
||||
@@ -448,10 +457,10 @@ export class FrigateCardThumbnail extends LitElement {
|
||||
></frigate-card-thumbnail-feature-text>`
|
||||
: html``}
|
||||
${shouldShowFavoriteControl
|
||||
? html` <ha-icon
|
||||
? html` <frigate-card-icon
|
||||
class="${classMap(starClasses)}"
|
||||
icon=${this.media.isFavorite() ? 'mdi:star' : 'mdi:star-outline'}
|
||||
title=${localize('thumbnail.retain_indefinitely')}
|
||||
.icon=${{ icon: this.media.isFavorite() ? 'mdi:star' : 'mdi:star-outline' }}
|
||||
@click=${async (ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (this.hass && this.media) {
|
||||
@@ -467,7 +476,7 @@ export class FrigateCardThumbnail extends LitElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
}}
|
||||
/></ha-icon>`
|
||||
/></frigate-card-icon>`
|
||||
: ``}
|
||||
${this.details && ViewMediaClassifier.isEvent(this.media)
|
||||
? html`<frigate-card-thumbnail-details-event
|
||||
@@ -483,9 +492,9 @@ export class FrigateCardThumbnail extends LitElement {
|
||||
></frigate-card-thumbnail-details-recording>`
|
||||
: html``}
|
||||
${shouldShowTimelineControl
|
||||
? html`<ha-icon
|
||||
? html`<frigate-card-icon
|
||||
class="timeline"
|
||||
icon="mdi:target"
|
||||
.icon=${{ icon: 'mdi:target' }}
|
||||
title=${localize('thumbnail.timeline')}
|
||||
@click=${(ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
@@ -503,12 +512,12 @@ export class FrigateCardThumbnail extends LitElement {
|
||||
modifiers: [new RemoveContextViewModifier(['timeline'])],
|
||||
});
|
||||
}}
|
||||
></ha-icon>`
|
||||
></frigate-card-icon>`
|
||||
: ''}
|
||||
${shouldShowDownloadControl
|
||||
? html` <ha-icon
|
||||
? html` <frigate-card-icon
|
||||
class="download"
|
||||
icon=${'mdi:download'}
|
||||
.icon=${{ icon: 'mdi:download' }}
|
||||
title=${localize('thumbnail.download')}
|
||||
@click=${async (ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
@@ -520,7 +529,7 @@ export class FrigateCardThumbnail extends LitElement {
|
||||
}
|
||||
}
|
||||
}}
|
||||
></ha-icon>`
|
||||
></frigate-card-icon>`
|
||||
: ``}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ import { MediaQueriesResults } from '../view/media-queries-results';
|
||||
import { mergeViewContext } from '../view/view';
|
||||
import './date-picker.js';
|
||||
import { DatePickerEvent, FrigateCardDatePicker } from './date-picker.js';
|
||||
import './icon';
|
||||
import './thumbnail.js';
|
||||
|
||||
interface FrigateCardGroupData {
|
||||
@@ -304,8 +305,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
>
|
||||
<div class="timeline-tools">
|
||||
${this._shouldSupportSeeking()
|
||||
? html` <ha-icon
|
||||
.icon=${panIcon}
|
||||
? html` <frigate-card-icon
|
||||
.icon=${{ icon: panIcon }}
|
||||
@click=${() => {
|
||||
this._panMode =
|
||||
panMode === 'pan'
|
||||
@@ -319,7 +320,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
aria-label="${panTitle}"
|
||||
title="${panTitle}"
|
||||
>
|
||||
</ha-icon>`
|
||||
</frigate-card-icon>`
|
||||
: ''}
|
||||
<frigate-card-date-picker
|
||||
${ref(this._refDatePicker)}
|
||||
|
||||
@@ -402,8 +402,11 @@ export class FrigateCardViewerCarousel extends LitElement {
|
||||
</frigate-card-ptz>`
|
||||
: ''}
|
||||
<div class="seek-warning">
|
||||
<ha-icon title="${localize('media_viewer.unseekable')}" icon="mdi:clock-remove">
|
||||
</ha-icon>
|
||||
<frigate-card-icon
|
||||
title="${localize('media_viewer.unseekable')}"
|
||||
.icon=${{ icon: 'mdi:clock-remove' }}
|
||||
>
|
||||
</frigate-card-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1721,7 +1721,7 @@ const statusBarItemDefault = {
|
||||
};
|
||||
|
||||
const statusBarConfigDefault = {
|
||||
height: 46,
|
||||
height: 30,
|
||||
items: {
|
||||
engine: statusBarItemDefault,
|
||||
resolution: statusBarItemDefault,
|
||||
|
||||
+25
-20
@@ -6,6 +6,7 @@ import {
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import './components/icon.js';
|
||||
import './components/key-assigner.js';
|
||||
import { KeyboardShortcut } from './config/keyboard-shortcuts.js';
|
||||
import {
|
||||
@@ -32,9 +33,10 @@ import {
|
||||
THUMBNAIL_WIDTH_MIN,
|
||||
} from './config/types.js';
|
||||
import {
|
||||
CONF_CAMERAS,
|
||||
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
|
||||
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT,
|
||||
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE,
|
||||
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT,
|
||||
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH,
|
||||
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH,
|
||||
CONF_CAMERAS_ARRAY_CAST_METHOD,
|
||||
@@ -60,8 +62,8 @@ import {
|
||||
CONF_CAMERAS_ARRAY_GO2RTC_STREAM,
|
||||
CONF_CAMERAS_ARRAY_ICON,
|
||||
CONF_CAMERAS_ARRAY_ID,
|
||||
CONF_CAMERAS_ARRAY_IMAGE_ENTITY_PARAMETERS,
|
||||
CONF_CAMERAS_ARRAY_IMAGE_ENTITY,
|
||||
CONF_CAMERAS_ARRAY_IMAGE_ENTITY_PARAMETERS,
|
||||
CONF_CAMERAS_ARRAY_IMAGE_MODE,
|
||||
CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS,
|
||||
CONF_CAMERAS_ARRAY_IMAGE_URL,
|
||||
@@ -84,12 +86,11 @@ import {
|
||||
CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY,
|
||||
CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY,
|
||||
CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL,
|
||||
CONF_CAMERAS,
|
||||
CONF_DIMENSIONS_ASPECT_RATIO_MODE,
|
||||
CONF_DIMENSIONS_ASPECT_RATIO,
|
||||
CONF_DIMENSIONS_ASPECT_RATIO_MODE,
|
||||
CONF_DIMENSIONS_HEIGHT,
|
||||
CONF_IMAGE_ENTITY_PARAMETERS,
|
||||
CONF_IMAGE_ENTITY,
|
||||
CONF_IMAGE_ENTITY_PARAMETERS,
|
||||
CONF_IMAGE_MODE,
|
||||
CONF_IMAGE_REFRESH_SECONDS,
|
||||
CONF_IMAGE_URL,
|
||||
@@ -200,14 +201,15 @@ import {
|
||||
CONF_TIMELINE_WINDOW_SECONDS,
|
||||
CONF_VIEW_CAMERA_SELECT,
|
||||
CONF_VIEW_DARK_MODE,
|
||||
CONF_VIEW_DEFAULT,
|
||||
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
|
||||
CONF_VIEW_DEFAULT_RESET,
|
||||
CONF_VIEW_DEFAULT_RESET_AFTER_INTERACTION,
|
||||
CONF_VIEW_DEFAULT_RESET_ENTITIES,
|
||||
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
|
||||
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
|
||||
CONF_VIEW_DEFAULT_RESET,
|
||||
CONF_VIEW_DEFAULT,
|
||||
CONF_VIEW_INTERACTION_SECONDS,
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS,
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS_ENABLED,
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_DOWN,
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_HOME,
|
||||
@@ -216,22 +218,20 @@ import {
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_UP,
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_IN,
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_OUT,
|
||||
CONF_VIEW_KEYBOARD_SHORTCUTS,
|
||||
CONF_VIEW_TRIGGERS,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_INTERACTION_MODE,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS,
|
||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||
CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS,
|
||||
CONF_VIEW_TRIGGERS_UNTRIGGER_SECONDS,
|
||||
CONF_VIEW_TRIGGERS,
|
||||
MEDIA_CHUNK_SIZE_MAX,
|
||||
} from './const.js';
|
||||
import { localize } from './localize/localize.js';
|
||||
import frigate_card_editor_style from './scss/editor.scss';
|
||||
import { arrayMove, prettifyTitle } from './utils/basic.js';
|
||||
import { getCameraID } from './utils/camera.js';
|
||||
import { getCustomIconURL } from './utils/custom-icons.js';
|
||||
import {
|
||||
getEntitiesFromHASS,
|
||||
getEntityTitle,
|
||||
@@ -960,7 +960,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
.key=${optionSetName}
|
||||
>
|
||||
<div class="row">
|
||||
<ha-icon .icon=${`mdi:${optionSet.icon}`}></ha-icon>
|
||||
<frigate-card-icon
|
||||
.icon=${{ icon: `mdi:${optionSet.icon}` }}
|
||||
></frigate-card-icon>
|
||||
<div class="title ${titleClass ?? ''}">${optionSet.name}</div>
|
||||
</div>
|
||||
<div class="secondary">${optionSet.secondary}</div>
|
||||
@@ -1406,7 +1408,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
selected: selected,
|
||||
};
|
||||
|
||||
const customIconURL = getCustomIconURL(icon);
|
||||
return html` <div class="${classMap(submenuClasses)}">
|
||||
<div
|
||||
class="submenu-header"
|
||||
@@ -1414,9 +1415,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
.domain=${domain}
|
||||
.key=${key}
|
||||
>
|
||||
${customIconURL
|
||||
? html` <img src=${customIconURL} /> `
|
||||
: html` <ha-icon .icon=${icon}></ha-icon> `}
|
||||
<frigate-card-icon .icon=${{ icon: icon }}></frigate-card-icon>
|
||||
<span>${localize(labelPath)}</span>
|
||||
</div>
|
||||
${selected ? html`<div class="values">${template}</div>` : ''}
|
||||
@@ -1909,7 +1908,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
.domain=${MENU_CAMERAS}
|
||||
.key=${cameraIndex}
|
||||
>
|
||||
<ha-icon .icon=${addNewCamera ? 'mdi:video-plus' : 'mdi:video'}></ha-icon>
|
||||
<frigate-card-icon
|
||||
.icon=${{ icon: addNewCamera ? 'mdi:video-plus' : 'mdi:video' }}
|
||||
></frigate-card-icon>
|
||||
<span>
|
||||
${addNewCamera
|
||||
? html` <span class="new-camera">
|
||||
@@ -1944,7 +1945,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
return false;
|
||||
})}
|
||||
>
|
||||
<ha-icon icon="mdi:arrow-up"></ha-icon>
|
||||
<frigate-card-icon
|
||||
.icon=${{ icon: 'mdi:arrow-up' }}
|
||||
></frigate-card-icon>
|
||||
</ha-icon-button>
|
||||
<ha-icon-button
|
||||
class="button"
|
||||
@@ -1967,7 +1970,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
return false;
|
||||
})}
|
||||
>
|
||||
<ha-icon icon="mdi:arrow-down"></ha-icon>
|
||||
<frigate-card-icon
|
||||
.icon=${{ icon: 'mdi:arrow-down' }}
|
||||
></frigate-card-icon>
|
||||
</ha-icon-button>
|
||||
<ha-icon-button
|
||||
class="button"
|
||||
@@ -1984,7 +1989,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ha-icon icon="mdi:delete"></ha-icon>
|
||||
<frigate-card-icon .icon=${{ icon: 'mdi:delete' }}></frigate-card-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
${this._renderEntitySelector(
|
||||
|
||||
@@ -660,7 +660,6 @@
|
||||
},
|
||||
"key_assigner": {
|
||||
"assign": "",
|
||||
"assigning": "",
|
||||
"modifiers": {
|
||||
"alt": "",
|
||||
"ctrl": "",
|
||||
|
||||
@@ -660,7 +660,6 @@
|
||||
},
|
||||
"key_assigner": {
|
||||
"assign": "Assign",
|
||||
"assigning": "Assigning",
|
||||
"modifiers": {
|
||||
"alt": "Alt",
|
||||
"ctrl": "Ctrl",
|
||||
|
||||
@@ -660,7 +660,6 @@
|
||||
},
|
||||
"key_assigner": {
|
||||
"assign": "Assigner",
|
||||
"assigning": "Assignation",
|
||||
"modifiers": {
|
||||
"alt": "",
|
||||
"ctrl": "",
|
||||
|
||||
@@ -660,7 +660,6 @@
|
||||
},
|
||||
"key_assigner": {
|
||||
"assign": "",
|
||||
"assigning": "",
|
||||
"modifiers": {
|
||||
"alt": "",
|
||||
"ctrl": "",
|
||||
|
||||
@@ -660,7 +660,6 @@
|
||||
},
|
||||
"key_assigner": {
|
||||
"assign": "",
|
||||
"assigning": "",
|
||||
"modifiers": {
|
||||
"alt": "",
|
||||
"ctrl": "",
|
||||
|
||||
@@ -660,7 +660,6 @@
|
||||
},
|
||||
"key_assigner": {
|
||||
"assign": "",
|
||||
"assigning": "",
|
||||
"modifiers": {
|
||||
"alt": "",
|
||||
"ctrl": "",
|
||||
|
||||
+1
-86
@@ -5,93 +5,8 @@ ha-icon-button.button {
|
||||
padding: 0px;
|
||||
margin: 3px;
|
||||
--ha-icon-display: block;
|
||||
|
||||
/* Buttons can always be clicked */
|
||||
pointer-events: auto;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// From: https://github.com/home-assistant/frontend/blob/dev/src/common/style/icon_color_css.ts
|
||||
// Modified version for ha-icon and ha-icon-button.
|
||||
// =====================================================================================
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin state-icon($element) {
|
||||
#{$element}[data-domain='alert'][data-state='on'],
|
||||
#{$element}[data-domain='automation'][data-state='on'],
|
||||
#{$element}[data-domain='binary_sensor'][data-state='on'],
|
||||
#{$element}[data-domain='calendar'][data-state='on'],
|
||||
#{$element}[data-domain='camera'][data-state='streaming'],
|
||||
#{$element}[data-domain='cover'][data-state='open'],
|
||||
#{$element}[data-domain='fan'][data-state='on'],
|
||||
#{$element}[data-domain='humidifier'][data-state='on'],
|
||||
#{$element}[data-domain='light'][data-state='on'],
|
||||
#{$element}[data-domain='input_boolean'][data-state='on'],
|
||||
#{$element}[data-domain='lock'][data-state='unlocked'],
|
||||
#{$element}[data-domain='media_player'][data-state='on'],
|
||||
#{$element}[data-domain='media_player'][data-state='paused'],
|
||||
#{$element}[data-domain='media_player'][data-state='playing'],
|
||||
#{$element}[data-domain='script'][data-state='on'],
|
||||
#{$element}[data-domain='sun'][data-state='above_horizon'],
|
||||
#{$element}[data-domain='switch'][data-state='on'],
|
||||
#{$element}[data-domain='timer'][data-state='active'],
|
||||
#{$element}[data-domain='vacuum'][data-state='cleaning'],
|
||||
#{$element}[data-domain='group'][data-state='on'],
|
||||
#{$element}[data-domain='group'][data-state='home'],
|
||||
#{$element}[data-domain='group'][data-state='open'],
|
||||
#{$element}[data-domain='group'][data-state='locked'],
|
||||
#{$element}[data-domain='group'][data-state='problem'] {
|
||||
color: var(--paper-item-icon-active-color, #fdd835);
|
||||
}
|
||||
|
||||
#{$element}[data-domain='climate'][data-state='cooling'] {
|
||||
color: var(--cool-color, var(--state-climate-cool-color));
|
||||
}
|
||||
|
||||
#{$element}[data-domain='climate'][data-state='heating'] {
|
||||
color: var(--heat-color, var(--state-climate-heat-color));
|
||||
}
|
||||
|
||||
#{$element}[data-domain='climate'][data-state='drying'] {
|
||||
color: var(--dry-color, var(--state-climate-dry-color));
|
||||
}
|
||||
|
||||
#{$element}[data-domain='alarm_control_panel'] {
|
||||
color: var(--alarm-color-armed, var(--label-badge-red));
|
||||
}
|
||||
#{$element}[data-domain='alarm_control_panel'][data-state='disarmed'] {
|
||||
color: var(--alarm-color-disarmed, var(--label-badge-green));
|
||||
}
|
||||
#{$element}[data-domain='alarm_control_panel'][data-state='pending'],
|
||||
#{$element}[data-domain='alarm_control_panel'][data-state='arming'] {
|
||||
color: var(--alarm-color-pending, var(--label-badge-yellow));
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
#{$element}[data-domain='alarm_control_panel'][data-state='triggered'] {
|
||||
color: var(--alarm-color-triggered, var(--label-badge-red));
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
#{$element}[data-domain='plant'][data-state='problem'],
|
||||
#{$element}[data-domain='zwave'][data-state='dead'] {
|
||||
color: var(--state-icon-error-color);
|
||||
}
|
||||
|
||||
/* Color the icon if unavailable */
|
||||
#{$element}[data-state='unavailable'] {
|
||||
color: var(--state-unavailable-color);
|
||||
}
|
||||
}
|
||||
|
||||
@include state-icon('ha-icon');
|
||||
@include state-icon('ha-icon-button');
|
||||
|
||||
@@ -42,7 +42,7 @@ input {
|
||||
}
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
frigate-card-icon {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
@@ -36,7 +36,7 @@ div.control-surround {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
ha-icon.control {
|
||||
frigate-card-icon.control {
|
||||
color: var(--secondary-color, white);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
opacity: 0.5;
|
||||
@@ -49,20 +49,20 @@ ha-icon.control {
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
:host([open]) ha-icon.control,
|
||||
ha-icon.control:hover {
|
||||
:host([open]) frigate-card-icon.control,
|
||||
frigate-card-icon.control:hover {
|
||||
// When the drawer is open or hovered make the button to close it more
|
||||
// prominent.
|
||||
opacity: 1;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
:host([location='left']) ha-icon.control {
|
||||
:host([location='left']) frigate-card-icon.control {
|
||||
border-top-right-radius: $drawer-icon-size;
|
||||
border-bottom-right-radius: $drawer-icon-size;
|
||||
}
|
||||
|
||||
:host([location='right']) ha-icon.control {
|
||||
:host([location='right']) frigate-card-icon.control {
|
||||
border-top-left-radius: $drawer-icon-size;
|
||||
border-bottom-left-radius: $drawer-icon-size;
|
||||
}
|
||||
|
||||
@@ -70,9 +70,8 @@ div.upgrade span {
|
||||
color: var(--secondary-text-color, 'black');
|
||||
}
|
||||
|
||||
.submenu-header ha-icon,
|
||||
.submenu-header img {
|
||||
padding-right: 15px;
|
||||
.submenu-header frigate-card-icon {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.submenu.selected {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
:host {
|
||||
display: inline-block;
|
||||
width: var(--mdc-icon-size, 24px);
|
||||
height: var(--mdc-icon-size, 24px);
|
||||
|
||||
--ha-icon-display: block;
|
||||
}
|
||||
|
||||
* {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -12,11 +12,11 @@
|
||||
}
|
||||
|
||||
:host([assigning]) ha-button.assign span,
|
||||
:host([assigning]) ha-button.assign ha-icon {
|
||||
:host([assigning]) ha-button.assign frigate-card-icon {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
frigate-card-icon {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
frigate-card-icon {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
|
||||
+2
-2
@@ -84,11 +84,11 @@
|
||||
/***********
|
||||
* PTZ Icons
|
||||
***********/
|
||||
ha-icon {
|
||||
frigate-card-icon {
|
||||
position: absolute;
|
||||
--mdc-icon-size: var(--frigate-card-ptz-icon-size);
|
||||
}
|
||||
ha-icon:not(.disabled) {
|
||||
frigate-card-icon:not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.disabled {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
@use './button.scss';
|
||||
|
||||
:host {
|
||||
--mdc-icon-button-size: calc(var(--frigate-card-status-bar-height) - 6px);
|
||||
--mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);
|
||||
--mdc-icon-size: calc(var(--frigate-card-status-bar-height) - 6px);
|
||||
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -61,8 +60,7 @@
|
||||
.item {
|
||||
display: inline-block;
|
||||
|
||||
margin: 3px;
|
||||
padding: 3px;
|
||||
margin: 3px 5px;
|
||||
|
||||
align-content: center;
|
||||
|
||||
@@ -84,6 +82,10 @@
|
||||
}
|
||||
|
||||
img.item {
|
||||
width: var(--mdc-icon-size, 24px);
|
||||
display: block;
|
||||
|
||||
// To ensure images render somewhat reasonably looking their height is kept to
|
||||
// the same height as icons.
|
||||
height: var(--mdc-icon-size, 24px);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@@ -27,3 +27,12 @@ div.details {
|
||||
// Details panel can shrink as well as grow.
|
||||
min-height: 0px;
|
||||
}
|
||||
|
||||
div.details div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.details div * {
|
||||
margin: 0px 3px;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img.background {
|
||||
frigate-card-icon.background {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -14,7 +14,7 @@ img {
|
||||
}
|
||||
|
||||
img,
|
||||
ha-icon {
|
||||
frigate-card-icon {
|
||||
// Safari will occasionally not load thumbnails correctly with display block.
|
||||
display: inline-block;
|
||||
|
||||
@@ -37,7 +37,7 @@ ha-icon {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
frigate-card-icon {
|
||||
--mdc-icon-size: 50%;
|
||||
color: var(--primary-text-color);
|
||||
display: flex;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
frigate-card-icon {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
opacity: 0.5;
|
||||
@@ -43,24 +43,24 @@ ha-icon {
|
||||
opacity 0.2s ease-in-out,
|
||||
color 0.2s ease-in-out;
|
||||
}
|
||||
ha-icon:hover {
|
||||
frigate-card-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
ha-icon.star {
|
||||
frigate-card-icon.star {
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
}
|
||||
ha-icon.star.starred {
|
||||
frigate-card-icon.star.starred {
|
||||
color: gold;
|
||||
}
|
||||
|
||||
ha-icon.timeline {
|
||||
frigate-card-icon.timeline {
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
}
|
||||
|
||||
ha-icon.download {
|
||||
frigate-card-icon.download {
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
}
|
||||
|
||||
@@ -174,12 +174,15 @@ div.vis-tooltip {
|
||||
.timeline-tools {
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
color: var(--primary-color);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.timeline-tools ha-icon {
|
||||
.timeline-tools * {
|
||||
margin: 2px 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.timeline-tools *:last-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
+15
-11
@@ -5,7 +5,6 @@ import {
|
||||
LovelaceCardEditor,
|
||||
Themes,
|
||||
} from '@dermotduffy/custom-card-helpers';
|
||||
import { StyleInfo } from 'lit/directives/style-map.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
export type ClipsOrSnapshots = 'clips' | 'snapshots';
|
||||
@@ -65,16 +64,6 @@ export interface Message {
|
||||
dotdotdot?: boolean;
|
||||
}
|
||||
|
||||
export interface StateParameters {
|
||||
entity?: string;
|
||||
icon?: string;
|
||||
title?: string | null;
|
||||
state_color?: boolean;
|
||||
style?: StyleInfo;
|
||||
data_domain?: string;
|
||||
data_state?: string;
|
||||
}
|
||||
|
||||
export interface FrigateCardMediaPlayer {
|
||||
play(): Promise<void>;
|
||||
pause(): Promise<void>;
|
||||
@@ -143,6 +132,21 @@ export const capabilityKeys: readonly [CapabilityKey, ...CapabilityKey[]] = [
|
||||
'substream',
|
||||
] as const;
|
||||
|
||||
export interface Icon {
|
||||
// If set, this icon will be used.
|
||||
icon?: string;
|
||||
|
||||
// If icon is not set, this entity's icon will be used (and HA will be asked
|
||||
// to render it).
|
||||
entity?: string;
|
||||
|
||||
// Whether or not to change the icon color depending on entity state.
|
||||
stateColor?: boolean;
|
||||
|
||||
// If an icon is not otherwise resolved / available, this will be used instead.
|
||||
fallback?: string;
|
||||
}
|
||||
|
||||
// *************************************************************************
|
||||
// Home Assistant API types.
|
||||
// *************************************************************************
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import frigateSVG from '../camera-manager/frigate/assets/frigate.svg';
|
||||
import motioneyeSVG from '../camera-manager/motioneye/assets/motioneye.svg';
|
||||
import reolinkSVG from '../camera-manager/reolink/assets/reolink.svg';
|
||||
|
||||
export const getCustomIconURL = (icon?: string): string | null => {
|
||||
switch (icon) {
|
||||
case 'frigate':
|
||||
return frigateSVG;
|
||||
case 'motioneye':
|
||||
return motioneyeSVG;
|
||||
case 'reolink':
|
||||
return reolinkSVG;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
+17
-124
@@ -1,19 +1,12 @@
|
||||
import {
|
||||
computeDomain,
|
||||
computeStateDomain,
|
||||
HomeAssistant,
|
||||
} from '@dermotduffy/custom-card-helpers';
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
import { StyleInfo } from 'lit/directives/style-map.js';
|
||||
import {
|
||||
CardHelpers,
|
||||
ExtendedHomeAssistant,
|
||||
LovelaceCardWithEditor,
|
||||
SignedPath,
|
||||
signedPathSchema,
|
||||
StateParameters,
|
||||
} from '../../types.js';
|
||||
import { domainIcon } from '../icons/domain-icon.js';
|
||||
import { homeAssistantWSRequest } from './ws-request.js';
|
||||
|
||||
/**
|
||||
@@ -116,128 +109,14 @@ export function isHassDifferent(
|
||||
}).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a style brightness from a hass state.
|
||||
* Inspired by https://github.com/home-assistant/frontend/blob/7d5b5663123bb16d1da0c5bac3f2fc26d5f69ae8/src/panels/lovelace/cards/hui-button-card.ts#L296
|
||||
* @param state The hass state object.
|
||||
* @returns A CSS brightness string.
|
||||
*/
|
||||
function computeBrightnessFromState(state: HassEntity): string {
|
||||
if (state.state === 'off' || !state.attributes.brightness) {
|
||||
return '';
|
||||
}
|
||||
const brightness = state.attributes.brightness;
|
||||
return `brightness(${(brightness + 245) / 5}%)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a style color from a hass state.
|
||||
* Inspired by https://github.com/home-assistant/frontend/blob/7d5b5663123bb16d1da0c5bac3f2fc26d5f69ae8/src/panels/lovelace/cards/hui-button-card.ts#L304
|
||||
* @param state The hass state object.
|
||||
* @returns A CSS color string.
|
||||
*/
|
||||
function computeColorFromState(state: HassEntity): string {
|
||||
if (state.state === 'off') {
|
||||
return '';
|
||||
}
|
||||
return state.attributes.rgb_color
|
||||
? `rgb(${state.attributes.rgb_color.join(',')})`
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the style of emphasized menu items.
|
||||
* @returns A StyleInfo.
|
||||
*/
|
||||
function computeStyle(state: HassEntity): StyleInfo {
|
||||
return {
|
||||
color: computeColorFromState(state),
|
||||
filter: computeBrightnessFromState(state),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the string state of a given stateObj.
|
||||
* From: https://github.com/home-assistant/frontend/blob/dev/src/common/entity/compute_active_state.ts
|
||||
* @param stateObj The HassEntity object from `hass.states`.
|
||||
* @returns A string state, e.g. 'on'.
|
||||
*/
|
||||
const computeActiveState = (stateObj: HassEntity): string => {
|
||||
const domain = stateObj.entity_id.split('.')[0];
|
||||
let state = stateObj.state;
|
||||
|
||||
if (domain === 'climate') {
|
||||
state = stateObj.attributes.hvac_action;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
/**
|
||||
* Use Home Assistant state to refresh state parameters for an item to be rendered.
|
||||
* @param hass Home Assistant object.
|
||||
* @param params A StateParameters object to modify in place.
|
||||
* @returns A StateParameters object updated based on HASS state.
|
||||
*/
|
||||
export function refreshDynamicStateParameters(
|
||||
hass: HomeAssistant,
|
||||
params: StateParameters,
|
||||
): StateParameters {
|
||||
if (!params.entity) {
|
||||
return params;
|
||||
}
|
||||
const state = hass.states[params.entity];
|
||||
if (!!state && !!params.state_color) {
|
||||
params.style = { ...computeStyle(state), ...params.style };
|
||||
}
|
||||
params.title = params.title ?? (state?.attributes?.friendly_name || params.entity);
|
||||
params.icon = params.icon ?? getEntityIcon(hass, params.entity);
|
||||
|
||||
const domain = state ? computeStateDomain(state) : undefined;
|
||||
params.data_domain =
|
||||
params.state_color || (domain === 'light' && params.state_color !== false)
|
||||
? domain
|
||||
: undefined;
|
||||
if (state) {
|
||||
params.data_state = computeActiveState(state);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title of an entity.
|
||||
* @param entity The entity id.
|
||||
* @param hass The Home Assistant object.
|
||||
* @returns The title or undefined.
|
||||
*/
|
||||
export function getEntityTitle(
|
||||
hass?: HomeAssistant,
|
||||
entity?: string,
|
||||
): string | undefined {
|
||||
return entity ? hass?.states[entity]?.attributes?.friendly_name : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the icon of an entity.
|
||||
* @param entityID The entity id.
|
||||
* @param hass The Home Assistant object.
|
||||
* @returns The icon or undefined.
|
||||
*/
|
||||
export function getEntityIcon(
|
||||
hass: HomeAssistant,
|
||||
entityID: string,
|
||||
defaultIcon?: string,
|
||||
): string {
|
||||
const entityState = hass.states[entityID];
|
||||
if (entityState && entityState.attributes.icon) {
|
||||
return entityState.attributes.icon;
|
||||
}
|
||||
return domainIcon(
|
||||
computeDomain(entityID),
|
||||
entityState,
|
||||
entityState?.state,
|
||||
defaultIcon,
|
||||
);
|
||||
export function getEntityTitle(hass?: HomeAssistant, entity?: string): string | null {
|
||||
return entity ? hass?.states[entity]?.attributes?.friendly_name ?? null : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,12 +132,17 @@ export const sideLoadHomeAssistantElements = async (): Promise<boolean> => {
|
||||
'ha-camera-stream',
|
||||
'ha-card',
|
||||
'ha-circular-progress',
|
||||
'ha-combo-box',
|
||||
'ha-hls-player',
|
||||
'ha-icon-button',
|
||||
'ha-icon',
|
||||
'ha-menu-button',
|
||||
'ha-selector',
|
||||
'ha-state-icon',
|
||||
'ha-web-rtc-player',
|
||||
'mwc-button',
|
||||
'mwc-list-item',
|
||||
'state-badge',
|
||||
];
|
||||
|
||||
if (neededElements.every((element) => customElements.get(element))) {
|
||||
@@ -367,3 +251,12 @@ export const hasHAConnectionStateChanged = (
|
||||
): boolean => {
|
||||
return oldHass?.connected !== newHass?.connected;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if a state object supports a given feature.
|
||||
* @param stateObj The state object.
|
||||
* @param feature The feature to check.
|
||||
* @returns `true` if the feature is supported, `false` otherwise.
|
||||
*/
|
||||
export const supportsFeature = (stateObj: HassEntity, feature: number): boolean =>
|
||||
((stateObj.attributes.supported_features ?? 0) & feature) !== 0;
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import {
|
||||
HassEntity,
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from 'home-assistant-js-websocket';
|
||||
|
||||
const UPDATE_SUPPORT_PROGRESS = 4;
|
||||
|
||||
interface UpdateEntityAttributes extends HassEntityAttributeBase {
|
||||
auto_update: boolean | null;
|
||||
installed_version: string | null;
|
||||
in_progress: boolean | number;
|
||||
latest_version: string | null;
|
||||
release_summary: string | null;
|
||||
release_url: string | null;
|
||||
skipped_version: string | null;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateEntity extends HassEntityBase {
|
||||
attributes: UpdateEntityAttributes;
|
||||
}
|
||||
|
||||
export const supportsFeature = (stateObj: HassEntity, feature: number): boolean =>
|
||||
((stateObj.attributes.supported_features ?? 0) & feature) !== 0;
|
||||
|
||||
const updateUsesProgress = (entity: UpdateEntity): boolean =>
|
||||
supportsFeature(entity, UPDATE_SUPPORT_PROGRESS) &&
|
||||
typeof entity.attributes.in_progress === 'number';
|
||||
|
||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||
updateUsesProgress(entity) || !!entity.attributes.in_progress;
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,7 +0,0 @@
|
||||
## Icons
|
||||
|
||||
These entity icon mappings are copied (with slight tweaks to work with this
|
||||
card), from the [Lovelace Mushroom
|
||||
Card](https://github.com/piitaya/lovelace-mushroom/tree/main/src/utils/icons), which are in turn copied from [the Home Assistant frontend](https://github.com/home-assistant/frontend/tree/dev/src/common/entity) in order to
|
||||
replace the out of date entity icons in
|
||||
[custom-card-helpers](https://github.com/custom-cards/custom-card-helpers).
|
||||
@@ -1,23 +0,0 @@
|
||||
export const alarmPanelIcon = (state?: string) => {
|
||||
switch (state) {
|
||||
case 'armed_away':
|
||||
return 'mdi:shield-lock';
|
||||
case 'armed_vacation':
|
||||
return 'mdi:shield-airplane';
|
||||
case 'armed_home':
|
||||
return 'mdi:shield-home';
|
||||
case 'armed_night':
|
||||
return 'mdi:shield-moon';
|
||||
case 'armed_custom_bypass':
|
||||
return 'mdi:security';
|
||||
case 'pending':
|
||||
case 'arming':
|
||||
return 'mdi:shield-sync';
|
||||
case 'triggered':
|
||||
return 'mdi:bell-ring';
|
||||
case 'disarmed':
|
||||
return 'mdi:shield-off';
|
||||
default:
|
||||
return 'mdi:shield';
|
||||
}
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
|
||||
export const binarySensorIcon = (state?: string, entity?: HassEntity) => {
|
||||
const isOff = state === 'off';
|
||||
switch (entity?.attributes.device_class) {
|
||||
case 'battery':
|
||||
return isOff ? 'mdi:battery' : 'mdi:battery-outline';
|
||||
case 'battery_charging':
|
||||
return isOff ? 'mdi:battery' : 'mdi:battery-charging';
|
||||
case 'cold':
|
||||
return isOff ? 'mdi:thermometer' : 'mdi:snowflake';
|
||||
case 'connectivity':
|
||||
return isOff ? 'mdi:close-network-outline' : 'mdi:check-network-outline';
|
||||
case 'door':
|
||||
return isOff ? 'mdi:door-closed' : 'mdi:door-open';
|
||||
case 'garage_door':
|
||||
return isOff ? 'mdi:garage' : 'mdi:garage-open';
|
||||
case 'power':
|
||||
return isOff ? 'mdi:power-plug-off' : 'mdi:power-plug';
|
||||
case 'gas':
|
||||
case 'problem':
|
||||
case 'safety':
|
||||
case 'tamper':
|
||||
return isOff ? 'mdi:check-circle' : 'mdi:alert-circle';
|
||||
case 'smoke':
|
||||
return isOff ? 'mdi:check-circle' : 'mdi:smoke';
|
||||
case 'heat':
|
||||
return isOff ? 'mdi:thermometer' : 'mdi:fire';
|
||||
case 'light':
|
||||
return isOff ? 'mdi:brightness5' : 'mdi:brightness-7';
|
||||
case 'lock':
|
||||
return isOff ? 'mdi:lock' : 'mdi:lock-open';
|
||||
case 'moisture':
|
||||
return isOff ? 'mdi:water-off' : 'mdi:water';
|
||||
case 'motion':
|
||||
return isOff ? 'mdi:motion-sensor-off' : 'mdi:motion-sensor';
|
||||
case 'occupancy':
|
||||
return isOff ? 'mdi:home-outline' : 'mdi:home';
|
||||
case 'opening':
|
||||
return isOff ? 'mdi:square' : 'mdi:square-outline';
|
||||
case 'plug':
|
||||
return isOff ? 'mdi:power-plug-off' : 'mdi:power-plug';
|
||||
case 'presence':
|
||||
return isOff ? 'mdi:home-outline' : 'mdi:home';
|
||||
case 'running':
|
||||
return isOff ? 'mdi:stop' : 'mdi:play';
|
||||
case 'sound':
|
||||
return isOff ? 'mdi:music-note-off' : 'mdi:music-note';
|
||||
case 'update':
|
||||
return isOff ? 'mdi:package' : 'mdi:package-up';
|
||||
case 'vibration':
|
||||
return isOff ? 'mdi:crop-portrait' : 'mdi:vibrate';
|
||||
case 'window':
|
||||
return isOff ? 'mdi:window-closed' : 'mdi:window-open';
|
||||
default:
|
||||
return isOff ? 'mdi:radiobox-blank' : 'mdi:checkbox-marked-circle';
|
||||
}
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
|
||||
export const coverIcon = (state?: string, entity?: HassEntity): string => {
|
||||
const open = state !== 'closed';
|
||||
|
||||
switch (entity?.attributes.device_class) {
|
||||
case 'garage':
|
||||
switch (state) {
|
||||
case 'opening':
|
||||
return 'mdi:arrow-up-box';
|
||||
case 'closing':
|
||||
return 'mdi:arrow-down-box';
|
||||
case 'closed':
|
||||
return 'mdi:garage';
|
||||
default:
|
||||
return 'mdi:garage-open';
|
||||
}
|
||||
case 'gate':
|
||||
switch (state) {
|
||||
case 'opening':
|
||||
case 'closing':
|
||||
return 'mdi:gate-arrow-right';
|
||||
case 'closed':
|
||||
return 'mdi:gate';
|
||||
default:
|
||||
return 'mdi:gate-open';
|
||||
}
|
||||
case 'door':
|
||||
return open ? 'mdi:door-open' : 'mdi:door-closed';
|
||||
case 'damper':
|
||||
return open ? 'md:circle' : 'mdi:circle-slice-8';
|
||||
case 'shutter':
|
||||
switch (state) {
|
||||
case 'opening':
|
||||
return 'mdi:arrow-up-box';
|
||||
case 'closing':
|
||||
return 'mdi:arrow-down-box';
|
||||
case 'closed':
|
||||
return 'mdi:window-shutter';
|
||||
default:
|
||||
return 'mdi:window-shutter-open';
|
||||
}
|
||||
case 'curtain':
|
||||
switch (state) {
|
||||
case 'opening':
|
||||
return 'mdi:arrow-split-vertical';
|
||||
case 'closing':
|
||||
return 'mdi:arrow-collapse-horizontal';
|
||||
case 'closed':
|
||||
return 'mdi:curtains-closed';
|
||||
default:
|
||||
return 'mdi:curtains';
|
||||
}
|
||||
case 'blind':
|
||||
case 'shade':
|
||||
switch (state) {
|
||||
case 'opening':
|
||||
return 'mdi:arrow-up-box';
|
||||
case 'closing':
|
||||
return 'mdi:arrow-down-box';
|
||||
case 'closed':
|
||||
return 'mdi:blinds';
|
||||
default:
|
||||
return 'mdi:blinds-open';
|
||||
}
|
||||
case 'window':
|
||||
switch (state) {
|
||||
case 'opening':
|
||||
return 'mdi:arrow-up-box';
|
||||
case 'closing':
|
||||
return 'mdi:arrow-down-box';
|
||||
case 'closed':
|
||||
return 'mdi:window-closed';
|
||||
default:
|
||||
return 'mdi:window-open';
|
||||
}
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case 'opening':
|
||||
return 'mdi:arrow-up-box';
|
||||
case 'closing':
|
||||
return 'mdi:arrow-down-box';
|
||||
case 'closed':
|
||||
return 'mdi:window-closed';
|
||||
default:
|
||||
return 'mdi:window-open';
|
||||
}
|
||||
};
|
||||
@@ -1,204 +0,0 @@
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
import { UpdateEntity, updateIsInstalling } from '../ha/update.js';
|
||||
import { alarmPanelIcon } from './alarm-panel-icon';
|
||||
import { binarySensorIcon } from './binary-sensor-icon';
|
||||
import { coverIcon } from './cover-icon';
|
||||
import { sensorIcon } from './sensor-icon';
|
||||
|
||||
const DEFAULT_DOMAIN_ICON = 'mdi:bookmark';
|
||||
|
||||
const FIXED_DOMAIN_ICONS = {
|
||||
alert: 'mdi:alert',
|
||||
air_quality: 'mdi:air-filter',
|
||||
automation: 'mdi:robot',
|
||||
calendar: 'mdi:calendar',
|
||||
camera: 'mdi:video',
|
||||
climate: 'mdi:thermostat',
|
||||
configurator: 'mdi:cog',
|
||||
conversation: 'mdi:text-to-speech',
|
||||
counter: 'mdi:counter',
|
||||
fan: 'mdi:fan',
|
||||
google_assistant: 'mdi:google-assistant',
|
||||
group: 'mdi:google-circles-communities',
|
||||
homeassistant: 'mdi:home-assistant',
|
||||
homekit: 'mdi:home-automation',
|
||||
image_processing: 'mdi:image-filter-frames',
|
||||
input_button: 'mdi:gesture-tap-button',
|
||||
input_datetime: 'mdi:calendar-clock',
|
||||
input_number: 'mdi:ray-vertex',
|
||||
input_select: 'mdi:format-list-bulleted',
|
||||
input_text: 'mdi:form-textbox',
|
||||
light: 'mdi:lightbulb',
|
||||
mailbox: 'mdi:mailbox',
|
||||
notify: 'mdi:comment-alert',
|
||||
number: 'mdi:ray-vertex',
|
||||
persistent_notification: 'mdi:bell',
|
||||
person: 'mdi:account',
|
||||
plant: 'mdi:flower',
|
||||
proximity: 'mdi:apple-safari',
|
||||
remote: 'mdi:remote',
|
||||
scene: 'mdi:palette',
|
||||
script: 'mdi:script-text',
|
||||
select: 'mdi:format-list-bulleted',
|
||||
sensor: 'mdi:eye',
|
||||
siren: 'mdi:bullhorn',
|
||||
simple_alarm: 'mdi:bell',
|
||||
sun: 'mdi:white-balance-sunny',
|
||||
timer: 'mdi:timer-outline',
|
||||
updater: 'mdi:cloud-upload',
|
||||
vacuum: 'mdi:robot-vacuum',
|
||||
water_heater: 'mdi:thermometer',
|
||||
weather: 'mdi:weather-cloudy',
|
||||
zone: 'mdi:map-marker-radius',
|
||||
};
|
||||
|
||||
export function domainIcon(
|
||||
domain: string,
|
||||
entity?: HassEntity,
|
||||
state?: string,
|
||||
defaultIcon?: string,
|
||||
): string {
|
||||
switch (domain) {
|
||||
case 'alarm_control_panel':
|
||||
return alarmPanelIcon(state);
|
||||
|
||||
case 'binary_sensor':
|
||||
return binarySensorIcon(state, entity);
|
||||
|
||||
case 'button':
|
||||
switch (entity?.attributes.device_class) {
|
||||
case 'restart':
|
||||
return 'mdi:restart';
|
||||
case 'update':
|
||||
return 'mdi:package-up';
|
||||
default:
|
||||
return 'mdi:gesture-tap-button';
|
||||
}
|
||||
|
||||
case 'cover':
|
||||
return coverIcon(state, entity);
|
||||
|
||||
case 'device_tracker':
|
||||
if (entity?.attributes.source_type === 'router') {
|
||||
return state === 'home' ? 'mdi:lan-connect' : 'mdi:lan-disconnect';
|
||||
}
|
||||
if (['bluetooth', 'bluetooth_le'].includes(entity?.attributes.source_type)) {
|
||||
return state === 'home' ? 'mdi:bluetooth-connect' : 'mdi:bluetooth';
|
||||
}
|
||||
return state === 'not_home' ? 'mdi:account-arrow-right' : 'mdi:account';
|
||||
|
||||
case 'humidifier':
|
||||
return state && state === 'off' ? 'mdi:air-humidifier-off' : 'mdi:air-humidifier';
|
||||
|
||||
case 'input_boolean':
|
||||
return state === 'on' ? 'mdi:check-circle-outline' : 'mdi:close-circle-outline';
|
||||
|
||||
case 'lock':
|
||||
switch (state) {
|
||||
case 'unlocked':
|
||||
return 'mdi:lock-open';
|
||||
case 'jammed':
|
||||
return 'mdi:lock-alert';
|
||||
case 'locking':
|
||||
case 'unlocking':
|
||||
return 'mdi:lock-clock';
|
||||
default:
|
||||
return 'mdi:lock';
|
||||
}
|
||||
|
||||
// Taken from https://github.com/home-assistant/frontend/blob/45646eaf0bfc86aa0d0d6b73809ef0b32abb5729/src/common/entity/domain_icon.ts#L138-L172
|
||||
case 'media_player':
|
||||
switch (entity?.attributes.device_class) {
|
||||
case 'speaker':
|
||||
switch (state) {
|
||||
case 'playing':
|
||||
return 'mdi:speaker-play';
|
||||
case 'paused':
|
||||
return 'mdi:speaker-pause';
|
||||
case 'off':
|
||||
return 'mdi:speaker-off';
|
||||
default:
|
||||
return 'mdi:speaker';
|
||||
}
|
||||
case 'tv':
|
||||
switch (state) {
|
||||
case 'playing':
|
||||
return 'mdi:television-play';
|
||||
case 'paused':
|
||||
return 'mdi:television-pause';
|
||||
case 'off':
|
||||
return 'mdi:television-off';
|
||||
default:
|
||||
return 'mdi:television';
|
||||
}
|
||||
default:
|
||||
switch (state) {
|
||||
case 'playing':
|
||||
case 'paused':
|
||||
return 'mdi:cast-connected';
|
||||
case 'off':
|
||||
return 'mdi:cast-off';
|
||||
default:
|
||||
return 'mdi:cast';
|
||||
}
|
||||
}
|
||||
|
||||
case 'switch':
|
||||
switch (entity?.attributes.device_class) {
|
||||
case 'outlet':
|
||||
return state === 'on' ? 'mdi:power-plug' : 'mdi:power-plug-off';
|
||||
case 'switch':
|
||||
return state === 'on' ? 'mdi:toggle-switch' : 'mdi:toggle-switch-off';
|
||||
default:
|
||||
return 'mdi:flash';
|
||||
}
|
||||
|
||||
case 'zwave':
|
||||
switch (state) {
|
||||
case 'dead':
|
||||
return 'mdi:emoticon-dead';
|
||||
case 'sleeping':
|
||||
return 'mdi:sleep';
|
||||
case 'initializing':
|
||||
return 'mdi:timer-sand';
|
||||
default:
|
||||
return 'mdi:z-wave';
|
||||
}
|
||||
|
||||
case 'sensor': {
|
||||
const icon = sensorIcon(entity);
|
||||
if (icon) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'input_datetime':
|
||||
if (!entity?.attributes.has_date) {
|
||||
return 'mdi:clock';
|
||||
}
|
||||
if (!entity.attributes.has_time) {
|
||||
return 'mdi:calendar';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sun':
|
||||
return entity?.state === 'above_horizon'
|
||||
? FIXED_DOMAIN_ICONS[domain]
|
||||
: 'mdi:weather-night';
|
||||
|
||||
case 'update':
|
||||
return entity?.state === 'on'
|
||||
? updateIsInstalling(entity as UpdateEntity)
|
||||
? 'mdi:package-down'
|
||||
: 'mdi:package-up'
|
||||
: 'mdi:package';
|
||||
}
|
||||
|
||||
if (domain in FIXED_DOMAIN_ICONS) {
|
||||
return FIXED_DOMAIN_ICONS[domain];
|
||||
}
|
||||
|
||||
return defaultIcon ?? DEFAULT_DOMAIN_ICON;
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import { UNIT_C, UNIT_F } from '@dermotduffy/custom-card-helpers';
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
|
||||
const FIXED_DEVICE_CLASS_ICONS = {
|
||||
apparent_power: 'mdi:flash',
|
||||
aqi: 'mdi:air-filter',
|
||||
carbon_dioxide: 'mdi:molecule-co2',
|
||||
carbon_monoxide: 'mdi:molecule-co',
|
||||
current: 'mdi:current-ac',
|
||||
date: 'mdi:calendar',
|
||||
energy: 'mdi:lightning-bolt',
|
||||
frequency: 'mdi:sine-wave',
|
||||
gas: 'mdi:gas-cylinder',
|
||||
humidity: 'mdi:water-percent',
|
||||
illuminance: 'mdi:brightness-5',
|
||||
monetary: 'mdi:cash',
|
||||
nitrogen_dioxide: 'mdi:molecule',
|
||||
nitrogen_monoxide: 'mdi:molecule',
|
||||
nitrous_oxide: 'mdi:molecule',
|
||||
ozone: 'mdi:molecule',
|
||||
pm1: 'mdi:molecule',
|
||||
pm10: 'mdi:molecule',
|
||||
pm25: 'mdi:molecule',
|
||||
power: 'mdi:flash',
|
||||
power_factor: 'mdi:angle-acute',
|
||||
pressure: 'mdi:gauge',
|
||||
reactive_power: 'mdi:flash',
|
||||
signal_strength: 'mdi:wifi',
|
||||
sulphur_dioxide: 'mdi:molecule',
|
||||
temperature: 'mdi:thermometer',
|
||||
timestamp: 'mdi:clock',
|
||||
volatile_organic_compounds: 'mdi:molecule',
|
||||
voltage: 'mdi:sine-wave',
|
||||
};
|
||||
|
||||
const SENSOR_DEVICE_CLASS_BATTERY = 'battery';
|
||||
|
||||
const BATTERY_ICONS = {
|
||||
10: 'mdi:battery-10',
|
||||
20: 'mdi:battery-20',
|
||||
30: 'mdi:battery-30',
|
||||
40: 'mdi:battery-40',
|
||||
50: 'mdi:battery-50',
|
||||
60: 'mdi:battery-60',
|
||||
70: 'mdi:battery-70',
|
||||
80: 'mdi:battery-80',
|
||||
90: 'mdi:battery-90',
|
||||
100: 'mdi:battery',
|
||||
};
|
||||
const BATTERY_CHARGING_ICONS = {
|
||||
10: 'mdi:battery-charging-10',
|
||||
20: 'mdi:battery-charging-20',
|
||||
30: 'mdi:battery-charging-30',
|
||||
40: 'mdi:battery-charging-40',
|
||||
50: 'mdi:battery-charging-50',
|
||||
60: 'mdi:battery-charging-60',
|
||||
70: 'mdi:battery-charging-70',
|
||||
80: 'mdi:battery-charging-80',
|
||||
90: 'mdi:battery-charging-90',
|
||||
100: 'mdi:battery-charging',
|
||||
};
|
||||
|
||||
const batteryStateIcon = (
|
||||
batteryEntity: HassEntity,
|
||||
batteryChargingEntity?: HassEntity,
|
||||
) => {
|
||||
const battery = batteryEntity.state;
|
||||
const batteryCharging = batteryChargingEntity?.state === 'on';
|
||||
|
||||
return batteryIcon(battery, batteryCharging);
|
||||
};
|
||||
|
||||
const batteryIcon = (batteryState: number | string, batteryCharging?: boolean) => {
|
||||
const batteryValue = Number(batteryState);
|
||||
if (isNaN(batteryValue)) {
|
||||
if (batteryState === 'off') {
|
||||
return 'mdi:battery';
|
||||
}
|
||||
if (batteryState === 'on') {
|
||||
return 'mdi:battery-alert';
|
||||
}
|
||||
return 'mdi:battery-unknown';
|
||||
}
|
||||
|
||||
const batteryRound = Math.round(batteryValue / 10) * 10;
|
||||
if (batteryCharging && batteryValue >= 10) {
|
||||
return BATTERY_CHARGING_ICONS[batteryRound];
|
||||
}
|
||||
if (batteryCharging) {
|
||||
return 'mdi:battery-charging-outline';
|
||||
}
|
||||
if (batteryValue <= 5) {
|
||||
return 'mdi:battery-alert-variant-outline';
|
||||
}
|
||||
return BATTERY_ICONS[batteryRound];
|
||||
};
|
||||
|
||||
export const sensorIcon = (entity?: HassEntity): string | undefined => {
|
||||
const dclass = entity?.attributes.device_class;
|
||||
|
||||
if (dclass && dclass in FIXED_DEVICE_CLASS_ICONS) {
|
||||
return FIXED_DEVICE_CLASS_ICONS[dclass];
|
||||
}
|
||||
|
||||
if (dclass === SENSOR_DEVICE_CLASS_BATTERY) {
|
||||
return entity ? batteryStateIcon(entity) : 'mdi:battery';
|
||||
}
|
||||
|
||||
const unit = entity?.attributes.unit_of_measurement;
|
||||
if (unit === UNIT_C || unit === UNIT_F) {
|
||||
return 'mdi:thermometer';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@@ -206,7 +206,11 @@ describe('GenericCameraManagerEngine', () => {
|
||||
expect(
|
||||
createEngine().getCameraMetadata(createHASS(), createGenericCameraConfig()),
|
||||
).toEqual({
|
||||
icon: 'mdi:video',
|
||||
icon: {
|
||||
entity: undefined,
|
||||
icon: undefined,
|
||||
fallback: 'mdi:video',
|
||||
},
|
||||
title: '',
|
||||
});
|
||||
});
|
||||
@@ -217,10 +221,11 @@ describe('GenericCameraManagerEngine', () => {
|
||||
createHASS(),
|
||||
createGenericCameraConfig({ id: 'https://go2rtc#stream' }),
|
||||
),
|
||||
).toEqual({
|
||||
icon: 'mdi:video',
|
||||
title: 'https://go2rtc#stream',
|
||||
});
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
title: 'https://go2rtc#stream',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('with configured title', async () => {
|
||||
@@ -231,10 +236,11 @@ describe('GenericCameraManagerEngine', () => {
|
||||
title: 'My Camera',
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
icon: 'mdi:video',
|
||||
title: 'My Camera',
|
||||
});
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
title: 'My Camera',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('with entity title', () => {
|
||||
@@ -250,10 +256,11 @@ describe('GenericCameraManagerEngine', () => {
|
||||
camera_entity: 'camera.test',
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
icon: 'mdi:video',
|
||||
title: 'My Entity Camera',
|
||||
});
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
title: 'My Entity Camera',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('webrtc_card.entity', async () => {
|
||||
@@ -270,10 +277,11 @@ describe('GenericCameraManagerEngine', () => {
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toEqual({
|
||||
icon: 'mdi:video',
|
||||
title: 'My Entity Camera',
|
||||
});
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
title: 'My Entity Camera',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -259,8 +259,12 @@ describe('ReolinkCameraManagerEngine', () => {
|
||||
});
|
||||
const engine = createEngine();
|
||||
expect(engine.getCameraMetadata(createHASS(), cameraConfig)).toEqual({
|
||||
engineLogo: '/src/camera-manager/reolink/assets/reolink.svg',
|
||||
icon: 'mdi:camera',
|
||||
engineIcon: 'reolink',
|
||||
icon: {
|
||||
icon: 'mdi:camera',
|
||||
entity: 'camera.office',
|
||||
fallback: 'mdi:video',
|
||||
},
|
||||
title: 'Office',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ describe('StatusBarItemManager', () => {
|
||||
const cameraManager = createCameraManager(store);
|
||||
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
|
||||
title: 'Camera Title',
|
||||
icon: 'mdi:camera',
|
||||
icon: { icon: 'mdi:camera' },
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -225,8 +225,10 @@ describe('StatusBarItemManager', () => {
|
||||
const cameraManager = createCameraManager(store);
|
||||
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
|
||||
title: 'Camera Title',
|
||||
icon: 'mdi:camera',
|
||||
engineLogo: 'IMAGE_LOGO',
|
||||
icon: {
|
||||
icon: 'mdi:camera',
|
||||
},
|
||||
engineIcon: 'ENGINE_ICON',
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -235,8 +237,8 @@ describe('StatusBarItemManager', () => {
|
||||
view: createView({ view: 'live', camera: 'camera-1' }),
|
||||
}),
|
||||
).toContainEqual({
|
||||
type: 'custom:frigate-card-status-bar-image' as const,
|
||||
image: 'IMAGE_LOGO',
|
||||
type: 'custom:frigate-card-status-bar-icon' as const,
|
||||
icon: 'ENGINE_ICON',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { IconController } from '../../src/components-lib/icon-controller';
|
||||
import { createHASS, createStateEntity } from '../test-utils';
|
||||
|
||||
describe('IconController', () => {
|
||||
describe('should get custom icon', () => {
|
||||
it('should return frigate SVG for frigate icon', () => {
|
||||
expect(new IconController().getCustomIcon({ icon: 'frigate' })).toMatch(
|
||||
/frigate.svg$/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return motioneye SVG for motioneye icon', () => {
|
||||
expect(new IconController().getCustomIcon({ icon: 'motioneye' })).toMatch(
|
||||
/motioneye.svg$/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return reolink SVG for reolink icon', () => {
|
||||
expect(new IconController().getCustomIcon({ icon: 'reolink' })).toMatch(
|
||||
/reolink.svg$/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null for mdi icon', () => {
|
||||
expect(new IconController().getCustomIcon({ icon: 'mdi:car' })).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for undefined icon', () => {
|
||||
expect(new IconController().getCustomIcon()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should create state object for state badge', () => {
|
||||
it('should return null for non-existent entity', () => {
|
||||
expect(
|
||||
new IconController().createStateObjectForStateBadge(
|
||||
createHASS(),
|
||||
'sensor.DOES_NOT_EXIST',
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should return modified state object for existing entity', () => {
|
||||
expect(
|
||||
new IconController().createStateObjectForStateBadge(
|
||||
createHASS({
|
||||
'sensor.existing': createStateEntity({
|
||||
entity_id: 'sensor.existing',
|
||||
attributes: {
|
||||
friendly_name: 'Existing',
|
||||
icon: 'mdi:car',
|
||||
entity_picture: 'http://example.com/image.jpg',
|
||||
entity_picture_local: 'local.jpg',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
'sensor.existing',
|
||||
),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
entity_id: 'sensor.existing',
|
||||
attributes: expect.objectContaining({
|
||||
friendly_name: 'Existing',
|
||||
icon: 'mdi:car',
|
||||
entity_picture: undefined,
|
||||
entity_picture_local: undefined,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -127,7 +127,9 @@ describe('MenuButtonController', () => {
|
||||
);
|
||||
vi.mocked(cameraManager).getCameraMetadata.mockReturnValue({
|
||||
title: 'title',
|
||||
icon: 'icon',
|
||||
icon: {
|
||||
icon: 'icon',
|
||||
},
|
||||
});
|
||||
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
||||
|
||||
@@ -178,7 +180,9 @@ describe('MenuButtonController', () => {
|
||||
);
|
||||
vi.mocked(cameraManager).getCameraMetadata.mockReturnValue({
|
||||
title: 'title',
|
||||
icon: 'icon',
|
||||
icon: {
|
||||
icon: 'icon',
|
||||
},
|
||||
});
|
||||
const buttons = calculateButtons(controller, { cameraManager: cameraManager });
|
||||
|
||||
@@ -332,7 +336,10 @@ describe('MenuButtonController', () => {
|
||||
return cameraID === 'camera-1'
|
||||
? {
|
||||
title: 'title',
|
||||
icon: 'icon',
|
||||
icon: {
|
||||
icon: 'icon',
|
||||
entity: 'entity',
|
||||
},
|
||||
}
|
||||
: null;
|
||||
},
|
||||
@@ -351,7 +358,7 @@ describe('MenuButtonController', () => {
|
||||
{
|
||||
enabled: true,
|
||||
icon: 'icon',
|
||||
entity: 'camera.1',
|
||||
entity: 'entity',
|
||||
state_color: true,
|
||||
title: 'title',
|
||||
selected: true,
|
||||
@@ -364,7 +371,7 @@ describe('MenuButtonController', () => {
|
||||
{
|
||||
enabled: true,
|
||||
icon: undefined,
|
||||
entity: 'camera.2',
|
||||
entity: undefined,
|
||||
state_color: true,
|
||||
title: undefined,
|
||||
selected: false,
|
||||
@@ -377,7 +384,7 @@ describe('MenuButtonController', () => {
|
||||
{
|
||||
enabled: true,
|
||||
icon: undefined,
|
||||
entity: 'camera.3',
|
||||
entity: undefined,
|
||||
state_color: true,
|
||||
title: undefined,
|
||||
selected: false,
|
||||
@@ -443,7 +450,7 @@ describe('MenuButtonController', () => {
|
||||
{
|
||||
enabled: true,
|
||||
icon: undefined,
|
||||
entity: 'camera.1',
|
||||
entity: undefined,
|
||||
state_color: true,
|
||||
title: undefined,
|
||||
selected: false,
|
||||
@@ -456,7 +463,7 @@ describe('MenuButtonController', () => {
|
||||
{
|
||||
enabled: true,
|
||||
icon: undefined,
|
||||
entity: 'camera.2',
|
||||
entity: undefined,
|
||||
state_color: true,
|
||||
title: undefined,
|
||||
// camera-2 is selected in this test scenario because of the view
|
||||
@@ -471,7 +478,7 @@ describe('MenuButtonController', () => {
|
||||
{
|
||||
enabled: true,
|
||||
icon: undefined,
|
||||
entity: 'camera.3',
|
||||
entity: undefined,
|
||||
state_color: true,
|
||||
title: undefined,
|
||||
selected: false,
|
||||
@@ -1212,7 +1219,6 @@ describe('MenuButtonController', () => {
|
||||
{
|
||||
enabled: true,
|
||||
selected: false,
|
||||
icon: 'mdi:cast',
|
||||
entity: 'media_player.tv',
|
||||
state_color: false,
|
||||
title: 'media_player.tv',
|
||||
@@ -1266,7 +1272,6 @@ describe('MenuButtonController', () => {
|
||||
{
|
||||
enabled: true,
|
||||
selected: false,
|
||||
icon: 'mdi:bookmark',
|
||||
entity: 'not_a_real_player',
|
||||
state_color: false,
|
||||
title: 'not_a_real_player',
|
||||
|
||||
@@ -2,9 +2,7 @@ import { handleActionConfig } from '@dermotduffy/custom-card-helpers';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { MenuController } from '../../src/components-lib/menu-controller';
|
||||
import { MenuConfig, menuConfigSchema } from '../../src/config/types';
|
||||
import { StateParameters } from '../../src/types';
|
||||
import { refreshDynamicStateParameters } from '../../src/utils/ha';
|
||||
import { createInteractionEvent, createHASS, createLitElement } from '../test-utils';
|
||||
import { createInteractionEvent, createLitElement } from '../test-utils';
|
||||
|
||||
vi.mock('@dermotduffy/custom-card-helpers');
|
||||
vi.mock('../../src/utils/ha');
|
||||
@@ -362,38 +360,6 @@ describe('MenuController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get fresh button state', () => {
|
||||
it('on state icon', () => {
|
||||
const controller = new MenuController(createLitElement());
|
||||
const stateButton = {
|
||||
type: 'custom:frigate-card-menu-state-icon' as const,
|
||||
icon: 'mdi:sheep',
|
||||
entity: 'switch.foo',
|
||||
state_color: true,
|
||||
};
|
||||
|
||||
const stateParameters: StateParameters = {};
|
||||
vi.mocked(refreshDynamicStateParameters).mockReturnValue(stateParameters);
|
||||
|
||||
expect(controller.getFreshButtonState(createHASS(), stateButton)).toBe(
|
||||
stateParameters,
|
||||
);
|
||||
|
||||
expect(vi.mocked(refreshDynamicStateParameters)).toBeCalled();
|
||||
});
|
||||
|
||||
it('on non state icon', () => {
|
||||
const controller = new MenuController(createLitElement());
|
||||
const button = {
|
||||
type: 'custom:frigate-card-menu-icon' as const,
|
||||
icon: 'mdi:sheep',
|
||||
};
|
||||
|
||||
expect(controller.getFreshButtonState(createHASS(), button)).toEqual(button);
|
||||
expect(vi.mocked(refreshDynamicStateParameters)).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle actions', () => {
|
||||
it('should bail without config', () => {
|
||||
const controller = new MenuController(createLitElement());
|
||||
|
||||
@@ -285,7 +285,7 @@ describe('config defaults', () => {
|
||||
},
|
||||
},
|
||||
status_bar: {
|
||||
height: 46,
|
||||
height: 30,
|
||||
items: {
|
||||
engine: {
|
||||
enabled: true,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import frigateSVG from '../../src/camera-manager/frigate/assets/frigate.svg';
|
||||
import motioneyeSVG from '../../src/camera-manager/motioneye/assets/motioneye.svg';
|
||||
import reolinkSVG from '../../src/camera-manager/reolink/assets/reolink.svg';
|
||||
import { getCustomIconURL } from '../../src/utils/custom-icons';
|
||||
|
||||
describe('getCustomIconURL', () => {
|
||||
it('should return frigate SVG for frigate icon', () => {
|
||||
expect(getCustomIconURL('frigate')).toBe(frigateSVG);
|
||||
});
|
||||
|
||||
it('should return motioneye SVG for motioneye icon', () => {
|
||||
expect(getCustomIconURL('motioneye')).toBe(motioneyeSVG);
|
||||
});
|
||||
|
||||
it('should return reolink SVG for reolink icon', () => {
|
||||
expect(getCustomIconURL('reolink')).toBe(reolinkSVG);
|
||||
});
|
||||
|
||||
it('should return null for mdi icon', () => {
|
||||
expect(getCustomIconURL('mdi:car')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for undefined icon', () => {
|
||||
expect(getCustomIconURL()).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -2,11 +2,10 @@ import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
canonicalizeHAURL,
|
||||
getEntityIcon,
|
||||
hasHAConnectionStateChanged,
|
||||
isHARelativeURL,
|
||||
} from '../../../src/utils/ha/index.js';
|
||||
import { createHASS, createStateEntity } from '../../test-utils.js';
|
||||
import { createHASS } from '../../test-utils.js';
|
||||
|
||||
const createConnected = (connected: boolean): HomeAssistant => {
|
||||
const hass = createHASS();
|
||||
@@ -49,27 +48,6 @@ describe('hasHAConnectionStateChanged', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntityIcon', () => {
|
||||
it('should get icon from attributes', () => {
|
||||
expect(
|
||||
getEntityIcon(
|
||||
createHASS({
|
||||
'camera.test': createStateEntity({
|
||||
attributes: {
|
||||
icon: 'mdi:cow',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
'camera.test',
|
||||
),
|
||||
).toBe('mdi:cow');
|
||||
});
|
||||
|
||||
it('should get icon from domain', () => {
|
||||
expect(getEntityIcon(createHASS(), 'camera.test')).toBe('mdi:video');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isHARelativeURL', () => {
|
||||
it('should return true when URL is HA relative', () => {
|
||||
expect(isHARelativeURL('/api/foo')).toBeTruthy();
|
||||
|
||||
+7
-1
@@ -25,14 +25,20 @@
|
||||
"globalTags": [
|
||||
"ha-button-menu",
|
||||
"ha-button",
|
||||
"ha-camera-stream",
|
||||
"ha-card",
|
||||
"ha-circular-progress",
|
||||
"ha-combo-box",
|
||||
"ha-hls-player",
|
||||
"ha-icon-button",
|
||||
"ha-icon",
|
||||
"ha-menu-button",
|
||||
"ha-selector",
|
||||
"ha-state-icon",
|
||||
"ha-web-rtc-player",
|
||||
"mwc-button",
|
||||
"mwc-list-item"
|
||||
"mwc-list-item",
|
||||
"state-badge"
|
||||
],
|
||||
"rules": {
|
||||
"no-unknown-tag-name": "error",
|
||||
|
||||
Reference in New Issue
Block a user