Bunch of features (#231)
* Add support for any card in custom_fields * Fix version script * Drop support for custom_updater * confirmation template support and per action confirmation * Confirmation supports templates and exemptions * Support for Safari 10 * Using createThing from cch * Proper events handling for embedded cards * Lock with exemptions and delay * Fix locking documentation * Fix default color documentation * Updating templates documentation * Support for different unlock clicks * Update embedded card documentation
This commit is contained in:
parent
5ef600a6e0
commit
b1fa112fbc
|
@ -1,4 +1,4 @@
|
|||
/node_modules/
|
||||
yarn.lock
|
||||
.rpt2_cache/
|
||||
/dist/*.map
|
||||
/dist/**
|
||||
|
|
305
README.md
305
README.md
|
@ -1,4 +1,4 @@
|
|||
# Button Card <!-- omit in toc -->
|
||||
# Button Card by [@RomRider](https://github.com/RomRider) <!-- omit in toc -->
|
||||
|
||||
[![GitHub Release][releases-shield]][releases]
|
||||
[![License][license-shield]](LICENSE.md)
|
||||
|
@ -19,6 +19,8 @@ Lovelace Button card for your entities.
|
|||
- [Configuration](#configuration)
|
||||
- [Main Options](#main-options)
|
||||
- [Action](#action)
|
||||
- [Confirmation](#confirmation)
|
||||
- [Lock Object](#lock-object)
|
||||
- [State](#state)
|
||||
- [Available operators](#available-operators)
|
||||
- [Layout](#layout)
|
||||
|
@ -33,7 +35,7 @@ Lovelace Button card for your entities.
|
|||
- [Merging state by id](#merging-state-by-id)
|
||||
- [Installation](#installation)
|
||||
- [Manual Installation](#manual-installation)
|
||||
- [Installation and tracking with `custom_updater`](#installation-and-tracking-with-customupdater)
|
||||
- [Installation and tracking with `hacs`](#installation-and-tracking-with-hacs)
|
||||
- [Examples](#examples)
|
||||
- [Configuration with states](#configuration-with-states)
|
||||
- [Default behavior](#default-behavior)
|
||||
|
@ -86,12 +88,12 @@ 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`. 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` |
|
||||
| `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-color)`, for `on` it will be `var(--paper-item-icon-active-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. |
|
||||
| `double_tap_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. 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 |
|
||||
|
@ -105,9 +107,8 @@ Lovelace Button card for your entities.
|
|||
| `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 |
|
||||
| `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 |
|
||||
| `confirmation` | object | optional | See [confirmation](#confirmation) | Display a confirmation popup |
|
||||
| `lock` | object | optional | See [#lock-object] | Displays a lock on 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) |
|
||||
|
||||
|
@ -120,11 +121,58 @@ All the fields support templates, see [templates](#templates).
|
|||
| `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 |
|
||||
| `url_path` | string | none | Eg: `https://www.google.fr` | URL to open on click when action is `url`. The URL will open in a new tab |
|
||||
| `service` | string | none | Any service | Service to call (e.g. `media_player.media_play_pause`) when `action` defined as `call-service` |
|
||||
| `service_data` | object | none | Any service data | Service data to include (e.g. `entity_id: media_player.bedroom`) when `action` defined as `call-service`. If your `service_data` requires an `entity_id`, you can use the keywork `entity`, this will actually call the service on the entity defined in the main configuration of this card. Useful for [configuration templates](#configuration-templates) |
|
||||
| `haptic` | string | none | `success`, `warning`, `failure`, `light`, `medium`, `heavy`, `selection` | Haptic feedback for the [Beta IOS App](http://home-assistant.io/ios/beta) |
|
||||
| `repeat` | number | none | eg: `500` | For a hold_action, you can optionally configure the action to repeat while the button is being held down (for example, to repeatedly increase the volume of a media player). Define the number of milliseconds between repeat actions here. |
|
||||
| `confirmation` | object | none | See [confirmation](#confirmation) | Display a confirmation popup, overrides the default `confirmation` object |
|
||||
|
||||
### Confirmation
|
||||
|
||||
This will popup a dialog box before running the action.
|
||||
|
||||
| Name | Type | Default | Supported options | Description |
|
||||
| --- | ---- | ------- | ----------------- | ----------- |
|
||||
| `text` | string | none | Any text | This text will be displayed in the popup. Supports templates, see [templates](#templates) |
|
||||
| `exemptions` | array of users | none | `user: USER_ID` | Any user declared in this list will not see the confirmation dialog |
|
||||
|
||||
Example:
|
||||
```yaml
|
||||
confirmation:
|
||||
text: '[[[ return `Are you sure you want to toggle ${entity.attributes.friendly_name}?` ]]]'
|
||||
exemptions:
|
||||
- user: befc8496799848bda1824f2a8111e30a
|
||||
```
|
||||
|
||||
### Lock Object
|
||||
|
||||
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 `delay` seconds (5 by default).
|
||||
|
||||
| Name | Type | Default | Supported options | Description |
|
||||
| --- | ---- | ------- | ----------------- | ----------- |
|
||||
| `enabled` | boolean | `false` | `true` \| `false` | Enables or disables the lock. Supports templates, see [templates](#templates) |
|
||||
| `duration` | number | `5` | any number | Duration of the unlocked state in seconds
|
||||
| `exemptions` | array of user id or username | none | `user: USER_ID` \| `username: test` | Any user declared in this list will not see the confirmation dialog. It can be a user id (`user`) or a username (`username`) |
|
||||
| `unlock` | string | `tap` | `tap` \| `hold` \| `double_tap` | The type of click which will unlock the button |
|
||||
|
||||
Example:
|
||||
```yaml
|
||||
lock:
|
||||
enabled: '[[[ return entity.state === 'on'; ]]]'
|
||||
duration: 10
|
||||
unlock: hold
|
||||
exemptions:
|
||||
- username: test
|
||||
- user: befc8496799848bda1824f2a8111e30a
|
||||
```
|
||||
|
||||
If you want to lock the button for everyone and disable the unlocking possibility, set the exemptions object to `[]`:
|
||||
```yaml
|
||||
lock:
|
||||
enabled: true
|
||||
exemptions: []
|
||||
```
|
||||
|
||||
### State
|
||||
|
||||
|
@ -203,6 +251,9 @@ Those are the configuration fields which support templating:
|
|||
* Else: The function for `value` needs to return a string or a number
|
||||
* All the `custom_fields` (Support also HTML rendering)
|
||||
* All the `styles`: Each entry needs to return a string (See [here](#custom-fields) for some examples)
|
||||
* Everything field in `*_action`
|
||||
* The confirmation text (`confirmation.text`)
|
||||
* The lock being enabled or not (`lock.enabled`)
|
||||
|
||||
Inside the javascript code, you'll have access to those variables:
|
||||
* `entity`: The current entity object, if the entity is defined in the card
|
||||
|
@ -376,6 +427,8 @@ Some examples:
|
|||
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.
|
||||
|
||||
Custom fields also support embeded cards, see [exemple below](#custom_fields_card_example).
|
||||
|
||||
Each custom field supports its own styling config, the name needs to match between both objects needs to match:
|
||||
```yaml
|
||||
- type: custom:button-card
|
||||
|
@ -427,91 +480,124 @@ Examples are better than a long text, so here you go:
|
|||
![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
|
||||
- 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
|
||||
- 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"; ]]]'
|
||||
- --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>`
|
||||
]]]
|
||||
```
|
||||
|
||||
* <a name="custom_fields_card_example"></a>Or you can embed a card (or multiple) inside the button card (note, this configuration uses [card-mod](https://github.com/thomasloven/lovelace-card-mod) to remove the `box-shadow` of the sensor card. This is what the `style` inside the embedded card is for):
|
||||
|
||||
![custom_fields_3](examples/custom_fields_card.png)
|
||||
```yaml
|
||||
- type: custom:button-card
|
||||
aspect_ratio: 1/1
|
||||
custom_fields:
|
||||
graph:
|
||||
card:
|
||||
type: sensor
|
||||
entity: sensor.sensor1
|
||||
graph: line
|
||||
style: |
|
||||
ha-card {
|
||||
box-shadow: none;
|
||||
}
|
||||
styles:
|
||||
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>`
|
||||
]]]
|
||||
graph:
|
||||
- filter: opacity(50%)
|
||||
- overflow: unset
|
||||
card:
|
||||
- overflow: unset
|
||||
grid:
|
||||
- grid-template-areas: '"i" "n" "graph"'
|
||||
- grid-template-columns: 1fr
|
||||
- grid-template-rows: 1fr min-content min-content
|
||||
|
||||
entity: light.test_light
|
||||
hold_action:
|
||||
action: more-info
|
||||
```
|
||||
|
||||
### Configuration Templates
|
||||
|
@ -627,29 +713,28 @@ state:
|
|||
1. Download the [button-card](https://raw.githubusercontent.com/custom-cards/button-card/master/dist/button-card.js)
|
||||
2. Place the file in your `config/www` folder
|
||||
3. Include the card code in your `ui-lovelace-card.yaml`
|
||||
|
||||
```yaml
|
||||
title: Home
|
||||
resources:
|
||||
- url: /local/button-card.js
|
||||
type: module
|
||||
```
|
||||
```yaml
|
||||
title: Home
|
||||
resources:
|
||||
- url: /local/button-card.js
|
||||
type: module
|
||||
```
|
||||
|
||||
4. Write configuration for the card in your `ui-lovelace.yaml`
|
||||
|
||||
### Installation and tracking with `custom_updater`
|
||||
### Installation and tracking with `hacs`
|
||||
|
||||
1. Make sure the [custom_updater](https://github.com/custom-components/custom_updater) component is installed and working.
|
||||
2. Configure Lovelace to load the card.
|
||||
1. Make sure the [HACS](https://github.com/custom-components/hacs) component is installed and working.
|
||||
2. Search for `button-card` and add it through HACS
|
||||
3. Add the configuration to your `ui-lovelace.yaml`
|
||||
|
||||
```yaml
|
||||
resources:
|
||||
- url: /customcards/github/custom-cards/button-card.js?track=true
|
||||
type: module
|
||||
```
|
||||
```yaml
|
||||
resources:
|
||||
- url: /community_plugin/button-card/button-card.js
|
||||
type: module
|
||||
```
|
||||
|
||||
3. Run the service `custom_updater.check_all` or click the "CHECK" button if you use the [`tracker-card`](https://github.com/custom-cards/tracker-card).
|
||||
4. Refresh the website.
|
||||
4. Refresh home-assistant.
|
||||
|
||||
## Examples
|
||||
|
||||
|
@ -1127,10 +1212,12 @@ Example with `template`:
|
|||
cards:
|
||||
- type: "custom:button-card"
|
||||
entity: switch.test
|
||||
lock: true
|
||||
lock:
|
||||
enabled: true
|
||||
- type: "custom:button-card"
|
||||
color_type: card
|
||||
lock: true
|
||||
lock:
|
||||
enabled: true
|
||||
color: black
|
||||
entity: switch.test
|
||||
```
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
After Width: | Height: | Size: 7.0 KiB |
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION=$(jq -r .version package.json)
|
||||
|
||||
cat <<EOF >src/version-const.ts
|
||||
export const BUTTON_CARD_VERSION = '${VERSION}';
|
||||
EOF
|
|
@ -5,4 +5,5 @@ set -euo pipefail
|
|||
echo "Pre-Commit hooks running..."
|
||||
|
||||
npm run build
|
||||
git add dist/button-card.js
|
||||
npm run update-version
|
||||
git add src/version-const.ts
|
||||
|
|
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
|
@ -12,7 +12,8 @@
|
|||
"rollup": "rollup -c",
|
||||
"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"
|
||||
"watch": "rollup -c rollup.debug.config.js --watch",
|
||||
"update-version": "./hooks/bump-version.sh"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -32,35 +33,35 @@
|
|||
},
|
||||
"homepage": "https://github.com/custom-cards/button-card#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/core": "^7.6.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@babel/plugin-proposal-decorators": "^7.4.4",
|
||||
"@typescript-eslint/eslint-plugin": "^1.12.0",
|
||||
"@typescript-eslint/parser": "^1.12.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^1.13.0",
|
||||
"@typescript-eslint/parser": "^1.13.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-airbnb-base": "^13.2.0",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"npm": "^6.10.1",
|
||||
"npm": "^6.12.0",
|
||||
"pre-commit": "^1.2.2",
|
||||
"prettier": "^1.18.2",
|
||||
"rollup": "^1.17.0",
|
||||
"rollup": "^1.25.0",
|
||||
"rollup-plugin-babel": "^4.3.3",
|
||||
"rollup-plugin-commonjs": "^10.0.1",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"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.5.3",
|
||||
"ts-lit-plugin": "^1.1.9",
|
||||
"typescript": "^3.6.4",
|
||||
"typescript-styled-plugin": "^0.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^2.5.3",
|
||||
"bowser": "^2.5.2",
|
||||
"custom-card-helpers": "^1.2.2",
|
||||
"@ctrl/tinycolor": "^2.5.4",
|
||||
"bowser": "^2.7.0",
|
||||
"custom-card-helpers": "^1.2.7",
|
||||
"home-assistant-js-websocket": "^3.4.0",
|
||||
"lit-element": "^2.2.0",
|
||||
"lit-html": "^1.1.1"
|
||||
"lit-element": "^2.2.1",
|
||||
"lit-html": "^1.1.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,9 @@ export default {
|
|||
exclude: 'node_modules/**',
|
||||
}),
|
||||
terser({
|
||||
mangle: {
|
||||
safari10: true,
|
||||
},
|
||||
output: {
|
||||
comments: function (node, comment) {
|
||||
var text = comment.value;
|
||||
|
|
|
@ -22,12 +22,14 @@ import {
|
|||
timerTimeRemaining,
|
||||
secondsToDuration,
|
||||
durationToSeconds,
|
||||
// Still not working...
|
||||
// longPress,
|
||||
createThing,
|
||||
} from 'custom-card-helpers';
|
||||
import { BUTTON_CARD_VERSION } from './version-const';
|
||||
import {
|
||||
ButtonCardConfig,
|
||||
StateConfig,
|
||||
ExemptionUserConfig,
|
||||
ExemptionUsernameConfig,
|
||||
} from './types';
|
||||
import { longPress } from './long-press';
|
||||
import {
|
||||
|
@ -44,6 +46,13 @@ import {
|
|||
import { styles } from './styles';
|
||||
import myComputeStateDisplay from './compute_state_display';
|
||||
|
||||
/* eslint no-console: 0 */
|
||||
console.info(
|
||||
`%c BUTTON-CARD \n%c Version ${BUTTON_CARD_VERSION} `,
|
||||
'color: orange; font-weight: bold; background: black',
|
||||
'color: white; font-weight: bold; background: dimgray',
|
||||
);
|
||||
|
||||
@customElement('button-card')
|
||||
class ButtonCard extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
@ -54,6 +63,8 @@ class ButtonCard extends LitElement {
|
|||
|
||||
@property() private _hasTemplate?: boolean;
|
||||
|
||||
@property() private _stateObj: HassEntity | undefined;
|
||||
|
||||
private _interval?: number;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
|
@ -78,6 +89,7 @@ class ButtonCard extends LitElement {
|
|||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
this._stateObj = this.config!.entity ? this.hass!.states[this.config!.entity] : undefined;
|
||||
if (!this.config || !this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
@ -85,8 +97,6 @@ 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 = this._hasTemplate
|
||||
|| this.config!.state
|
||||
&& this.config!.state.find(elt => elt.operator === 'template')
|
||||
|
@ -486,16 +496,25 @@ class ButtonCard extends LitElement {
|
|||
): TemplateResult {
|
||||
let result = html``;
|
||||
const fields: any = {};
|
||||
const cards: any = {};
|
||||
if (this.config!.custom_fields) {
|
||||
Object.keys(this.config!.custom_fields).forEach((key) => {
|
||||
const value = this.config!.custom_fields![key];
|
||||
fields[key] = this._getTemplateOrValue(state, value);
|
||||
if (!value.card) {
|
||||
fields[key] = this._getTemplateOrValue(state, value);
|
||||
} else {
|
||||
cards[key] = value.card;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (configState && configState.custom_fields) {
|
||||
Object.keys(configState.custom_fields).forEach((key) => {
|
||||
const value = configState!.custom_fields![key];
|
||||
fields[key] = this._getTemplateOrValue(state, value);
|
||||
if (!value!.card) {
|
||||
fields[key] = this._getTemplateOrValue(state, value);
|
||||
} else {
|
||||
cards[key] = value.card;
|
||||
}
|
||||
});
|
||||
}
|
||||
Object.keys(fields).forEach((key) => {
|
||||
|
@ -508,6 +527,18 @@ class ButtonCard extends LitElement {
|
|||
<div id=${key} class="ellipsis" style=${styleMap(customStyle)}>${unsafeHTML(fields[key])}</div>`;
|
||||
}
|
||||
});
|
||||
Object.keys(cards).forEach((key) => {
|
||||
if (cards[key] != undefined) {
|
||||
const customStyle: StyleInfo = {
|
||||
...this._buildCustomStyleGeneric(state, configState, key),
|
||||
'grid-area': key,
|
||||
};
|
||||
const thing = createThing(cards[key]);
|
||||
thing.hass = this.hass;
|
||||
result = html`${result}
|
||||
<div id=${key} class="ellipsis" @click=${this._stopPropagation} @touchstart=${this._stopPropagation} style=${styleMap(customStyle)}>${thing}</div>`;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -516,13 +547,13 @@ class ButtonCard extends LitElement {
|
|||
if (
|
||||
this.config!.tap_action!.action === 'toggle'
|
||||
&& this.config!.hold_action!.action === 'none'
|
||||
&& this.config!.dbltap_action!.action === 'none'
|
||||
&& this.config!.double_tap_action!.action === 'none'
|
||||
|
||||
|| this.config!.hold_action!.action === 'toggle'
|
||||
&& this.config!.tap_action!.action === 'none'
|
||||
&& this.config!.dbltap_action!.action === 'none'
|
||||
&& this.config!.double_tap_action!.action === 'none'
|
||||
|
||||
|| this.config!.dbltap_action!.action === 'toggle'
|
||||
|| this.config!.double_tap_action!.action === 'toggle'
|
||||
&& this.config!.tap_action!.action === 'none'
|
||||
&& this.config!.hold_action!.action === 'none'
|
||||
) {
|
||||
|
@ -543,7 +574,7 @@ class ButtonCard extends LitElement {
|
|||
} else if (
|
||||
this.config!.tap_action!.action != 'none'
|
||||
|| this.config!.hold_action!.action != 'none'
|
||||
|| this.config!.dbltap_action!.action != 'none'
|
||||
|| this.config!.double_tap_action!.action != 'none'
|
||||
) {
|
||||
clickable = true;
|
||||
} else {
|
||||
|
@ -552,7 +583,7 @@ class ButtonCard extends LitElement {
|
|||
return clickable;
|
||||
}
|
||||
|
||||
private _rotate(configState: StateConfig | undefined): Boolean {
|
||||
private _rotate(configState: StateConfig | undefined): boolean {
|
||||
return configState && configState.spin ? true : false;
|
||||
}
|
||||
|
||||
|
@ -572,18 +603,17 @@ class ButtonCard extends LitElement {
|
|||
}
|
||||
|
||||
private _cardHtml(): TemplateResult {
|
||||
const state = this.config!.entity ? this.hass!.states[this.config!.entity] : undefined;
|
||||
const configState = this._getMatchingConfigState(state);
|
||||
const color = this._buildCssColorAttribute(state, configState);
|
||||
const configState = this._getMatchingConfigState(this._stateObj);
|
||||
const color = this._buildCssColorAttribute(this._stateObj, configState);
|
||||
let buttonColor = color;
|
||||
let cardStyle: any = {};
|
||||
let lockStyle: any = {};
|
||||
const aspectRatio: any = {};
|
||||
const lockStyleFromConfig = this._buildStyleGeneric(state, configState, 'lock');
|
||||
const configCardStyle = this._buildStyleGeneric(state, configState, 'card');
|
||||
const lockStyleFromConfig = this._buildStyleGeneric(this._stateObj, configState, 'lock');
|
||||
const configCardStyle = this._buildStyleGeneric(this._stateObj, configState, 'card');
|
||||
const classList: ClassInfo = {
|
||||
'button-card-main': true,
|
||||
disabled: !this._isClickable(state),
|
||||
disabled: !this._isClickable(this._stateObj),
|
||||
};
|
||||
if (configCardStyle.width) {
|
||||
this.style.setProperty('flex', '0 0 auto');
|
||||
|
@ -612,8 +642,8 @@ class ButtonCard extends LitElement {
|
|||
} else {
|
||||
aspectRatio.display = 'inline';
|
||||
}
|
||||
this.style.setProperty('--button-card-light-color', this._getColorForLightEntity(state, true));
|
||||
this.style.setProperty('--button-card-light-color-no-temperature', this._getColorForLightEntity(state, false));
|
||||
this.style.setProperty('--button-card-light-color', this._getColorForLightEntity(this._stateObj, true));
|
||||
this.style.setProperty('--button-card-light-color-no-temperature', this._getColorForLightEntity(this._stateObj, false));
|
||||
lockStyle = { ...lockStyle, ...lockStyleFromConfig };
|
||||
|
||||
return html`
|
||||
|
@ -625,28 +655,35 @@ class ButtonCard extends LitElement {
|
|||
@ha-click="${this._handleTap}"
|
||||
@ha-hold="${this._handleHold}"
|
||||
@ha-dblclick=${this._handleDblTap}
|
||||
.hasDblClick=${this.config!.dbltap_action!.action !== 'none'}
|
||||
.hasDblClick=${this.config!.double_tap_action!.action !== 'none'}
|
||||
.repeat=${ifDefined(this.config!.hold_action!.repeat)}
|
||||
.longpress=${longPress()}
|
||||
.config="${this.config}"
|
||||
>
|
||||
${this._buttonContent(this._stateObj, configState, buttonColor)}
|
||||
${this._getLock(lockStyle)}
|
||||
${this._buttonContent(state, configState, buttonColor)}
|
||||
${this.config!.lock ? '' : html`<mwc-ripple id="ripple"></mwc-ripple>`}
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getLock(lockStyle: StyleInfo): TemplateResult {
|
||||
if (this.config!.lock) {
|
||||
if (this.config!.lock
|
||||
&& this._getTemplateOrValue(this._stateObj, this.config!.lock.enabled)) {
|
||||
return html`
|
||||
<div id="overlay" style=${styleMap(lockStyle)} @click=${this._handleLock} @touchstart=${this._handleLock}>
|
||||
<div id="overlay" style=${styleMap(lockStyle)}
|
||||
@ha-click=${ev => this._handleUnlockType(ev, 'tap')}
|
||||
@ha-hold=${ev => this._handleUnlockType(ev, 'hold')}
|
||||
@ha-dblclick=${ev => this._handleUnlockType(ev, 'double_tap')}
|
||||
.hasDblClick=${this.config!.lock!.unlock === 'double_tap'}
|
||||
.longpress=${longPress()}
|
||||
.config="${this.config}"
|
||||
>
|
||||
<ha-icon id="lock" icon="mdi:lock-outline"></ha-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html``;
|
||||
return html`<mwc-ripple id="ripple"></mwc-ripple>`;
|
||||
}
|
||||
|
||||
private _buttonContent(
|
||||
|
@ -760,7 +797,7 @@ class ButtonCard extends LitElement {
|
|||
this.config = {
|
||||
tap_action: { action: 'toggle' },
|
||||
hold_action: { action: 'none' },
|
||||
dbltap_action: { action: 'none' },
|
||||
double_tap_action: { action: 'none' },
|
||||
layout: 'vertical',
|
||||
size: '40%',
|
||||
color_type: 'icon',
|
||||
|
@ -772,6 +809,12 @@ class ButtonCard extends LitElement {
|
|||
show_entity_picture: false,
|
||||
...template,
|
||||
};
|
||||
this.config.lock = {
|
||||
enabled: false,
|
||||
duration: 5,
|
||||
unlock: 'tap',
|
||||
...this.config.lock,
|
||||
};
|
||||
this.config!.default_color = 'var(--primary-text-color)';
|
||||
if (this.config!.color_type !== 'icon') {
|
||||
this.config!.color_off = 'var(--paper-card-background-color)';
|
||||
|
@ -809,67 +852,83 @@ class ButtonCard extends LitElement {
|
|||
return configEval;
|
||||
};
|
||||
configDuplicate[action] = __evalObject(configDuplicate[action]);
|
||||
if (!configDuplicate[action].confirmation && configDuplicate.confirmation) {
|
||||
configDuplicate[action].confirmation = __evalObject(configDuplicate.confirmation);
|
||||
}
|
||||
return configDuplicate;
|
||||
}
|
||||
|
||||
private _handleTap(ev): void {
|
||||
/* eslint no-alert: 0 */
|
||||
if (this.config!.confirmation
|
||||
&& !window.confirm(this.config!.confirmation)) {
|
||||
return;
|
||||
}
|
||||
const config = ev.target.config;
|
||||
handleClick(this, this.hass!, this._evalActions(config, 'tap_action'), false, false);
|
||||
}
|
||||
|
||||
private _handleHold(ev): void {
|
||||
/* eslint no-alert: 0 */
|
||||
if (this.config!.confirmation
|
||||
&& !window.confirm(this.config!.confirmation)) {
|
||||
return;
|
||||
}
|
||||
const config = ev.target.config;
|
||||
handleClick(this, this.hass!, this._evalActions(config, 'hold_action'), true, false);
|
||||
}
|
||||
|
||||
private _handleDblTap(ev): void {
|
||||
/* eslint no-alert: 0 */
|
||||
if (this.config!.confirmation
|
||||
&& !window.confirm(this.config!.confirmation)) {
|
||||
return;
|
||||
}
|
||||
const config = ev.target.config;
|
||||
handleClick(this, this.hass!, this._evalActions(config, 'dbltap_action'), false, true);
|
||||
handleClick(this, this.hass!, this._evalActions(config, 'double_tap_action'), false, true);
|
||||
}
|
||||
|
||||
private _handleUnlockType(ev, type: string) {
|
||||
const config = ev.target.config as ButtonCardConfig;
|
||||
if (config.lock.unlock === type) {
|
||||
this._handleLock(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleLock(ev): void {
|
||||
ev.stopPropagation();
|
||||
if (this.config!.unlock_users) {
|
||||
if (!this.hass!.user.name) return;
|
||||
if (this.config!.unlock_users.indexOf(this.hass!.user.name) < 0) return;
|
||||
const lock = this.shadowRoot!.getElementById('lock') as LitElement;
|
||||
if (!lock) return;
|
||||
if (this.config!.lock!.exemptions) {
|
||||
if (!this.hass!.user.name || !this.hass!.user.id) return;
|
||||
let matched = false;
|
||||
this.config!.lock!.exemptions.forEach((e) => {
|
||||
if (!matched && (e as ExemptionUserConfig).user === this.hass!.user.id
|
||||
|| (e as ExemptionUsernameConfig).username === this.hass!.user.name) {
|
||||
matched = true;
|
||||
}
|
||||
});
|
||||
if (!matched) {
|
||||
lock.classList.add('invalid');
|
||||
window.setTimeout(() => {
|
||||
if (lock) {
|
||||
lock.classList.remove('invalid');
|
||||
}
|
||||
}, 3000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const overlay = this.shadowRoot!.getElementById('overlay') as LitElement;
|
||||
const haCard = this.shadowRoot!.getElementById('card') as LitElement;
|
||||
overlay.style.setProperty('pointer-events', 'none');
|
||||
const paperRipple = document.createElement('paper-ripple');
|
||||
|
||||
const lock = this.shadowRoot!.getElementById('lock') as LitElement;
|
||||
if (lock) {
|
||||
haCard.appendChild(paperRipple);
|
||||
const icon = document.createAttribute('icon');
|
||||
icon.value = 'mdi:lock-open-outline';
|
||||
lock.attributes.setNamedItem(icon);
|
||||
lock.classList.add('fadeOut');
|
||||
lock.classList.add('hidden');
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
overlay.style.setProperty('pointer-events', '');
|
||||
if (lock) {
|
||||
lock.classList.remove('fadeOut');
|
||||
lock.classList.remove('hidden');
|
||||
const icon = document.createAttribute('icon');
|
||||
icon.value = 'mdi:lock-outline';
|
||||
lock.attributes.setNamedItem(icon);
|
||||
haCard.removeChild(paperRipple);
|
||||
}
|
||||
}, 5000);
|
||||
}, this.config!.lock!.duration! * 1000);
|
||||
}
|
||||
|
||||
private _stopPropagation(ev) {
|
||||
ev.stopPropagation();
|
||||
console.log('BRRRR');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { directive, PropertyPart } from 'lit-html';
|
||||
import * as Bowser from 'bowser';
|
||||
import Bowser from 'bowser';
|
||||
// See https://github.com/home-assistant/home-assistant-polymer/pull/2457
|
||||
// on how to undo mwc -> paper migration
|
||||
// import '@material/mwc-ripple';
|
||||
|
|
|
@ -48,23 +48,19 @@ export const styles = css`
|
|||
display: flex;
|
||||
}
|
||||
#lock {
|
||||
-webkit-animation-duration: 5s;
|
||||
animation-duration: 5s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
margin: unset;
|
||||
}
|
||||
@keyframes fadeOut{
|
||||
0% {opacity: 0.5;}
|
||||
20% {opacity: 0;}
|
||||
80% {opacity: 0;}
|
||||
100% {opacity: 0.5;}
|
||||
.invalid {
|
||||
animation: blink 1s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite;
|
||||
}
|
||||
.fadeOut {
|
||||
-webkit-animation-name: fadeOut;
|
||||
animation-name: fadeOut;
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: visibility 0s 1s, opacity 1s linear;
|
||||
}
|
||||
@keyframes blink{
|
||||
@keyframes blink {
|
||||
0%{opacity:0;}
|
||||
50%{opacity:1;}
|
||||
100%{opacity:0;}
|
||||
|
|
22
src/types.ts
22
src/types.ts
|
@ -6,15 +6,14 @@ export interface ButtonCardConfig {
|
|||
entity?: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
color_type: 'icon' | 'card' | 'label-card' | 'blank-card'
|
||||
color_type: 'icon' | 'card' | 'label-card' | 'blank-card';
|
||||
color?: 'auto' | 'auto-no-temperature' | string;
|
||||
size: string;
|
||||
aspect_ratio?: string;
|
||||
lock: boolean;
|
||||
unlock_users?: string[];
|
||||
lock: LockConfig;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
dbltap_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
show_name?: boolean;
|
||||
show_state?: boolean;
|
||||
show_icon?: boolean;
|
||||
|
@ -45,6 +44,21 @@ export type Layout = 'vertical'
|
|||
| 'icon_state_name2nd'
|
||||
| 'icon_label';
|
||||
|
||||
export interface LockConfig {
|
||||
enabled: boolean;
|
||||
duration: number;
|
||||
unlock: 'tap' | 'double_tap' | 'hold';
|
||||
exemptions?: (ExemptionUserConfig | ExemptionUsernameConfig)[];
|
||||
}
|
||||
|
||||
export interface ExemptionUserConfig {
|
||||
user: string;
|
||||
}
|
||||
|
||||
export interface ExemptionUsernameConfig {
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface StateConfig {
|
||||
id?: string;
|
||||
operator?: '<' | '<=' | '==' | '>=' | '>' | '!=' | 'regex' | 'template' | 'default';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const BUTTON_CARD_VERSION = '2.0.5';
|
|
@ -23,8 +23,9 @@
|
|||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
},
|
||||
"include": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue