feat(actions)!: Support for the new action (assist) and all the future ones
This also fixes the latest translations missing and starts the work to remove custom-cards-helper from the dependencies. BREAKING CHANGE: Requires HA 2023.4 minimum. Support for the new action format (`target` is also be supported), `service_data` should be renamed to `data` (but it still works with the old format) Fix #711, #685
This commit is contained in:
parent
4351895cd3
commit
d9c17a4065
|
@ -453,6 +453,8 @@ views:
|
|||
type: entity-button
|
||||
entity: switch.skylight
|
||||
name: Default HASS
|
||||
show_name: false
|
||||
show_state: true
|
||||
- type: custom:card-modder
|
||||
styles:
|
||||
card:
|
||||
|
@ -460,6 +462,8 @@ views:
|
|||
card:
|
||||
type: 'custom:button-card'
|
||||
entity: switch.skylight
|
||||
show_state: true
|
||||
show_name: false
|
||||
- type: custom:card-modder
|
||||
styles:
|
||||
card:
|
||||
|
@ -984,6 +988,7 @@ views:
|
|||
name: more-info
|
||||
tap_action:
|
||||
action: more-info
|
||||
entity: light.test_light
|
||||
- type: 'custom:button-card'
|
||||
entity: switch.skylight
|
||||
name: call-service
|
||||
|
@ -992,6 +997,19 @@ views:
|
|||
service: switch.toggle
|
||||
service_data:
|
||||
entity_id: switch.skylight
|
||||
- type: 'custom:button-card'
|
||||
entity: switch.skylight
|
||||
name: call-service
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: timer.start
|
||||
target:
|
||||
entity_id: timer.laundry
|
||||
- type: 'custom:button-card'
|
||||
entity: switch.skylight
|
||||
name: assist
|
||||
tap_action:
|
||||
action: assist
|
||||
- type: 'custom:button-card'
|
||||
entity: switch.skylight
|
||||
name: none
|
||||
|
@ -1634,7 +1652,7 @@ views:
|
|||
cards:
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: entity-button
|
||||
- type: button-card
|
||||
entity: switch.skylight
|
||||
name: Default
|
||||
- type: 'custom:button-card'
|
||||
|
|
29
README.md
29
README.md
|
@ -129,17 +129,18 @@ Lovelace Button card for your entities.
|
|||
|
||||
All the fields support templates, see [templates](#javascript-templates).
|
||||
|
||||
| Name | Type | Default | Supported options | Description |
|
||||
| ----------------- | ------ | -------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `action` | string | `toggle` | `more-info`, `toggle`, `call-service`, `none`, `navigate`, `url` | Action to perform |
|
||||
| `entity` | string | none | Any entity id | **Only valid for `action: more-info`** to override the entity on which you want to call `more-info` |
|
||||
| `navigation_path` | string | none | Eg: `/lovelace/0/` | Path to navigate to (e.g. `/lovelace/0/`) when action defined as navigate |
|
||||
| `url_path` | string | none | Eg: `https://www.google.fr` | URL to open on click when action is `url`. The URL will open in a new tab |
|
||||
| `service` | string | none | Any service | Service to call (e.g. `media_player.media_play_pause`) when `action` defined as `call-service` |
|
||||
| `service_data` | object | none | Any service data | Service data to include (e.g. `entity_id: media_player.bedroom`) when `action` defined as `call-service`. If your `service_data` requires an `entity_id`, you can use the keywork `entity`, this will actually call the service on the entity defined in the main configuration of this card. Useful for [configuration templates](#configuration-templates) |
|
||||
| `haptic` | string | none | `success`, `warning`, `failure`, `light`, `medium`, `heavy`, `selection` | Haptic feedback for the [Beta IOS App](http://home-assistant.io/ios/beta) |
|
||||
| `repeat` | number | none | eg: `500` | For a hold_action, you can optionally configure the action to repeat while the button is being held down (for example, to repeatedly increase the volume of a media player). Define the number of milliseconds between repeat actions here. |
|
||||
| `confirmation` | object | none | See [confirmation](#confirmation) | Display a confirmation popup, overrides the default `confirmation` object |
|
||||
| Name | Type | Default | Supported options | Description |
|
||||
| ------------------------ | ------ | -------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `action` | string | `toggle` | `more-info`, `toggle`, `call-service`, `none`, `navigate`, `url`, `assist` | Action to perform |
|
||||
| `entity` | string | none | Any entity id | **Only valid for `action: more-info`** to override the entity on which you want to call `more-info` |
|
||||
| `target` | object | none | | Only works with `call-service`. Follows the [home-assistant syntax](https://www.home-assistant.io/docs/scripts/service-calls/#targeting-areas-and-devices) |
|
||||
| `navigation_path` | string | none | Eg: `/lovelace/0/` | Path to navigate to (e.g. `/lovelace/0/`) when action defined as navigate |
|
||||
| `url_path` | string | none | Eg: `https://www.google.fr` | URL to open on click when action is `url`. The URL will open in a new tab |
|
||||
| `service` | string | none | Any service | Service to call (e.g. `media_player.media_play_pause`) when `action` defined as `call-service` |
|
||||
| `data` or `service_data` | object | none | Any service data | Service data to include (e.g. `entity_id: media_player.bedroom`) when `action` defined as `call-service`. If your `data` requires an `entity_id`, you can use the keywork `entity`, this will actually call the service on the entity defined in the main configuration of this card. Useful for [configuration templates](#configuration-templates) |
|
||||
| `haptic` | string | none | `success`, `warning`, `failure`, `light`, `medium`, `heavy`, `selection` | Haptic feedback for the [Beta IOS App](http://home-assistant.io/ios/beta) |
|
||||
| `repeat` | number | none | eg: `500` | For a hold_action, you can optionally configure the action to repeat while the button is being held down (for example, to repeatedly increase the volume of a media player). Define the number of milliseconds between repeat actions here. |
|
||||
| `confirmation` | object | none | See [confirmation](#confirmation) | Display a confirmation popup, overrides the default `confirmation` object |
|
||||
|
||||
### Confirmation
|
||||
|
||||
|
@ -1031,7 +1032,7 @@ Horizontal stack with :
|
|||
tap_action:
|
||||
action: call-service
|
||||
service: media_player.volume_up
|
||||
service_data:
|
||||
data:
|
||||
entity_id: media_player.living_room_speaker
|
||||
- type: 'custom:button-card'
|
||||
color_type: card
|
||||
|
@ -1040,7 +1041,7 @@ Horizontal stack with :
|
|||
tap_action:
|
||||
action: call-service
|
||||
service: media_player.volume_down
|
||||
service_data:
|
||||
data:
|
||||
entity_id: media_player.living_room_speaker
|
||||
- type: 'custom:button-card'
|
||||
color_type: blank-card
|
||||
|
@ -1115,7 +1116,7 @@ If you don't specify any operator, `==` will be used to match the current state
|
|||
tap_action:
|
||||
action: call-service
|
||||
service: input_select.select_next
|
||||
service_data:
|
||||
data:
|
||||
entity_id: input_select.cube_mode
|
||||
show_state: true
|
||||
state:
|
||||
|
|
10
package.json
10
package.json
|
@ -68,10 +68,12 @@
|
|||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.1.6",
|
||||
"@material/mwc-ripple": "^0.19.1",
|
||||
"custom-card-helpers": "^1.7.0",
|
||||
"custom-card-helpers": "^1.9.0",
|
||||
"fast-copy": "^2.1.0",
|
||||
"home-assistant-js-websocket": "^5.7.0",
|
||||
"lit-element": "^2.4.0",
|
||||
"lit-html": "^1.3.0"
|
||||
"home-assistant-js-websocket": "^8.2.0",
|
||||
"lit": "^2.7.6",
|
||||
"lit-element": "^3.3.2",
|
||||
"lit-html": "^2.7.5",
|
||||
"memoize-one": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { directive, PropertyPart } from 'lit-html';
|
||||
import { PropertyPart, noChange } from 'lit-html';
|
||||
// import '@material/mwc-ripple';
|
||||
// tslint:disable-next-line
|
||||
import { Ripple } from '@material/mwc-ripple';
|
||||
import { myFireEvent } from './my-fire-event';
|
||||
import { deepEqual } from './deep-equal';
|
||||
import { AttributePart, Directive, DirectiveParameters, directive } from 'lit-html/directive';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||
|
||||
interface ActionHandler extends HTMLElement {
|
||||
|
@ -254,14 +257,21 @@ const getActionHandler = (): ActionHandler => {
|
|||
return actionhandler as ActionHandler;
|
||||
};
|
||||
|
||||
export const actionHandlerBind = (element: ActionHandlerElement, options: ActionHandlerOptions): void => {
|
||||
export const actionHandlerBind = (element: ActionHandlerElement, options?: ActionHandlerOptions) => {
|
||||
const actionhandler: ActionHandler = getActionHandler();
|
||||
if (!actionhandler) {
|
||||
return;
|
||||
}
|
||||
actionhandler.bind(element, options);
|
||||
};
|
||||
export const actionHandler = directive(
|
||||
class extends Directive {
|
||||
update(part: AttributePart, [options]: DirectiveParameters<this>) {
|
||||
actionHandlerBind(part.element as ActionHandlerElement, options);
|
||||
return noChange;
|
||||
}
|
||||
|
||||
export const actionHandler = directive((options: ActionHandlerOptions = {}) => (part: PropertyPart): void => {
|
||||
actionHandlerBind(part.committer.element as ActionHandlerElement, options);
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
render(_options?: ActionHandlerOptions) {}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -11,24 +11,15 @@ import {
|
|||
queryAsync,
|
||||
eventOptions,
|
||||
} from 'lit-element';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { Ripple } from '@material/mwc-ripple';
|
||||
import { RippleHandlers } from '@material/mwc-ripple/ripple-handlers';
|
||||
import { styleMap, StyleInfo } from 'lit-html/directives/style-map';
|
||||
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
|
||||
import { classMap, ClassInfo } from 'lit-html/directives/class-map';
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
import {
|
||||
HomeAssistant,
|
||||
handleClick,
|
||||
timerTimeRemaining,
|
||||
secondsToDuration,
|
||||
durationToSeconds,
|
||||
createThing,
|
||||
fireEvent,
|
||||
DOMAINS_TOGGLE,
|
||||
LovelaceCard,
|
||||
computeStateDomain,
|
||||
} from 'custom-card-helpers';
|
||||
import { timerTimeRemaining, createThing, DOMAINS_TOGGLE, computeStateDomain } from 'custom-card-helpers';
|
||||
import { LovelaceCard } from './types/lovelace';
|
||||
import {
|
||||
ButtonCardConfig,
|
||||
ExternalButtonCardConfig,
|
||||
|
@ -38,7 +29,7 @@ import {
|
|||
CustomFieldCard,
|
||||
ButtonCardEmbeddedCards,
|
||||
ButtonCardEmbeddedCardsConfig,
|
||||
} from './types';
|
||||
} from './types/types';
|
||||
import { actionHandler } from './action-handler';
|
||||
import {
|
||||
computeDomain,
|
||||
|
@ -52,17 +43,22 @@ import {
|
|||
mergeStatesById,
|
||||
getLovelace,
|
||||
getLovelaceCast,
|
||||
secondsToDuration,
|
||||
durationToSeconds,
|
||||
} from './helpers';
|
||||
import { styles } from './styles';
|
||||
import { myComputeStateDisplay } from './compute_state_display';
|
||||
import { computeStateDisplay } from './compute_state_display';
|
||||
import copy from 'fast-copy';
|
||||
import * as pjson from '../package.json';
|
||||
import { deepEqual } from './deep-equal';
|
||||
import { stateColorCss } from './state_color';
|
||||
import { ON } from './const';
|
||||
import { ON } from './common/const';
|
||||
import { handleAction } from './handle-action';
|
||||
import { myFireEvent } from './my-fire-event';
|
||||
import { HomeAssistant } from './types/homeassistant';
|
||||
|
||||
let helpers = (window as any).cardHelpers;
|
||||
const helperPromise = new Promise(async (resolve) => {
|
||||
const helperPromise = new Promise<void>(async (resolve) => {
|
||||
if (helpers) resolve();
|
||||
if ((window as any).loadCardHelpers) {
|
||||
helpers = await (window as any).loadCardHelpers();
|
||||
|
@ -157,7 +153,7 @@ class ButtonCard extends LitElement {
|
|||
else {
|
||||
const element = createThing(config);
|
||||
helperPromise.then(() => {
|
||||
fireEvent(element, 'll-rebuild', {});
|
||||
myFireEvent(element, 'll-rebuild', {});
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
@ -175,7 +171,7 @@ class ButtonCard extends LitElement {
|
|||
? this._objectEvalTemplate(this._stateObj, this._config!.variables)
|
||||
: undefined;
|
||||
return this._cardHtml();
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
if (e.stack) console.error(e.stack);
|
||||
else console.error(e);
|
||||
const errorCard = document.createElement('hui-error-card') as LovelaceCard;
|
||||
|
@ -240,7 +236,7 @@ class ButtonCard extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
private _computeTimeDisplay(stateObj: HassEntity): string | undefined {
|
||||
private _computeTimeDisplay(stateObj: HassEntity): string | undefined | null {
|
||||
if (!stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -309,7 +305,7 @@ class ButtonCard extends LitElement {
|
|||
this._evaledVariables,
|
||||
html,
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
const funcTrimmed = func.length <= 100 ? func.trim() : `${func.trim().substring(0, 98)}...`;
|
||||
e.message = `${e.name}: ${e.message} in '${funcTrimmed}'`;
|
||||
e.name = 'ButtonCardJSTemplateError';
|
||||
|
@ -493,30 +489,43 @@ class ButtonCard extends LitElement {
|
|||
return this._getTemplateOrValue(state, name);
|
||||
}
|
||||
|
||||
private _buildStateString(stateObj: HassEntity | undefined): string | undefined {
|
||||
let stateString: string | undefined;
|
||||
private _buildStateString(stateObj: HassEntity | undefined): string | undefined | null {
|
||||
let stateString: string | undefined | null;
|
||||
if (this._config!.show_state && stateObj && stateObj.state) {
|
||||
const units = this._buildUnits(stateObj);
|
||||
if (units) {
|
||||
stateString = `${stateObj.state} ${units}`;
|
||||
} else if (computeDomain(stateObj.entity_id) === 'timer') {
|
||||
if (stateObj.state === 'idle' || this._timeRemaining === 0) {
|
||||
stateString = myComputeStateDisplay(this._hass!, this._hass!.localize, stateObj, this._hass!.language);
|
||||
stateString = computeStateDisplay(
|
||||
this._hass!.localize,
|
||||
stateObj,
|
||||
this._hass!.locale,
|
||||
this._hass!.config,
|
||||
this._hass!.entities,
|
||||
);
|
||||
} else {
|
||||
stateString = this._computeTimeDisplay(stateObj);
|
||||
if (stateObj.state === 'paused') {
|
||||
stateString += ` (${myComputeStateDisplay(
|
||||
this._hass!,
|
||||
stateString += ` (${computeStateDisplay(
|
||||
this._hass!.localize,
|
||||
stateObj,
|
||||
this._hass!.language,
|
||||
this._hass!.locale,
|
||||
this._hass!.config,
|
||||
this._hass!.entities,
|
||||
)})`;
|
||||
}
|
||||
}
|
||||
} else if (!this._config?.show_units && computeDomain(stateObj.entity_id) === 'sensor') {
|
||||
stateString = stateObj.state;
|
||||
} else {
|
||||
stateString = myComputeStateDisplay(this._hass!, this._hass!.localize, stateObj, this._hass!.language);
|
||||
stateString = computeStateDisplay(
|
||||
this._hass!.localize,
|
||||
stateObj,
|
||||
this._hass!.locale,
|
||||
this._hass!.config,
|
||||
this._hass!.entities,
|
||||
);
|
||||
}
|
||||
}
|
||||
return stateString;
|
||||
|
@ -893,7 +902,7 @@ class ButtonCard extends LitElement {
|
|||
<ha-state-icon
|
||||
.state=${state}
|
||||
?data-domain=${domain}
|
||||
data-state=${state?.state}
|
||||
data-state=${ifDefined(state?.state)}
|
||||
style=${styleMap(haIconStyle)}
|
||||
.icon="${icon}"
|
||||
id="icon"
|
||||
|
@ -1075,6 +1084,15 @@ class ButtonCard extends LitElement {
|
|||
});
|
||||
return configEval;
|
||||
};
|
||||
if (configDuplicate[action]?.service_data?.entity_id === 'entity') {
|
||||
configDuplicate[action].service_data.entity_id = config.entity;
|
||||
}
|
||||
if (configDuplicate[action]?.data?.entity_id === 'entity') {
|
||||
configDuplicate[action].data.entity_id = config.entity;
|
||||
}
|
||||
if (configDuplicate[action]?.entity) {
|
||||
configDuplicate.entity = configDuplicate[action].entity;
|
||||
}
|
||||
configDuplicate[action] = __evalObject(configDuplicate[action]);
|
||||
if (!configDuplicate[action].confirmation && configDuplicate.confirmation) {
|
||||
configDuplicate[action].confirmation = __evalObject(configDuplicate.confirmation);
|
||||
|
@ -1090,32 +1108,32 @@ class ButtonCard extends LitElement {
|
|||
// backward compatibility
|
||||
@eventOptions({ passive: true })
|
||||
private handleRippleActivate(evt?: Event): void {
|
||||
this._ripple.then((r) => r && r.startPress && this._rippleHandlers.startPress(evt));
|
||||
this._ripple.then((r) => r && typeof r.startPress === 'function' && this._rippleHandlers.startPress(evt));
|
||||
}
|
||||
|
||||
private handleRippleDeactivate(): void {
|
||||
this._ripple.then((r) => r && r.endPress && this._rippleHandlers.endPress());
|
||||
this._ripple.then((r) => r && typeof r.endPress === 'function' && this._rippleHandlers.endPress());
|
||||
}
|
||||
|
||||
private handleRippleFocus(): void {
|
||||
this._ripple.then((r) => r && r.startFocus && this._rippleHandlers.startFocus());
|
||||
this._ripple.then((r) => r && typeof r.startFocus === 'function' && this._rippleHandlers.startFocus());
|
||||
}
|
||||
|
||||
private handleRippleBlur(): void {
|
||||
this._ripple.then((r) => r && r.endFocus && this._rippleHandlers.endFocus());
|
||||
this._ripple.then((r) => r && typeof r.endFocus === 'function' && this._rippleHandlers.endFocus());
|
||||
}
|
||||
|
||||
private _handleAction(ev: any): void {
|
||||
if (ev.detail?.action) {
|
||||
switch (ev.detail.action) {
|
||||
case 'tap':
|
||||
this._handleTap();
|
||||
break;
|
||||
case 'hold':
|
||||
this._handleHold();
|
||||
break;
|
||||
case 'double_tap':
|
||||
this._handleDblTap();
|
||||
const config = this._config;
|
||||
if (!config) return;
|
||||
const action = ev.detail.action;
|
||||
const localAction = this._evalActions(config, `${action}_action`);
|
||||
handleAction(this, this._hass!, localAction, action);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -1123,24 +1141,6 @@ class ButtonCard extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
private _handleTap(): void {
|
||||
const config = this._config;
|
||||
if (!config) return;
|
||||
handleClick(this, this._hass!, this._evalActions(config, 'tap_action'), false, false);
|
||||
}
|
||||
|
||||
private _handleHold(): void {
|
||||
const config = this._config;
|
||||
if (!config) return;
|
||||
handleClick(this, this._hass!, this._evalActions(config, 'hold_action'), true, false);
|
||||
}
|
||||
|
||||
private _handleDblTap(): void {
|
||||
const config = this._config;
|
||||
if (!config) return;
|
||||
handleClick(this, this._hass!, this._evalActions(config, 'double_tap_action'), false, true);
|
||||
}
|
||||
|
||||
private _handleUnlockType(ev): void {
|
||||
const config = this._config as ButtonCardConfig;
|
||||
if (!config) return;
|
||||
|
@ -1153,12 +1153,12 @@ class ButtonCard extends LitElement {
|
|||
const lock = this.shadowRoot!.getElementById('lock') as LitElement;
|
||||
if (!lock) return;
|
||||
if (this._config!.lock!.exemptions) {
|
||||
if (!this._hass!.user.name || !this._hass!.user.id) return;
|
||||
if (!this._hass!.user?.name || !this._hass!.user.id) return;
|
||||
let matched = false;
|
||||
this._config!.lock!.exemptions.forEach((e) => {
|
||||
if (
|
||||
(!matched && (e as ExemptionUserConfig).user === this._hass!.user.id) ||
|
||||
(e as ExemptionUsernameConfig).username === this._hass!.user.name
|
||||
(!matched && (e as ExemptionUserConfig).user === this._hass!.user?.id) ||
|
||||
(e as ExemptionUsernameConfig).username === this._hass!.user?.name
|
||||
) {
|
||||
matched = true;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export const UNAVAILABLE = 'unavailable';
|
||||
export const BINARY_STATE_ON = 'on';
|
||||
export const BINARY_STATE_OFF = 'off';
|
||||
const arrayLiteralIncludes = <T extends readonly unknown[]>(array: T) => (
|
||||
searchElement: unknown,
|
||||
fromIndex?: number,
|
|
@ -0,0 +1,41 @@
|
|||
const DAY_IN_MILLISECONDS = 86400000;
|
||||
|
||||
const leftPad = (num: number, digits = 2) => {
|
||||
let paddedNum = '' + num;
|
||||
for (let i = 1; i < digits; i++) {
|
||||
paddedNum = parseInt(paddedNum) < 10 ** i ? `0${paddedNum}` : paddedNum;
|
||||
}
|
||||
return paddedNum;
|
||||
};
|
||||
|
||||
export default function millisecondsToDuration(d: number): string | null {
|
||||
const h = Math.floor(d / 1000 / 3600);
|
||||
const m = Math.floor(((d / 1000) % 3600) / 60);
|
||||
const s = Math.floor(((d / 1000) % 3600) % 60);
|
||||
const ms = Math.floor(d % 1000);
|
||||
|
||||
if (h > 0) {
|
||||
return `${h}:${leftPad(m)}:${leftPad(s)}`;
|
||||
}
|
||||
if (m > 0) {
|
||||
return `${m}:${leftPad(s)}`;
|
||||
}
|
||||
if (s > 0 || ms > 0) {
|
||||
return `${s}${ms > 0 ? `.${leftPad(ms, 3)}` : ``}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const HOUR_IN_MILLISECONDS = 3600000;
|
||||
const MINUTE_IN_MILLISECONDS = 60000;
|
||||
const SECOND_IN_MILLISECONDS = 1000;
|
||||
|
||||
export const UNIT_TO_MILLISECOND_CONVERT = {
|
||||
ms: 1,
|
||||
s: SECOND_IN_MILLISECONDS,
|
||||
min: MINUTE_IN_MILLISECONDS,
|
||||
h: HOUR_IN_MILLISECONDS,
|
||||
d: DAY_IN_MILLISECONDS,
|
||||
};
|
||||
|
||||
export const formatDuration = (duration: string, units: string): string =>
|
||||
millisecondsToDuration(parseFloat(duration) * UNIT_TO_MILLISECOND_CONVERT[units]) || '0';
|
|
@ -0,0 +1,156 @@
|
|||
import { HassConfig } from 'home-assistant-js-websocket';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { FrontendLocaleData, DateFormat } from '../types/translation';
|
||||
|
||||
// Tuesday, August 10
|
||||
export const formatDateWeekdayDay = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatDateWeekdayDayMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayDayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: 'long',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// August 10, 2021
|
||||
export const formatDate = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatDateMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// 10/08/2021
|
||||
export const formatDateNumeric = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) => {
|
||||
const formatter = formatDateNumericMem(locale, config.time_zone);
|
||||
|
||||
if (locale.date_format === DateFormat.language || locale.date_format === DateFormat.system) {
|
||||
return formatter.format(dateObj);
|
||||
}
|
||||
|
||||
const parts = formatter.formatToParts(dateObj);
|
||||
|
||||
const literal = parts.find((value) => value.type === 'literal')?.value;
|
||||
const day = parts.find((value) => value.type === 'day')?.value;
|
||||
const month = parts.find((value) => value.type === 'month')?.value;
|
||||
const year = parts.find((value) => value.type === 'year')?.value;
|
||||
|
||||
const lastPart = parts[parts.length - 1];
|
||||
let lastLiteral = lastPart?.type === 'literal' ? lastPart?.value : '';
|
||||
|
||||
if (locale.language === 'bg' && locale.date_format === DateFormat.YMD) {
|
||||
lastLiteral = '';
|
||||
}
|
||||
|
||||
const formats = {
|
||||
[DateFormat.DMY]: `${day}${literal}${month}${literal}${year}${lastLiteral}`,
|
||||
[DateFormat.MDY]: `${month}${literal}${day}${literal}${year}${lastLiteral}`,
|
||||
[DateFormat.YMD]: `${year}${literal}${month}${literal}${day}${lastLiteral}`,
|
||||
};
|
||||
|
||||
return formats[locale.date_format];
|
||||
};
|
||||
|
||||
const formatDateNumericMem = memoizeOne((locale: FrontendLocaleData, serverTimeZone: string) => {
|
||||
const localeString = locale.date_format === DateFormat.system ? undefined : locale.language;
|
||||
|
||||
if (locale.date_format === DateFormat.language || locale.date_format === DateFormat.system) {
|
||||
return new Intl.DateTimeFormat(localeString, {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(localeString, {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// Aug 10
|
||||
export const formatDateShort = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatDateShortMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateShortMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// August 2021
|
||||
export const formatDateMonthYear = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatDateMonthYearMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateMonthYearMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// August
|
||||
export const formatDateMonth = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatDateMonthMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateMonthMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
month: 'long',
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// 2021
|
||||
export const formatDateYear = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatDateYearMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateYearMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: 'numeric',
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// Monday
|
||||
export const formatDateWeekday = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatDateWeekdayMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: 'long',
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// Mon
|
||||
export const formatDateWeekdayShort = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatDateWeekdayShortMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayShortMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: 'short',
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
|
@ -0,0 +1,77 @@
|
|||
import { HassConfig } from 'home-assistant-js-websocket';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { FrontendLocaleData } from '../types/translation';
|
||||
import { formatDateNumeric } from './format_date';
|
||||
import { formatTime, useAmPm } from './format_time';
|
||||
|
||||
// August 9, 2021, 8:23 AM
|
||||
export const formatDateTime = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatDateTimeMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language === 'en' && !useAmPm(locale) ? 'en-u-hc-h23' : locale.language, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: useAmPm(locale) ? 'numeric' : '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: useAmPm(locale),
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// Aug 9, 2021, 8:23 AM
|
||||
export const formatShortDateTimeWithYear = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatShortDateTimeWithYearMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatShortDateTimeWithYearMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language === 'en' && !useAmPm(locale) ? 'en-u-hc-h23' : locale.language, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: useAmPm(locale) ? 'numeric' : '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: useAmPm(locale),
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// Aug 9, 8:23 AM
|
||||
export const formatShortDateTime = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatShortDateTimeMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatShortDateTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language === 'en' && !useAmPm(locale) ? 'en-u-hc-h23' : locale.language, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: useAmPm(locale) ? 'numeric' : '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: useAmPm(locale),
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// August 9, 2021, 8:23:15 AM
|
||||
export const formatDateTimeWithSeconds = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatDateTimeWithSecondsMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateTimeWithSecondsMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language === 'en' && !useAmPm(locale) ? 'en-u-hc-h23' : locale.language, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: useAmPm(locale) ? 'numeric' : '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: useAmPm(locale),
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// 9/8/2021, 8:23 AM
|
||||
export const formatDateTimeNumeric = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
`${formatDateNumeric(dateObj, locale, config)}, ${formatTime(dateObj, locale, config)}`;
|
|
@ -0,0 +1,136 @@
|
|||
import { HassEntity, HassEntityAttributeBase } from 'home-assistant-js-websocket';
|
||||
import { EntityRegistryDisplayEntry } from '../types/homeassistant';
|
||||
import { FrontendLocaleData, NumberFormat } from '../types/translation';
|
||||
|
||||
// Logic based on https://en.wikipedia.org/wiki/Percent_sign#Form_and_spacing
|
||||
export const blankBeforePercent = (localeOptions: FrontendLocaleData): string => {
|
||||
switch (localeOptions.language) {
|
||||
case 'cz':
|
||||
case 'de':
|
||||
case 'fi':
|
||||
case 'fr':
|
||||
case 'sk':
|
||||
case 'sv':
|
||||
return ' ';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const round = (value: number, precision = 2): number => Math.round(value * 10 ** precision) / 10 ** precision;
|
||||
|
||||
/**
|
||||
* Returns true if the entity is considered numeric based on the attributes it has
|
||||
* @param stateObj The entity state object
|
||||
*/
|
||||
export const isNumericState = (stateObj: HassEntity): boolean => isNumericFromAttributes(stateObj.attributes);
|
||||
|
||||
export const isNumericFromAttributes = (attributes: HassEntityAttributeBase): boolean =>
|
||||
!!attributes.unit_of_measurement || !!attributes.state_class;
|
||||
|
||||
export const numberFormatToLocale = (localeOptions: FrontendLocaleData): string | string[] | undefined => {
|
||||
switch (localeOptions.number_format) {
|
||||
case NumberFormat.comma_decimal:
|
||||
return ['en-US', 'en']; // Use United States with fallback to English formatting 1,234,567.89
|
||||
case NumberFormat.decimal_comma:
|
||||
return ['de', 'es', 'it']; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
|
||||
case NumberFormat.space_comma:
|
||||
return ['fr', 'sv', 'cs']; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
|
||||
case NumberFormat.system:
|
||||
return undefined;
|
||||
default:
|
||||
return localeOptions.language;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility.
|
||||
*
|
||||
* @param num The number to format
|
||||
* @param localeOptions The user-selected language and formatting, from `hass.locale`
|
||||
* @param options Intl.NumberFormatOptions to use
|
||||
*/
|
||||
export const formatNumber = (
|
||||
num: string | number,
|
||||
localeOptions?: FrontendLocaleData,
|
||||
options?: Intl.NumberFormatOptions,
|
||||
): string => {
|
||||
const locale = localeOptions ? numberFormatToLocale(localeOptions) : undefined;
|
||||
|
||||
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
|
||||
Number.isNaN =
|
||||
Number.isNaN ||
|
||||
function isNaN(input) {
|
||||
return typeof input === 'number' && isNaN(input);
|
||||
};
|
||||
|
||||
if (localeOptions?.number_format !== NumberFormat.none && !Number.isNaN(Number(num)) && Intl) {
|
||||
try {
|
||||
return new Intl.NumberFormat(locale, getDefaultFormatOptions(num, options)).format(Number(num));
|
||||
} catch (err: any) {
|
||||
// Don't fail when using "TEST" language
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
return new Intl.NumberFormat(undefined, getDefaultFormatOptions(num, options)).format(Number(num));
|
||||
}
|
||||
}
|
||||
if (typeof num === 'string') {
|
||||
return num;
|
||||
}
|
||||
return `${round(num, options?.maximumFractionDigits).toString()}${
|
||||
options?.style === 'currency' ? ` ${options.currency}` : ''
|
||||
}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the current entity state should be formatted as an integer based on the `state` and `step` attribute and returns the appropriate `Intl.NumberFormatOptions` object with `maximumFractionDigits` set
|
||||
* @param entityState The state object of the entity
|
||||
* @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined`
|
||||
*/
|
||||
export const getNumberFormatOptions = (
|
||||
entityState: HassEntity,
|
||||
entity?: EntityRegistryDisplayEntry,
|
||||
): Intl.NumberFormatOptions | undefined => {
|
||||
const precision = entity?.display_precision;
|
||||
if (precision != null) {
|
||||
return {
|
||||
maximumFractionDigits: precision,
|
||||
minimumFractionDigits: precision,
|
||||
};
|
||||
}
|
||||
if (Number.isInteger(Number(entityState.attributes?.step)) && Number.isInteger(Number(entityState.state))) {
|
||||
return { maximumFractionDigits: 0 };
|
||||
}
|
||||
if (entityState.attributes.step != null) {
|
||||
return { maximumFractionDigits: Math.ceil(Math.log10(1 / entityState.attributes.step)) };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates default options for Intl.NumberFormat
|
||||
* @param num The number to be formatted
|
||||
* @param options The Intl.NumberFormatOptions that should be included in the returned options
|
||||
*/
|
||||
export const getDefaultFormatOptions = (
|
||||
num: string | number,
|
||||
options?: Intl.NumberFormatOptions,
|
||||
): Intl.NumberFormatOptions => {
|
||||
const defaultOptions: Intl.NumberFormatOptions = {
|
||||
maximumFractionDigits: 2,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (typeof num !== 'string') {
|
||||
return defaultOptions;
|
||||
}
|
||||
|
||||
// Keep decimal trailing zeros if they are present in a string numeric value
|
||||
if (!options || (options.minimumFractionDigits === undefined && options.maximumFractionDigits === undefined)) {
|
||||
const digits = num.indexOf('.') > -1 ? num.split('.')[1].length : 0;
|
||||
defaultOptions.minimumFractionDigits = digits;
|
||||
defaultOptions.maximumFractionDigits = digits;
|
||||
}
|
||||
|
||||
return defaultOptions;
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
import { HassConfig } from 'home-assistant-js-websocket';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { FrontendLocaleData, TimeFormat } from '../types/translation';
|
||||
|
||||
export const useAmPm = memoizeOne((locale: FrontendLocaleData): boolean => {
|
||||
if (locale.time_format === TimeFormat.language || locale.time_format === TimeFormat.system) {
|
||||
const testLanguage = locale.time_format === TimeFormat.language ? locale.language : undefined;
|
||||
const test = new Date().toLocaleString(testLanguage);
|
||||
return test.includes('AM') || test.includes('PM');
|
||||
}
|
||||
|
||||
return locale.time_format === TimeFormat.am_pm;
|
||||
});
|
||||
|
||||
export const formatTime = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatTimeMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language === 'en' && !useAmPm(locale) ? 'en-u-hc-h23' : locale.language, {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: useAmPm(locale),
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// 9:15:24 PM || 21:15:24
|
||||
export const formatTimeWithSeconds = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatTimeWithSecondsMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatTimeWithSecondsMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language === 'en' && !useAmPm(locale) ? 'en-u-hc-h23' : locale.language, {
|
||||
hour: useAmPm(locale) ? 'numeric' : '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: useAmPm(locale),
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// Tuesday 7:00 PM || Tuesday 19:00
|
||||
export const formatTimeWeekday = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatTimeWeekdayMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatTimeWeekdayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language === 'en' && !useAmPm(locale) ? 'en-u-hc-h23' : locale.language, {
|
||||
weekday: 'long',
|
||||
hour: useAmPm(locale) ? 'numeric' : '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: useAmPm(locale),
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
// 21:15
|
||||
export const formatTime24h = (dateObj: Date, locale: FrontendLocaleData, config: HassConfig) =>
|
||||
formatTime24hMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatTime24hMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
// en-GB to fix Chrome 24:59 to 0:59 https://stackoverflow.com/a/60898146
|
||||
new Intl.DateTimeFormat('en-GB', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
timeZone: locale.time_zone === 'server' ? serverTimeZone : undefined,
|
||||
}),
|
||||
);
|
|
@ -0,0 +1,14 @@
|
|||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
|
||||
export const supportsFeature = (stateObj: HassEntity, feature: number): boolean =>
|
||||
supportsFeatureFromAttributes(stateObj.attributes, feature);
|
||||
|
||||
export const supportsFeatureFromAttributes = (
|
||||
attributes: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any;
|
||||
},
|
||||
feature: number,
|
||||
): boolean =>
|
||||
// eslint-disable-next-line no-bitwise, @typescript-eslint/no-non-null-assertion
|
||||
(attributes.supported_features! & feature) !== 0;
|
|
@ -0,0 +1,40 @@
|
|||
import type { HassEntityAttributeBase, HassEntityBase } from 'home-assistant-js-websocket';
|
||||
import { BINARY_STATE_ON } from './const';
|
||||
import { supportsFeature, supportsFeatureFromAttributes } from './supports-features';
|
||||
|
||||
export const UPDATE_SUPPORT_INSTALL = 1;
|
||||
export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
|
||||
export const UPDATE_SUPPORT_PROGRESS = 4;
|
||||
export const UPDATE_SUPPORT_BACKUP = 8;
|
||||
export const UPDATE_SUPPORT_RELEASE_NOTES = 16;
|
||||
|
||||
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 updateUsesProgress = (entity: UpdateEntity): boolean =>
|
||||
updateUsesProgressFromAttributes(entity.attributes);
|
||||
|
||||
export const updateUsesProgressFromAttributes = (attributes: { [key: string]: any }): boolean =>
|
||||
supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) && typeof attributes.in_progress === 'number';
|
||||
|
||||
export const updateCanInstall = (entity: UpdateEntity, showSkipped = false): boolean =>
|
||||
(entity.state === BINARY_STATE_ON || (showSkipped && Boolean(entity.attributes.skipped_version))) &&
|
||||
supportsFeature(entity, UPDATE_SUPPORT_INSTALL);
|
||||
|
||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||
updateUsesProgress(entity) || !!entity.attributes.in_progress;
|
||||
|
||||
export const updateIsInstallingFromAttributes = (attributes: { [key: string]: any }): boolean =>
|
||||
updateUsesProgressFromAttributes(attributes) || !!attributes.in_progress;
|
|
@ -1,115 +1,195 @@
|
|||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
import { LocalizeFunc, HomeAssistant, formatDate, formatTime, formatDateTime } from 'custom-card-helpers';
|
||||
import { computeDomain } from './helpers';
|
||||
import { HassConfig, HassEntity } from 'home-assistant-js-websocket';
|
||||
import { LocalizeFunc } from 'custom-card-helpers';
|
||||
import { computeDomain, isNumericFromAttributes } from './helpers';
|
||||
import { atLeastVersion } from './at_least_version';
|
||||
import { formatNumber, getNumberFormatOptions, blankBeforePercent } from './common/format_number';
|
||||
import { EntityRegistryDisplayEntry, FrontendLocaleData, HomeAssistant, TimeZone } from './types/homeassistant';
|
||||
import { UNIT_TO_MILLISECOND_CONVERT, formatDuration } from './common/duration';
|
||||
import { formatDateTime } from './common/format_date_time';
|
||||
import { formatDate } from './common/format_date';
|
||||
import { formatTime } from './common/format_time';
|
||||
import { UPDATE_SUPPORT_PROGRESS, updateIsInstallingFromAttributes } from './common/update';
|
||||
import { supportsFeatureFromAttributes } from './common/supports-features';
|
||||
|
||||
const UNAVAILABLE = 'unavailable';
|
||||
const UNKNOWN = 'unknown';
|
||||
|
||||
function legacyComputeStateDisplay(localize: LocalizeFunc, stateObj: HassEntity): string | undefined {
|
||||
let display: string | undefined;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
||||
if (domain === 'binary_sensor') {
|
||||
// Try device class translation, then default binary sensor translation
|
||||
if (stateObj.attributes.device_class) {
|
||||
display = localize(`state.${domain}.${stateObj.attributes.device_class}.${stateObj.state}`);
|
||||
}
|
||||
|
||||
if (!display) {
|
||||
display = localize(`state.${domain}.default.${stateObj.state}`);
|
||||
}
|
||||
} else if (stateObj.attributes.unit_of_measurement && !['unknown', 'unavailable'].includes(stateObj.state)) {
|
||||
display = stateObj.state;
|
||||
} else if (domain === 'zwave') {
|
||||
if (['initializing', 'dead'].includes(stateObj.state)) {
|
||||
display = localize(`state.zwave.query_stage.${stateObj.state}`, 'query_stage', stateObj.attributes.query_stage);
|
||||
} else {
|
||||
display = localize(`state.zwave.default.${stateObj.state}`);
|
||||
}
|
||||
} else {
|
||||
display = localize(`state.${domain}.${stateObj.state}`);
|
||||
}
|
||||
|
||||
// Fall back to default, component backend translation, or raw state if nothing else matches.
|
||||
if (!display) {
|
||||
display =
|
||||
localize(`state.default.${stateObj.state}`) ||
|
||||
localize(`component.${domain}.state.${stateObj.state}`) ||
|
||||
stateObj.state;
|
||||
}
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
export const myComputeStateDisplay = (
|
||||
hass: HomeAssistant,
|
||||
export const computeStateDisplaySingleEntity = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
language: string,
|
||||
): string | undefined => {
|
||||
if (!atLeastVersion(hass.config.version, 0, 109)) {
|
||||
return legacyComputeStateDisplay(localize, stateObj);
|
||||
}
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
entity: EntityRegistryDisplayEntry | undefined,
|
||||
state?: string,
|
||||
): string =>
|
||||
computeStateDisplayFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
config,
|
||||
entity,
|
||||
stateObj.entity_id,
|
||||
stateObj.attributes,
|
||||
state !== undefined ? state : stateObj.state,
|
||||
);
|
||||
|
||||
if (stateObj.state === UNKNOWN || stateObj.state === UNAVAILABLE) {
|
||||
return localize(`state.default.${stateObj.state}`);
|
||||
}
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
entities: HomeAssistant['entities'],
|
||||
state?: string,
|
||||
): string => {
|
||||
const entity = entities[stateObj.entity_id] as EntityRegistryDisplayEntry | undefined;
|
||||
|
||||
if (stateObj.attributes.unit_of_measurement) {
|
||||
return `${stateObj.state} ${stateObj.attributes.unit_of_measurement}`;
|
||||
}
|
||||
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
||||
if (domain === 'input_datetime') {
|
||||
let date: Date;
|
||||
if (!stateObj.attributes.has_time) {
|
||||
date = new Date(stateObj.attributes.year, stateObj.attributes.month - 1, stateObj.attributes.day);
|
||||
return formatDate(date, language);
|
||||
}
|
||||
if (!stateObj.attributes.has_date) {
|
||||
const now = new Date();
|
||||
date = new Date(
|
||||
// Due to bugs.chromium.org/p/chromium/issues/detail?id=797548
|
||||
// don't use artificial 1970 year.
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDay(),
|
||||
stateObj.attributes.hour,
|
||||
stateObj.attributes.minute,
|
||||
);
|
||||
return formatTime(date, language);
|
||||
}
|
||||
|
||||
date = new Date(
|
||||
stateObj.attributes.year,
|
||||
stateObj.attributes.month - 1,
|
||||
stateObj.attributes.day,
|
||||
stateObj.attributes.hour,
|
||||
stateObj.attributes.minute,
|
||||
);
|
||||
return formatDateTime(date, language);
|
||||
}
|
||||
|
||||
if (!atLeastVersion(hass.config.version, 2023, 4)) {
|
||||
return (
|
||||
// Return device class translation
|
||||
(stateObj.attributes.device_class &&
|
||||
localize(`component.${domain}.state.${stateObj.attributes.device_class}.${stateObj.state}`)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.state._.${stateObj.state}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
stateObj.state
|
||||
);
|
||||
}
|
||||
return (
|
||||
// Return device class translation
|
||||
(stateObj.attributes.device_class &&
|
||||
localize(`component.${domain}.entity_component.${stateObj.attributes.device_class}.state.${stateObj.state}`)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.entity_component._.state.${stateObj.state}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
stateObj.state
|
||||
return computeStateDisplayFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
config,
|
||||
entity,
|
||||
stateObj.entity_id,
|
||||
stateObj.attributes,
|
||||
state !== undefined ? state : stateObj.state,
|
||||
);
|
||||
};
|
||||
|
||||
export const computeStateDisplayFromEntityAttributes = (
|
||||
localize: LocalizeFunc,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
entity: EntityRegistryDisplayEntry | undefined,
|
||||
entityId: string,
|
||||
attributes: any,
|
||||
state: string,
|
||||
): string => {
|
||||
if (state === UNKNOWN || state === UNAVAILABLE) {
|
||||
return localize(`state.default.${state}`);
|
||||
}
|
||||
|
||||
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
||||
if (isNumericFromAttributes(attributes)) {
|
||||
// state is duration
|
||||
if (
|
||||
attributes.device_class === 'duration' &&
|
||||
attributes.unit_of_measurement &&
|
||||
UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement]
|
||||
) {
|
||||
try {
|
||||
return formatDuration(state, attributes.unit_of_measurement);
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
if (attributes.device_class === 'monetary') {
|
||||
try {
|
||||
return formatNumber(state, locale, {
|
||||
style: 'currency',
|
||||
currency: attributes.unit_of_measurement,
|
||||
minimumFractionDigits: 2,
|
||||
// Override monetary options with number format
|
||||
...getNumberFormatOptions({ state, attributes } as HassEntity, entity),
|
||||
});
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
const unit = !attributes.unit_of_measurement
|
||||
? ''
|
||||
: attributes.unit_of_measurement === '%'
|
||||
? blankBeforePercent(locale) + '%'
|
||||
: ` ${attributes.unit_of_measurement}`;
|
||||
return `${formatNumber(state, locale, getNumberFormatOptions({ state, attributes } as HassEntity, entity))}${unit}`;
|
||||
}
|
||||
|
||||
const domain = computeDomain(entityId);
|
||||
|
||||
if (domain === 'datetime') {
|
||||
const time = new Date(state);
|
||||
return formatDateTime(time, locale, config);
|
||||
}
|
||||
|
||||
if (['date', 'input_datetime', 'time'].includes(domain)) {
|
||||
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
||||
// Attributes aren't available, we have to use `state`.
|
||||
|
||||
// These are timezone agnostic, so we should NOT use the system timezone here.
|
||||
try {
|
||||
const components = state.split(' ');
|
||||
if (components.length === 2) {
|
||||
// Date and time.
|
||||
return formatDateTime(new Date(components.join('T')), { ...locale, time_zone: TimeZone.local }, config);
|
||||
}
|
||||
if (components.length === 1) {
|
||||
if (state.includes('-')) {
|
||||
// Date only.
|
||||
return formatDate(new Date(`${state}T00:00`), { ...locale, time_zone: TimeZone.local }, config);
|
||||
}
|
||||
if (state.includes(':')) {
|
||||
// Time only.
|
||||
const now = new Date();
|
||||
return formatTime(
|
||||
new Date(`${now.toISOString().split('T')[0]}T${state}`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config,
|
||||
);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
} catch (_e) {
|
||||
// Formatting methods may throw error if date parsing doesn't go well,
|
||||
// just return the state string in that case.
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
|
||||
if (domain === 'counter' || domain === 'number' || domain === 'input_number') {
|
||||
// Format as an integer if the value and step are integers
|
||||
return formatNumber(state, locale, getNumberFormatOptions({ state, attributes } as HassEntity, entity));
|
||||
}
|
||||
|
||||
// state is a timestamp
|
||||
if (
|
||||
['button', 'event', 'input_button', 'scene', 'stt', 'tts'].includes(domain) ||
|
||||
(domain === 'sensor' && attributes.device_class === 'timestamp')
|
||||
) {
|
||||
try {
|
||||
return formatDateTime(new Date(state), locale, config);
|
||||
} catch (_err) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === 'update') {
|
||||
// When updating, and entity does not support % show "Installing"
|
||||
// When updating, and entity does support % show "Installing (xx%)"
|
||||
// When update available, show the version
|
||||
// When the latest version is skipped, show the latest version
|
||||
// When update is not available, show "Up-to-date"
|
||||
// When update is not available and there is no latest_version show "Unavailable"
|
||||
return state === 'on'
|
||||
? updateIsInstallingFromAttributes(attributes)
|
||||
? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) &&
|
||||
typeof attributes.in_progress === 'number'
|
||||
? localize('ui.card.update.installing_with_progress', {
|
||||
progress: attributes.in_progress,
|
||||
})
|
||||
: localize('ui.card.update.installing')
|
||||
: attributes.latest_version
|
||||
: attributes.skipped_version === attributes.latest_version
|
||||
? attributes.latest_version ?? localize('state.default.unavailable')
|
||||
: localize('ui.card.update.up_to_date');
|
||||
}
|
||||
|
||||
return (
|
||||
(entity?.translation_key &&
|
||||
localize(`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`)) ||
|
||||
// Return device class translation
|
||||
(attributes.device_class &&
|
||||
localize(`component.${domain}.entity_component.${attributes.device_class}.state.${state}`)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.entity_component._.state.${state}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
state
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { myFireEvent } from './my-fire-event';
|
||||
import { ActionConfig } from './types/types';
|
||||
import { HomeAssistant } from './types/homeassistant';
|
||||
|
||||
export type ActionConfigParams = {
|
||||
entity?: string;
|
||||
camera_image?: string;
|
||||
hold_action?: ActionConfig;
|
||||
tap_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
};
|
||||
|
||||
export const handleAction = async (
|
||||
node: HTMLElement,
|
||||
_hass: HomeAssistant,
|
||||
config: ActionConfigParams,
|
||||
action: string,
|
||||
): Promise<void> => {
|
||||
myFireEvent(node, 'hass-action', { config, action });
|
||||
};
|
||||
|
||||
type ActionParams = { config: ActionConfigParams; action: string };
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
'hass-action': ActionParams;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import { PropertyValues } from 'lit-element';
|
||||
import tinycolor, { TinyColor } from '@ctrl/tinycolor';
|
||||
import { HomeAssistant, LovelaceConfig } from 'custom-card-helpers';
|
||||
import { StateConfig } from './types';
|
||||
import { StateConfig } from './types/types';
|
||||
import { HassEntity, HassEntityAttributeBase, HassEntityBase } from 'home-assistant-js-websocket';
|
||||
import { OFF, UNAVAILABLE, isUnavailableState } from './const';
|
||||
import { OFF, UNAVAILABLE, isUnavailableState } from './common/const';
|
||||
|
||||
export function computeDomain(entityId: string): string {
|
||||
return entityId.substr(0, entityId.indexOf('.'));
|
||||
|
@ -331,3 +331,31 @@ export function computeCssValue(prop: string | string[], computedStyles: CSSStyl
|
|||
}
|
||||
return computedStyles.getPropertyValue(prop).trim() || undefined;
|
||||
}
|
||||
|
||||
export function durationToSeconds(duration: string): number {
|
||||
const parts = duration.split(':').map(Number);
|
||||
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||
}
|
||||
|
||||
const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
|
||||
|
||||
export function secondsToDuration(d: number): string | null {
|
||||
const h = Math.floor(d / 3600);
|
||||
const m = Math.floor((d % 3600) / 60);
|
||||
const s = Math.floor((d % 3600) % 60);
|
||||
|
||||
if (h > 0) {
|
||||
return `${h}:${leftPad(m)}:${leftPad(s)}`;
|
||||
}
|
||||
if (m > 0) {
|
||||
return `${m}:${leftPad(s)}`;
|
||||
}
|
||||
if (s > 0) {
|
||||
return '' + s;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isNumericFromAttributes(attributes: HassEntityAttributeBase): boolean {
|
||||
return !!attributes.unit_of_measurement || !!attributes.state_class;
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
declare global {
|
||||
// eslint-disable-next-line
|
||||
interface HASSDomEvents { }
|
||||
interface HASSDomEvents {}
|
||||
}
|
||||
|
||||
export type ValidHassDomEvent = keyof HASSDomEvents;
|
||||
|
@ -54,6 +54,7 @@ export interface HASSDomEvent<T> extends Event {
|
|||
* `node` on which to fire the event (HTMLElement, defaults to `this`).
|
||||
* @return {Event} The new event that was fired.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const myFireEvent = <HassEvent extends ValidHassDomEvent>(
|
||||
node: HTMLElement | Window,
|
||||
type: HassEvent,
|
||||
|
@ -63,8 +64,9 @@ export const myFireEvent = <HassEvent extends ValidHassDomEvent>(
|
|||
cancelable?: boolean;
|
||||
composed?: boolean;
|
||||
},
|
||||
): any => {
|
||||
) => {
|
||||
options = options || {};
|
||||
// @ts-ignore
|
||||
detail = detail === null || detail === undefined ? {} : detail;
|
||||
const event = new Event(type, {
|
||||
bubbles: options.bubbles === undefined ? true : options.bubbles,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/** Return an color representing a state. */
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
import { UNAVAILABLE } from './const';
|
||||
import { UNAVAILABLE } from './common/const';
|
||||
import { computeGroupDomain, GroupEntity } from './helpers';
|
||||
import { computeCssVariable } from './helpers';
|
||||
import { computeDomain, slugify } from './helpers';
|
||||
|
|
|
@ -0,0 +1,278 @@
|
|||
import {
|
||||
Auth,
|
||||
Connection,
|
||||
HassConfig,
|
||||
HassEntities,
|
||||
HassServiceTarget,
|
||||
HassServices,
|
||||
MessageBase,
|
||||
} from 'home-assistant-js-websocket';
|
||||
|
||||
export interface EntityRegistryDisplayEntry {
|
||||
entity_id: string;
|
||||
name?: string;
|
||||
device_id?: string;
|
||||
area_id?: string;
|
||||
hidden?: boolean;
|
||||
entity_category?: 'config' | 'diagnostic';
|
||||
translation_key?: string;
|
||||
platform?: string;
|
||||
display_precision?: number;
|
||||
}
|
||||
|
||||
export interface DeviceRegistryEntry {
|
||||
id: string;
|
||||
config_entries: string[];
|
||||
connections: Array<[string, string]>;
|
||||
identifiers: Array<[string, string]>;
|
||||
manufacturer: string | null;
|
||||
model: string | null;
|
||||
name: string | null;
|
||||
sw_version: string | null;
|
||||
hw_version: string | null;
|
||||
via_device_id: string | null;
|
||||
area_id: string | null;
|
||||
name_by_user: string | null;
|
||||
entry_type: 'service' | null;
|
||||
disabled_by: 'user' | 'integration' | 'config_entry' | null;
|
||||
configuration_url: string | null;
|
||||
}
|
||||
|
||||
export interface AreaRegistryEntry {
|
||||
area_id: string;
|
||||
name: string;
|
||||
picture: string | null;
|
||||
}
|
||||
|
||||
export interface ThemeSettings {
|
||||
theme: string;
|
||||
// Radio box selection for theme picker. Do not use in Lovelace rendering as
|
||||
// it can be undefined == auto.
|
||||
// Property hass.themes.darkMode carries effective current mode.
|
||||
dark?: boolean;
|
||||
primaryColor?: string;
|
||||
accentColor?: string;
|
||||
}
|
||||
|
||||
export interface PanelInfo<T = Record<string, any> | null> {
|
||||
component_name: string;
|
||||
config: T;
|
||||
icon: string | null;
|
||||
title: string | null;
|
||||
url_path: string;
|
||||
}
|
||||
|
||||
export interface Panels {
|
||||
[name: string]: PanelInfo;
|
||||
}
|
||||
|
||||
export interface Resources {
|
||||
[language: string]: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface Translation {
|
||||
nativeName: string;
|
||||
isRTL: boolean;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export interface TranslationMetadata {
|
||||
fragments: string[];
|
||||
translations: {
|
||||
[lang: string]: Translation;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Credential {
|
||||
auth_provider_type: string;
|
||||
auth_provider_id: string;
|
||||
}
|
||||
|
||||
export 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[];
|
||||
}
|
||||
|
||||
export interface ServiceCallRequest {
|
||||
domain: string;
|
||||
service: string;
|
||||
serviceData?: Record<string, any>;
|
||||
target?: HassServiceTarget;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
id: string;
|
||||
parent_id?: string;
|
||||
user_id?: string | null;
|
||||
}
|
||||
|
||||
export interface ServiceCallResponse {
|
||||
context: Context;
|
||||
}
|
||||
|
||||
export interface HomeAssistant {
|
||||
auth: Auth;
|
||||
connection: Connection;
|
||||
connected: boolean;
|
||||
states: HassEntities;
|
||||
entities: { [id: string]: EntityRegistryDisplayEntry };
|
||||
devices: { [id: string]: DeviceRegistryEntry };
|
||||
areas: { [id: string]: AreaRegistryEntry };
|
||||
services: HassServices;
|
||||
config: HassConfig;
|
||||
themes: Themes;
|
||||
selectedTheme: ThemeSettings | null;
|
||||
panels: Panels;
|
||||
panelUrl: string;
|
||||
// i18n
|
||||
// current effective language in that order:
|
||||
// - backend saved user selected language
|
||||
// - language in local app storage
|
||||
// - browser language
|
||||
// - english (en)
|
||||
language: string;
|
||||
// local stored language, keep that name for backward compatibility
|
||||
selectedLanguage: string | null;
|
||||
locale: FrontendLocaleData;
|
||||
resources: Resources;
|
||||
localize: LocalizeFunc;
|
||||
translationMetadata: TranslationMetadata;
|
||||
suspendWhenHidden: boolean;
|
||||
enableShortcuts: boolean;
|
||||
vibrate: boolean;
|
||||
dockedSidebar: 'docked' | 'always_hidden' | 'auto';
|
||||
defaultPanel: string;
|
||||
moreInfoEntityId: string | null;
|
||||
user?: CurrentUser;
|
||||
hassUrl(path?): string;
|
||||
callService(
|
||||
domain: ServiceCallRequest['domain'],
|
||||
service: ServiceCallRequest['service'],
|
||||
serviceData?: ServiceCallRequest['serviceData'],
|
||||
target?: ServiceCallRequest['target'],
|
||||
): Promise<ServiceCallResponse>;
|
||||
callApi<T>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
path: string,
|
||||
parameters?: Record<string, any>,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<T>;
|
||||
fetchWithAuth(path: string, init?: Record<string, any>): Promise<Response>;
|
||||
sendWS(msg: MessageBase): void;
|
||||
callWS<T>(msg: MessageBase): Promise<T>;
|
||||
loadBackendTranslation(
|
||||
category: TranslationCategory,
|
||||
integration?: string | string[],
|
||||
configFlow?: boolean,
|
||||
): Promise<LocalizeFunc>;
|
||||
}
|
||||
|
||||
export enum NumberFormat {
|
||||
language = 'language',
|
||||
system = 'system',
|
||||
comma_decimal = 'comma_decimal',
|
||||
decimal_comma = 'decimal_comma',
|
||||
space_comma = 'space_comma',
|
||||
none = 'none',
|
||||
}
|
||||
|
||||
export enum TimeFormat {
|
||||
language = 'language',
|
||||
system = 'system',
|
||||
am_pm = '12',
|
||||
twenty_four = '24',
|
||||
}
|
||||
|
||||
export enum TimeZone {
|
||||
local = 'local',
|
||||
server = 'server',
|
||||
}
|
||||
|
||||
export enum DateFormat {
|
||||
language = 'language',
|
||||
system = 'system',
|
||||
DMY = 'DMY',
|
||||
MDY = 'MDY',
|
||||
YMD = 'YMD',
|
||||
}
|
||||
|
||||
export enum FirstWeekday {
|
||||
language = 'language',
|
||||
monday = 'monday',
|
||||
tuesday = 'tuesday',
|
||||
wednesday = 'wednesday',
|
||||
thursday = 'thursday',
|
||||
friday = 'friday',
|
||||
saturday = 'saturday',
|
||||
sunday = 'sunday',
|
||||
}
|
||||
|
||||
export interface FrontendLocaleData {
|
||||
language: string;
|
||||
number_format: NumberFormat;
|
||||
time_format: TimeFormat;
|
||||
date_format: DateFormat;
|
||||
first_weekday: FirstWeekday;
|
||||
time_zone: TimeZone;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface FrontendUserData {
|
||||
language: FrontendLocaleData;
|
||||
}
|
||||
}
|
||||
|
||||
export type TranslationCategory =
|
||||
| 'title'
|
||||
| 'state'
|
||||
| 'entity'
|
||||
| 'entity_component'
|
||||
| 'config'
|
||||
| 'config_panel'
|
||||
| 'options'
|
||||
| 'device_automation'
|
||||
| 'mfa_setup'
|
||||
| 'system_health'
|
||||
| 'device_class'
|
||||
| 'application_credentials'
|
||||
| 'issues'
|
||||
| 'selector';
|
||||
|
||||
export type LocalizeFunc = (key: string, ...args: any[]) => string;
|
||||
|
||||
export interface ThemeVars {
|
||||
// Incomplete
|
||||
'primary-color': string;
|
||||
'text-primary-color': string;
|
||||
'accent-color': string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export type Theme = ThemeVars & {
|
||||
modes?: {
|
||||
light?: ThemeVars;
|
||||
dark?: ThemeVars;
|
||||
};
|
||||
};
|
||||
|
||||
export interface Themes {
|
||||
default_theme: string;
|
||||
default_dark_theme: string | null;
|
||||
themes: Record<string, Theme>;
|
||||
// Currently effective dark mode. Will never be undefined. If user selected "auto"
|
||||
// in theme picker, this property will still contain either true or false based on
|
||||
// what has been determined via system preferences and support from the selected theme.
|
||||
darkMode: boolean;
|
||||
// Currently globally active theme name
|
||||
theme: string;
|
||||
}
|
|
@ -0,0 +1,235 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { HassEventBase, HassServiceTarget } from 'home-assistant-js-websocket';
|
||||
import { FrontendLocaleData, HomeAssistant } from './homeassistant';
|
||||
import { Constructor } from 'lit-element';
|
||||
|
||||
export interface Lovelace {
|
||||
config: LovelaceConfig;
|
||||
// If not set, a strategy was used to generate everything
|
||||
rawConfig: LovelaceConfig | undefined;
|
||||
editMode: boolean;
|
||||
urlPath: string | null;
|
||||
mode: 'generated' | 'yaml' | 'storage';
|
||||
locale: FrontendLocaleData;
|
||||
enableFullEditMode: () => void;
|
||||
setEditMode: (editMode: boolean) => void;
|
||||
saveConfig: (newConfig: LovelaceConfig) => Promise<void>;
|
||||
deleteConfig: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface LovelaceCard extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
isPanel?: boolean;
|
||||
editMode?: boolean;
|
||||
getCardSize(): number | Promise<number>;
|
||||
setConfig(config: LovelaceCardConfig): void;
|
||||
}
|
||||
|
||||
export interface LovelaceCardConstructor extends Constructor<LovelaceCard> {
|
||||
getStubConfig?: (hass: HomeAssistant, entities: string[], entitiesFallback: string[]) => LovelaceCardConfig;
|
||||
getConfigElement?: () => LovelaceCardEditor;
|
||||
}
|
||||
|
||||
export interface LovelaceCardEditor extends LovelaceGenericElementEditor {
|
||||
setConfig(config: LovelaceCardConfig): void;
|
||||
}
|
||||
|
||||
export interface LovelaceGenericElementEditor extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
lovelace?: LovelaceConfig;
|
||||
setConfig(config: any): void;
|
||||
focusYamlEditor?: () => void;
|
||||
}
|
||||
|
||||
export interface LovelacePanelConfig {
|
||||
mode: 'yaml' | 'storage';
|
||||
}
|
||||
|
||||
export interface LovelaceConfig {
|
||||
title?: string;
|
||||
strategy?: {
|
||||
type: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
views: LovelaceViewConfig[];
|
||||
background?: string;
|
||||
}
|
||||
|
||||
export interface LegacyLovelaceConfig extends LovelaceConfig {
|
||||
resources?: LovelaceResource[];
|
||||
}
|
||||
|
||||
export interface LovelaceResource {
|
||||
id: string;
|
||||
type: 'css' | 'js' | 'module' | 'html';
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface LovelaceResourcesMutableParams {
|
||||
res_type: LovelaceResource['type'];
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type LovelaceDashboard = LovelaceYamlDashboard | LovelaceStorageDashboard;
|
||||
|
||||
interface LovelaceGenericDashboard {
|
||||
id: string;
|
||||
url_path: string;
|
||||
require_admin: boolean;
|
||||
show_in_sidebar: boolean;
|
||||
icon?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface LovelaceYamlDashboard extends LovelaceGenericDashboard {
|
||||
mode: 'yaml';
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface LovelaceStorageDashboard extends LovelaceGenericDashboard {
|
||||
mode: 'storage';
|
||||
}
|
||||
|
||||
export interface LovelaceDashboardMutableParams {
|
||||
require_admin: boolean;
|
||||
show_in_sidebar: boolean;
|
||||
icon?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface LovelaceDashboardCreateParams extends LovelaceDashboardMutableParams {
|
||||
url_path: string;
|
||||
mode: 'storage';
|
||||
}
|
||||
|
||||
export interface LovelaceViewConfig {
|
||||
index?: number;
|
||||
title?: string;
|
||||
type?: string;
|
||||
strategy?: {
|
||||
type: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
cards?: LovelaceCardConfig[];
|
||||
path?: string;
|
||||
icon?: string;
|
||||
theme?: string;
|
||||
panel?: boolean;
|
||||
background?: string;
|
||||
visible?: boolean | ShowViewConfig[];
|
||||
}
|
||||
|
||||
export interface LovelaceViewElement extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
lovelace?: Lovelace;
|
||||
narrow?: boolean;
|
||||
index?: number;
|
||||
cards?: Array<LovelaceCard>;
|
||||
isStrategy: boolean;
|
||||
setConfig(config: LovelaceViewConfig): void;
|
||||
}
|
||||
|
||||
export interface ShowViewConfig {
|
||||
user?: string;
|
||||
}
|
||||
|
||||
export interface LovelaceBadgeConfig {
|
||||
type?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface LovelaceCardConfig {
|
||||
index?: number;
|
||||
view_index?: number;
|
||||
view_layout?: any;
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ToggleActionConfig extends BaseActionConfig {
|
||||
action: 'toggle';
|
||||
}
|
||||
|
||||
export interface CallServiceActionConfig extends BaseActionConfig {
|
||||
action: 'call-service';
|
||||
service: string;
|
||||
target?: HassServiceTarget;
|
||||
// "service_data" is kept for backwards compatibility. Replaced by "data".
|
||||
service_data?: Record<string, unknown>;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
export interface NoActionConfig extends BaseActionConfig {
|
||||
action: 'none';
|
||||
}
|
||||
|
||||
export interface CustomActionConfig extends BaseActionConfig {
|
||||
action: 'fire-dom-event';
|
||||
}
|
||||
|
||||
export interface AssistActionConfig extends BaseActionConfig {
|
||||
action: 'assist';
|
||||
pipeline_id?: string;
|
||||
start_listening?: boolean;
|
||||
}
|
||||
|
||||
export interface BaseActionConfig {
|
||||
action: string;
|
||||
confirmation?: ConfirmationRestrictionConfig;
|
||||
}
|
||||
|
||||
export interface ConfirmationRestrictionConfig {
|
||||
text?: string;
|
||||
exemptions?: RestrictionConfig[];
|
||||
}
|
||||
|
||||
export interface RestrictionConfig {
|
||||
user: string;
|
||||
}
|
||||
|
||||
export type ActionConfig =
|
||||
| ToggleActionConfig
|
||||
| CallServiceActionConfig
|
||||
| NavigateActionConfig
|
||||
| UrlActionConfig
|
||||
| MoreInfoActionConfig
|
||||
| AssistActionConfig
|
||||
| NoActionConfig
|
||||
| CustomActionConfig;
|
||||
|
||||
type LovelaceUpdatedEvent = HassEventBase & {
|
||||
event_type: 'lovelace_updated';
|
||||
data: {
|
||||
url_path: string | null;
|
||||
mode: 'yaml' | 'storage';
|
||||
};
|
||||
};
|
||||
|
||||
export interface WindowWithLovelaceProm extends Window {
|
||||
llConfProm?: Promise<LovelaceConfig>;
|
||||
llResProm?: Promise<LovelaceResource[]>;
|
||||
}
|
||||
|
||||
export interface ActionHandlerOptions {
|
||||
hasHold?: boolean;
|
||||
hasDoubleClick?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ActionHandlerDetail {
|
||||
action: 'hold' | 'tap' | 'double_tap';
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
export enum NumberFormat {
|
||||
language = 'language',
|
||||
system = 'system',
|
||||
comma_decimal = 'comma_decimal',
|
||||
decimal_comma = 'decimal_comma',
|
||||
space_comma = 'space_comma',
|
||||
none = 'none',
|
||||
}
|
||||
|
||||
export enum TimeFormat {
|
||||
language = 'language',
|
||||
system = 'system',
|
||||
am_pm = '12',
|
||||
twenty_four = '24',
|
||||
}
|
||||
|
||||
export enum TimeZone {
|
||||
local = 'local',
|
||||
server = 'server',
|
||||
}
|
||||
|
||||
export enum DateFormat {
|
||||
language = 'language',
|
||||
system = 'system',
|
||||
DMY = 'DMY',
|
||||
MDY = 'MDY',
|
||||
YMD = 'YMD',
|
||||
}
|
||||
|
||||
export enum FirstWeekday {
|
||||
language = 'language',
|
||||
monday = 'monday',
|
||||
tuesday = 'tuesday',
|
||||
wednesday = 'wednesday',
|
||||
thursday = 'thursday',
|
||||
friday = 'friday',
|
||||
saturday = 'saturday',
|
||||
sunday = 'sunday',
|
||||
}
|
||||
|
||||
export interface FrontendLocaleData {
|
||||
language: string;
|
||||
number_format: NumberFormat;
|
||||
time_format: TimeFormat;
|
||||
date_format: DateFormat;
|
||||
first_weekday: FirstWeekday;
|
||||
time_zone: TimeZone;
|
||||
}
|
||||
|
||||
export type TranslationCategory =
|
||||
| 'title'
|
||||
| 'state'
|
||||
| 'entity'
|
||||
| 'entity_component'
|
||||
| 'config'
|
||||
| 'config_panel'
|
||||
| 'options'
|
||||
| 'device_automation'
|
||||
| 'mfa_setup'
|
||||
| 'system_health'
|
||||
| 'device_class'
|
||||
| 'application_credentials'
|
||||
| 'issues'
|
||||
| 'selector';
|
|
@ -1,4 +1,6 @@
|
|||
import { ActionConfig, LovelaceCardConfig, LovelaceCard } from 'custom-card-helpers';
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { LovelaceCardConfig, LovelaceCard } from './lovelace';
|
||||
import { HassServiceTarget } from 'home-assistant-js-websocket';
|
||||
|
||||
export interface ButtonCardConfig {
|
||||
template?: string | string[];
|
||||
|
@ -162,3 +164,69 @@ export interface ButtonCardEmbeddedCards {
|
|||
export interface ButtonCardEmbeddedCardsConfig {
|
||||
[key: string]: string | undefined;
|
||||
}
|
||||
|
||||
export interface ToggleActionConfig extends BaseActionConfig {
|
||||
action: 'toggle';
|
||||
}
|
||||
|
||||
export interface CallServiceActionConfig extends BaseActionConfig {
|
||||
action: 'call-service';
|
||||
service: string;
|
||||
target?: HassServiceTarget;
|
||||
// "service_data" is kept for backwards compatibility. Replaced by "data".
|
||||
service_data?: Record<string, unknown>;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
export interface NoActionConfig extends BaseActionConfig {
|
||||
action: 'none';
|
||||
}
|
||||
|
||||
export interface CustomActionConfig extends BaseActionConfig {
|
||||
action: 'fire-dom-event';
|
||||
}
|
||||
|
||||
export interface AssistActionConfig extends BaseActionConfig {
|
||||
action: 'assist';
|
||||
pipeline_id?: string;
|
||||
start_listening?: boolean;
|
||||
}
|
||||
|
||||
export interface BaseActionConfig {
|
||||
action: string;
|
||||
confirmation?: ConfirmationRestrictionConfig;
|
||||
repeat?: number;
|
||||
}
|
||||
|
||||
export interface ConfirmationRestrictionConfig {
|
||||
text?: string;
|
||||
exemptions?: RestrictionConfig[];
|
||||
}
|
||||
|
||||
export interface RestrictionConfig {
|
||||
user: string;
|
||||
}
|
||||
|
||||
export type ActionConfig =
|
||||
| ToggleActionConfig
|
||||
| CallServiceActionConfig
|
||||
| NavigateActionConfig
|
||||
| UrlActionConfig
|
||||
| MoreInfoActionConfig
|
||||
| AssistActionConfig
|
||||
| NoActionConfig
|
||||
| CustomActionConfig;
|
|
@ -3,11 +3,7 @@
|
|||
"target": "es2017",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
],
|
||||
"lib": ["es2017", "dom", "dom.iterable"],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "ts-lit-plugin"
|
||||
|
@ -23,9 +19,7 @@
|
|||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"src/*"
|
||||
]
|
||||
"include": ["src/*", "src/**/*"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue