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:
Jérôme W 2019-10-21 10:17:44 +02:00
parent 5ef600a6e0
commit b1fa112fbc
15 changed files with 746 additions and 2819 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
/node_modules/
yarn.lock
.rpt2_cache/
/dist/*.map
/dist/**

305
README.md
View File

@ -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
```

2227
dist/button-card.js vendored

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

9
hooks/bump-version.sh Executable file
View File

@ -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

View File

@ -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

782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -23,6 +23,9 @@ export default {
exclude: 'node_modules/**',
}),
terser({
mangle: {
safari10: true,
},
output: {
comments: function (node, comment) {
var text = comment.value;

View File

@ -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');
}
}

View File

@ -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';

View File

@ -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;}

View File

@ -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';

1
src/version-const.ts Normal file
View File

@ -0,0 +1 @@
export const BUTTON_CARD_VERSION = '2.0.5';

View File

@ -23,8 +23,9 @@
"resolveJsonModule": true,
"experimentalDecorators": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
},
"include": [
"src/*"
]
}
}