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
|
type: entity-button
|
||||||
entity: switch.skylight
|
entity: switch.skylight
|
||||||
name: Default HASS
|
name: Default HASS
|
||||||
|
show_name: false
|
||||||
|
show_state: true
|
||||||
- type: custom:card-modder
|
- type: custom:card-modder
|
||||||
styles:
|
styles:
|
||||||
card:
|
card:
|
||||||
|
@ -460,6 +462,8 @@ views:
|
||||||
card:
|
card:
|
||||||
type: 'custom:button-card'
|
type: 'custom:button-card'
|
||||||
entity: switch.skylight
|
entity: switch.skylight
|
||||||
|
show_state: true
|
||||||
|
show_name: false
|
||||||
- type: custom:card-modder
|
- type: custom:card-modder
|
||||||
styles:
|
styles:
|
||||||
card:
|
card:
|
||||||
|
@ -984,6 +988,7 @@ views:
|
||||||
name: more-info
|
name: more-info
|
||||||
tap_action:
|
tap_action:
|
||||||
action: more-info
|
action: more-info
|
||||||
|
entity: light.test_light
|
||||||
- type: 'custom:button-card'
|
- type: 'custom:button-card'
|
||||||
entity: switch.skylight
|
entity: switch.skylight
|
||||||
name: call-service
|
name: call-service
|
||||||
|
@ -992,6 +997,19 @@ views:
|
||||||
service: switch.toggle
|
service: switch.toggle
|
||||||
service_data:
|
service_data:
|
||||||
entity_id: switch.skylight
|
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'
|
- type: 'custom:button-card'
|
||||||
entity: switch.skylight
|
entity: switch.skylight
|
||||||
name: none
|
name: none
|
||||||
|
@ -1634,7 +1652,7 @@ views:
|
||||||
cards:
|
cards:
|
||||||
- type: horizontal-stack
|
- type: horizontal-stack
|
||||||
cards:
|
cards:
|
||||||
- type: entity-button
|
- type: button-card
|
||||||
entity: switch.skylight
|
entity: switch.skylight
|
||||||
name: Default
|
name: Default
|
||||||
- type: 'custom:button-card'
|
- 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).
|
All the fields support templates, see [templates](#javascript-templates).
|
||||||
|
|
||||||
| Name | Type | Default | Supported options | Description |
|
| Name | Type | Default | Supported options | Description |
|
||||||
| ----------------- | ------ | -------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ------------------------ | ------ | -------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `action` | string | `toggle` | `more-info`, `toggle`, `call-service`, `none`, `navigate`, `url` | Action to perform |
|
| `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` |
|
| `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 |
|
| `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) |
|
||||||
| `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 |
|
| `navigation_path` | string | none | Eg: `/lovelace/0/` | Path to navigate to (e.g. `/lovelace/0/`) when action defined as navigate |
|
||||||
| `service` | string | none | Any service | Service to call (e.g. `media_player.media_play_pause`) when `action` defined as `call-service` |
|
| `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_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) |
|
| `service` | string | none | Any service | Service to call (e.g. `media_player.media_play_pause`) when `action` defined as `call-service` |
|
||||||
| `haptic` | string | none | `success`, `warning`, `failure`, `light`, `medium`, `heavy`, `selection` | Haptic feedback for the [Beta IOS App](http://home-assistant.io/ios/beta) |
|
| `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) |
|
||||||
| `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. |
|
| `haptic` | string | none | `success`, `warning`, `failure`, `light`, `medium`, `heavy`, `selection` | Haptic feedback for the [Beta IOS App](http://home-assistant.io/ios/beta) |
|
||||||
| `confirmation` | object | none | See [confirmation](#confirmation) | Display a confirmation popup, overrides the default `confirmation` object |
|
| `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
|
### Confirmation
|
||||||
|
|
||||||
|
@ -1031,7 +1032,7 @@ Horizontal stack with :
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: media_player.volume_up
|
service: media_player.volume_up
|
||||||
service_data:
|
data:
|
||||||
entity_id: media_player.living_room_speaker
|
entity_id: media_player.living_room_speaker
|
||||||
- type: 'custom:button-card'
|
- type: 'custom:button-card'
|
||||||
color_type: card
|
color_type: card
|
||||||
|
@ -1040,7 +1041,7 @@ Horizontal stack with :
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: media_player.volume_down
|
service: media_player.volume_down
|
||||||
service_data:
|
data:
|
||||||
entity_id: media_player.living_room_speaker
|
entity_id: media_player.living_room_speaker
|
||||||
- type: 'custom:button-card'
|
- type: 'custom:button-card'
|
||||||
color_type: blank-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:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: input_select.select_next
|
service: input_select.select_next
|
||||||
service_data:
|
data:
|
||||||
entity_id: input_select.cube_mode
|
entity_id: input_select.cube_mode
|
||||||
show_state: true
|
show_state: true
|
||||||
state:
|
state:
|
||||||
|
|
10
package.json
10
package.json
|
@ -68,10 +68,12 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctrl/tinycolor": "^3.1.6",
|
"@ctrl/tinycolor": "^3.1.6",
|
||||||
"@material/mwc-ripple": "^0.19.1",
|
"@material/mwc-ripple": "^0.19.1",
|
||||||
"custom-card-helpers": "^1.7.0",
|
"custom-card-helpers": "^1.9.0",
|
||||||
"fast-copy": "^2.1.0",
|
"fast-copy": "^2.1.0",
|
||||||
"home-assistant-js-websocket": "^5.7.0",
|
"home-assistant-js-websocket": "^8.2.0",
|
||||||
"lit-element": "^2.4.0",
|
"lit": "^2.7.6",
|
||||||
"lit-html": "^1.3.0"
|
"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';
|
// import '@material/mwc-ripple';
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
import { Ripple } from '@material/mwc-ripple';
|
import { Ripple } from '@material/mwc-ripple';
|
||||||
import { myFireEvent } from './my-fire-event';
|
import { myFireEvent } from './my-fire-event';
|
||||||
import { deepEqual } from './deep-equal';
|
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;
|
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||||
|
|
||||||
interface ActionHandler extends HTMLElement {
|
interface ActionHandler extends HTMLElement {
|
||||||
|
@ -254,14 +257,21 @@ const getActionHandler = (): ActionHandler => {
|
||||||
return actionhandler as ActionHandler;
|
return actionhandler as ActionHandler;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actionHandlerBind = (element: ActionHandlerElement, options: ActionHandlerOptions): void => {
|
export const actionHandlerBind = (element: ActionHandlerElement, options?: ActionHandlerOptions) => {
|
||||||
const actionhandler: ActionHandler = getActionHandler();
|
const actionhandler: ActionHandler = getActionHandler();
|
||||||
if (!actionhandler) {
|
if (!actionhandler) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
actionhandler.bind(element, options);
|
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 => {
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
actionHandlerBind(part.committer.element as ActionHandlerElement, options);
|
render(_options?: ActionHandlerOptions) {}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
|
@ -11,24 +11,15 @@ import {
|
||||||
queryAsync,
|
queryAsync,
|
||||||
eventOptions,
|
eventOptions,
|
||||||
} from 'lit-element';
|
} from 'lit-element';
|
||||||
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { Ripple } from '@material/mwc-ripple';
|
import { Ripple } from '@material/mwc-ripple';
|
||||||
import { RippleHandlers } from '@material/mwc-ripple/ripple-handlers';
|
import { RippleHandlers } from '@material/mwc-ripple/ripple-handlers';
|
||||||
import { styleMap, StyleInfo } from 'lit-html/directives/style-map';
|
import { styleMap, StyleInfo } from 'lit-html/directives/style-map';
|
||||||
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
|
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
|
||||||
import { classMap, ClassInfo } from 'lit-html/directives/class-map';
|
import { classMap, ClassInfo } from 'lit-html/directives/class-map';
|
||||||
import { HassEntity } from 'home-assistant-js-websocket';
|
import { HassEntity } from 'home-assistant-js-websocket';
|
||||||
import {
|
import { timerTimeRemaining, createThing, DOMAINS_TOGGLE, computeStateDomain } from 'custom-card-helpers';
|
||||||
HomeAssistant,
|
import { LovelaceCard } from './types/lovelace';
|
||||||
handleClick,
|
|
||||||
timerTimeRemaining,
|
|
||||||
secondsToDuration,
|
|
||||||
durationToSeconds,
|
|
||||||
createThing,
|
|
||||||
fireEvent,
|
|
||||||
DOMAINS_TOGGLE,
|
|
||||||
LovelaceCard,
|
|
||||||
computeStateDomain,
|
|
||||||
} from 'custom-card-helpers';
|
|
||||||
import {
|
import {
|
||||||
ButtonCardConfig,
|
ButtonCardConfig,
|
||||||
ExternalButtonCardConfig,
|
ExternalButtonCardConfig,
|
||||||
|
@ -38,7 +29,7 @@ import {
|
||||||
CustomFieldCard,
|
CustomFieldCard,
|
||||||
ButtonCardEmbeddedCards,
|
ButtonCardEmbeddedCards,
|
||||||
ButtonCardEmbeddedCardsConfig,
|
ButtonCardEmbeddedCardsConfig,
|
||||||
} from './types';
|
} from './types/types';
|
||||||
import { actionHandler } from './action-handler';
|
import { actionHandler } from './action-handler';
|
||||||
import {
|
import {
|
||||||
computeDomain,
|
computeDomain,
|
||||||
|
@ -52,17 +43,22 @@ import {
|
||||||
mergeStatesById,
|
mergeStatesById,
|
||||||
getLovelace,
|
getLovelace,
|
||||||
getLovelaceCast,
|
getLovelaceCast,
|
||||||
|
secondsToDuration,
|
||||||
|
durationToSeconds,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
import { styles } from './styles';
|
import { styles } from './styles';
|
||||||
import { myComputeStateDisplay } from './compute_state_display';
|
import { computeStateDisplay } from './compute_state_display';
|
||||||
import copy from 'fast-copy';
|
import copy from 'fast-copy';
|
||||||
import * as pjson from '../package.json';
|
import * as pjson from '../package.json';
|
||||||
import { deepEqual } from './deep-equal';
|
import { deepEqual } from './deep-equal';
|
||||||
import { stateColorCss } from './state_color';
|
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;
|
let helpers = (window as any).cardHelpers;
|
||||||
const helperPromise = new Promise(async (resolve) => {
|
const helperPromise = new Promise<void>(async (resolve) => {
|
||||||
if (helpers) resolve();
|
if (helpers) resolve();
|
||||||
if ((window as any).loadCardHelpers) {
|
if ((window as any).loadCardHelpers) {
|
||||||
helpers = await (window as any).loadCardHelpers();
|
helpers = await (window as any).loadCardHelpers();
|
||||||
|
@ -157,7 +153,7 @@ class ButtonCard extends LitElement {
|
||||||
else {
|
else {
|
||||||
const element = createThing(config);
|
const element = createThing(config);
|
||||||
helperPromise.then(() => {
|
helperPromise.then(() => {
|
||||||
fireEvent(element, 'll-rebuild', {});
|
myFireEvent(element, 'll-rebuild', {});
|
||||||
});
|
});
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
@ -175,7 +171,7 @@ class ButtonCard extends LitElement {
|
||||||
? this._objectEvalTemplate(this._stateObj, this._config!.variables)
|
? this._objectEvalTemplate(this._stateObj, this._config!.variables)
|
||||||
: undefined;
|
: undefined;
|
||||||
return this._cardHtml();
|
return this._cardHtml();
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
if (e.stack) console.error(e.stack);
|
if (e.stack) console.error(e.stack);
|
||||||
else console.error(e);
|
else console.error(e);
|
||||||
const errorCard = document.createElement('hui-error-card') as LovelaceCard;
|
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) {
|
if (!stateObj) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -309,7 +305,7 @@ class ButtonCard extends LitElement {
|
||||||
this._evaledVariables,
|
this._evaledVariables,
|
||||||
html,
|
html,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
const funcTrimmed = func.length <= 100 ? func.trim() : `${func.trim().substring(0, 98)}...`;
|
const funcTrimmed = func.length <= 100 ? func.trim() : `${func.trim().substring(0, 98)}...`;
|
||||||
e.message = `${e.name}: ${e.message} in '${funcTrimmed}'`;
|
e.message = `${e.name}: ${e.message} in '${funcTrimmed}'`;
|
||||||
e.name = 'ButtonCardJSTemplateError';
|
e.name = 'ButtonCardJSTemplateError';
|
||||||
|
@ -493,30 +489,43 @@ class ButtonCard extends LitElement {
|
||||||
return this._getTemplateOrValue(state, name);
|
return this._getTemplateOrValue(state, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _buildStateString(stateObj: HassEntity | undefined): string | undefined {
|
private _buildStateString(stateObj: HassEntity | undefined): string | undefined | null {
|
||||||
let stateString: string | undefined;
|
let stateString: string | undefined | null;
|
||||||
if (this._config!.show_state && stateObj && stateObj.state) {
|
if (this._config!.show_state && stateObj && stateObj.state) {
|
||||||
const units = this._buildUnits(stateObj);
|
const units = this._buildUnits(stateObj);
|
||||||
if (units) {
|
if (units) {
|
||||||
stateString = `${stateObj.state} ${units}`;
|
stateString = `${stateObj.state} ${units}`;
|
||||||
} else if (computeDomain(stateObj.entity_id) === 'timer') {
|
} else if (computeDomain(stateObj.entity_id) === 'timer') {
|
||||||
if (stateObj.state === 'idle' || this._timeRemaining === 0) {
|
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 {
|
} else {
|
||||||
stateString = this._computeTimeDisplay(stateObj);
|
stateString = this._computeTimeDisplay(stateObj);
|
||||||
if (stateObj.state === 'paused') {
|
if (stateObj.state === 'paused') {
|
||||||
stateString += ` (${myComputeStateDisplay(
|
stateString += ` (${computeStateDisplay(
|
||||||
this._hass!,
|
|
||||||
this._hass!.localize,
|
this._hass!.localize,
|
||||||
stateObj,
|
stateObj,
|
||||||
this._hass!.language,
|
this._hass!.locale,
|
||||||
|
this._hass!.config,
|
||||||
|
this._hass!.entities,
|
||||||
)})`;
|
)})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!this._config?.show_units && computeDomain(stateObj.entity_id) === 'sensor') {
|
} else if (!this._config?.show_units && computeDomain(stateObj.entity_id) === 'sensor') {
|
||||||
stateString = stateObj.state;
|
stateString = stateObj.state;
|
||||||
} else {
|
} 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;
|
return stateString;
|
||||||
|
@ -893,7 +902,7 @@ class ButtonCard extends LitElement {
|
||||||
<ha-state-icon
|
<ha-state-icon
|
||||||
.state=${state}
|
.state=${state}
|
||||||
?data-domain=${domain}
|
?data-domain=${domain}
|
||||||
data-state=${state?.state}
|
data-state=${ifDefined(state?.state)}
|
||||||
style=${styleMap(haIconStyle)}
|
style=${styleMap(haIconStyle)}
|
||||||
.icon="${icon}"
|
.icon="${icon}"
|
||||||
id="icon"
|
id="icon"
|
||||||
|
@ -1075,6 +1084,15 @@ class ButtonCard extends LitElement {
|
||||||
});
|
});
|
||||||
return configEval;
|
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]);
|
configDuplicate[action] = __evalObject(configDuplicate[action]);
|
||||||
if (!configDuplicate[action].confirmation && configDuplicate.confirmation) {
|
if (!configDuplicate[action].confirmation && configDuplicate.confirmation) {
|
||||||
configDuplicate[action].confirmation = __evalObject(configDuplicate.confirmation);
|
configDuplicate[action].confirmation = __evalObject(configDuplicate.confirmation);
|
||||||
|
@ -1090,32 +1108,32 @@ class ButtonCard extends LitElement {
|
||||||
// backward compatibility
|
// backward compatibility
|
||||||
@eventOptions({ passive: true })
|
@eventOptions({ passive: true })
|
||||||
private handleRippleActivate(evt?: Event): void {
|
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 {
|
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 {
|
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 {
|
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 {
|
private _handleAction(ev: any): void {
|
||||||
if (ev.detail?.action) {
|
if (ev.detail?.action) {
|
||||||
switch (ev.detail.action) {
|
switch (ev.detail.action) {
|
||||||
case 'tap':
|
case 'tap':
|
||||||
this._handleTap();
|
|
||||||
break;
|
|
||||||
case 'hold':
|
case 'hold':
|
||||||
this._handleHold();
|
|
||||||
break;
|
|
||||||
case 'double_tap':
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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 {
|
private _handleUnlockType(ev): void {
|
||||||
const config = this._config as ButtonCardConfig;
|
const config = this._config as ButtonCardConfig;
|
||||||
if (!config) return;
|
if (!config) return;
|
||||||
|
@ -1153,12 +1153,12 @@ class ButtonCard extends LitElement {
|
||||||
const lock = this.shadowRoot!.getElementById('lock') as LitElement;
|
const lock = this.shadowRoot!.getElementById('lock') as LitElement;
|
||||||
if (!lock) return;
|
if (!lock) return;
|
||||||
if (this._config!.lock!.exemptions) {
|
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;
|
let matched = false;
|
||||||
this._config!.lock!.exemptions.forEach((e) => {
|
this._config!.lock!.exemptions.forEach((e) => {
|
||||||
if (
|
if (
|
||||||
(!matched && (e as ExemptionUserConfig).user === this._hass!.user.id) ||
|
(!matched && (e as ExemptionUserConfig).user === this._hass!.user?.id) ||
|
||||||
(e as ExemptionUsernameConfig).username === this._hass!.user.name
|
(e as ExemptionUsernameConfig).username === this._hass!.user?.name
|
||||||
) {
|
) {
|
||||||
matched = true;
|
matched = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
export const UNAVAILABLE = 'unavailable';
|
export const UNAVAILABLE = 'unavailable';
|
||||||
|
export const BINARY_STATE_ON = 'on';
|
||||||
|
export const BINARY_STATE_OFF = 'off';
|
||||||
const arrayLiteralIncludes = <T extends readonly unknown[]>(array: T) => (
|
const arrayLiteralIncludes = <T extends readonly unknown[]>(array: T) => (
|
||||||
searchElement: unknown,
|
searchElement: unknown,
|
||||||
fromIndex?: number,
|
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 { HassConfig, HassEntity } from 'home-assistant-js-websocket';
|
||||||
import { LocalizeFunc, HomeAssistant, formatDate, formatTime, formatDateTime } from 'custom-card-helpers';
|
import { LocalizeFunc } from 'custom-card-helpers';
|
||||||
import { computeDomain } from './helpers';
|
import { computeDomain, isNumericFromAttributes } from './helpers';
|
||||||
import { atLeastVersion } from './at_least_version';
|
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 UNAVAILABLE = 'unavailable';
|
||||||
const UNKNOWN = 'unknown';
|
const UNKNOWN = 'unknown';
|
||||||
|
|
||||||
function legacyComputeStateDisplay(localize: LocalizeFunc, stateObj: HassEntity): string | undefined {
|
export const computeStateDisplaySingleEntity = (
|
||||||
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,
|
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
stateObj: HassEntity,
|
stateObj: HassEntity,
|
||||||
language: string,
|
locale: FrontendLocaleData,
|
||||||
): string | undefined => {
|
config: HassConfig,
|
||||||
if (!atLeastVersion(hass.config.version, 0, 109)) {
|
entity: EntityRegistryDisplayEntry | undefined,
|
||||||
return legacyComputeStateDisplay(localize, stateObj);
|
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) {
|
export const computeStateDisplay = (
|
||||||
return localize(`state.default.${stateObj.state}`);
|
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 computeStateDisplayFromEntityAttributes(
|
||||||
return `${stateObj.state} ${stateObj.attributes.unit_of_measurement}`;
|
localize,
|
||||||
}
|
locale,
|
||||||
|
config,
|
||||||
const domain = computeDomain(stateObj.entity_id);
|
entity,
|
||||||
|
stateObj.entity_id,
|
||||||
if (domain === 'input_datetime') {
|
stateObj.attributes,
|
||||||
let date: Date;
|
state !== undefined ? state : stateObj.state,
|
||||||
if (!stateObj.attributes.has_time) {
|
);
|
||||||
date = new Date(stateObj.attributes.year, stateObj.attributes.month - 1, stateObj.attributes.day);
|
};
|
||||||
return formatDate(date, language);
|
|
||||||
}
|
export const computeStateDisplayFromEntityAttributes = (
|
||||||
if (!stateObj.attributes.has_date) {
|
localize: LocalizeFunc,
|
||||||
const now = new Date();
|
locale: FrontendLocaleData,
|
||||||
date = new Date(
|
config: HassConfig,
|
||||||
// Due to bugs.chromium.org/p/chromium/issues/detail?id=797548
|
entity: EntityRegistryDisplayEntry | undefined,
|
||||||
// don't use artificial 1970 year.
|
entityId: string,
|
||||||
now.getFullYear(),
|
attributes: any,
|
||||||
now.getMonth(),
|
state: string,
|
||||||
now.getDay(),
|
): string => {
|
||||||
stateObj.attributes.hour,
|
if (state === UNKNOWN || state === UNAVAILABLE) {
|
||||||
stateObj.attributes.minute,
|
return localize(`state.default.${state}`);
|
||||||
);
|
}
|
||||||
return formatTime(date, language);
|
|
||||||
}
|
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
||||||
|
if (isNumericFromAttributes(attributes)) {
|
||||||
date = new Date(
|
// state is duration
|
||||||
stateObj.attributes.year,
|
if (
|
||||||
stateObj.attributes.month - 1,
|
attributes.device_class === 'duration' &&
|
||||||
stateObj.attributes.day,
|
attributes.unit_of_measurement &&
|
||||||
stateObj.attributes.hour,
|
UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement]
|
||||||
stateObj.attributes.minute,
|
) {
|
||||||
);
|
try {
|
||||||
return formatDateTime(date, language);
|
return formatDuration(state, attributes.unit_of_measurement);
|
||||||
}
|
} catch (_err) {
|
||||||
|
// fallback to default
|
||||||
if (!atLeastVersion(hass.config.version, 2023, 4)) {
|
}
|
||||||
return (
|
}
|
||||||
// Return device class translation
|
if (attributes.device_class === 'monetary') {
|
||||||
(stateObj.attributes.device_class &&
|
try {
|
||||||
localize(`component.${domain}.state.${stateObj.attributes.device_class}.${stateObj.state}`)) ||
|
return formatNumber(state, locale, {
|
||||||
// Return default translation
|
style: 'currency',
|
||||||
localize(`component.${domain}.state._.${stateObj.state}`) ||
|
currency: attributes.unit_of_measurement,
|
||||||
// We don't know! Return the raw state.
|
minimumFractionDigits: 2,
|
||||||
stateObj.state
|
// Override monetary options with number format
|
||||||
);
|
...getNumberFormatOptions({ state, attributes } as HassEntity, entity),
|
||||||
}
|
});
|
||||||
return (
|
} catch (_err) {
|
||||||
// Return device class translation
|
// fallback to default
|
||||||
(stateObj.attributes.device_class &&
|
}
|
||||||
localize(`component.${domain}.entity_component.${stateObj.attributes.device_class}.state.${stateObj.state}`)) ||
|
}
|
||||||
// Return default translation
|
const unit = !attributes.unit_of_measurement
|
||||||
localize(`component.${domain}.entity_component._.state.${stateObj.state}`) ||
|
? ''
|
||||||
// We don't know! Return the raw state.
|
: attributes.unit_of_measurement === '%'
|
||||||
stateObj.state
|
? 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 { PropertyValues } from 'lit-element';
|
||||||
import tinycolor, { TinyColor } from '@ctrl/tinycolor';
|
import tinycolor, { TinyColor } from '@ctrl/tinycolor';
|
||||||
import { HomeAssistant, LovelaceConfig } from 'custom-card-helpers';
|
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 { 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 {
|
export function computeDomain(entityId: string): string {
|
||||||
return entityId.substr(0, entityId.indexOf('.'));
|
return entityId.substr(0, entityId.indexOf('.'));
|
||||||
|
@ -331,3 +331,31 @@ export function computeCssValue(prop: string | string[], computedStyles: CSSStyl
|
||||||
}
|
}
|
||||||
return computedStyles.getPropertyValue(prop).trim() || undefined;
|
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 {
|
declare global {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
interface HASSDomEvents { }
|
interface HASSDomEvents {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ValidHassDomEvent = keyof 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`).
|
* `node` on which to fire the event (HTMLElement, defaults to `this`).
|
||||||
* @return {Event} The new event that was fired.
|
* @return {Event} The new event that was fired.
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export const myFireEvent = <HassEvent extends ValidHassDomEvent>(
|
export const myFireEvent = <HassEvent extends ValidHassDomEvent>(
|
||||||
node: HTMLElement | Window,
|
node: HTMLElement | Window,
|
||||||
type: HassEvent,
|
type: HassEvent,
|
||||||
|
@ -63,8 +64,9 @@ export const myFireEvent = <HassEvent extends ValidHassDomEvent>(
|
||||||
cancelable?: boolean;
|
cancelable?: boolean;
|
||||||
composed?: boolean;
|
composed?: boolean;
|
||||||
},
|
},
|
||||||
): any => {
|
) => {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
// @ts-ignore
|
||||||
detail = detail === null || detail === undefined ? {} : detail;
|
detail = detail === null || detail === undefined ? {} : detail;
|
||||||
const event = new Event(type, {
|
const event = new Event(type, {
|
||||||
bubbles: options.bubbles === undefined ? true : options.bubbles,
|
bubbles: options.bubbles === undefined ? true : options.bubbles,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/** Return an color representing a state. */
|
/** Return an color representing a state. */
|
||||||
import { HassEntity } from 'home-assistant-js-websocket';
|
import { HassEntity } from 'home-assistant-js-websocket';
|
||||||
import { UNAVAILABLE } from './const';
|
import { UNAVAILABLE } from './common/const';
|
||||||
import { computeGroupDomain, GroupEntity } from './helpers';
|
import { computeGroupDomain, GroupEntity } from './helpers';
|
||||||
import { computeCssVariable } from './helpers';
|
import { computeCssVariable } from './helpers';
|
||||||
import { computeDomain, slugify } 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 {
|
export interface ButtonCardConfig {
|
||||||
template?: string | string[];
|
template?: string | string[];
|
||||||
|
@ -162,3 +164,69 @@ export interface ButtonCardEmbeddedCards {
|
||||||
export interface ButtonCardEmbeddedCardsConfig {
|
export interface ButtonCardEmbeddedCardsConfig {
|
||||||
[key: string]: string | undefined;
|
[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",
|
"target": "es2017",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"lib": [
|
"lib": ["es2017", "dom", "dom.iterable"],
|
||||||
"es2017",
|
|
||||||
"dom",
|
|
||||||
"dom.iterable"
|
|
||||||
],
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "ts-lit-plugin"
|
"name": "ts-lit-plugin"
|
||||||
|
@ -23,9 +19,7 @@
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/*", "src/**/*"]
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue