refactor: Retire the use of unmaintained custom-card-helpers (#1956)
- Closes #1295
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Custom cards for Home Assistant
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Background
|
||||
|
||||
Some of these functions are inspired by or modified from the unmaintained
|
||||
https://github.com/custom-cards/custom-card-helpers .
|
||||
|
||||
This directory is intended to contain usage agnostic Home Assistant
|
||||
functionality.
|
||||
@@ -0,0 +1,3 @@
|
||||
export const computeDomain = (entityId: string): string => {
|
||||
return entityId.substring(0, entityId.indexOf('.'));
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export const STATES_OFF = ['closed', 'locked', 'off'];
|
||||
export const STATES_ON = ['open', 'unlocked', 'on'];
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ValidHassDomEvent } from './types';
|
||||
|
||||
/**
|
||||
* Fire an event has per Home Assistant frontend specifications. All events
|
||||
* fired by this method has expected content/behavior based on type.
|
||||
*/
|
||||
export const fireHASSEvent = <HassEvent extends ValidHassDomEvent>(
|
||||
target: EventTarget,
|
||||
type: HassEvent,
|
||||
detail?: HASSDomEvents[HassEvent],
|
||||
options?: {
|
||||
bubbles?: boolean;
|
||||
cancelable?: boolean;
|
||||
composed?: boolean;
|
||||
},
|
||||
) => {
|
||||
target.dispatchEvent(
|
||||
new CustomEvent<HASSDomEvents[HassEvent]>(type, {
|
||||
bubbles: options?.bubbles ?? true,
|
||||
composed: options?.composed ?? true,
|
||||
cancelable: options?.cancelable ?? false,
|
||||
detail: detail,
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { fireHASSEvent } from './fire-hass-event.js';
|
||||
import { forwardHaptic } from './haptic.js';
|
||||
import { navigate } from './navigate.js';
|
||||
import { toggleEntity } from './toggle-entity.js';
|
||||
import { ActionConfig, HomeAssistant } from './types.js';
|
||||
|
||||
export const handleActionConfig = (
|
||||
node: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
config: {
|
||||
entity?: string;
|
||||
camera_image?: string;
|
||||
hold_action?: ActionConfig;
|
||||
tap_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
},
|
||||
actionConfig: ActionConfig | undefined,
|
||||
): void => {
|
||||
if (!actionConfig) {
|
||||
actionConfig = {
|
||||
action: 'more-info',
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
actionConfig.confirmation &&
|
||||
(!actionConfig.confirmation.exemptions ||
|
||||
!actionConfig.confirmation.exemptions.some((e) => e.user === hass!.user!.id))
|
||||
) {
|
||||
forwardHaptic('warning');
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
actionConfig.confirmation.text ||
|
||||
`Are you sure you want to ${actionConfig.action}?`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (actionConfig.action) {
|
||||
case 'more-info':
|
||||
if (config.entity || config.camera_image) {
|
||||
fireHASSEvent(node, 'hass-more-info', {
|
||||
entityId: config.entity ? config.entity : config.camera_image!,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'navigate':
|
||||
if (actionConfig.navigation_path) {
|
||||
navigate(node, actionConfig.navigation_path);
|
||||
}
|
||||
break;
|
||||
case 'url':
|
||||
if (actionConfig.url_path) {
|
||||
window.open(actionConfig.url_path);
|
||||
}
|
||||
break;
|
||||
case 'toggle':
|
||||
if (config.entity) {
|
||||
toggleEntity(hass, config.entity!);
|
||||
forwardHaptic('success');
|
||||
}
|
||||
break;
|
||||
case 'perform-action': {
|
||||
if (!actionConfig.perform_action) {
|
||||
forwardHaptic('failure');
|
||||
return;
|
||||
}
|
||||
const [domain, service] = actionConfig.perform_action.split('.', 2);
|
||||
hass.callService(domain, service, actionConfig.data, actionConfig.target);
|
||||
forwardHaptic('success');
|
||||
break;
|
||||
}
|
||||
case 'call-service': {
|
||||
if (!actionConfig.service) {
|
||||
forwardHaptic('failure');
|
||||
return;
|
||||
}
|
||||
const [domain, service] = actionConfig.service.split('.', 2);
|
||||
hass.callService(domain, service, actionConfig.data, actionConfig.target);
|
||||
forwardHaptic('success');
|
||||
break;
|
||||
}
|
||||
case 'fire-dom-event': {
|
||||
fireHASSEvent(node, 'll-custom', actionConfig);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Broadcast haptic feedback requests
|
||||
*/
|
||||
import { fireHASSEvent } from './fire-hass-event';
|
||||
|
||||
// Allowed types are from iOS HIG.
|
||||
// https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/feedback/#haptics
|
||||
// Implementors on platforms other than iOS should attempt to match the patterns (shown in HIG) as closely as possible.
|
||||
export type HapticType =
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'failure'
|
||||
| 'light'
|
||||
| 'medium'
|
||||
| 'heavy'
|
||||
| 'selection';
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
haptic: HapticType;
|
||||
}
|
||||
|
||||
interface GlobalEventHandlersEventMap {
|
||||
haptic: CustomEvent<HapticType>;
|
||||
}
|
||||
}
|
||||
|
||||
export const forwardHaptic = (hapticType: HapticType) => {
|
||||
fireHASSEvent(window, 'haptic', hapticType);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ActionConfig } from './types.js';
|
||||
|
||||
export function hasAction(config?: ActionConfig): boolean {
|
||||
return config !== undefined && config.action !== 'none';
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { fireHASSEvent } from './fire-hass-event.js';
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
'location-changed': {
|
||||
replace: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const navigate = (_node: unknown, path: string, replace: boolean = false) => {
|
||||
if (replace) {
|
||||
history.replaceState(null, '', path);
|
||||
} else {
|
||||
history.pushState(null, '', path);
|
||||
}
|
||||
fireHASSEvent(window, 'location-changed', {
|
||||
replace,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { STATES_OFF } from './const.js';
|
||||
import { turnOnOffEntity } from './turn-on-off-entity.js';
|
||||
import { HomeAssistant } from './types.js';
|
||||
|
||||
export const toggleEntity = (hass: HomeAssistant, entityId: string): Promise<void> => {
|
||||
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
|
||||
return turnOnOffEntity(hass, entityId, turnOn);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { HomeAssistant } from './types.js';
|
||||
import { computeDomain } from './compute-domain.js';
|
||||
|
||||
export const turnOnOffEntity = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
turnOn = true,
|
||||
): Promise<void> => {
|
||||
const stateDomain = computeDomain(entityId);
|
||||
const serviceDomain = stateDomain === 'group' ? 'homeassistant' : stateDomain;
|
||||
|
||||
let service;
|
||||
switch (stateDomain) {
|
||||
case 'lock':
|
||||
service = turnOn ? 'unlock' : 'lock';
|
||||
break;
|
||||
case 'cover':
|
||||
service = turnOn ? 'open_cover' : 'close_cover';
|
||||
break;
|
||||
default:
|
||||
service = turnOn ? 'turn_on' : 'turn_off';
|
||||
}
|
||||
|
||||
return hass.callService(serviceDomain, service, { entity_id: entityId });
|
||||
};
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
import {
|
||||
Auth,
|
||||
Connection,
|
||||
HassConfig,
|
||||
HassEntities,
|
||||
HassServices,
|
||||
HassServiceTarget,
|
||||
MessageBase,
|
||||
} from 'home-assistant-js-websocket';
|
||||
import { HapticType } from './haptic.js';
|
||||
|
||||
interface ToggleMenuActionConfig extends BaseActionConfig {
|
||||
action: 'toggle-menu';
|
||||
}
|
||||
|
||||
export interface ToggleActionConfig extends BaseActionConfig {
|
||||
action: 'toggle';
|
||||
}
|
||||
|
||||
export interface CallServiceActionConfig extends BaseActionConfig {
|
||||
action: 'call-service';
|
||||
service: string;
|
||||
data?: {
|
||||
entity_id?: string | [string];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
target?: HassServiceTarget;
|
||||
repeat?: number;
|
||||
haptic?: HapticType;
|
||||
}
|
||||
|
||||
export interface PerformActionActionConfig extends BaseActionConfig {
|
||||
action: 'perform-action';
|
||||
perform_action: string;
|
||||
data?: {
|
||||
entity_id?: string | [string];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
target?: HassServiceTarget;
|
||||
repeat?: number;
|
||||
haptic?: HapticType;
|
||||
}
|
||||
|
||||
export interface NavigateActionConfig extends BaseActionConfig {
|
||||
action: 'navigate';
|
||||
navigation_path: string;
|
||||
}
|
||||
|
||||
export interface UrlActionConfig extends BaseActionConfig {
|
||||
action: 'url';
|
||||
url_path: string;
|
||||
}
|
||||
export interface MoreInfoActionConfig extends BaseActionConfig {
|
||||
action: 'more-info';
|
||||
entity?: string;
|
||||
}
|
||||
export interface NoActionConfig extends BaseActionConfig {
|
||||
action: 'none';
|
||||
}
|
||||
interface CustomActionConfig extends BaseActionConfig {
|
||||
action: 'fire-dom-event';
|
||||
}
|
||||
/**
|
||||
* `repeat` and `haptic` are specifically for use in custom cards like the Button-Card
|
||||
*/
|
||||
interface BaseActionConfig {
|
||||
confirmation?: ConfirmationRestrictionConfig;
|
||||
repeat?: number;
|
||||
haptic?: HapticType;
|
||||
}
|
||||
|
||||
export interface ConfirmationRestrictionConfig {
|
||||
text?: string;
|
||||
exemptions?: RestrictionConfig[];
|
||||
}
|
||||
|
||||
interface RestrictionConfig {
|
||||
user: string;
|
||||
}
|
||||
|
||||
export declare type ActionConfig =
|
||||
| ToggleActionConfig
|
||||
| CallServiceActionConfig
|
||||
| PerformActionActionConfig
|
||||
| NavigateActionConfig
|
||||
| UrlActionConfig
|
||||
| MoreInfoActionConfig
|
||||
| NoActionConfig
|
||||
| CustomActionConfig
|
||||
| ToggleMenuActionConfig;
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
'value-changed': {
|
||||
value: unknown;
|
||||
};
|
||||
'config-changed': {
|
||||
config: unknown;
|
||||
};
|
||||
'hass-more-info': {
|
||||
entityId: string | undefined;
|
||||
};
|
||||
'll-rebuild': object;
|
||||
'll-custom': object;
|
||||
'location-changed': {
|
||||
replace: boolean;
|
||||
};
|
||||
'show-dialog': object;
|
||||
undefined: unknown;
|
||||
action: {
|
||||
action: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type ValidHassDomEvent = keyof HASSDomEvents;
|
||||
|
||||
declare type LocalizeFunc = (key: string, ...args: unknown[]) => string;
|
||||
|
||||
interface Credential {
|
||||
auth_provider_type: string;
|
||||
auth_provider_id: string;
|
||||
}
|
||||
|
||||
interface MFAModule {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface CurrentUser {
|
||||
id: string;
|
||||
is_owner: boolean;
|
||||
is_admin: boolean;
|
||||
name: string;
|
||||
credentials: Credential[];
|
||||
mfa_modules: MFAModule[];
|
||||
}
|
||||
|
||||
interface Theme {
|
||||
'primary-color': string;
|
||||
'text-primary-color': string;
|
||||
'accent-color': string;
|
||||
}
|
||||
|
||||
interface Themes {
|
||||
darkMode?: boolean;
|
||||
default_theme: string;
|
||||
themes: {
|
||||
[key: string]: Theme;
|
||||
};
|
||||
}
|
||||
|
||||
interface Panel {
|
||||
component_name: string;
|
||||
config: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
icon: string | null;
|
||||
title: string | null;
|
||||
url_path: string;
|
||||
}
|
||||
|
||||
interface Panels {
|
||||
[name: string]: Panel;
|
||||
}
|
||||
|
||||
interface Resources {
|
||||
[language: string]: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Translation {
|
||||
nativeName: string;
|
||||
isRTL: boolean;
|
||||
fingerprints: {
|
||||
[fragment: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ServiceCallRequest {
|
||||
domain: string;
|
||||
service: string;
|
||||
serviceData?: Record<string, unknown>;
|
||||
target?: HassServiceTarget;
|
||||
}
|
||||
|
||||
export interface HomeAssistant {
|
||||
auth: Auth;
|
||||
connection: Connection;
|
||||
connected: boolean;
|
||||
states: HassEntities;
|
||||
services: HassServices;
|
||||
config: HassConfig;
|
||||
themes: Themes;
|
||||
selectedTheme?: string | null;
|
||||
panels: Panels;
|
||||
panelUrl: string;
|
||||
language: string;
|
||||
locale: FrontendLocaleData;
|
||||
selectedLanguage: string | null;
|
||||
resources: Resources;
|
||||
localize: LocalizeFunc;
|
||||
translationMetadata: {
|
||||
fragments: string[];
|
||||
translations: {
|
||||
[lang: string]: Translation;
|
||||
};
|
||||
};
|
||||
dockedSidebar: boolean;
|
||||
moreInfoEntityId: string;
|
||||
user: CurrentUser;
|
||||
callService: (
|
||||
domain: ServiceCallRequest['domain'],
|
||||
service: ServiceCallRequest['service'],
|
||||
serviceData?: ServiceCallRequest['serviceData'],
|
||||
target?: ServiceCallRequest['target'],
|
||||
) => Promise<void>;
|
||||
callApi: <T>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
path: string,
|
||||
parameters?: {
|
||||
[key: string]: unknown;
|
||||
},
|
||||
) => Promise<T>;
|
||||
fetchWithAuth: (
|
||||
path: string,
|
||||
init?: {
|
||||
[key: string]: unknown;
|
||||
},
|
||||
) => Promise<Response>;
|
||||
hassUrl(path?): string;
|
||||
sendWS: (msg: MessageBase) => Promise<void>;
|
||||
callWS: <T>(msg: MessageBase) => Promise<T>;
|
||||
}
|
||||
|
||||
declare enum NumberFormat {
|
||||
language = 'language',
|
||||
system = 'system',
|
||||
comma_decimal = 'comma_decimal',
|
||||
decimal_comma = 'decimal_comma',
|
||||
space_comma = 'space_comma',
|
||||
none = 'none',
|
||||
}
|
||||
|
||||
declare enum TimeFormat {
|
||||
language = 'language',
|
||||
system = 'system',
|
||||
am_pm = '12',
|
||||
twenty_four = '24',
|
||||
}
|
||||
|
||||
interface FrontendLocaleData {
|
||||
language: string;
|
||||
number_format: NumberFormat;
|
||||
time_format: TimeFormat;
|
||||
}
|
||||
|
||||
export interface LovelaceCardConfig {
|
||||
index?: number;
|
||||
view_index?: number;
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface LovelaceCard extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
isPanel?: boolean;
|
||||
editMode?: boolean;
|
||||
getCardSize(): number | Promise<number>;
|
||||
setConfig(config: LovelaceCardConfig): void;
|
||||
}
|
||||
|
||||
export interface LovelaceCardEditor extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
lovelace?: LovelaceConfig;
|
||||
setConfig(config: LovelaceCardConfig): void;
|
||||
}
|
||||
|
||||
interface LovelaceConfig {
|
||||
title?: string;
|
||||
views: LovelaceViewConfig[];
|
||||
background?: string;
|
||||
}
|
||||
|
||||
interface LovelaceViewConfig {
|
||||
index?: number;
|
||||
title?: string;
|
||||
badges?: Array<string | LovelaceBadgeConfig>;
|
||||
cards?: LovelaceCardConfig[];
|
||||
path?: string;
|
||||
icon?: string;
|
||||
theme?: string;
|
||||
panel?: boolean;
|
||||
background?: string;
|
||||
visible?: boolean | ShowViewConfig[];
|
||||
}
|
||||
|
||||
interface ShowViewConfig {
|
||||
user?: string;
|
||||
}
|
||||
|
||||
interface LovelaceBadgeConfig {
|
||||
type?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ActionHandlerDetail {
|
||||
action: string;
|
||||
}
|
||||
|
||||
export interface ActionHandlerOptions {
|
||||
hasHold?: boolean;
|
||||
hasDoubleClick?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user