1.10.0 (#166)
* Add an option to ignore light temperature * Deprecate style in favor of styles.card * pre-commit hooks * name_template and entity_picture_template * Use custom-card-helpers * Deep merging state by id * Using latest custom-card-helpers * Aspect ratio support and auto icon resizing * Updating documentation
This commit is contained in:
parent
6f12577e15
commit
5ec35fd72c
|
@ -10,8 +10,7 @@
|
|||
"name": "Chrome Localhost",
|
||||
"url": "http://localhost:8123",
|
||||
"webRoot": "${workspaceFolder}/dist",
|
||||
"sourceMaps": true,
|
||||
"preLaunchTask": "watch"
|
||||
"sourceMaps": true
|
||||
}
|
||||
]
|
||||
}
|
134
README.md
134
README.md
|
@ -28,14 +28,16 @@ Lovelace Button card for your entities.
|
|||
- [Light entity color variable](#light-entity-color-variable)
|
||||
- [ADVANCED styling options](#advanced-styling-options)
|
||||
- [Configuration Templates](#configuration-templates)
|
||||
- [General](#general)
|
||||
- [Merging state by id](#merging-state-by-id)
|
||||
- [Installation](#installation)
|
||||
- [Manual Installation](#manual-installation)
|
||||
- [Installation and tracking with `custom_updater`](#installation-and-tracking-with-custom_updater)
|
||||
- [Installation and tracking with `custom_updater`](#installation-and-tracking-with-customupdater)
|
||||
- [Examples](#examples)
|
||||
- [Configuration with states](#configuration-with-states)
|
||||
- [Default behavior](#default-behavior)
|
||||
- [With Operator on state](#with-operator-on-state)
|
||||
- [`tap_action` Navigate](#tap_action-navigate)
|
||||
- [`tap_action` Navigate](#tapaction-navigate)
|
||||
- [blink](#blink)
|
||||
- [Play with width, height and icon size](#play-with-width-height-and-icon-size)
|
||||
- [Templates Support](#templates-support)
|
||||
|
@ -43,6 +45,7 @@ Lovelace Button card for your entities.
|
|||
- [State Templates](#state-templates)
|
||||
- [Styling](#styling)
|
||||
- [Lock](#lock)
|
||||
- [Aspect Ratio](#aspect-ratio)
|
||||
- [Credits](#credits)
|
||||
|
||||
|
||||
|
@ -54,6 +57,7 @@ Lovelace Button card for your entities.
|
|||
- custom color (optional), or based on light rgb value/temperature
|
||||
- custom state definition with customizable color, icon and style (optional)
|
||||
- [custom size of the icon, width and height](#Play-with-width-height-and-icon-size) (optional)
|
||||
- [aspect ratio support](#aspect-ratio) (optional)
|
||||
- Support for [templates](#templates) in some fields
|
||||
- custom icon (optional)
|
||||
- custom css style (optional)
|
||||
|
@ -81,8 +85,9 @@ Lovelace Button card for your entities.
|
|||
| `entity` | string | optional | `switch.ac` | entity_id |
|
||||
| `icon` | string | optional | `mdi:air-conditioner` | Icon to display. Will be overriden by the icon defined in a state (if present). Defaults to the entity icon. Hide with `show_icon: false` |
|
||||
| `color_type` | string | `icon` | `icon` \| `card` \| `blank-card` \| `label-card` | Color either the background of the card or the icon inside the card. Setting this to `card` enable automatic `font` and `icon` color. This allows the text/icon to be readable even if the background color is bright/dark. Additional color-type options `blank-card` and `label-card` can be used for organisation (see examples). |
|
||||
| `color` | string | optional | `auto` \| `rgb(28, 128, 199)` | Color of the icon/card. `auto` sets the color based on the color of a light. By default, if the entity state is `off`, the color will be `var(--paper-item-icon-active-color)`, for `on` it will be `var(--paper-item-icon-color)` and for any other state it will be `var(--primary-text-color)`. You can redefine each colors using `state` |
|
||||
| `size` | string | `40%` | `20px` | Size of the icon. Can be percentage or pixel |
|
||||
| `color` | string | optional | `auto` \| `auto-no-temperature` \| `rgb(28, 128, 199)` | Color of the icon/card. `auto` sets the color based on the color of a light including the temperature of the light. Setting this to `auto-no-temperature` will behave like home-assistant's default ignoring the temperature of the light. By default, if the entity state is `off`, the color will be `var(--paper-item-icon-active-color)`, for `on` it will be `var(--paper-item-icon-color)` and for any other state it will be `var(--primary-text-color)`. You can redefine each colors using `state` |
|
||||
| `size` | string | `40%` | `20px` | Size of the icon. Can be percentage or pixel |
|
||||
| `aspect_ratio` | string | optional | `1/1`, `2/1`, `1/1.5`, ... | See [here](#aspect-ratio) for an example. Aspect ratio of the card. `1/1` being a square. This will auto adapt to your screen size |
|
||||
| `tap_action` | object | optional | See [Action](#Action) | Define the type of action on click, if undefined, toggle will be used. |
|
||||
| `hold_action` | object | optional | See [Action](#Action) | Define the type of action on hold, if undefined, nothing happens. |
|
||||
| `dbltap_action` | object | optional | See [Action](#Action) | Define the type of action on double click, if undefined, nothing happens. |
|
||||
|
@ -90,6 +95,7 @@ Lovelace Button card for your entities.
|
|||
| `label` | string | optional | Any string that you want | Display a label below the card. See [Layouts](#layout) for more information. |
|
||||
| `label_template` | string | optional | | See [templates](#templates). Any javascript code which returns a string. Overrides `label` |
|
||||
| `show_name` | boolean | `true` | `true` \| `false` | Wether to show the name or not. Will pick entity_id's name by default, unless redefined in the `name` property or in any state `name` property |
|
||||
| `name_template` | string | optional | | See [templates](#templates). Any javascript code which returns a string. Overrides `name` |
|
||||
| `show_state` | boolean | `false` | `true` \| `false` | Show the state on the card. defaults to false if not set |
|
||||
| `show_icon` | boolean | `true` | `true` \| `false` | Wether to show the icon or not. Unless redefined in `icon`, uses the default entity icon from hass |
|
||||
| `show_units` | boolean | `true` | `true` \| `false` | Display or hide the units of a sensor, if any. |
|
||||
|
@ -97,6 +103,7 @@ Lovelace Button card for your entities.
|
|||
| `show_last_changed` | boolean | `false` | `true` \| `false` | Replace the label altogether and display the the `last_changed` attribute in a nice way (eg: `12 minutes ago`) |
|
||||
| `show_entity_picture` | boolean | `false` | `true` \| `false` | Replace the icon by the entity picture (if any) or the custom picture (if any). Falls back to using the icon if both are undefined |
|
||||
| `entity_picture` | string | optional | Can be any of `/local/*` file or a URL | Will override the icon/the default entity_picture with your own image. Best is to use a square image. You can also define one per state |
|
||||
| `entity_picture_template` | string | optional | | See [templates](#templates). Any javascript code which returns a path to a file or a url as a string. Overrides `entity_picture` |
|
||||
| `units` | string | optional | `Kb/s`, `lux`, ... | Override or define the units to display after the state of the entity. If omitted, it's using the entity's units |
|
||||
| `styles` | object list | optional | | See [styles](#styles) |
|
||||
| `state` | object list | optional | See [State](#State) | State to use for the color, icon and style of the button. Multiple states can be defined |
|
||||
|
@ -109,6 +116,7 @@ Lovelace Button card for your entities.
|
|||
| Name | Type | Default | Supported options | Description |
|
||||
| ----------------- | ------ | -------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `action` | string | `toggle` | `more-info`, `toggle`, `call-service`, `none`, `navigate`, `url` | Action to perform |
|
||||
| `entity` | string | none | Any entity id | **Only valid for `action: more-info`** to override the entity on which you want to call `more-info` |
|
||||
| `navigation_path` | string | none | Eg: `/lovelace/0/` | Path to navigate to (e.g. `/lovelace/0/`) when action defined as navigate |
|
||||
| `url` | string | none | Eg: `https://www.google.fr` | URL to open on click when action is `url`. The URL will open in a new tab |
|
||||
| `service` | string | none | Any service | Service to call (e.g. `media_player.media_play_pause`) when `action` defined as `call-service` |
|
||||
|
@ -123,11 +131,13 @@ Lovelace Button card for your entities.
|
|||
| `operator` | string | `==` | See [Available Operators](#Available-operators) | The operator used to compare the current state against the `value` |
|
||||
| `value` | string/number | **required** (unless operator is `default`) | If your entity is a sensor with numbers, use a number directly, else use a string | The value which will be compared against the current state of the entity |
|
||||
| `name` | string | optional | Any string, `'Alert'`, `'My little switch is on'`, ... | if `show_name` is `true`, the name to display for this state. If undefined, uses the general config `name`, and if undefined uses the entity name |
|
||||
| `name_template` | string | optional | | See [templates](#templates). Any javascript code which returns a string. Overrides `name` |
|
||||
| `icon` | string | optional | `mdi:battery` | The icon to display for this state. Defaults to the entity icon. Hide with `show_icon: false` |
|
||||
| `color` | string | `var(--primary-text-color)` | Any color, eg: `rgb(28, 128, 199)` or `blue` | The color of the icon (if `color_type: icon`) or the background (if `color_type: card`) |
|
||||
| `styles` | string | optional | | See [styles](#styles) |
|
||||
| `spin` | boolean | `false` | `true` \| `false` | Should the icon spin for this state? |
|
||||
| `entity_picture` | string | optional | Can be any of `/local/*` file or a URL | Will override the icon/the default entity_picture with your own image for this state. Best is to use a square image |
|
||||
| `entity_picture_template` | string | optional | | See [templates](#templates). Any javascript code which returns a path to a file or a url as a string. Overrides `entity_picture` |
|
||||
| `label` | string | optional | Any string that you want | Display a label below the card. See [Layouts](#layout) for more information. |
|
||||
| `label_template` | string | optional | | See [templates](#templates). Any javascript code which returns a string. Overrides `label` |
|
||||
|
||||
|
@ -167,7 +177,7 @@ Multiple values are possible, see the image below for examples:
|
|||
|
||||
### Templates
|
||||
|
||||
`label_template` supports templating as well as `value` for `state` when `operator: template`
|
||||
`label_template`, `name_template`, `entity_picture_template` supports templating as well as `value` for `state` when `operator: template`
|
||||
* `label_template`: It will be interpreted as javascript code and the code should return a string.
|
||||
`label_template` supports inline HTML, so you can do stuff like:
|
||||
```yaml
|
||||
|
@ -180,6 +190,8 @@ Multiple values are possible, see the image below for examples:
|
|||
+ (states['binary_sensor.status'].state === 'on' ? 'connected' : 'disconnected')
|
||||
```
|
||||
![label-template-example](examples/label_template.png)
|
||||
* `name_template`: It will be interpreted as javascript code and the code should return a string.
|
||||
* `entity_picture_template`: It will be interpreted as javascript code and the code should return a path to a file or a url as a string.
|
||||
* `value` for `state` when `operator: template`: It will be interpreted as javascript code and the code should return a boolean (`true` or `false`)
|
||||
|
||||
Inside the javascript code, you'll have access to those variables:
|
||||
|
@ -356,8 +368,12 @@ Some examples:
|
|||
```
|
||||
|
||||
### Configuration Templates
|
||||
|
||||
#### General
|
||||
|
||||
* Define your config template in the main lovelace configuration and then use it in your button-card. This will avoid a lot of repetitions! It's basically YAML anchors, but without using YAML anchors and is very useful if you split your config in multiple files 😄
|
||||
* You can overload any parameter with a new one, **appart from the states**. The state arrays in templates will be concatenated together with your config, meaning you can **add** new states but not change/delete states. Your main config states will be appended to the ones in the template.
|
||||
* You can overload any parameter with a new one
|
||||
* You can merge states together **by `id`** when using templates. The states you want to merge have to have the same `id`. This `id` parameter is new and can be anything (string, number, ...). States without `id` will be appended to the state array. Styles embedded in a state are merged together as usual. See [here](#merging-state-by-id) for an example.
|
||||
* You can also inherit another template from within a template.
|
||||
|
||||
In `ui-lovelace.yaml` (or in another file using `!import`)
|
||||
|
@ -396,6 +412,67 @@ And then where you use button-card, you can apply this template, and/or overload
|
|||
name: My Test Header
|
||||
```
|
||||
|
||||
#### Merging state by id
|
||||
|
||||
Example to merge state by `id`:
|
||||
```yaml
|
||||
button_card_templates:
|
||||
sensor:
|
||||
styles:
|
||||
card:
|
||||
- font-size: 16px
|
||||
- width: 75px
|
||||
tap_action:
|
||||
action: more-info
|
||||
state:
|
||||
- color: orange
|
||||
value: 75
|
||||
id: my_id
|
||||
|
||||
sensor_humidity:
|
||||
template: sensor
|
||||
icon: 'mdi:weather-rainy'
|
||||
state:
|
||||
- color: 'rgb(255,0,0)'
|
||||
operator: '>'
|
||||
value: 50
|
||||
- color: 'rgb(0,0,255)'
|
||||
operator: '<'
|
||||
value: 25
|
||||
|
||||
sensor_test:
|
||||
template: sensor_humidity
|
||||
state:
|
||||
- color: pink
|
||||
id: my_id
|
||||
operator: '>'
|
||||
value: 75
|
||||
styles:
|
||||
name:
|
||||
- color: '#ff0000'
|
||||
############### Used like this ##############
|
||||
- type: custom:button-card
|
||||
template: sensor_test
|
||||
entity: input_number.test
|
||||
show_entity_picture: true
|
||||
```
|
||||
Will result in this state object for your button (styles, operator and color are overridden for the `id: my_id` as you can see):
|
||||
```yaml
|
||||
state:
|
||||
- color: pink
|
||||
operator: '>'
|
||||
value: 75
|
||||
styles:
|
||||
name:
|
||||
- color: '#ff0000'
|
||||
- color: 'rgb(255,0,0)'
|
||||
operator: '>'
|
||||
value: 50
|
||||
- color: 'rgb(0,0,255)'
|
||||
operator: '<'
|
||||
value: 25
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Manual Installation
|
||||
|
@ -903,6 +980,51 @@ Example with `template`:
|
|||
entity: switch.test
|
||||
```
|
||||
|
||||
### Aspect Ratio
|
||||
|
||||
![aspect-ratio-image](examples/aspect_ratio.png)
|
||||
|
||||
```yaml
|
||||
- type: vertical-stack
|
||||
cards:
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: custom:button-card
|
||||
name: 1/1
|
||||
icon: mdi:lightbulb
|
||||
aspect_ratio: 1/1
|
||||
- type: custom:button-card
|
||||
name: 2/1
|
||||
icon: mdi:lightbulb
|
||||
aspect_ratio: 2/1
|
||||
- type: custom:button-card
|
||||
name: 3/1
|
||||
icon: mdi:lightbulb
|
||||
aspect_ratio: 3/1
|
||||
- type: custom:button-card
|
||||
name: 4/1
|
||||
icon: mdi:lightbulb
|
||||
aspect_ratio: 4/1
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: custom:button-card
|
||||
name: 1/1.2
|
||||
icon: mdi:lightbulb
|
||||
aspect_ratio: 1/1.2
|
||||
- type: custom:button-card
|
||||
name: 1/1.3
|
||||
icon: mdi:lightbulb
|
||||
aspect_ratio: 1/1.3
|
||||
- type: custom:button-card
|
||||
name: 1/1.4
|
||||
icon: mdi:lightbulb
|
||||
aspect_ratio: 1/1.4
|
||||
- type: custom:button-card
|
||||
name: 1/1.5
|
||||
icon: mdi:lightbulb
|
||||
aspect_ratio: 1/1.5
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
- [ciotlosm](https://github.com/ciotlosm) for the readme template and the awesome examples
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "Pre-Commit hooks running..."
|
||||
|
||||
npm run build
|
||||
git add dist/button-card.js
|
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
|
@ -4,13 +4,12 @@
|
|||
"description": "Button card for lovelace",
|
||||
"main": "dist/button-card.js",
|
||||
"pre-commit": [
|
||||
"build",
|
||||
"commit_dist"
|
||||
"pre-commit"
|
||||
],
|
||||
"scripts": {
|
||||
"pre-commit": "./hooks/pre-commit.sh",
|
||||
"build": "npm run rollup && npm run babel",
|
||||
"rollup": "rollup -c",
|
||||
"commit_dist": "git add dist/button-card.js",
|
||||
"babel": "babel dist/button-card.js --out-file dist/button-card.js",
|
||||
"lint": "eslint src/button-card.ts",
|
||||
"watch": "rollup -c rollup.debug.config.js --watch"
|
||||
|
@ -33,30 +32,31 @@
|
|||
},
|
||||
"homepage": "https://github.com/custom-cards/button-card#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.3",
|
||||
"@babel/plugin-proposal-class-properties": "^7.4.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^1.7.0",
|
||||
"@typescript-eslint/parser": "^1.7.0",
|
||||
"@babel/core": "^7.4.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.4.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.4",
|
||||
"@typescript-eslint/eslint-plugin": "^1.9.0",
|
||||
"@typescript-eslint/parser": "^1.9.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-airbnb-base": "^13.1.0",
|
||||
"eslint-plugin-import": "^2.17.2",
|
||||
"npm": "^6.9.0",
|
||||
"pre-commit": "^1.2.2",
|
||||
"prettier": "^1.17.0",
|
||||
"rollup": "^1.10.1",
|
||||
"prettier": "^1.17.1",
|
||||
"rollup": "^1.12.2",
|
||||
"rollup-plugin-babel": "^4.3.2",
|
||||
"rollup-plugin-json": "^4.0.0",
|
||||
"rollup-plugin-node-resolve": "^4.2.3",
|
||||
"rollup-plugin-node-resolve": "^4.2.4",
|
||||
"rollup-plugin-terser": "^4.0.4",
|
||||
"rollup-plugin-typescript2": "^0.20.1",
|
||||
"ts-lit-plugin": "^1.0.5",
|
||||
"ts-lit-plugin": "^1.0.6",
|
||||
"typescript": "^3.4.4",
|
||||
"typescript-styled-plugin": "^0.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^2.4.0",
|
||||
"custom-card-helpers": "^1.2.1",
|
||||
"home-assistant-js-websocket": "^3.4.0",
|
||||
"lit-element": "^2.1.0",
|
||||
"lit-html": "^1.0.0"
|
||||
|
|
|
@ -10,31 +10,36 @@ import {
|
|||
import { styleMap, StyleInfo } from 'lit-html/directives/style-map';
|
||||
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import { classMap, ClassInfo } from 'lit-html/directives/class-map.js';
|
||||
import {
|
||||
HassEntity,
|
||||
} from 'home-assistant-js-websocket';
|
||||
import domainIcon from './domain_icons';
|
||||
import {
|
||||
ButtonCardConfig,
|
||||
HomeAssistant,
|
||||
StateConfig,
|
||||
CssStyleConfig,
|
||||
} from './types';
|
||||
import {
|
||||
domainIcon,
|
||||
HomeAssistant,
|
||||
handleClick,
|
||||
getLovelace,
|
||||
// Still not working...
|
||||
// longPress,
|
||||
} from 'custom-card-helpers';
|
||||
import { longPress } from './long-press';
|
||||
import {
|
||||
computeDomain,
|
||||
computeEntity,
|
||||
getFontColorBasedOnBackgroundColor,
|
||||
buildNameStateConcat,
|
||||
applyBrightnessToColor,
|
||||
hasConfigOrEntityChanged,
|
||||
myHasConfigOrEntityChanged,
|
||||
getLightColorBasedOnTemperature,
|
||||
getLovelace,
|
||||
mergeDeep,
|
||||
mergeStatesById,
|
||||
} from './helpers';
|
||||
import { handleClick } from './handle-click';
|
||||
import { longPress } from './long-press';
|
||||
import { styles } from './styles';
|
||||
import computeStateDisplay from './compute_state_display';
|
||||
import myComputeStateDisplay from './compute_state_display';
|
||||
|
||||
@customElement('button-card')
|
||||
class ButtonCard extends LitElement {
|
||||
|
@ -64,7 +69,7 @@ class ButtonCard extends LitElement {
|
|||
|| this.config!.state
|
||||
&& this.config!.state.find(elt => elt.operator === 'template')
|
||||
? true : false;
|
||||
return hasConfigOrEntityChanged(this, changedProps, forceUpdate);
|
||||
return myHasConfigOrEntityChanged(this, changedProps, forceUpdate);
|
||||
}
|
||||
|
||||
private _getMatchingConfigState(state: HassEntity | undefined): StateConfig | undefined {
|
||||
|
@ -100,9 +105,7 @@ class ButtonCard extends LitElement {
|
|||
return matches;
|
||||
}
|
||||
case 'template': {
|
||||
return new Function('states', 'entity', 'user', 'hass',
|
||||
`'use strict'; ${elt.value}`)
|
||||
.call(this, this.hass!.states, state, this.hass!.user, this.hass);
|
||||
return this._evalTemplate(state, elt.value);
|
||||
}
|
||||
case 'default':
|
||||
def = elt;
|
||||
|
@ -120,6 +123,12 @@ class ButtonCard extends LitElement {
|
|||
return retval;
|
||||
}
|
||||
|
||||
private _evalTemplate(state: HassEntity | undefined, func: any): any {
|
||||
return new Function('states', 'entity', 'user', 'hass',
|
||||
`'use strict'; ${func}`)
|
||||
.call(this, this.hass!.states, state, this.hass!.user, this.hass);
|
||||
}
|
||||
|
||||
private _getDefaultColorForState(state: HassEntity): string {
|
||||
switch (state.state) {
|
||||
case 'on':
|
||||
|
@ -131,7 +140,10 @@ class ButtonCard extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
private _getColorForLightEntity(state: HassEntity | undefined): string {
|
||||
private _getColorForLightEntity(
|
||||
state: HassEntity | undefined,
|
||||
useTemperature: boolean,
|
||||
): string {
|
||||
let color: string = this.config!.default_color;
|
||||
if (state) {
|
||||
if (state.attributes.rgb_color) {
|
||||
|
@ -139,7 +151,8 @@ class ButtonCard extends LitElement {
|
|||
if (state.attributes.brightness) {
|
||||
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
|
||||
}
|
||||
} else if (state.attributes.color_temp
|
||||
} else if (useTemperature
|
||||
&& state.attributes.color_temp
|
||||
&& state.attributes.min_mireds
|
||||
&& state.attributes.max_mireds) {
|
||||
color = getLightColorBasedOnTemperature(
|
||||
|
@ -173,8 +186,8 @@ class ButtonCard extends LitElement {
|
|||
} else if (this.config!.color) {
|
||||
colorValue = this.config!.color;
|
||||
}
|
||||
if (colorValue == 'auto') {
|
||||
color = this._getColorForLightEntity(state);
|
||||
if (colorValue == 'auto' || colorValue == 'auto-no-temperature') {
|
||||
color = this._getColorForLightEntity(state, colorValue !== 'auto-no-temperature');
|
||||
} else if (colorValue) {
|
||||
color = colorValue;
|
||||
} else if (state) {
|
||||
|
@ -212,15 +225,26 @@ class ButtonCard extends LitElement {
|
|||
return undefined;
|
||||
}
|
||||
let entityPicture: string | undefined;
|
||||
if (configState && configState.entity_picture) {
|
||||
entityPicture = configState.entity_picture;
|
||||
} else if (this.config!.entity_picture) {
|
||||
entityPicture = this.config!.entity_picture;
|
||||
let matchingEntityPictureTemplate: string | undefined;
|
||||
|
||||
if (configState && configState.entity_picture_template) {
|
||||
matchingEntityPictureTemplate = configState.entity_picture_template;
|
||||
} else {
|
||||
entityPicture = state && state.attributes && state.attributes.entity_picture
|
||||
? state.attributes.entity_picture : undefined;
|
||||
matchingEntityPictureTemplate = this.config!.entity_picture_template;
|
||||
}
|
||||
return entityPicture;
|
||||
if (!matchingEntityPictureTemplate) {
|
||||
if (configState && configState.entity_picture) {
|
||||
entityPicture = configState.entity_picture;
|
||||
} else if (this.config!.entity_picture) {
|
||||
entityPicture = this.config!.entity_picture;
|
||||
} else if (state) {
|
||||
entityPicture = state.attributes && state.attributes.entity_picture
|
||||
? state.attributes.entity_picture : undefined;
|
||||
}
|
||||
return entityPicture;
|
||||
}
|
||||
|
||||
return this._evalTemplate(state, matchingEntityPictureTemplate);
|
||||
}
|
||||
|
||||
private _buildStyleGeneric(
|
||||
|
@ -228,10 +252,10 @@ class ButtonCard extends LitElement {
|
|||
styleType: string,
|
||||
): StyleInfo {
|
||||
let style: StyleInfo = {};
|
||||
if (this.config!.styles[styleType]) {
|
||||
if (this.config!.styles && this.config!.styles[styleType]) {
|
||||
style = Object.assign(style, ...this.config!.styles[styleType]);
|
||||
}
|
||||
if (configState && configState.styles[styleType]) {
|
||||
if (configState && configState.styles && configState.styles[styleType]) {
|
||||
let configStateStyle: StyleInfo = {};
|
||||
configStateStyle = Object.assign(configStateStyle, ...configState.styles[styleType]);
|
||||
style = {
|
||||
|
@ -249,21 +273,32 @@ class ButtonCard extends LitElement {
|
|||
return undefined;
|
||||
}
|
||||
let name: string | undefined;
|
||||
if (configState && configState.name) {
|
||||
name = configState.name;
|
||||
} else if (this.config!.name) {
|
||||
name = this.config!.name;
|
||||
} else if (state) {
|
||||
name = state.attributes && state.attributes.friendly_name
|
||||
? state.attributes.friendly_name : computeEntity(state.entity_id);
|
||||
let matchingNameTemplate: string | undefined;
|
||||
|
||||
if (configState && configState.name_template) {
|
||||
matchingNameTemplate = configState.name_template;
|
||||
} else {
|
||||
matchingNameTemplate = this.config!.name_template;
|
||||
}
|
||||
return name;
|
||||
if (!matchingNameTemplate) {
|
||||
if (configState && configState.name) {
|
||||
name = configState.name;
|
||||
} else if (this.config!.name) {
|
||||
name = this.config!.name;
|
||||
} else if (state) {
|
||||
name = state.attributes && state.attributes.friendly_name
|
||||
? state.attributes.friendly_name : computeEntity(state.entity_id);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
return this._evalTemplate(state, matchingNameTemplate);
|
||||
}
|
||||
|
||||
private _buildStateString(state: HassEntity | undefined): string | undefined {
|
||||
let stateString: string | undefined;
|
||||
if (this.config!.show_state && state && state.state) {
|
||||
const localizedState = computeStateDisplay(this.hass!.localize, state);
|
||||
const localizedState = myComputeStateDisplay(this.hass!.localize, state);
|
||||
const units = this._buildUnits(state);
|
||||
if (units) {
|
||||
stateString = `${state.state} ${units}`;
|
||||
|
@ -292,7 +327,15 @@ class ButtonCard extends LitElement {
|
|||
state: HassEntity | undefined,
|
||||
style: StyleInfo,
|
||||
): TemplateResult | undefined {
|
||||
return this.config!.show_last_changed && state ? html`<ha-relative-time id="label" class="ellipsis" .hass="${this.hass}" .datetime="${state.last_changed}" style=${styleMap(style)}></ha-relative-time>` : undefined;
|
||||
return this.config!.show_last_changed && state ?
|
||||
html`
|
||||
<ha-relative-time
|
||||
id="label"
|
||||
class="ellipsis"
|
||||
.hass="${this.hass}"
|
||||
.datetime="${state.last_changed}"
|
||||
style=${styleMap(style)}
|
||||
></ha-relative-time>` : undefined;
|
||||
}
|
||||
|
||||
private _buildLabel(
|
||||
|
@ -319,16 +362,24 @@ class ButtonCard extends LitElement {
|
|||
return label;
|
||||
}
|
||||
|
||||
/* eslint no-new-func: 0 */
|
||||
return new Function('states', 'entity', 'user', 'hass',
|
||||
`'use strict'; ${matchingLabelTemplate}`)
|
||||
.call(this, this.hass!.states, state, this.hass!.user, this.hass);
|
||||
return this._evalTemplate(state, matchingLabelTemplate);
|
||||
}
|
||||
|
||||
private _isClickable(state: HassEntity | undefined): boolean {
|
||||
let clickable = true;
|
||||
if (this.config!.tap_action!.action === 'toggle' && this.config!.hold_action!.action === 'none'
|
||||
|| this.config!.hold_action!.action === 'toggle' && this.config!.tap_action!.action === 'none') {
|
||||
if (
|
||||
this.config!.tap_action!.action === 'toggle'
|
||||
&& this.config!.hold_action!.action === 'none'
|
||||
&& this.config!.dbltap_action!.action === 'none'
|
||||
|
||||
|| this.config!.hold_action!.action === 'toggle'
|
||||
&& this.config!.tap_action!.action === 'none'
|
||||
&& this.config!.dbltap_action!.action === 'none'
|
||||
|
||||
|| this.config!.dbltap_action!.action === 'toggle'
|
||||
&& this.config!.tap_action!.action === 'none'
|
||||
&& this.config!.hold_action!.action === 'none'
|
||||
) {
|
||||
if (state) {
|
||||
switch (computeDomain(state.entity_id)) {
|
||||
case 'sensor':
|
||||
|
@ -343,8 +394,11 @@ class ButtonCard extends LitElement {
|
|||
} else {
|
||||
clickable = false;
|
||||
}
|
||||
} else if (this.config!.tap_action!.action != 'none'
|
||||
|| this.config!.hold_action!.action != 'none') {
|
||||
} else if (
|
||||
this.config!.tap_action!.action != 'none'
|
||||
|| this.config!.hold_action!.action != 'none'
|
||||
|| this.config!.dbltap_action!.action != 'none'
|
||||
) {
|
||||
clickable = true;
|
||||
} else {
|
||||
clickable = false;
|
||||
|
@ -380,7 +434,10 @@ class ButtonCard extends LitElement {
|
|||
let lockStyle: StyleInfo = {};
|
||||
const lockStyleFromConfig = this._buildStyleGeneric(configState, 'lock');
|
||||
const configCardStyle = this._buildStyleGeneric(configState, 'card');
|
||||
|
||||
const classList: ClassInfo = {
|
||||
'button-card-main': true,
|
||||
disabled: !this._isClickable(state),
|
||||
}
|
||||
if (configCardStyle.width) {
|
||||
this.style.setProperty('flex', '0 0 auto');
|
||||
this.style.setProperty('max-width', 'fit-content');
|
||||
|
@ -402,11 +459,25 @@ class ButtonCard extends LitElement {
|
|||
cardStyle = configCardStyle;
|
||||
break;
|
||||
}
|
||||
this.style.setProperty('--button-card-light-color', this._getColorForLightEntity(state));
|
||||
if (this.config!.aspect_ratio) {
|
||||
cardStyle['--aspect-ratio'] = this.config!.aspect_ratio;
|
||||
cardStyle.padding = '0px';
|
||||
}
|
||||
this.style.setProperty('--button-card-light-color', this._getColorForLightEntity(state, true));
|
||||
lockStyle = { ...lockStyle, ...lockStyleFromConfig };
|
||||
|
||||
return html`
|
||||
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" @ha-dblclick=${this._handleDblTap} .hasDblClick=${this.config!.dbltap_action!.action !== 'none'} .repeat=${ifDefined(this.config!.hold_action!.repeat)} .longpress="${longPress()}" .config="${this.config}">
|
||||
<ha-card
|
||||
class=${classMap(classList)}
|
||||
style=${styleMap(cardStyle)}
|
||||
@ha-click="${this._handleTap}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
@ha-dblclick=${this._handleDblTap}
|
||||
.hasDblClick=${this.config!.dbltap_action!.action !== 'none'}
|
||||
.repeat=${ifDefined(this.config!.hold_action!.repeat)}
|
||||
.longpress=${longPress()}
|
||||
.config="${this.config}"
|
||||
>
|
||||
${this._getLock(lockStyle)}
|
||||
${this._buttonContent(state, configState, buttonColor)}
|
||||
${this.config!.lock ? '' : html`<mwc-ripple id="ripple"></mwc-ripple>`}
|
||||
|
@ -418,7 +489,7 @@ class ButtonCard extends LitElement {
|
|||
if (this.config!.lock) {
|
||||
return html`
|
||||
<div id="overlay" style=${styleMap(lockStyle)} @click=${this._handleLock} @touchstart=${this._handleLock}>
|
||||
<ha-icon id="lock" icon="mdi:lock-outline"></iron-icon>
|
||||
<ha-icon id="lock" icon="mdi:lock-outline"></ha-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
@ -487,13 +558,15 @@ class ButtonCard extends LitElement {
|
|||
const entityPictureStyleFromConfig = this._buildStyleGeneric(configState, 'entity_picture');
|
||||
const haIconStyleFromConfig = this._buildStyleGeneric(configState, 'icon');
|
||||
const imgCellStyleFromConfig = this._buildStyleGeneric(configState, 'img_cell');
|
||||
const haCardStyleFromConfig = this._buildStyleGeneric(configState, 'card');
|
||||
|
||||
const haIconStyle = {
|
||||
const haIconStyle: StyleInfo = {
|
||||
color,
|
||||
width: this.config!.size,
|
||||
position: !this.config!.aspect_ratio && !haCardStyleFromConfig.height ? 'relative' : 'absolute',
|
||||
...haIconStyleFromConfig,
|
||||
};
|
||||
const entityPictureStyle = {
|
||||
const entityPictureStyle: StyleInfo = {
|
||||
...haIconStyle,
|
||||
...entityPictureStyleFromConfig,
|
||||
};
|
||||
|
@ -520,10 +593,13 @@ class ButtonCard extends LitElement {
|
|||
const ll = getLovelace();
|
||||
let template: ButtonCardConfig = { ...config };
|
||||
let tplName: string | undefined = template.template;
|
||||
let mergedStateConfig: StateConfig[] | undefined = config.state;
|
||||
while (tplName && ll.config.button_card_templates && ll.config.button_card_templates[tplName]) {
|
||||
template = mergeDeep(ll.config.button_card_templates[tplName], template);
|
||||
mergedStateConfig = mergeStatesById((ll.config.button_card_templates[tplName] as ButtonCardConfig).state, mergedStateConfig);
|
||||
tplName = (ll.config.button_card_templates[tplName] as ButtonCardConfig).template;
|
||||
}
|
||||
template.state = mergedStateConfig;
|
||||
this.config = {
|
||||
tap_action: { action: 'toggle' },
|
||||
hold_action: { action: 'none' },
|
||||
|
@ -546,31 +622,6 @@ class ButtonCard extends LitElement {
|
|||
this.config!.color_off = 'var(--paper-item-icon-color)';
|
||||
}
|
||||
this.config!.color_on = 'var(--paper-item-icon-active-color)';
|
||||
|
||||
/* Temporary until we deprecate style and entity_picture_style config option */
|
||||
if (!this.config.styles) {
|
||||
this.config.styles = {};
|
||||
}
|
||||
if (this.config.style && !this.config.styles.card) {
|
||||
this.config.styles.card = this.config.style;
|
||||
}
|
||||
if (this.config.entity_picture_style && !this.config.styles.entity_picture) {
|
||||
this.config.styles.entity_picture = this.config.entity_picture_style;
|
||||
}
|
||||
if (this.config.state) {
|
||||
/* eslint no-param-reassign: ["error", { "props": false }] */
|
||||
this.config.state.forEach((s) => {
|
||||
if (!s.styles) {
|
||||
s.styles = {};
|
||||
}
|
||||
if (s.entity_picture_style && !s.styles.entity_picture) {
|
||||
s.styles.entity_picture = s.entity_picture_style;
|
||||
}
|
||||
if (s.style && !s.styles.card) {
|
||||
s.styles.card = s.style;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The height of your card. Home Assistant uses this to automatically
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
import { computeDomain } from './helpers';
|
||||
import { LocalizeFunc } from './types';
|
||||
import { LocalizeFunc } from 'custom-card-helpers';
|
||||
|
||||
export default (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
): string => {
|
||||
): string | undefined => {
|
||||
let display: string | undefined;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
||||
|
|
89
src/const.ts
89
src/const.ts
|
@ -1,89 +0,0 @@
|
|||
/** Constants to be used in the frontend. */
|
||||
|
||||
// Constants should be alphabetically sorted by name.
|
||||
// Arrays with values should be alphabetically sorted if order doesn't matter.
|
||||
// Each constant should have a description what it is supposed to be used for.
|
||||
|
||||
/** Icon to use when no icon specified for domain. */
|
||||
export const DEFAULT_DOMAIN_ICON = "hass:bookmark";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
export const DEFAULT_PANEL = "lovelace";
|
||||
|
||||
/** Domains that have a state card. */
|
||||
export const DOMAINS_WITH_CARD = [
|
||||
"climate",
|
||||
"cover",
|
||||
"configurator",
|
||||
"input_select",
|
||||
"input_number",
|
||||
"input_text",
|
||||
"lock",
|
||||
"media_player",
|
||||
"scene",
|
||||
"script",
|
||||
"timer",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
"weblink",
|
||||
];
|
||||
|
||||
/** Domains with separate more info dialog. */
|
||||
export const DOMAINS_WITH_MORE_INFO = [
|
||||
"alarm_control_panel",
|
||||
"automation",
|
||||
"camera",
|
||||
"climate",
|
||||
"configurator",
|
||||
"cover",
|
||||
"fan",
|
||||
"group",
|
||||
"history_graph",
|
||||
"input_datetime",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"script",
|
||||
"sun",
|
||||
"updater",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
"weather",
|
||||
];
|
||||
|
||||
/** Domains that show no more info dialog. */
|
||||
export const DOMAINS_HIDE_MORE_INFO = [
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_text",
|
||||
"scene",
|
||||
"weblink",
|
||||
];
|
||||
|
||||
/** Domains that should have the history hidden in the more info dialog. */
|
||||
export const DOMAINS_MORE_INFO_NO_HISTORY = [
|
||||
"camera",
|
||||
"configurator",
|
||||
"history_graph",
|
||||
"scene",
|
||||
];
|
||||
|
||||
/** States that we consider "off". */
|
||||
export const STATES_OFF = ["closed", "locked", "off"];
|
||||
|
||||
/** Domains where we allow toggle in Lovelace. */
|
||||
export const DOMAINS_TOGGLE = new Set([
|
||||
"fan",
|
||||
"input_boolean",
|
||||
"light",
|
||||
"switch",
|
||||
"group",
|
||||
"automation",
|
||||
]);
|
||||
|
||||
/** Temperature units. */
|
||||
export const UNIT_C = "°C";
|
||||
export const UNIT_F = "°F";
|
||||
|
||||
/** Entity ID of the default view. */
|
||||
export const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
|
|
@ -1,103 +0,0 @@
|
|||
/**
|
||||
* Return the icon to be used for a domain.
|
||||
*
|
||||
* Optionally pass in a state to influence the domain icon.
|
||||
*/
|
||||
import { DEFAULT_DOMAIN_ICON } from "./const";
|
||||
|
||||
const fixedIcons = {
|
||||
alert: "hass:alert",
|
||||
automation: "hass:playlist-play",
|
||||
calendar: "hass:calendar",
|
||||
camera: "hass:video",
|
||||
climate: "hass:thermostat",
|
||||
configurator: "hass:settings",
|
||||
conversation: "hass:text-to-speech",
|
||||
device_tracker: "hass:account",
|
||||
fan: "hass:fan",
|
||||
group: "hass:google-circles-communities",
|
||||
history_graph: "hass:chart-line",
|
||||
homeassistant: "hass:home-assistant",
|
||||
homekit: "hass:home-automation",
|
||||
image_processing: "hass:image-filter-frames",
|
||||
input_boolean: "hass:drawing",
|
||||
input_datetime: "hass:calendar-clock",
|
||||
input_number: "hass:ray-vertex",
|
||||
input_select: "hass:format-list-bulleted",
|
||||
input_text: "hass:textbox",
|
||||
light: "hass:lightbulb",
|
||||
mailbox: "hass:mailbox",
|
||||
notify: "hass:comment-alert",
|
||||
person: "hass:account",
|
||||
plant: "hass:flower",
|
||||
proximity: "hass:apple-safari",
|
||||
remote: "hass:remote",
|
||||
scene: "hass:google-pages",
|
||||
script: "hass:file-document",
|
||||
sensor: "hass:eye",
|
||||
simple_alarm: "hass:bell",
|
||||
sun: "hass:white-balance-sunny",
|
||||
switch: "hass:flash",
|
||||
timer: "hass:timer",
|
||||
updater: "hass:cloud-upload",
|
||||
vacuum: "hass:robot-vacuum",
|
||||
water_heater: "hass:thermometer",
|
||||
weblink: "hass:open-in-new",
|
||||
};
|
||||
|
||||
export default function domainIcon(domain: string, state?: string): string {
|
||||
if (domain in fixedIcons) {
|
||||
return fixedIcons[domain];
|
||||
}
|
||||
|
||||
switch (domain) {
|
||||
case "alarm_control_panel":
|
||||
switch (state) {
|
||||
case "armed_home":
|
||||
return "hass:bell-plus";
|
||||
case "armed_night":
|
||||
return "hass:bell-sleep";
|
||||
case "disarmed":
|
||||
return "hass:bell-outline";
|
||||
case "triggered":
|
||||
return "hass:bell-ring";
|
||||
default:
|
||||
return "hass:bell";
|
||||
}
|
||||
|
||||
case "binary_sensor":
|
||||
return state && state === "off"
|
||||
? "hass:radiobox-blank"
|
||||
: "hass:checkbox-marked-circle";
|
||||
|
||||
case "cover":
|
||||
return state === "closed" ? "hass:window-closed" : "hass:window-open";
|
||||
|
||||
case "lock":
|
||||
return state && state === "unlocked" ? "hass:lock-open" : "hass:lock";
|
||||
|
||||
case "media_player":
|
||||
return state && state !== "off" && state !== "idle"
|
||||
? "hass:cast-connected"
|
||||
: "hass:cast";
|
||||
|
||||
case "zwave":
|
||||
switch (state) {
|
||||
case "dead":
|
||||
return "hass:emoticon-dead";
|
||||
case "sleeping":
|
||||
return "hass:sleep";
|
||||
case "initializing":
|
||||
return "hass:timer-sand";
|
||||
default:
|
||||
return "hass:z-wave";
|
||||
}
|
||||
|
||||
default:
|
||||
// tslint:disable-next-line
|
||||
console.warn(
|
||||
"Unable to find icon for domain " + domain + " (" + state + ")"
|
||||
);
|
||||
return DEFAULT_DOMAIN_ICON;
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
// Polymer legacy event helpers used courtesy of the Polymer project.
|
||||
//
|
||||
// Copyright (c) 2017 The Polymer Authors. All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
declare global {
|
||||
// tslint:disable-next-line
|
||||
interface HASSDomEvents { }
|
||||
}
|
||||
|
||||
export type ValidHassDomEvent = keyof HASSDomEvents;
|
||||
|
||||
export interface HASSDomEvent<T> extends Event {
|
||||
detail: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a custom event with an optional detail value.
|
||||
*
|
||||
* @param {string} type Name of event type.
|
||||
* @param {*=} detail Detail value containing event-specific
|
||||
* payload.
|
||||
* @param {{ bubbles: (boolean|undefined),
|
||||
* cancelable: (boolean|undefined),
|
||||
* composed: (boolean|undefined) }=}
|
||||
* options Object specifying options. These may include:
|
||||
* `bubbles` (boolean, defaults to `true`),
|
||||
* `cancelable` (boolean, defaults to false), and
|
||||
* `node` on which to fire the event (HTMLElement, defaults to `this`).
|
||||
* @return {Event} The new event that was fired.
|
||||
*/
|
||||
export const fireEvent = <HassEvent extends ValidHassDomEvent>(
|
||||
node: HTMLElement | Window,
|
||||
type: HassEvent,
|
||||
detail?: HASSDomEvents[HassEvent],
|
||||
options?: {
|
||||
bubbles?: boolean;
|
||||
cancelable?: boolean;
|
||||
composed?: boolean;
|
||||
}
|
||||
): Event => {
|
||||
options = options || {};
|
||||
// @ts-ignore
|
||||
detail = detail === null || detail === undefined ? {} : detail;
|
||||
const event = new Event(type, {
|
||||
bubbles: options.bubbles === undefined ? true : options.bubbles,
|
||||
cancelable: Boolean(options.cancelable),
|
||||
composed: options.composed === undefined ? true : options.composed,
|
||||
});
|
||||
(event as any).detail = detail;
|
||||
node.dispatchEvent(event);
|
||||
return event;
|
||||
};
|
|
@ -1,74 +0,0 @@
|
|||
import { HomeAssistant, ActionConfig } from "./types";
|
||||
import { fireEvent } from "./fire_event";
|
||||
import { navigate } from "./navigate";
|
||||
import { toggleEntity } from "./toggle-entity";
|
||||
import { forwardHaptic } from "./haptic";
|
||||
|
||||
export const handleClick = (
|
||||
node: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
config: {
|
||||
entity?: string;
|
||||
camera_image?: string;
|
||||
hold_action?: ActionConfig;
|
||||
tap_action?: ActionConfig;
|
||||
dbltap_action?: ActionConfig;
|
||||
},
|
||||
hold: boolean,
|
||||
dblClick: boolean,
|
||||
): void => {
|
||||
let actionConfig: ActionConfig | undefined;
|
||||
|
||||
if (dblClick && config.dbltap_action) {
|
||||
actionConfig = config.dbltap_action;
|
||||
} else if (hold && config.hold_action) {
|
||||
actionConfig = config.hold_action;
|
||||
} else if (!hold && config.tap_action) {
|
||||
actionConfig = config.tap_action;
|
||||
}
|
||||
|
||||
if (!actionConfig) {
|
||||
actionConfig = {
|
||||
action: "toggle",
|
||||
};
|
||||
}
|
||||
|
||||
switch (actionConfig.action) {
|
||||
case "more-info":
|
||||
if (config.entity || config.camera_image) {
|
||||
fireEvent(node, "hass-more-info", {
|
||||
entityId: config.entity ? config.entity : config.camera_image!,
|
||||
});
|
||||
if (actionConfig.haptic) forwardHaptic(node, actionConfig.haptic);
|
||||
}
|
||||
break;
|
||||
case "navigate":
|
||||
if (actionConfig.navigation_path) {
|
||||
navigate(node, actionConfig.navigation_path);
|
||||
if (actionConfig.haptic) forwardHaptic(node, actionConfig.haptic);
|
||||
}
|
||||
break;
|
||||
case 'url':
|
||||
actionConfig.url && window.open(actionConfig.url);
|
||||
if (actionConfig.haptic) forwardHaptic(node, actionConfig.haptic);
|
||||
break;
|
||||
case "toggle":
|
||||
if (config.entity) {
|
||||
toggleEntity(hass, config.entity!);
|
||||
if (actionConfig.haptic) forwardHaptic(node, actionConfig.haptic);
|
||||
}
|
||||
break;
|
||||
case "call-service": {
|
||||
if (!actionConfig.service) {
|
||||
return;
|
||||
}
|
||||
const [domain, service] = actionConfig.service.split(".", 2);
|
||||
const localActionConfig = { ...actionConfig };
|
||||
if (actionConfig.service_data && actionConfig.service_data.entity_id === 'entity') {
|
||||
localActionConfig.service_data!.entity_id = config.entity;
|
||||
}
|
||||
hass.callService(domain, service, localActionConfig.service_data);
|
||||
if (actionConfig.haptic) forwardHaptic(node, actionConfig.haptic);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
|
||||
/**
|
||||
* Utility function that enables haptic feedback
|
||||
*/
|
||||
|
||||
import { fireEvent } from "./fire_event";
|
||||
|
||||
// Allowed types are from iOS HIG.
|
||||
// https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/feedback/#haptics
|
||||
// Implementors on platforms other than iOS should attempt to match the patterns (shown in HIG) as closely as possible.
|
||||
export type HapticType =
|
||||
| "success"
|
||||
| "warning"
|
||||
| "failure"
|
||||
| "light"
|
||||
| "medium"
|
||||
| "heavy"
|
||||
| "selection";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
haptic: HapticType;
|
||||
}
|
||||
}
|
||||
|
||||
export const forwardHaptic = (el: HTMLElement, hapticType: HapticType) => {
|
||||
fireEvent(el, "haptic", hapticType);
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import { PropertyValues } from 'lit-element';
|
||||
import tinycolor, { TinyColor } from '@ctrl/tinycolor';
|
||||
import { HomeAssistant } from './types';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { StateConfig } from './types';
|
||||
|
||||
export function computeDomain(entityId: string): string {
|
||||
return entityId.substr(0, entityId.indexOf('.'));
|
||||
|
@ -74,7 +75,7 @@ export function applyBrightnessToColor(
|
|||
}
|
||||
|
||||
// Check if config or Entity changed
|
||||
export function hasConfigOrEntityChanged(
|
||||
export function myHasConfigOrEntityChanged(
|
||||
element: any,
|
||||
changedProps: PropertyValues,
|
||||
forceUpdate: Boolean,
|
||||
|
@ -97,24 +98,6 @@ export function hasConfigOrEntityChanged(
|
|||
}
|
||||
}
|
||||
|
||||
export function getLovelace() {
|
||||
let root: any = document.querySelector('home-assistant');
|
||||
root = root && root.shadowRoot;
|
||||
root = root && root.querySelector('home-assistant-main');
|
||||
root = root && root.shadowRoot;
|
||||
root = root && root.querySelector('app-drawer-layout partial-panel-resolver');
|
||||
root = root && root.shadowRoot || root;
|
||||
root = root && root.querySelector('ha-panel-lovelace');
|
||||
root = root && root.shadowRoot;
|
||||
root = root && root.querySelector('hui-root');
|
||||
if (root) {
|
||||
const ll = root.lovelace;
|
||||
ll.current_view = root.___curView;
|
||||
return ll;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a deep merge of objects and returns new object. Does not modify
|
||||
* objects (immutable) and merges arrays via concatenation and filtering.
|
||||
|
@ -143,3 +126,26 @@ export function mergeDeep(...objects: any): any {
|
|||
return prev;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function mergeStatesById(
|
||||
intoStates: StateConfig[] | undefined,
|
||||
fromStates: StateConfig[] | undefined,
|
||||
): StateConfig[] {
|
||||
let resultStateConfigs: StateConfig[] = [];
|
||||
if (intoStates) {
|
||||
intoStates.forEach((intoState) => {
|
||||
let localState = intoState;
|
||||
if (fromStates) {
|
||||
fromStates.forEach((fromState) => {
|
||||
if (fromState.id && intoState.id && fromState.id == intoState.id)
|
||||
localState = mergeDeep(localState, fromState);
|
||||
})
|
||||
}
|
||||
resultStateConfigs.push(localState);
|
||||
});
|
||||
}
|
||||
if (fromStates) {
|
||||
resultStateConfigs = resultStateConfigs.concat(fromStates.filter(x => !intoStates ? true : !intoStates.find(y => y.id && x.id ? y.id == x.id : false)));
|
||||
}
|
||||
return resultStateConfigs;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import { fireEvent } from "./fire_event";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"location-changed": {
|
||||
replace: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const navigate = (
|
||||
_node: any,
|
||||
path: string,
|
||||
replace: boolean = false
|
||||
) => {
|
||||
if (replace) {
|
||||
history.replaceState(null, "", path);
|
||||
} else {
|
||||
history.pushState(null, "", path);
|
||||
}
|
||||
fireEvent(window, "location-changed", {
|
||||
replace
|
||||
});
|
||||
};
|
|
@ -107,25 +107,37 @@ export const styles = css`
|
|||
|
||||
#container {
|
||||
display: grid;
|
||||
max-height: 100%;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
#img-cell {
|
||||
/* display: flex; */
|
||||
display: flex;
|
||||
grid-area: i;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ha-icon#icon, img#icon {
|
||||
ha-icon#icon {
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
img#icon {
|
||||
display: block;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
#name {
|
||||
grid-area: n;
|
||||
|
@ -149,9 +161,6 @@ export const styles = css`
|
|||
justify-self: center;
|
||||
}
|
||||
|
||||
#container {
|
||||
width: 100%;
|
||||
}
|
||||
#container.vertical {
|
||||
grid-template-areas: "i" "n" "s" "l";
|
||||
grid-template-columns: 1fr;
|
||||
|
@ -348,6 +357,30 @@ export const styles = css`
|
|||
grid-template-columns: 40% 1fr;
|
||||
grid-template-rows: 1fr min-content min-content;
|
||||
}
|
||||
|
||||
[style*="--aspect-ratio"] > :first-child {
|
||||
width: 100%;
|
||||
}
|
||||
[style*="--aspect-ratio"] > img {
|
||||
height: auto;
|
||||
}
|
||||
@supports (--custom:property) {
|
||||
[style*="--aspect-ratio"] {
|
||||
position: relative;
|
||||
padding: 0px;
|
||||
}
|
||||
[style*="--aspect-ratio"]::before {
|
||||
content: "";
|
||||
display: block;
|
||||
padding-bottom: calc(100% / (var(--aspect-ratio)));
|
||||
}
|
||||
[style*="--aspect-ratio"] > :first-child {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default styles;
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import { STATES_OFF } from "./const";
|
||||
import { turnOnOffEntity } from "./turn-on-off-entity";
|
||||
import { HomeAssistant } from "./types";
|
||||
export const toggleEntity = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
): Promise<void> => {
|
||||
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
|
||||
return turnOnOffEntity(hass, entityId, turnOn);
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
import * as helpers from "./helpers";
|
||||
import { HomeAssistant } from "./types";
|
||||
|
||||
export const turnOnOffEntity = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
turnOn = true
|
||||
): Promise<void> => {
|
||||
const stateDomain = helpers.computeDomain(entityId);
|
||||
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
|
||||
|
||||
let service: string;
|
||||
switch (stateDomain) {
|
||||
case "lock":
|
||||
service = turnOn ? "unlock" : "lock";
|
||||
break;
|
||||
case "cover":
|
||||
service = turnOn ? "open_cover" : "close_cover";
|
||||
break;
|
||||
default:
|
||||
service = turnOn ? "turn_on" : "turn_off";
|
||||
}
|
||||
|
||||
return hass.callService(serviceDomain, service, { entity_id: entityId });
|
||||
};
|
202
src/types.ts
202
src/types.ts
|
@ -1,22 +1,16 @@
|
|||
import {
|
||||
HassEntities,
|
||||
HassConfig,
|
||||
Auth,
|
||||
Connection,
|
||||
MessageBase,
|
||||
HassServices,
|
||||
} from 'home-assistant-js-websocket';
|
||||
import { HapticType } from './haptic';
|
||||
import { ActionConfig } from 'custom-card-helpers';
|
||||
|
||||
export interface ButtonCardConfig {
|
||||
template?: string;
|
||||
type: string;
|
||||
entity?: string;
|
||||
name?: string;
|
||||
name_template?: string;
|
||||
icon?: string;
|
||||
color_type: 'icon' | 'card' | 'label-card' | 'blank-card'
|
||||
color?: string;
|
||||
color?: 'auto' | 'auto-no-temperature' | string;
|
||||
size: string;
|
||||
aspect_ratio?: string?
|
||||
lock: boolean;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
|
@ -31,10 +25,10 @@ export interface ButtonCardConfig {
|
|||
label?: string;
|
||||
label_template?: string;
|
||||
entity_picture?: string;
|
||||
entity_picture_template?: string;
|
||||
units?: string;
|
||||
style?: CssStyleConfig[];
|
||||
state?: StateConfig[];
|
||||
styles: StylesConfig;
|
||||
styles?: StylesConfig;
|
||||
confirmation?: string;
|
||||
layout: Layout;
|
||||
entity_picture_style?: CssStyleConfig[];
|
||||
|
@ -54,15 +48,17 @@ export type Layout = 'vertical'
|
|||
| 'icon_label';
|
||||
|
||||
export interface StateConfig {
|
||||
id?: string;
|
||||
operator?: '<' | '<=' | '==' | '>=' | '>' | '!=' | 'regex' | 'template' | 'default';
|
||||
value?: any;
|
||||
name?: string;
|
||||
name_template?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
style?: CssStyleConfig[];
|
||||
color?: 'auto' | 'auto-no-temperature' | string;
|
||||
entity_picture_style?: CssStyleConfig[];
|
||||
entity_picture?: string;
|
||||
styles: StylesConfig;
|
||||
entity_picture_template?: string;
|
||||
styles?: StylesConfig;
|
||||
spin?: boolean;
|
||||
label?: string;
|
||||
label_template?: string;
|
||||
|
@ -83,179 +79,3 @@ export interface StylesConfig {
|
|||
export interface CssStyleConfig {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ToggleActionConfig {
|
||||
action: 'toggle';
|
||||
repeat?: number | undefined;
|
||||
haptic?: HapticType;
|
||||
}
|
||||
|
||||
export interface CallServiceActionConfig {
|
||||
action: 'call-service';
|
||||
haptic?: HapticType;
|
||||
repeat?: number | undefined;
|
||||
service: string;
|
||||
service_data?: {
|
||||
entity_id?: string | [string];
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NavigateActionConfig {
|
||||
action: 'navigate';
|
||||
haptic?: HapticType;
|
||||
repeat?: number | undefined;
|
||||
navigation_path: string;
|
||||
}
|
||||
|
||||
export interface MoreInfoActionConfig {
|
||||
action: 'more-info';
|
||||
repeat?: number | undefined;
|
||||
haptic?: HapticType;
|
||||
}
|
||||
|
||||
export interface NoActionConfig {
|
||||
action: 'none';
|
||||
repeat?: number | undefined;
|
||||
}
|
||||
|
||||
export interface UrlActionConfig {
|
||||
action: 'url';
|
||||
haptic?: HapticType;
|
||||
repeat?: number | undefined;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type ActionConfig =
|
||||
| ToggleActionConfig
|
||||
| CallServiceActionConfig
|
||||
| NavigateActionConfig
|
||||
| UrlActionConfig
|
||||
| MoreInfoActionConfig
|
||||
| NoActionConfig;
|
||||
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
'value-changed': {
|
||||
value: unknown;
|
||||
};
|
||||
'config-changed': {
|
||||
config: ButtonCardConfig;
|
||||
};
|
||||
'hass-more-info': {
|
||||
entityId: string | null;
|
||||
};
|
||||
'll-rebuild': {};
|
||||
undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export type ValidHassDomEvent = keyof HASSDomEvents;
|
||||
|
||||
export type LocalizeFunc = (key: string, ...args: any[]) => string;
|
||||
|
||||
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;
|
||||
name: string;
|
||||
credentials: Credential[];
|
||||
mfa_modules: MFAModule[];
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
// Incomplete
|
||||
'primary-color': string;
|
||||
'text-primary-color': string;
|
||||
'accent-color': string;
|
||||
}
|
||||
|
||||
export interface Themes {
|
||||
default_theme: string;
|
||||
themes: { [key: string]: Theme };
|
||||
}
|
||||
|
||||
export interface Panel {
|
||||
component_name: string;
|
||||
config: { [key: string]: any } | null;
|
||||
icon: string | null;
|
||||
title: string | null;
|
||||
url_path: string;
|
||||
}
|
||||
|
||||
export interface Panels {
|
||||
[name: string]: Panel;
|
||||
}
|
||||
|
||||
export interface Resources {
|
||||
[language: string]: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface Translation {
|
||||
nativeName: string;
|
||||
isRTL: boolean;
|
||||
fingerprints: { [fragment: string]: string };
|
||||
}
|
||||
|
||||
export interface HomeAssistant {
|
||||
auth: Auth;
|
||||
connection: Connection;
|
||||
connected: boolean;
|
||||
states: HassEntities;
|
||||
services: HassServices;
|
||||
config: HassConfig;
|
||||
themes: Themes;
|
||||
selectedTheme?: string | null;
|
||||
panels: Panels;
|
||||
panelUrl: string;
|
||||
|
||||
// i18n
|
||||
// current effective language, in that order:
|
||||
// - backend saved user selected lanugage
|
||||
// - language in local appstorage
|
||||
// - browser language
|
||||
// - english (en)
|
||||
language: string;
|
||||
// local stored language, keep that name for backward compability
|
||||
selectedLanguage: string;
|
||||
resources: Resources;
|
||||
localize: LocalizeFunc;
|
||||
translationMetadata: {
|
||||
fragments: string[];
|
||||
translations: {
|
||||
[lang: string]: Translation;
|
||||
};
|
||||
};
|
||||
|
||||
dockedSidebar: boolean;
|
||||
moreInfoEntityId: string;
|
||||
user: CurrentUser;
|
||||
callService: (
|
||||
domain: string,
|
||||
service: string,
|
||||
serviceData?: { [key: string]: any }
|
||||
) => Promise<void>;
|
||||
callApi: <T>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
path: string,
|
||||
parameters?: { [key: string]: any }
|
||||
) => Promise<T>;
|
||||
fetchWithAuth: (
|
||||
path: string,
|
||||
init?: { [key: string]: any }
|
||||
) => Promise<Response>;
|
||||
sendWS: (msg: MessageBase) => Promise<void>;
|
||||
callWS: <T>(msg: MessageBase) => Promise<T>;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue