* _template deprecation and custom fields support (#199)

* Update custom-card-helpers

* _template deprecated and custom_fields support

* Update dependencies

* Add npm audit fix to circleci

* Styles support templating

* *_action support templates

* Some linting

* Fixes #200

* Fixing template eval if input is a number

* State values support templating

* Documentation update

* Doc formating issue

* Bump major version
This commit is contained in:
Jérôme W 2019-07-26 21:27:18 +02:00 committed by GitHub
parent 9ef93a2a35
commit a9f27a64a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1522 additions and 954 deletions

View File

@ -17,9 +17,7 @@ jobs:
- restore_cache:
keys:
- deps-{{ .Environment.CACHE_VERSION }}-{{ checksum "package-lock.json" }}
- deps-{{ .Environment.CACHE_VERSION }}-
- run: rm -rf ~/repo/node_modules/custom-card-helpers/.git
- run: npm install
- run: npm install && npm audit fix
- save_cache:
paths:

331
README.md
View File

@ -27,6 +27,7 @@ Lovelace Button card for your entities.
- [Easy styling options](#easy-styling-options)
- [Light entity color variable](#light-entity-color-variable)
- [ADVANCED styling options](#advanced-styling-options)
- [Custom Fields](#custom-fields)
- [Configuration Templates](#configuration-templates)
- [General](#general)
- [Merging state by id](#merging-state-by-id)
@ -78,85 +79,83 @@ Lovelace Button card for your entities.
### Main Options
| Name | Type | Default | Supported options | Description |
| Name | Type | Default | Supported options | Description |
| -------------- | ----------- | ------------ | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | string | **Required** | `custom:button-card` | Type of the card |
| `type` | string | **Required** | `custom:button-card` | Type of the card |
| `template` | string | optional | any valid template from `button_card_templates` | See [configuration template](#Configuration-Templates) |
| `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). |
| `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`. Supports templates, see [templates](#templates) |
| `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` \| `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 |
| `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. |
| `name` | string | optional | `Air conditioner` | Define an optional text to show below the icon |
| `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 |
| `name` | string | optional | `Air conditioner` | Define an optional text to show below the icon. Supports templates, see [templates](#templates) |
| `label` | string | optional | Any string that you want | Display a label below the card. See [Layouts](#layout) for more information. Supports templates, see [templates](#templates) |
| `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 |
| `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. |
| `show_label` | boolean | `false` | `true` \| `false` | Display or hide the `label`/`label_template` |
| `show_label` | boolean | `false` | `true` \| `false` | Display or hide the `label` |
| `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` |
| `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. Supports templates, see [templates](#templates) |
| `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 |
| `confirmation` | string | optional | Free-form text | Show a confirmation popup on tap with defined text |
| `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 |
| `confirmation` | string | optional | Free-form text | Show a confirmation popup on tap with defined text |
| `lock` | boolean | `false` | `true` \| `false` | See [lock](#lock). This will display a normal button with a lock symbol in the corner. Clicking the button will make the lock go away and enable the button to be manoeuvred for five seconds |
| `unlock_users` | string list | optional | A list of users | List of users allowed to unlock the button when `lock: true`. If not defined, everyone is allowed to unlock the button |
| `layout` | string | optional | See [Layout](#Layout) | The layout of the button can be modified using this option |
| `custom_fields` | object | optional | See [Custom Fields](#Custom-Fields) |
### Action
| Name | Type | Default | Supported options | Description |
All the fields support templates, see [templates](#templates).
| 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` | 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` |
| `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) |
| `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` |
| `service_data` | object | none | Any service data | Service data to include (e.g. `entity_id: media_player.bedroom`) when `action` defined as `call-service`. If your `service_data` requires an `entity_id`, you can use the keywork `entity`, this will actually call the service on the entity defined in the main configuration of this card. Useful for [configuration templates](#configuration-templates) |
| `haptic` | string | none | `success`, `warning`, `failure`, `light`, `medium`, `heavy`, `selection` | Haptic feedback for the [Beta IOS App](http://home-assistant.io/ios/beta) |
| `repeat` | number | none | eg: `500` | For a hold_action, you can optionally configure the action to repeat while the button is being held down (for example, to repeatedly increase the volume of a media player). Define the number of milliseconds between repeat actions here. |
### State
| Name | Type | Default | Supported options | Description |
| Name | Type | Default | Supported options | Description |
| ---------- | ------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `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` |
| `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. Supports templates, see [templates](#templates) |
| `icon` | string | optional | `mdi:battery` | The icon to display for this state. Defaults to the entity icon. Hide with `show_icon: false`. Supports templates, see [templates](#templates) |
| `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. Supports templates, see [templates](#templates) |
| `label` | string | optional | Any string that you want | Display a label below the card. See [Layouts](#layout) for more information. Supports templates, see [templates](#templates) |
### Available operators
The order of your elements in the `state` object matters. The first one which is `true` will match.
The `value` field for all operators except `regex` support templating, see [templates](#templates)
| Operator | `value` example | Description |
| Operator | `value` example | Description |
| :-------: | --------------- | -------------------------------------------------------------------------------------------------------- |
| `<` | `5` | Current state is inferior to `value` |
| `<=` | `4` | Current state is inferior or equal to `value` |
| `==` | `42` or `'on'` | **This is the default if no operator is specified.** Current state is equal (`==` javascript) to `value` |
| `>=` | `32` | Current state is superior or equal to `value` |
| `>` | `12` | Current state is superior to `value` |
| `!=` | `'normal'` | Current state is not equal (`!=` javascript) to `value` |
| `regex` | `'^norm.*$'` | `value` regex applied to current state does match |
| `<` | `5` | Current state is inferior to `value` |
| `<=` | `4` | Current state is inferior or equal to `value` |
| `==` | `42` or `'on'` | **This is the default if no operator is specified.** Current state is equal (`==` javascript) to `value` |
| `>=` | `32` | Current state is superior or equal to `value` |
| `>` | `12` | Current state is superior to `value` |
| `!=` | `'normal'` | Current state is not equal (`!=` javascript) to `value` |
| `regex` | `'^norm.*$'` | `value` regex applied to current state does match |
| `template` | | See [templates](#templates) for examples. `value` needs to be a javascript expression which returns a boolean. If the boolean is true, it will match this state |
| `default` | N/A | If nothing matches, this is used |
| `default` | N/A | If nothing matches, this is used |
### Layout
@ -178,22 +177,31 @@ Multiple values are possible, see the image below for examples:
### Templates
`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
label_template: >
return 'Connection: '
+ (states['switch.connection'].state === 'on'
? '<span style="color: #00FF00;">enabled</span>'
: '<span style="color: #FF0000;">disabled</span>')
+ ' / '
+ (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`)
The template rendering uses a special format. All the fields where template is supported also support plain text. To activate the templating feature for such a field, you'll need to enclose the javascript function inside 3 square brakets:
`[[[ javascript function here ]]]`
Don't forget to quote if it's on one line:
```yaml
name: '[[[ if (entity.state > 42) return "Above 42"; else return "Below 42" ]]]'
name: >
[[[
if (entity.state > 42)
return "Above 42;
else
return "Below 42;
]]]
```
Those are the configuration fields which support templating:
* `name` (Supports also HTML rendering): This needs to return a string
* `label` (Supports also HTML rendering): This needs to return a string
* `entity_picture`: This needs to return a path to a file or a url as a string.
* `icon`: This needs to return a string in the format `mdi:icon`
* All the styles in the style object: This needs to return a string
* All the value of the state object, appart when the operator is `regex`
* `operator: template`: The function for `value` needs to return a boolean
* Else: The function for `value` needs to return a string or a number
* All the `custom_fields` (Support also HTML rendering)
Inside the javascript code, you'll have access to those variables:
* `entity`: The current entity object, if the entity is defined in the card
@ -201,23 +209,12 @@ Inside the javascript code, you'll have access to those variables:
* `user`: The user object (equivalent to `hass.user`)
* `hass`: The complete `hass` object
The value shouldn't be enclosed in quotes:
```yaml
label_template: >
return states['light.mylight'].attributes.brightness
```
or
```yaml
state:
- operator: template
value: >
return states['input_select.light_mode'].state === 'night_mode'
```
See [here](#templates-support) for some examples.
See [here](#templates-support) for some examples or [here](#custom-fields) for some crazy advanced stuff using templates!
### Styles
All the styles entries, support Templating, see [here](#custom-fields) for some examples.
#### Easy styling options
For each element in the card, styles can be defined in 2 places:
@ -372,6 +369,149 @@ Some examples:
- filter: grayscale(100%)
```
### Custom Fields
Custom fields support, using the `custom_fields` object, enables you to create your own fields on top of the pre-defined ones (name, state, label and icon).
This is an advanced feature which leverages (if you require it) the CSS Grid.
Each custom field supports its own styling config, the name needs to match between both objects needs to match:
```yaml
- type: custom:button-card
[...]
custom_element:
test_element: My test element
styles:
custom_fields:
test_element:
- color: red
- font-size: 13px
```
Examples are better than a long text, so here you go:
* Placing an element wherever you want (that means bypassing the grid). Set the grid to `position: relative` and set the element to `position: absolute`
![custom_fields_1](examples/custom_fields_1.gif)
```yaml
- type: custom:button-card
icon: mdi:lightbulb
aspect_ratio: 1/1
name: Nb lights on
styles:
grid:
- position: relative
custom_fields:
notification:
- background-color: >
[[[
if (states['input_number.test'].state == 0)
return "green";
return "red";
]]]
- border-radius: 50%
- position: absolute
- left: 60%
- top: 10%
- height: 20px
- width: 20px
- font-size: 8px
- line-height: 20px
custom_fields:
notification: >
[[[ return Math.floor(states['input_number.test'].state / 10) ]]]
```
* Or you can use the grid. Each element will have it's name positioned as the `grid-area`:
![custom_fields_2](examples/custom_fields_2.png)
```yaml
- type: custom:button-card
entity: 'sensor.raspi_temp'
icon: 'mdi:raspberry-pi'
aspect_ratio: 1/1
name: HassOS
styles:
card:
- background-color: '#000044'
- border-radius: 10%
- padding: 10%
- color: ivory
- font-size: 10px
- text-shadow: 0px 0px 5px black
- text-transform: capitalize
grid:
- grid-template-areas: '"i temp" "n n" "cpu cpu" "ram ram" "sd sd"'
- grid-template-columns: 1fr 1fr
- grid-template-rows: 1fr min-content min-content min-content min-content
name:
- font-weight: bold
- font-size: 13px
- color: white
- align-self: middle
- justify-self: start
- padding-bottom: 4px
img_cell:
- justify-content: start
- align-items: start
- margin: none
icon:
- color: >
[[[
if (entity.state < 60) return 'lime';
if (entity.state >= 60 && entity.state < 80) return 'orange';
else return 'red';
]]]
- width: 70%
- margin-top: -10%
custom_fields:
temp:
- align-self: start
- justify-self: end
cpu:
- padding-bottom: 2px
- align-self: middle
- justify-self: start
- --text-color-sensor: '[[[ if (states["sensor.raspi_cpu"].state > 80) return "red"; ]]]'
ram:
- padding-bottom: 2px
- align-self: middle
- justify-self: start
- --text-color-sensor: '[[[ if (states["sensor.raspi_ram"].state > 80) return "red"; ]]]'
sd:
- align-self: middle
- justify-self: start
- --text-color-sensor: '[[[ if (states["sensor.raspi_sd"].state > 80) return "red"; ]]]'
custom_fields:
temp: >
[[[
return `<ha-icon
icon="mdi:thermometer"
style="width: 12px; height: 12px; color: yellow;">
</ha-icon><span>${entity.state}°C</span>`
]]]
cpu: >
[[[
return `<ha-icon
icon="mdi:server"
style="width: 12px; height: 12px; color: deepskyblue;">
</ha-icon><span>CPU: <span style="color: var(--text-color-sensor);">${states['sensor.raspi_cpu'].state}%</span></span>`
]]]
ram: >
[[[
return `<ha-icon
icon="mdi:memory"
style="width: 12px; height: 12px; color: deepskyblue;">
</ha-icon><span>RAM: <span style="color: var(--text-color-sensor);">${states['sensor.raspi_ram'].state}%</span></span>`
]]]
sd: >
[[[
return `<ha-icon
icon="mdi:harddisk"
style="width: 12px; height: 12px; color: deepskyblue;">
</ha-icon><span>SD: <span style="color: var(--text-color-sensor);">${states['sensor.raspi_sd'].state}%</span></span>`
]]]
```
### Configuration Templates
#### General
@ -843,9 +983,11 @@ If you specify a width for the card, it has to be in `px`. All the cards without
- type: "custom:button-card"
color_type: icon
entity: light.test_light
label_template: >
var bri = states['light.test_light'].attributes.brightness;
return 'Brightness: ' + (bri ? bri : '0') + '%';
label: >
[[[
var bri = states['light.test_light'].attributes.brightness;
return 'Brightness: ' + (bri ? bri : '0') + '%';
]]]
show_label: true
size: 15%
styles:
@ -855,8 +997,10 @@ If you specify a width for the card, it has to be in `px`. All the cards without
color_type: icon
entity: light.test_light
layout: icon_label
label_template: >
return 'Other State: ' + states['switch.skylight'].state;
label: >
[[[
return 'Other State: ' + states['switch.skylight'].state;
]]]
show_label: true
show_name: false
styles:
@ -878,8 +1022,10 @@ Example with `template`:
state:
- operator: template
value: >
return states['light.test_light'].attributes
&& (states['light.test_light'].attributes.brightness <= 100)
[[[
return states['light.test_light'].attributes
&& (states['light.test_light'].attributes.brightness <= 100)
]]]
icon: mdi:alert
- operator: default
icon: mdi:lightbulb
@ -890,7 +1036,7 @@ Example with `template`:
state:
- operator: template
value: >
return states['input_select.light_mode'].state === 'night_mode'
[[[ return states['input_select.light_mode'].state === 'night_mode' ]]]
icon: mdi:weather-night
label: Night Mode
- operator: default
@ -906,9 +1052,11 @@ Example with `template`:
- type: "custom:button-card"
color_type: icon
entity: light.test_light
label_template: >
var bri = states['light.test_light'].attributes.brightness;
return 'Brightness: ' + (bri ? bri : '0') + '%';
label: >
[[[
var bri = states['light.test_light'].attributes.brightness;
return 'Brightness: ' + (bri ? bri : '0') + '%';
]]]
show_label: true
show_state: true
size: 10%
@ -945,8 +1093,8 @@ Example with `template`:
color_type: icon
entity: light.test_light
layout: icon_label
label_template: >
return 'Other State: ' + states['switch.skylight'].state;
label: >
[[[ return 'Other State: ' + states['switch.skylight'].state; ]]]
show_label: true
show_name: false
size: 100%
@ -1032,7 +1180,8 @@ Example with `template`:
## Credits
- [ciotlosm](https://github.com/ciotlosm) for the readme template and the awesome examples
- [ciotlosm](https://github.com/ciotlosm) for the readme template and some awesome examples
- [iantrich](https://github.com/iantrich), [LbDab](https://github.com/lbdab) and [jimz011](https://github.com/jimz011) for the inspiration and the awesome templates and cards you've created.
[commits-shield]: https://img.shields.io/github/commit-activity/y/custom-cards/button-card.svg?style=for-the-badge
[commits]: https://github.com/custom-cards/button-card/commits/master

873
dist/button-card.js vendored

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

913
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "button-card",
"version": "1.11.1",
"version": "2.0.0",
"description": "Button card for lovelace",
"main": "dist/button-card.js",
"pre-commit": [
@ -32,33 +32,33 @@
},
"homepage": "https://github.com/custom-cards/button-card#readme",
"devDependencies": {
"@babel/core": "^7.4.4",
"@babel/plugin-proposal-class-properties": "^7.4.4",
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-decorators": "^7.4.4",
"@typescript-eslint/eslint-plugin": "^1.9.0",
"@typescript-eslint/parser": "^1.9.0",
"@typescript-eslint/eslint-plugin": "^1.12.0",
"@typescript-eslint/parser": "^1.12.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",
"eslint-config-airbnb-base": "^13.2.0",
"eslint-plugin-import": "^2.18.2",
"npm": "^6.10.1",
"pre-commit": "^1.2.2",
"prettier": "^1.17.1",
"rollup": "^1.12.2",
"rollup-plugin-babel": "^4.3.2",
"prettier": "^1.18.2",
"rollup": "^1.17.0",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^4.2.4",
"rollup-plugin-terser": "^4.0.4",
"rollup-plugin-typescript2": "^0.20.1",
"ts-lit-plugin": "^1.0.6",
"typescript": "^3.4.4",
"typescript": "^3.5.3",
"typescript-styled-plugin": "^0.14.0"
},
"dependencies": {
"@ctrl/tinycolor": "^2.4.0",
"custom-card-helpers": "github:custom-cards/custom-card-helpers#fix-service-call",
"@ctrl/tinycolor": "^2.5.3",
"custom-card-helpers": "^1.2.2",
"home-assistant-js-websocket": "^3.4.0",
"lit-element": "^2.1.0",
"lit-html": "^1.0.0"
"lit-element": "^2.2.0",
"lit-html": "^1.1.1"
}
}

View File

@ -10,14 +10,10 @@ 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 { classMap, ClassInfo } from 'lit-html/directives/class-map';
import {
HassEntity,
} from 'home-assistant-js-websocket';
import {
ButtonCardConfig,
StateConfig,
} from './types';
import {
domainIcon,
HomeAssistant,
@ -25,10 +21,14 @@ import {
getLovelace,
timerTimeRemaining,
secondsToDuration,
durationToSeconds
durationToSeconds,
// Still not working...
// longPress,
} from 'custom-card-helpers';
import {
ButtonCardConfig,
StateConfig,
} from './types';
import { longPress } from './long-press';
import {
computeDomain,
@ -52,6 +52,8 @@ class ButtonCard extends LitElement {
@property() private _timeRemaining?: number;
@property() private _hasTemplate?: boolean;
private _interval?: number;
public disconnectedCallback(): void {
@ -62,9 +64,9 @@ class ButtonCard extends LitElement {
public connectedCallback(): void {
super.connectedCallback();
if (
this.config &&
this.config.entity &&
computeDomain(this.config.entity) === 'timer'
this.config
&& this.config.entity
&& computeDomain(this.config.entity) === 'timer'
) {
const stateObj = this.hass!.states[this.config.entity];
this._startInterval(stateObj);
@ -85,16 +87,7 @@ class ButtonCard extends LitElement {
protected shouldUpdate(changedProps: PropertyValues): boolean {
const state = this.config!.entity ? this.hass!.states[this.config!.entity] : undefined;
const configState = this._getMatchingConfigState(state);
const forceUpdate = (
configState && (
this.config!.show_label && configState.label_template
|| this.config!.show_entity_picture && configState.entity_picture_template
|| this.config!.show_name && configState.name_template
)
|| this.config!.show_label && this.config!.label_template
|| this.config!.show_name && this.config!.name_template
|| this.config!.show_entity_picture && this.config!.entity_picture_template
)
const forceUpdate = this._hasTemplate
|| this.config!.state
&& this.config!.state.find(elt => elt.operator === 'template')
|| changedProps.has('_timeRemaining')
@ -106,10 +99,10 @@ class ButtonCard extends LitElement {
super.updated(changedProps);
if (
this.config &&
this.config.entity &&
computeDomain(this.config.entity) === 'timer' &&
changedProps.has('hass')
this.config
&& this.config.entity
&& computeDomain(this.config.entity) === 'timer'
&& changedProps.has('hass')
) {
const stateObj = this.hass!.states[this.config.entity];
const oldHass = changedProps.get('hass') as this['hass'];
@ -139,7 +132,7 @@ class ButtonCard extends LitElement {
if (stateObj.state === 'active') {
this._interval = window.setInterval(
() => this._calculateRemaining(stateObj),
1000
1000,
);
}
}
@ -154,7 +147,7 @@ class ButtonCard extends LitElement {
}
return secondsToDuration(
this._timeRemaining || durationToSeconds(stateObj.attributes['duration'])
this._timeRemaining || durationToSeconds(stateObj.attributes.duration),
);
}
@ -174,24 +167,25 @@ class ButtonCard extends LitElement {
switch (elt.operator) {
case '==':
/* eslint eqeqeq: 0 */
return (state && state.state == elt.value);
return (state && state.state == this._getTemplateOrString(state, elt.value));
case '<=':
return (state && state.state <= elt.value);
return (state && state.state <= this._getTemplateOrString(state, elt.value));
case '<':
return (state && state.state < elt.value);
return (state && state.state < this._getTemplateOrString(state, elt.value));
case '>=':
return (state && state.state >= elt.value);
return (state && state.state >= this._getTemplateOrString(state, elt.value));
case '>':
return (state && state.state > elt.value);
return (state && state.state > this._getTemplateOrString(state, elt.value));
case '!=':
return (state && state.state != elt.value);
return (state && state.state != this._getTemplateOrString(state, elt.value));
case 'regex': {
/* eslint no-unneeded-ternary: 0 */
const matches = state && state.state.match(elt.value) ? true : false;
const matches = state
&& state.state.match(this._getTemplateOrString(state, elt.value)) ? true : false;
return matches;
}
case 'template': {
return this._evalTemplate(state, elt.value);
return this._getTemplateOrString(state, elt.value);
}
case 'default':
def = elt;
@ -200,7 +194,7 @@ class ButtonCard extends LitElement {
return false;
}
} else {
return state && (elt.value == state.state);
return state && (this._getTemplateOrString(state, elt.value) == state.state);
}
});
if (!retval && def) {
@ -210,11 +204,29 @@ class ButtonCard extends LitElement {
}
private _evalTemplate(state: HassEntity | undefined, func: any): any {
/* eslint no-new-func: 0 */
return new Function('states', 'entity', 'user', 'hass',
`'use strict'; ${func}`)
.call(this, this.hass!.states, state, this.hass!.user, this.hass);
}
private _getTemplateOrString(
state: HassEntity | undefined,
value: any | undefined,
): any | undefined {
if (!value) return undefined;
if (typeof value === 'number') return value;
const trimmed = value.trim();
if (
trimmed.substring(0, 3) === '[[['
&& trimmed.slice(-3) === ']]]'
) {
return this._evalTemplate(state, trimmed.slice(3, -3));
} else {
return value;
}
}
private _getDefaultColorForState(state: HassEntity): string {
switch (state.state) {
case 'on':
@ -300,7 +312,7 @@ class ButtonCard extends LitElement {
? state.attributes.icon
: domainIcon(computeDomain(state.entity_id), state.state);
}
return icon;
return this._getTemplateOrString(state, icon);
}
private _buildEntityPicture(
@ -311,33 +323,24 @@ class ButtonCard extends LitElement {
return undefined;
}
let entityPicture: string | undefined;
let matchingEntityPictureTemplate: string | undefined;
if (configState && configState.entity_picture_template) {
matchingEntityPictureTemplate = configState.entity_picture_template;
} else {
matchingEntityPictureTemplate = this.config!.entity_picture_template;
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;
}
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);
return this._getTemplateOrString(state, entityPicture);
}
private _buildStyleGeneric(
state: HassEntity | undefined,
configState: StateConfig | undefined,
styleType: string,
): StyleInfo {
let style: StyleInfo = {};
let style: any = {};
if (this.config!.styles && this.config!.styles[styleType]) {
style = Object.assign(style, ...this.config!.styles[styleType]);
}
@ -349,6 +352,41 @@ class ButtonCard extends LitElement {
...configStateStyle,
};
}
Object.keys(style).forEach((key) => {
style[key] = this._getTemplateOrString(state, style[key]);
});
return style;
}
private _buildCustomStyleGeneric(
state: HassEntity | undefined,
configState: StateConfig | undefined,
styleType: string,
): StyleInfo {
let style: any = {};
if (this.config!.styles
&& this.config!.styles.custom_fields
&& this.config!.styles.custom_fields[styleType]
) {
style = Object.assign(style, ...this.config!.styles.custom_fields[styleType]);
}
if (configState && configState.styles
&& configState.styles.custom_fields
&& configState.styles.custom_fields[styleType]
) {
let configStateStyle: StyleInfo = {};
configStateStyle = Object.assign(
configStateStyle,
...configState.styles.custom_fields[styleType],
);
style = {
...style,
...configStateStyle,
};
}
Object.keys(style).forEach((key) => {
style[key] = this._getTemplateOrString(state, style[key]);
});
return style;
}
@ -359,26 +397,17 @@ class ButtonCard extends LitElement {
return undefined;
}
let name: string | undefined;
let matchingNameTemplate: string | undefined;
if (configState && configState.name_template) {
matchingNameTemplate = configState.name_template;
} else {
matchingNameTemplate = this.config!.name_template;
}
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;
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 this._evalTemplate(state, matchingNameTemplate);
return this._getTemplateOrString(state, name);
}
private _buildStateString(state: HassEntity | undefined): string | undefined {
@ -394,7 +423,7 @@ class ButtonCard extends LitElement {
} else {
stateString = this._computeTimeDisplay(state);
if (state.state === 'paused') {
stateString += ` (${localizedState})`
stateString += ` (${localizedState})`;
}
}
} else {
@ -422,8 +451,8 @@ class ButtonCard extends LitElement {
state: HassEntity | undefined,
style: StyleInfo,
): TemplateResult | undefined {
return this.config!.show_last_changed && state ?
html`
return this.config!.show_last_changed && state
? html`
<ha-relative-time
id="label"
class="ellipsis"
@ -441,23 +470,45 @@ class ButtonCard extends LitElement {
return undefined;
}
let label: string | undefined;
let matchingLabelTemplate: string | undefined;
if (configState && configState.label_template) {
matchingLabelTemplate = configState.label_template;
if (configState && configState.label) {
label = configState.label;
} else {
matchingLabelTemplate = this.config!.label_template;
}
if (!matchingLabelTemplate) {
if (configState && configState.label) {
label = configState.label;
} else {
label = this.config!.label;
}
return label;
label = this.config!.label;
}
return this._evalTemplate(state, matchingLabelTemplate);
return this._getTemplateOrString(state, label);
}
private _buildCustomFields(
state: HassEntity | undefined,
configState: StateConfig | undefined,
): TemplateResult {
let result = html``;
const fields: any = {};
if (this.config!.custom_fields) {
Object.keys(this.config!.custom_fields).forEach((key) => {
const value = this.config!.custom_fields![key];
fields[key] = this._getTemplateOrString(state, value);
});
}
if (configState && configState.custom_fields) {
Object.keys(configState.custom_fields).forEach((key) => {
const value = configState!.custom_fields![key];
fields[key] = this._getTemplateOrString(state, value);
});
}
Object.keys(fields).forEach((key) => {
if (fields[key] != undefined) {
const customStyle: StyleInfo = {
...this._buildCustomStyleGeneric(state, configState, key),
'grid-area': key,
};
result = html`${result}
<div id=${key} class="ellipsis" style=${styleMap(customStyle)}>${unsafeHTML(fields[key])}</div>`;
}
});
return result;
}
private _isClickable(state: HassEntity | undefined): boolean {
@ -525,15 +576,15 @@ class ButtonCard extends LitElement {
const configState = this._getMatchingConfigState(state);
const color = this._buildCssColorAttribute(state, configState);
let buttonColor = color;
let cardStyle: StyleInfo = {};
let lockStyle: StyleInfo = {};
let aspectRatio: StyleInfo = {};
const lockStyleFromConfig = this._buildStyleGeneric(configState, 'lock');
const configCardStyle = this._buildStyleGeneric(configState, 'card');
let cardStyle: any = {};
let lockStyle: any = {};
const aspectRatio: any = {};
const lockStyleFromConfig = this._buildStyleGeneric(state, configState, 'lock');
const configCardStyle = this._buildStyleGeneric(state, 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');
@ -629,11 +680,11 @@ class ButtonCard extends LitElement {
const iconTemplate = this._getIconHtml(state, configState, color);
const itemClass: string[] = [containerClass];
const label = this._buildLabel(state, configState);
const nameStyleFromConfig = this._buildStyleGeneric(configState, 'name');
const stateStyleFromConfig = this._buildStyleGeneric(configState, 'state');
const labelStyleFromConfig = this._buildStyleGeneric(configState, 'label');
const nameStyleFromConfig = this._buildStyleGeneric(state, configState, 'name');
const stateStyleFromConfig = this._buildStyleGeneric(state, configState, 'state');
const labelStyleFromConfig = this._buildStyleGeneric(state, configState, 'label');
const lastChangedTemplate = this._buildLastChanged(state, labelStyleFromConfig);
const gridStyleFromConfig = this._buildStyleGeneric(configState, 'grid');
const gridStyleFromConfig = this._buildStyleGeneric(state, configState, 'grid');
if (!iconTemplate) itemClass.push('no-icon');
if (!name) itemClass.push('no-name');
if (!stateString) itemClass.push('no-state');
@ -646,6 +697,7 @@ class ButtonCard extends LitElement {
${stateString ? html`<div id="state" class="ellipsis" style=${styleMap(stateStyleFromConfig)}>${stateString}</div>` : ''}
${label && !lastChangedTemplate ? html`<div id="label" class="ellipsis" style=${styleMap(labelStyleFromConfig)}>${unsafeHTML(label)}</div>` : ''}
${lastChangedTemplate ? lastChangedTemplate : ''}
${this._buildCustomFields(state, configState)}
</div>
`;
}
@ -657,10 +709,10 @@ class ButtonCard extends LitElement {
): TemplateResult | undefined {
const icon = this._buildIcon(state, configState);
const entityPicture = this._buildEntityPicture(state, configState);
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 entityPictureStyleFromConfig = this._buildStyleGeneric(state, configState, 'entity_picture');
const haIconStyleFromConfig = this._buildStyleGeneric(state, configState, 'icon');
const imgCellStyleFromConfig = this._buildStyleGeneric(state, configState, 'img_cell');
const haCardStyleFromConfig = this._buildStyleGeneric(state, configState, 'card');
const haIconStyle: StyleInfo = {
color,
@ -698,7 +750,10 @@ class ButtonCard extends LitElement {
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);
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;
@ -724,6 +779,10 @@ class ButtonCard extends LitElement {
this.config!.color_off = 'var(--paper-item-icon-color)';
}
this.config!.color_on = 'var(--paper-item-icon-active-color)';
const jsonConfig = JSON.stringify(this.config);
const rxp = new RegExp('\\[\\[\\[.*\\]\\]\\]', 'gm');
this._hasTemplate = jsonConfig.match(rxp) ? true : false;
}
// The height of your card. Home Assistant uses this to automatically
@ -732,6 +791,25 @@ class ButtonCard extends LitElement {
return 3;
}
private _evalActions(config: ButtonCardConfig, action: string): ButtonCardConfig {
const state = this.config!.entity ? this.hass!.states[this.config!.entity] : undefined;
const configDuplicate = JSON.parse(JSON.stringify(config));
Object.keys(configDuplicate![action]).forEach((key) => {
if (key === 'service_data') {
Object.keys(configDuplicate![action].service_data).forEach((sdKey) => {
configDuplicate![action].service_data[sdKey] = this._getTemplateOrString(
state, configDuplicate![action].service_data[sdKey],
);
});
} else {
configDuplicate![action][key] = this._getTemplateOrString(
state, configDuplicate![action][key],
);
}
});
return configDuplicate;
}
private _handleTap(ev): void {
/* eslint no-alert: 0 */
if (this.config!.confirmation
@ -739,7 +817,7 @@ class ButtonCard extends LitElement {
return;
}
const config = ev.target.config;
handleClick(this, this.hass!, config, false, false);
handleClick(this, this.hass!, this._evalActions(config, 'tap_action'), false, false);
}
private _handleHold(ev): void {
@ -749,7 +827,7 @@ class ButtonCard extends LitElement {
return;
}
const config = ev.target.config;
handleClick(this, this.hass!, config, true, false);
handleClick(this, this.hass!, this._evalActions(config, 'hold_action'), true, false);
}
private _handleDblTap(ev): void {
@ -759,7 +837,7 @@ class ButtonCard extends LitElement {
return;
}
const config = ev.target.config;
handleClick(this, this.hass!, config, false, true);
handleClick(this, this.hass!, this._evalActions(config, 'dbltap_action'), false, true);
}
private _handleLock(ev): void {

View File

@ -1,6 +1,6 @@
import { HassEntity } from 'home-assistant-js-websocket';
import { computeDomain } from './helpers';
import { LocalizeFunc } from 'custom-card-helpers';
import { computeDomain } from './helpers';
export default (
localize: LocalizeFunc,

View File

@ -139,13 +139,20 @@ export function mergeStatesById(
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)));
/* eslint eqeqeq: 0 no-confusing-arrow: 0 */
resultStateConfigs = resultStateConfigs.concat(
fromStates.filter(
x => !intoStates
? true
: !intoStates.find(y => y.id && x.id ? y.id == x.id : false),
),
);
}
return resultStateConfigs;
}
}

View File

@ -105,6 +105,7 @@ class LongPress extends HTMLElement implements LongPress {
});
const clickStart = (ev: Event) => {
ev.stopPropagation();
if (this.cooldownStart) {
return;
}
@ -134,6 +135,7 @@ class LongPress extends HTMLElement implements LongPress {
};
const clickEnd = (ev: Event) => {
ev.stopPropagation();
if (
this.cooldownEnd
|| (['touchend', 'touchcancel'].includes(ev.type)

View File

@ -5,7 +5,6 @@ export interface ButtonCardConfig {
type: string;
entity?: string;
name?: string;
name_template?: string;
icon?: string;
color_type: 'icon' | 'card' | 'label-card' | 'blank-card'
color?: 'auto' | 'auto-no-temperature' | string;
@ -24,19 +23,17 @@ export interface ButtonCardConfig {
show_last_changed?: boolean;
show_label?: boolean;
label?: string;
label_template?: string;
entity_picture?: string;
entity_picture_template?: string;
units?: string;
state?: StateConfig[];
styles?: StylesConfig;
confirmation?: string;
layout: Layout;
entity_picture_style?: CssStyleConfig[];
default_color: string;
color_on: string;
color_off: string;
custom_fields?: CustomFields;
}
export type Layout = 'vertical'
@ -53,16 +50,14 @@ export interface StateConfig {
operator?: '<' | '<=' | '==' | '>=' | '>' | '!=' | 'regex' | 'template' | 'default';
value?: any;
name?: string;
name_template?: string;
icon?: string;
color?: 'auto' | 'auto-no-temperature' | string;
entity_picture_style?: CssStyleConfig[];
entity_picture?: string;
entity_picture_template?: string;
styles?: StylesConfig;
spin?: boolean;
label?: string;
label_template?: string;
custom_fields?: CustomFields;
}
export interface StylesConfig {
@ -75,8 +70,17 @@ export interface StylesConfig {
grid?: CssStyleConfig[];
img_cell?: CssStyleConfig[];
lock?: CssStyleConfig[];
custom_fields?: CustomStyleConfig;
}
export interface CustomStyleConfig {
[key: string]: CssStyleConfig[];
}
export interface CssStyleConfig {
[key: string]: any;
}
export interface CustomFields {
[key: string]: any;
}