├── .eslintrc.js
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── build.yml
│ ├── release.yml
│ └── validate.yaml
├── .gitignore
├── .nvmrc
├── .prettierrc.js
├── CONTRIBUTE.md
├── LICENSE
├── README.md
├── assets
├── card-editor.png
├── examples
│ ├── action-custom-icon.png
│ ├── action-custom.png
│ ├── action-minimal.png
│ ├── climate.gif
│ ├── cover.gif
│ ├── fan.gif
│ ├── general-compact.png
│ ├── general-minimal.png
│ ├── icon-icon-override.png
│ ├── icon-minimal.png
│ ├── lock.gif
│ ├── media.gif
│ ├── slider-force-square.png
│ ├── slider-minimal.png
│ ├── slider-show-track.png
│ ├── slider-state-color.png
│ └── switch.gif
├── grid-full-width.png
├── grid-not-square.png
├── preview-2.gif
└── preview.gif
├── elements
├── formfield.js
├── ignore
│ ├── switch.js
│ └── textfield.js
├── switch.js
└── textfield.js
├── hacs.json
├── package-lock.json
├── package.json
├── rollup-plugins
├── ignore.js
└── ignoreWrapper.js
├── rollup.config.dev.js
├── rollup.config.js
├── src
├── action-handler-directive.ts
├── const.ts
├── controllers
│ ├── automation-controller.ts
│ ├── climate-controller.ts
│ ├── controller.ts
│ ├── cover-controller.ts
│ ├── fan-controller.ts
│ ├── get-controller.ts
│ ├── input-boolean-controller.ts
│ ├── input-number-controller.ts
│ ├── light-controller.ts
│ ├── lock-controller.ts
│ ├── media-controller.ts
│ ├── number-controller.ts
│ └── switch-controller.ts
├── editor.ts
├── localize
│ ├── languages
│ │ ├── de.json
│ │ ├── en.json
│ │ ├── fr.json
│ │ ├── he.json
│ │ ├── ko.json
│ │ ├── nl.json
│ │ ├── pl.json
│ │ ├── pt.json
│ │ ├── ru.json
│ │ └── sk.json
│ └── localize.ts
├── slider-button-card.ts
├── types.ts
└── utils.ts
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser
3 | parserOptions: {
4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
5 | sourceType: "module" // Allows for the use of imports
6 | },
7 | extends: [
8 | "plugin:@typescript-eslint/recommended" // Uses the recommended rules from the @typescript-eslint/eslint-plugin
9 | ],
10 | rules: {
11 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
12 | // e.g. "@typescript-eslint/explicit-function-return-type": "off",
13 | }
14 | };
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [mattieha]
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 | ---
8 |
9 |
14 |
15 | **Checklist:**
16 |
17 | - [ ] I updated to the latest version available
18 | - [ ] I cleared the cache of my browser
19 |
20 | **Release with the issue:**
21 |
22 | **Last working release (if known):**
23 |
24 | **Browser and Operating System:**
25 |
26 |
29 |
30 | **Description of problem:**
31 |
32 |
35 |
36 | **Javascript errors shown in the web inspector (if applicable):**
37 |
38 | ```
39 |
40 | ```
41 |
42 | **Additional information:**
43 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: feature request
6 | assignees: ''
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: 'Build'
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | build:
13 | name: Test build
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v1
17 | - name: Build
18 | run: |
19 | npm install
20 | npm run build
21 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | release:
9 | name: Prepare release
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v1
13 |
14 | # Build
15 | - name: Build the file
16 | run: |
17 | cd /home/runner/work/slider-button-card/slider-button-card
18 | npm install
19 | npm run build
20 |
21 | # Upload build file to the releas as an asset.
22 | - name: Upload zip to release
23 | uses: svenstaro/upload-release-action@v2-release
24 |
25 | with:
26 | repo_token: ${{ secrets.GITHUB_TOKEN }}
27 | file: /home/runner/work/slider-button-card/slider-button-card/dist/slider-button-card.js
28 | asset_name: slider-button-card.js
29 | tag: ${{ github.ref }}
30 | overwrite: true
31 |
--------------------------------------------------------------------------------
/.github/workflows/validate.yaml:
--------------------------------------------------------------------------------
1 | name: Validate
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: "0 0 * * *"
8 |
9 | jobs:
10 | validate:
11 | runs-on: "ubuntu-latest"
12 | steps:
13 | - uses: "actions/checkout@v2"
14 | - name: HACS validation
15 | uses: "hacs/action@main"
16 | with:
17 | CATEGORY: "plugin"
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist/
2 | /node_modules/
3 | /.rpt2_cache/
4 | /.idea/
5 | /.vscode/
6 | /.devcontainer/
7 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v18.17.1
2 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | semi: true,
3 | trailingComma: 'all',
4 | singleQuote: true,
5 | printWidth: 120,
6 | tabWidth: 2,
7 | };
--------------------------------------------------------------------------------
/CONTRIBUTE.md:
--------------------------------------------------------------------------------
1 | # Contributing to the Slider button card by
2 |
3 | Thanks for taking the time to contribute to this card.
4 |
5 | ## Adding a new translation
6 | Steps for adding a new translation:
7 | ### Json translation file
8 | Translation files are located under `src/localize/languages`
9 | 1. Make a copy of `en.json`
10 | 2. Rename it to your two characters language `xx.json`
11 | 3. Translate only the values
12 | ### Make it available
13 | For the card to be able to use the translation it needs to be added to `src/localize/localize.ts`
14 | 1. Add an import statement: `import * as xx from './languages/xx.json';`
15 | 2. Add it to the language variable
16 | ```` typescript
17 | const languages: any = {
18 | en: en,
19 | he: he,
20 | nl: nl,
21 | pl: pl,
22 | ru: ru,
23 | xx: xx, // Add alphabeticly (without this comment)
24 | };
25 | ````
26 |
27 | ### Update Readme
28 | Add your translation to the `README.md` under the languages section:
29 | ```markdown
30 | - English
31 | - Hebrew
32 | - Nederlands (Dutch)
33 | - Polish (polski)
34 | - Russian
35 | - Your new language
36 | ```
37 |
38 | ### Create a PR
39 | Commit your changes and create a [Pull Request](https://github.com/mattieha/slider-button-card/pulls)
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Custom cards for Home Assistant
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Slider button card by [@mattieha](https://www.github.com/mattieha)
2 | [![GitHub Release][releases-shield]][releases]
3 | [](https://github.com/custom-components/hacs)
4 |
5 | A button card with integrated slider for `automation, light, switch, fan, cover, input_boolean, input_number, media_player, number, climate, lock` entities.
6 |
7 | ![Preview][preview]
8 | ![Preview 2][preview-2]
9 |
10 | #### Please ⭐️ this repo if you find it useful
11 |
12 | ## TOC
13 | - [Installation](#installation)
14 | - [HACS](#hacs)
15 | - [Manual](#manual)
16 | - [Configuration](#configuration)
17 | - [Visual Editor](#visual-editor)
18 | - [Options](#options)
19 | - [Icon options](#icon-options)
20 | - [Slider options](#slider-options)
21 | - [Action button options](#action-button-options)
22 | - [Tap action](#action-options)
23 | - [Styles](#styles)
24 | - [Examples](#examples)
25 | - [Minimal working config](#minimal-working-config)
26 | - [Per feature](#per-feature)
27 | - [General](#general)
28 | - [Icon](#icon)
29 | - [Action button](#action-button)
30 | - [Slider](#slider)
31 | - [Full examples](#full-examples)
32 | - [Fan](#fan)
33 | - [Switch](#switch)
34 | - [Cover](#cover)
35 | - [Media player](#media-player)
36 | - [Climate](#climate)
37 | - [Lock](#lock)
38 | - [In a grid](#grid)
39 | - [Group support](#groups)
40 | - [Known issues](#known-issues)
41 | - [Languages](#languages)
42 | - [Credits](#credits)
43 |
44 | ## Installation
45 |
46 | ### HACS
47 | This card is available in [HACS][hacs] (Home Assistant Community Store).
48 | Just search for `Slider Button Card` in Frontend tab.
49 |
50 | ### Manual
51 |
52 | 1. Download `slider-button-card.js` file from the [latest-release].
53 | 2. Put `slider-button-card.js` file into your `config/www` folder.
54 | 3. Go to _Configuration_ → _Lovelace Dashboards_ → _Resources_ → Click Plus button → Set _Url_ as `/local/slider-button-card.js` → Set _Resource type_ as `JavaScript Module`.
55 | 4. Add `custom:slider-button-card` to Lovelace UI as any other card (using either editor or YAML configuration).
56 |
57 | ## Configuration
58 |
59 | ### Visual Editor
60 |
61 | Slider Button Card supports Lovelace's Visual Editor.
62 |
63 | Show screenshot
64 |
65 | ![Visual Editor][visual-editor]
66 |
67 |
68 |
69 | ### Options
70 |
71 | | Name | Type | Requirement | Description | Default |
72 | | ----------------- | ------- | ------------ | ------------------------------------------- | ------------------- |
73 | | type | string | **Required** | `custom:slider-button-card` |
74 | | entity | string | **Required** | HA entity ID from domain `automation, light, switch, fan, cover, input_boolean, input_number, media_player, number climate, lock` | |
75 | | name | string | **Optional** | Name | `entity.friendly_name` |
76 | | show_attribute | boolean | **Optional** | Show attribute | `false` (except for `media_player` entities) |
77 | | show_name | boolean | **Optional** | Show name | `true` |
78 | | show_state | boolean | **Optional** | Show state | `true` |
79 | | compact | boolean | **Optional** | Compact mode, display name and state inline with icon. Useful for full width cards. | `false` |
80 | | attribute | string | **Optional** | Name of the attribute to display if `show_attribute` is `true`.
81 | | icon | object | **Optional** | [Icon options](#icon-options) | |
82 | | slider | object | **Optional** | [Slider options](#slider-options) | |
83 | | action_button | object | **Optional** | [Action button options](#action-button-options) | |
84 |
85 | ### Icon Options
86 |
87 | | Name | Type | Requirement | Description | Default |
88 | | ----------------- | ------- | ------------ | ------------------------------------------- | ------------------- |
89 | | icon | string | **Optional** | Icon | `default entity icon` |
90 | | show | boolean | **Optional** | Show icon | `true` |
91 | | use_state_color | boolean | **Optional** | Use state color | `true` |
92 | | tap_action | object | **Optional** | [Action](#action-options) to take on tap | `action: more-info` |
93 |
94 | ### Slider Options
95 |
96 | | Name | Type | Requirement | Description | Default |
97 | | ----------------- | ------- | ------------ | ------------------------------------------- | ------------------- |
98 | | direction | string | **Optional** | Direction `left-right, right-left, top-bottom, bottom-top` | `left-right` |
99 | | background | string | **Optional** | Background `solid, gradient, triangle, striped, custom` | `gradient` |
100 | | use_state_color | boolean | **Optional** | Use state color | `true` |
101 | | use_percentage_bg_opacity | boolean | **Optional** | Apply opacity to background based on percentage | `true` |
102 | | show_track | boolean | **Optional** | Show track when state is on | `false` |
103 | | force_square | boolean | **Optional** | Force the button as a square | `false` |
104 | | toggle_on_click | boolean | **Optional** | Force the slider to act as a toggle, if `true` sliding is disabled | `false` |
105 | | attribute | string | **Optional** | Control an [attribute](#attributes) for `light` or `cover` entities | |
106 | | invert | boolean | **Optional** | Invert calculation of state and percentage, useful for `cover` entities | `false` `true` for `cover` |
107 |
108 | ### Attributes
109 | Light:
110 | - `brightness_pct` **default**
111 | - `brightness`
112 | - `color_temp`
113 | - `hue`
114 | - `saturation`
115 |
116 | _Warning options other than `brightness_pct` and `brightness` may give strange results_
117 |
118 | For example when `color_temp` is selected as attribute and the current `color_mode` of the light is **not** `color_temp` there is no value available for the slider, so the min value will be displayed. Same for `hue` and `saturation`, slider will only show correct value when the `color_mode` is `hs`.
119 |
120 | Cover:
121 | - `position` **default**
122 | - `tilt`
123 | ### Action button Options
124 |
125 | | Name | Type | Requirement | Description | Default |
126 | | ----------------- | ------- | ------------ | ------------------------------------------- | ------------------- |
127 | | mode | string | **Optional** | Mode `toggle, custom` | `toggle` |
128 | | show | boolean | **Optional** | Show the action button | `true` |
129 | | icon | string | **Optional** | Icon when mode is `custom` | `mdi:power` |
130 | | show_spinner | boolean | **Optional** | Show spinner when mode is `custom` | `true` |
131 | | tap_action | object | **Optional** | [Action](#action-options) to take on tap | `action: toggle` |
132 |
133 | ### Action Options
134 |
135 | | Name | Type | Requirement | Description | Default |
136 | | --------------- | ------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
137 | | action | string | **Required** | Action to perform (more-info, toggle, call-service, navigate url, none) | `more-info` |
138 | | navigation_path | string | **Optional** | Path to navigate to (e.g. /lovelace/0/) when action defined as navigate | `none` |
139 | | url | string | **Optional** | URL to open on click when action is url. The URL will open in a new tab | `none` |
140 | | service | string | **Optional** | Service to call (e.g. media_player.media_play_pause) when action defined as call-service | `none` |
141 | | service_data | object | **Optional** | Service data to include (e.g. entity_id: media_player.bedroom) when action defined as call-service | `none` |
142 | | haptic | string | **Optional** | Haptic feedback for the [Beta IOS App](http://home-assistant.io/ios/beta) _success, warning, failure, light, medium, heavy, selection_ | `none` |
143 | | repeat | number | **Optional** | How often to repeat the `hold_action` in milliseconds. | `non` |
144 | ### Styles
145 | Custom styles can be set by using [Card mod](https://github.com/thomasloven/lovelace-card-mod)
146 | ```yaml
147 | style: |
148 | :host {
149 | --VARIABLE: VALUE;
150 | }
151 | ```
152 |
153 | | Variable | Description | Default |
154 | | ----------------------- | ------------------------------------------- | ------------------- |
155 | | `--icon-color` | Color of the icon when `icon.use_state_color === false` | `var(--paper-item-icon-color)` |
156 | | `--label-color-on` | Color of the label when state is on | `var(--primary-text-color, white)` |
157 | | `--label-color-off` | Color of the label when state is off | `var(--primary-text-color, white)` |
158 | | `--state-color-on` | Color of the state value when state is on | `var(--label-badge-text-color, white)` |
159 | | `--state-color-off` | Color of the state value when state is off | `var(--disabled-text-color)` |
160 | | `--action-icon-color-on` | Color of the action button icon when state is on | `var(--paper-item-icon-color, black)` |
161 | | `--action-icon-color-off` | Color of the action button icon when state is off | `var(--paper-item-icon-color, black)` |
162 | | `--action-spinner-color` | Color of the spinner action button | `var(--label-badge-text-color, white)` |
163 |
164 | ## Examples
165 |
166 | ### Minimal working config
167 |
168 |
169 |
170 | Minimal working config
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | ```yaml
179 | type: custom:slider-button-card
180 | entity: light.couch
181 | slider:
182 | direction: left-right
183 | background: gradient
184 | icon:
185 | tap_action:
186 | action: more-info
187 | action_button:
188 | mode: toggle
189 | ```
190 |
191 |
192 |
193 |
194 | ### Per feature
195 |
196 | #### General
197 |
198 |
199 |
200 |
201 | Compact, best used in full width (not in grid)
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 | ```yaml
210 | compact: true
211 | ```
212 |
213 |
214 |
215 |
216 | #### Icon
217 |
218 |
219 |
220 |
221 | Minimal config
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 | ```yaml
230 | icon:
231 | tap_action:
232 | action: more-info
233 | ```
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 | Icon override
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 | ```yaml
250 | icon:
251 | icon: mdi:lightbulb
252 | tap_action:
253 | action: more-info
254 | ```
255 |
256 |
257 |
258 |
259 |
260 | #### Action button
261 |
262 |
263 |
264 |
265 | Minimal config
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 | ```yaml
274 | action_button:
275 | mode: toggle
276 | show: true
277 | ```
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 | Custom
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 | ```yaml
294 | action_button:
295 | mode: custom
296 | show: true
297 | tap_action:
298 | action: toggle
299 | ```
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 | Custom icon and tap action
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 | ```yaml
316 | action_button:
317 | mode: custom
318 | show: true
319 | icon: mdi:palette
320 | tap_action:
321 | action: call-service
322 | service: scene.turn_on
323 | service_data:
324 | entity_id: scene.test
325 | ```
326 |
327 |
328 |
329 |
330 | #### Slider
331 |
332 |
333 |
334 |
335 | Minimal config
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 | ```yaml
344 | slider:
345 | direction: left-right
346 | background: gradient
347 | ```
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 | Background uses color or color_temp if available
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 | ```yaml
364 | slider:
365 | direction: left-right
366 | background: gradient
367 | use_state_color: true
368 | ```
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 | Show track, best used in full width or triangle
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 | ```yaml
385 | slider:
386 | direction: left-right
387 | background: triangle
388 | use_state_color: true
389 | show_track: true
390 | ```
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 | Force square
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 | ```yaml
407 | slider:
408 | direction: left-right
409 | background: triangle
410 | use_state_color: true
411 | show_track: true
412 | force_square: true
413 | ```
414 |
415 |
416 |
417 |
418 |
419 |
420 | ### Full examples
421 | #### Fan
422 | For fan entities the icon auto rotates based on the speed of the fan.
423 |
424 |
425 |
426 | Icon rotate animation
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 | ```yaml
435 | type: custom:slider-button-card
436 | entity: fan.living_fan
437 | slider:
438 | direction: left-right
439 | background: triangle
440 | show_track: true
441 | icon:
442 | tap_action:
443 | action: more-info
444 | action_button:
445 | tap_action:
446 | action: toggle
447 | mode: custom
448 | name: Fan
449 | ```
450 |
451 |
452 |
453 |
454 | #### Switch
455 | Use `slider.toggle_on_click: true` so the slider acts as a toggle (sliding is disabled).
456 |
457 |
458 |
459 | Toggle on click
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 | ```yaml
468 | type: custom:slider-button-card
469 | entity: switch.socket
470 | slider:
471 | direction: left-right
472 | background: custom
473 | toggle_on_click: true
474 | icon:
475 | use_state_color: true
476 | tap_action:
477 | action: more-info
478 | action_button:
479 | tap_action:
480 | action: toggle
481 | mode: custom
482 | name: Switch
483 | ```
484 |
485 |
486 |
487 |
488 | #### Cover
489 | For most use cases: set `slider.direction: top-bottom` and `slider.background: striped`;
490 |
491 |
492 |
493 | Direction top to bottom, custom action icon
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 | ```yaml
502 | type: custom:slider-button-card
503 | entity: cover.living_cover
504 | slider:
505 | direction: top-bottom
506 | background: striped
507 | icon:
508 | show: true
509 | tap_action:
510 | action: more-info
511 | action_button:
512 | tap_action:
513 | action: toggle
514 | mode: custom
515 | icon: mdi:swap-vertical
516 | name: Cover
517 | ```
518 |
519 |
520 |
521 |
522 | #### Media player
523 | Default behavior: slider is used for volume control, when there is an entity picture it will be used instead of the icon.
524 | In this example the action button is used to toggle play/pause.
525 |
526 |
527 |
528 | Action button to toggle play/pause
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 | ```yaml
537 | type: custom:slider-button-card
538 | entity: media_player.spotify_mha
539 | slider:
540 | direction: left-right
541 | background: triangle
542 | show_track: true
543 | icon:
544 | tap_action:
545 | action: more-info
546 | action_button:
547 | mode: custom
548 | icon: mdi:play-pause
549 | tap_action:
550 | action: call-service
551 | service: media_player.media_play_pause
552 | service_data:
553 | entity_id: media_player.spotify_mha
554 | name: Media
555 |
556 | ```
557 |
558 |
559 |
560 |
561 | #### Climate
562 | Default behavior: slider is used to set target temperature, it doesn't alter state.
563 |
564 |
565 |
566 | Target temperature and state disabled in card
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 | ```yaml
575 | type: custom:slider-button-card
576 | entity: climate.harmony_climate_controller
577 | slider:
578 | direction: left-right
579 | background: triangle
580 | show_track: true
581 | icon:
582 | tap_action:
583 | action: more-info
584 | action_button:
585 | mode: custom
586 | tap_action:
587 | action: toggle
588 | name: Airco
589 |
590 | ```
591 |
592 |
593 |
594 |
595 | #### Lock
596 | Default behavior: `slider.toggle_on_click: true`
597 |
598 |
599 |
600 | Action button hidden
601 |
602 |
603 |
604 |
605 |
606 |
607 |
608 | ```yaml
609 | type: custom:slider-button-card
610 | entity: lock.virtual_lock
611 | slider:
612 | direction: left-right
613 | background: solid
614 | toggle_on_click: true
615 | icon:
616 | use_state_color: true
617 | tap_action:
618 | action: more-info
619 | action_button:
620 | show: false
621 | name: Lock
622 | ```
623 |
624 |
625 |
626 |
627 | #### Grid
628 |
629 |
630 |
631 |
632 | 4 columns, square: false
633 |
634 |
635 |
636 |
637 |
638 |
639 |
640 | ```yaml
641 | type: grid
642 | cards:
643 | - type: custom:slider-button-card
644 | entity: light.couch
645 | slider:
646 | direction: left-right
647 | background: gradient
648 | use_state_color: true
649 | icon:
650 | tap_action:
651 | action: more-info
652 | use_state_color: true
653 | action_button:
654 | mode: toggle
655 | - type: custom:slider-button-card
656 | entity: switch.socket
657 | slider:
658 | direction: left-right
659 | background: custom
660 | toggle_on_click: true
661 | icon:
662 | use_state_color: true
663 | tap_action:
664 | action: more-info
665 | action_button:
666 | tap_action:
667 | action: toggle
668 | mode: toggle
669 | name: Switch
670 | - type: custom:slider-button-card
671 | entity: fan.living_fan
672 | slider:
673 | direction: left-right
674 | background: triangle
675 | show_track: true
676 | icon:
677 | tap_action:
678 | action: more-info
679 | action_button:
680 | tap_action:
681 | action: toggle
682 | mode: custom
683 | name: Fan
684 | - type: custom:slider-button-card
685 | entity: cover.living_cover
686 | slider:
687 | direction: top-bottom
688 | background: striped
689 | icon:
690 | show: true
691 | tap_action:
692 | action: more-info
693 | action_button:
694 | tap_action:
695 | action: toggle
696 | mode: custom
697 | icon: mdi:swap-vertical
698 | name: Cover
699 | square: false
700 | columns: 4
701 |
702 | ```
703 |
704 |
705 |
706 |
707 | ## Groups
708 | Mixed `group` entities are not supported, if you want to control multiple
709 | - lights use [Light group](https://www.home-assistant.io/integrations/light.group/)
710 | - covers use [Cover group](https://www.home-assistant.io/integrations/cover.group/)
711 | - media players use [Media player group](https://www.home-assistant.io/integrations/media_player.group/)
712 |
713 | ## Known issues
714 | When you discover any bugs please open an [issue](https://github.com/custom-cards/slider-button-card/issues).
715 |
716 | ### Input Number & Number entities
717 | - If the `input_number.entity.min value` is not cleanly divisible by the `input_number.entity.step value`, then the slider card is off by an amount. If your `input_number` has `min = 5`, `max = 25`, `step = 5` then it will work just fine. But if the `step` is 2, then it will be off. This also has the side effect of changing the `input_number` to an "out of bounds" value when modified via this card. Using `step = 1` avoids this problem.
718 | - The same limitation applies to `number` entities.
719 |
720 | ## Languages
721 |
722 | This card supports translations. Please, help to add more translations and improve existing ones. Here's a list of supported languages:
723 |
724 | - English
725 | - French
726 | - German
727 | - Hebrew
728 | - Korean
729 | - Nederlands (Dutch)
730 | - Polish (polski)
731 | - Portuguese
732 | - Russian
733 | - Slovak
734 | - [_Your language?_][add-translation]
735 |
736 | ## Credits
737 | - Originally inspired by [Slider entity row](https://github.com/thomasloven/lovelace-slider-entity-row)
738 | - Forked from [Slider button card](https://github.com/mattieha/slider-button-card/) by [@mattieha](https://www.github.com/mattieha)
739 |
740 | ---
741 | [](https://www.buymeacoffee.com/mattijsha)
742 |
743 |
744 | [hacs]: https://hacs.xyz
745 | [add-translation]: https://github.com/custom-cards/slider-button-card/blob/main/CONTRIBUTE.md#adding-a-new-translation
746 | [visual-editor]: https://raw.githubusercontent.com/custom-cards/slider-button-card/main/assets/card-editor.png
747 | [preview]: https://raw.githubusercontent.com/custom-cards/slider-button-card/main/assets/preview.gif
748 | [preview-2]: https://raw.githubusercontent.com/custom-cards/slider-button-card/main/assets/preview-2.gif
749 | [grid]: https://raw.githubusercontent.com/custom-cards/slider-button-card/main/assets/grid-not-square.png
750 | [full-width]: https://raw.githubusercontent.com/custom-cards/slider-button-card/main/assets/grid-full-width.png
751 | [latest-release]: https://github.com/custom-cards/slider-button-card/releases/latest
752 | [releases-shield]: https://img.shields.io/github/release/custom-cards/slider-button-card.svg?style=for-the-badge
753 | [releases]: https://github.com/custom-cards/slider-button-card/releases
754 | [icon-minimal]: https://raw.githubusercontent.com/custom-cards/slider-button-card/main/assets/grid-full-width.png
755 |
--------------------------------------------------------------------------------
/assets/card-editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/card-editor.png
--------------------------------------------------------------------------------
/assets/examples/action-custom-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/action-custom-icon.png
--------------------------------------------------------------------------------
/assets/examples/action-custom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/action-custom.png
--------------------------------------------------------------------------------
/assets/examples/action-minimal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/action-minimal.png
--------------------------------------------------------------------------------
/assets/examples/climate.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/climate.gif
--------------------------------------------------------------------------------
/assets/examples/cover.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/cover.gif
--------------------------------------------------------------------------------
/assets/examples/fan.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/fan.gif
--------------------------------------------------------------------------------
/assets/examples/general-compact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/general-compact.png
--------------------------------------------------------------------------------
/assets/examples/general-minimal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/general-minimal.png
--------------------------------------------------------------------------------
/assets/examples/icon-icon-override.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/icon-icon-override.png
--------------------------------------------------------------------------------
/assets/examples/icon-minimal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/icon-minimal.png
--------------------------------------------------------------------------------
/assets/examples/lock.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/lock.gif
--------------------------------------------------------------------------------
/assets/examples/media.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/media.gif
--------------------------------------------------------------------------------
/assets/examples/slider-force-square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/slider-force-square.png
--------------------------------------------------------------------------------
/assets/examples/slider-minimal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/slider-minimal.png
--------------------------------------------------------------------------------
/assets/examples/slider-show-track.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/slider-show-track.png
--------------------------------------------------------------------------------
/assets/examples/slider-state-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/slider-state-color.png
--------------------------------------------------------------------------------
/assets/examples/switch.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/examples/switch.gif
--------------------------------------------------------------------------------
/assets/grid-full-width.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/grid-full-width.png
--------------------------------------------------------------------------------
/assets/grid-not-square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/grid-not-square.png
--------------------------------------------------------------------------------
/assets/preview-2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/preview-2.gif
--------------------------------------------------------------------------------
/assets/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/custom-cards/slider-button-card/f63628332e1febc28642fdbba872715bf8096bde/assets/preview.gif
--------------------------------------------------------------------------------
/elements/formfield.js:
--------------------------------------------------------------------------------
1 | import { FormfieldBase } from '@material/mwc-formfield/mwc-formfield-base.js';
2 | import { styles as formfieldStyles } from '@material/mwc-formfield/mwc-formfield.css.js';
3 |
4 | export const formfieldDefinition = {
5 | 'mwc-formfield': class extends FormfieldBase {
6 | static get styles() {
7 | return formfieldStyles;
8 | }
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/elements/ignore/switch.js:
--------------------------------------------------------------------------------
1 | export const ignoreSwitchFiles = ['@material/mwc-ripple/mwc-ripple.js'];
2 |
--------------------------------------------------------------------------------
/elements/ignore/textfield.js:
--------------------------------------------------------------------------------
1 | export const ignoreTextfieldFiles = ['@material/mwc-notched-outline/mwc-notched-outline.js'];
2 |
--------------------------------------------------------------------------------
/elements/switch.js:
--------------------------------------------------------------------------------
1 | import { SwitchBase } from '@material/mwc-switch/deprecated/mwc-switch-base.js';
2 | import { RippleBase } from '@material/mwc-ripple/mwc-ripple-base.js';
3 | import { styles as switchStyles } from '@material/mwc-switch/deprecated/mwc-switch.css';
4 | import { styles as rippleStyles } from '@material/mwc-ripple/mwc-ripple.css';
5 |
6 | export const switchDefinition = {
7 | 'mwc-switch': class extends SwitchBase {
8 | static get styles() {
9 | return switchStyles;
10 | }
11 | },
12 | 'mwc-ripple': class extends RippleBase {
13 | static get styles() {
14 | return rippleStyles;
15 | }
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/elements/textfield.js:
--------------------------------------------------------------------------------
1 | import { TextFieldBase } from '@material/mwc-textfield/mwc-textfield-base.js';
2 | import { NotchedOutlineBase } from '@material/mwc-notched-outline/mwc-notched-outline-base.js';
3 |
4 | import { styles as textfieldStyles } from '@material/mwc-textfield/mwc-textfield.css';
5 | import { styles as notchedOutlineStyles } from '@material/mwc-notched-outline/mwc-notched-outline.css';
6 |
7 | export const textfieldDefinition = {
8 | 'mwc-textfield': class extends TextFieldBase {
9 | static get styles() {
10 | return textfieldStyles;
11 | }
12 | },
13 | 'mwc-notched-outline': class extends NotchedOutlineBase {
14 | static get styles() {
15 | return notchedOutlineStyles;
16 | }
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Slider Button Card",
3 | "render_readme": true,
4 | "filename": "slider-button-card.js"
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "slider-button-card",
3 | "version": "1.13.0",
4 | "description": "Lovelace slider-button-card",
5 | "keywords": [
6 | "home-assistant",
7 | "homeassistant",
8 | "hass",
9 | "automation",
10 | "lovelace",
11 | "custom-cards"
12 | ],
13 | "module": "slider-button-card.js",
14 | "repository": {
15 | "url": "https://github.com/custom-cards/slider-button-card"
16 | },
17 | "author": "M Hoog Antink",
18 | "license": "MIT",
19 | "dependencies": {
20 | "@ctrl/tinycolor": "^3.4.0",
21 | "@lit-labs/scoped-registry-mixin": "^1.0.1",
22 | "@material/mwc-formfield": "^0.27.0",
23 | "@material/mwc-list": "^0.27.0",
24 | "@material/mwc-notched-outline": "^0.27.0",
25 | "@material/mwc-switch": "^0.27.0",
26 | "@material/mwc-textarea": "^0.27.0",
27 | "@material/mwc-textfield": "^0.27.0",
28 | "custom-card-helpers": "^1.6.6",
29 | "fast-copy": "^2.1.1",
30 | "home-assistant-js-websocket": "^4.5.0",
31 | "lit": "^2.8.0"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.12.3",
35 | "@babel/plugin-proposal-class-properties": "^7.12.1",
36 | "@babel/plugin-proposal-decorators": "^7.12.1",
37 | "@rollup/plugin-json": "^4.1.0",
38 | "@typescript-eslint/eslint-plugin": "^2.34.0",
39 | "@typescript-eslint/parser": "^2.34.0",
40 | "eslint": "^6.8.0",
41 | "eslint-config-airbnb-base": "^14.2.1",
42 | "eslint-config-prettier": "^6.15.0",
43 | "eslint-plugin-import": "^2.22.1",
44 | "eslint-plugin-prettier": "^3.1.4",
45 | "prettier": "^1.19.1",
46 | "rollup": "^1.32.1",
47 | "rollup-plugin-babel": "^4.4.0",
48 | "rollup-plugin-commonjs": "^10.1.0",
49 | "rollup-plugin-node-resolve": "^5.2.0",
50 | "rollup-plugin-serve": "^1.1.0",
51 | "rollup-plugin-terser": "^5.3.1",
52 | "rollup-plugin-typescript2": "^0.24.3",
53 | "typescript": "^3.9.7"
54 | },
55 | "scripts": {
56 | "start": "rollup -c rollup.config.dev.js --watch",
57 | "build": "npm run lint && npm run rollup",
58 | "lint": "eslint src/*.ts",
59 | "rollup": "rollup -c"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/rollup-plugins/ignore.js:
--------------------------------------------------------------------------------
1 | export default function (userOptions = {}) {
2 | // Files need to be absolute paths.
3 | // This only works if the file has no exports
4 | // and only is imported for its side effects
5 | const files = userOptions.files || [];
6 |
7 | if (files.length === 0) {
8 | return {
9 | name: 'ignore',
10 | };
11 | }
12 |
13 | return {
14 | name: 'ignore',
15 |
16 | load(id) {
17 | return files.some((toIgnorePath) => id.startsWith(toIgnorePath))
18 | ? {
19 | code: '',
20 | }
21 | : null;
22 | },
23 | };
24 | }
--------------------------------------------------------------------------------
/rollup-plugins/ignoreWrapper.js:
--------------------------------------------------------------------------------
1 | import ignore from "./ignore";
2 | import { ignoreSwitchFiles } from '../elements/ignore/switch';
3 | import { ignoreTextfieldFiles } from '../elements/ignore/textfield'
4 |
5 | export default function ignoreWrapper() {
6 | return ignore({
7 | files: [
8 | ...ignoreSwitchFiles,
9 | ...ignoreTextfieldFiles,
10 | ].map((file) => require.resolve(file)),
11 | })
12 | }
--------------------------------------------------------------------------------
/rollup.config.dev.js:
--------------------------------------------------------------------------------
1 | import resolve from "rollup-plugin-node-resolve";
2 | import typescript from "rollup-plugin-typescript2";
3 | import babel from "rollup-plugin-babel";
4 | import serve from "rollup-plugin-serve";
5 | import { terser } from "rollup-plugin-terser";
6 | import ignoreWrapper from './rollup-plugins/ignoreWrapper';
7 | import json from '@rollup/plugin-json';
8 |
9 | export default {
10 | input: ["src/slider-button-card.ts"],
11 | output: {
12 | dir: "./dist",
13 | format: "es",
14 | },
15 | plugins: [
16 | resolve(),
17 | typescript(),
18 | json(),
19 | babel({
20 | exclude: "node_modules/**",
21 | }),
22 | terser(),
23 | serve({
24 | contentBase: "./dist",
25 | host: "0.0.0.0",
26 | port: 5000,
27 | allowCrossOrigin: true,
28 | headers: {
29 | "Access-Control-Allow-Origin": "*",
30 | },
31 | }),
32 | ignoreWrapper()
33 | ],
34 | };
35 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import typescript from 'rollup-plugin-typescript2';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import nodeResolve from 'rollup-plugin-node-resolve';
4 | import babel from 'rollup-plugin-babel';
5 | import { terser } from 'rollup-plugin-terser';
6 | import ignoreWrapper from './rollup-plugins/ignoreWrapper';
7 | import serve from 'rollup-plugin-serve';
8 | import json from '@rollup/plugin-json';
9 |
10 | const dev = process.env.ROLLUP_WATCH;
11 |
12 | const serveopts = {
13 | contentBase: ['./dist'],
14 | host: '0.0.0.0',
15 | port: 5000,
16 | allowCrossOrigin: true,
17 | headers: {
18 | 'Access-Control-Allow-Origin': '*',
19 | },
20 | };
21 |
22 | const plugins = [
23 | nodeResolve({}),
24 | commonjs(),
25 | typescript(),
26 | json(),
27 | babel({
28 | exclude: 'node_modules/**',
29 | }),
30 | dev && serve(serveopts),
31 | !dev && terser(),
32 | ignoreWrapper()
33 | ];
34 |
35 | export default [
36 | {
37 | input: 'src/slider-button-card.ts',
38 | output: {
39 | dir: 'dist',
40 | format: 'es',
41 | },
42 | plugins: [...plugins],
43 | },
44 | ];
45 |
--------------------------------------------------------------------------------
/src/action-handler-directive.ts:
--------------------------------------------------------------------------------
1 | import { directive, PropertyPart } from 'lit-html';
2 |
3 | import { ActionHandlerDetail, ActionHandlerOptions } from 'custom-card-helpers/dist/types';
4 | import { fireEvent } from 'custom-card-helpers';
5 |
6 | const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
7 |
8 | interface ActionHandler extends HTMLElement {
9 | holdTime: number;
10 | bind(element: Element, options): void;
11 | }
12 | interface ActionHandlerElement extends HTMLElement {
13 | actionHandler?: boolean;
14 | }
15 |
16 | declare global {
17 | interface HASSDomEvents {
18 | action: ActionHandlerDetail;
19 | }
20 | }
21 |
22 | class ActionHandler extends HTMLElement implements ActionHandler {
23 | public holdTime = 500;
24 |
25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
26 | public ripple: any;
27 |
28 | protected timer?: number;
29 |
30 | protected held = false;
31 |
32 | private dblClickTimeout?: number;
33 |
34 | constructor() {
35 | super();
36 | this.ripple = document.createElement('mwc-ripple');
37 | }
38 |
39 | public connectedCallback(): void {
40 | Object.assign(this.style, {
41 | position: 'absolute',
42 | width: isTouch ? '100px' : '50px',
43 | height: isTouch ? '100px' : '50px',
44 | transform: 'translate(-50%, -50%)',
45 | pointerEvents: 'none',
46 | zIndex: '999',
47 | });
48 |
49 | this.appendChild(this.ripple);
50 | this.ripple.primary = true;
51 |
52 | ['touchcancel', 'mouseout', 'mouseup', 'touchmove', 'mousewheel', 'wheel', 'scroll'].forEach(ev => {
53 | document.addEventListener(
54 | ev,
55 | () => {
56 | clearTimeout(this.timer);
57 | this.stopAnimation();
58 | this.timer = undefined;
59 | },
60 | { passive: true },
61 | );
62 | });
63 | }
64 |
65 | public bind(element: ActionHandlerElement, options): void {
66 | if (element.actionHandler) {
67 | return;
68 | }
69 | element.actionHandler = true;
70 |
71 | element.addEventListener('contextmenu', (ev: Event) => {
72 | const e = ev || window.event;
73 | if (e.preventDefault) {
74 | e.preventDefault();
75 | }
76 | if (e.stopPropagation) {
77 | e.stopPropagation();
78 | }
79 | e.cancelBubble = true;
80 | e.returnValue = false;
81 | return false;
82 | });
83 |
84 | const start = (ev: Event): void => {
85 | this.held = false;
86 | let x;
87 | let y;
88 | if ((ev as TouchEvent).touches) {
89 | x = (ev as TouchEvent).touches[0].pageX;
90 | y = (ev as TouchEvent).touches[0].pageY;
91 | } else {
92 | x = (ev as MouseEvent).pageX;
93 | y = (ev as MouseEvent).pageY;
94 | }
95 |
96 | this.timer = window.setTimeout(() => {
97 | this.startAnimation(x, y);
98 | this.held = true;
99 | }, this.holdTime);
100 | };
101 |
102 | const end = (ev: Event): void => {
103 | // Prevent mouse event if touch event
104 | ev.preventDefault();
105 | if (['touchend', 'touchcancel'].includes(ev.type) && this.timer === undefined) {
106 | return;
107 | }
108 | clearTimeout(this.timer);
109 | this.stopAnimation();
110 | this.timer = undefined;
111 | if (this.held) {
112 | fireEvent(element, 'action', { action: 'hold' });
113 | } else if (options.hasDoubleClick) {
114 | if ((ev.type === 'click' && (ev as MouseEvent).detail < 2) || !this.dblClickTimeout) {
115 | this.dblClickTimeout = window.setTimeout(() => {
116 | this.dblClickTimeout = undefined;
117 | fireEvent(element, 'action', { action: 'tap' });
118 | }, 250);
119 | } else {
120 | clearTimeout(this.dblClickTimeout);
121 | this.dblClickTimeout = undefined;
122 | fireEvent(element, 'action', { action: 'double_tap' });
123 | }
124 | } else {
125 | fireEvent(element, 'action', { action: 'tap' });
126 | }
127 | };
128 |
129 | const handleEnter = (ev: KeyboardEvent): void => {
130 | if (ev.keyCode !== 13) {
131 | return;
132 | }
133 | end(ev);
134 | };
135 |
136 | element.addEventListener('touchstart', start, { passive: true });
137 | element.addEventListener('touchend', end);
138 | element.addEventListener('touchcancel', end);
139 |
140 | element.addEventListener('mousedown', start, { passive: true });
141 | element.addEventListener('click', end);
142 |
143 | element.addEventListener('keyup', handleEnter);
144 | }
145 |
146 | private startAnimation(x: number, y: number): void {
147 | Object.assign(this.style, {
148 | left: `${x}px`,
149 | top: `${y}px`,
150 | display: null,
151 | });
152 | this.ripple.disabled = false;
153 | this.ripple.active = true;
154 | this.ripple.unbounded = true;
155 | }
156 |
157 | private stopAnimation(): void {
158 | this.ripple.active = false;
159 | this.ripple.disabled = true;
160 | this.style.display = 'none';
161 | }
162 | }
163 |
164 | // TODO You need to replace all instances of "action-handler-boilerplate" with "action-handler-"
165 | customElements.define('action-handler-slider-button', ActionHandler);
166 |
167 | const getActionHandler = (): ActionHandler => {
168 | const body = document.body;
169 | if (body.querySelector('action-handler-slider-button')) {
170 | return body.querySelector('action-handler-slider-button') as ActionHandler;
171 | }
172 |
173 | const actionhandler = document.createElement('action-handler-slider-button');
174 | body.appendChild(actionhandler);
175 |
176 | return actionhandler as ActionHandler;
177 | };
178 |
179 | export const actionHandlerBind = (element: ActionHandlerElement, options: ActionHandlerOptions): void => {
180 | const actionhandler: ActionHandler = getActionHandler();
181 | if (!actionhandler) {
182 | return;
183 | }
184 | actionhandler.bind(element, options);
185 | };
186 |
187 | export const actionHandler = directive((options: ActionHandlerOptions = {}) => (part: PropertyPart): void => {
188 | actionHandlerBind(part.committer.element as ActionHandlerElement, options);
189 | });
190 |
--------------------------------------------------------------------------------
/src/const.ts:
--------------------------------------------------------------------------------
1 | import * as pack from '../package.json';
2 | export const CARD_VERSION = pack.version;
3 |
--------------------------------------------------------------------------------
/src/controllers/automation-controller.ts:
--------------------------------------------------------------------------------
1 | import { STATES_OFF } from 'custom-card-helpers';
2 | import { Controller } from './controller';
3 |
4 | export class AutomationController extends Controller {
5 | _min = 0;
6 | _max = 1;
7 | _targetValue;
8 | _invert = false;
9 |
10 | get _value(): number {
11 | return !STATES_OFF.includes(this.stateObj.state)
12 | ? 1
13 | : 0;
14 | }
15 |
16 | set _value(value) {
17 | const service = value > 0 ? 'turn_on' : 'turn_off';
18 | this._hass.callService('automation', service, {
19 | // eslint-disable-next-line @typescript-eslint/camelcase
20 | entity_id: this.stateObj.entity_id
21 | });
22 | }
23 |
24 | get _step(): number {
25 | return 1;
26 | }
27 |
28 | get label(): string {
29 | if (this.percentage > 0) {
30 | return this._hass.localize('component.automation.state._.on');
31 | }
32 | return this._hass.localize('component.automation.state._.off');
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/controllers/climate-controller.ts:
--------------------------------------------------------------------------------
1 | import { STATES_OFF } from 'custom-card-helpers';
2 | import { capitalizeFirst } from '../utils';
3 | import { Controller } from './controller';
4 |
5 | export class ClimateController extends Controller {
6 | _targetValue;
7 | _invert = false;
8 |
9 | get _value(): number {
10 | return this.stateObj.attributes.temperature;
11 | }
12 |
13 | set _value(value) {
14 | this._hass.callService('climate', 'set_temperature', {
15 | // eslint-disable-next-line @typescript-eslint/camelcase
16 | entity_id: this.stateObj.entity_id,
17 | temperature: value,
18 | });
19 | }
20 |
21 | get isOff(): boolean {
22 | return STATES_OFF.includes(this.state);
23 | }
24 |
25 | get _step(): number {
26 | return this.stateObj.attributes?.target_temp_step || 1;
27 | }
28 |
29 | get _min(): number {
30 | return this.stateObj.attributes?.min_temp || 7;
31 | }
32 |
33 | get _max(): number {
34 | return this.stateObj.attributes?.max_temp || 35;
35 | }
36 |
37 | get isValuePercentage(): boolean {
38 | return false;
39 | }
40 |
41 | get label(): string {
42 | const unit = this._hass.config.unit_system.temperature;
43 | const mode = capitalizeFirst(this.state);
44 | // const current = this.stateObj.attributes?.current_temperature ? ` | ${this.stateObj.attributes.current_temperature}${unit}` : '';
45 | return `${this.targetValue}${unit} | ${mode}`;
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/src/controllers/controller.ts:
--------------------------------------------------------------------------------
1 | import { computeStateDomain, domainIcon, HomeAssistant } from 'custom-card-helpers';
2 | import { HassEntity } from 'home-assistant-js-websocket';
3 | import { SliderBackground, SliderButtonCardConfig, SliderDirections } from '../types';
4 | import { getLightColorBasedOnTemperature, normalize, percentageToValue, toPercentage } from '../utils';
5 |
6 | export interface Style {
7 | icon: ObjectStyle;
8 | slider: ObjectStyle;
9 | }
10 |
11 | export interface ObjectStyle {
12 | filter: string;
13 | color: string;
14 | rotateSpeed?: string;
15 | }
16 |
17 | export abstract class Controller {
18 | _config: SliderButtonCardConfig;
19 | _hass: any;
20 | _sliderPrevColor = '';
21 |
22 | abstract _value?: number;
23 | abstract _targetValue?: number;
24 | abstract _min?: number;
25 | abstract _max?: number;
26 | abstract _step?: number;
27 | abstract _invert?: boolean;
28 |
29 | protected constructor(config: SliderButtonCardConfig) {
30 | this._config = config;
31 | }
32 |
33 | set hass(hass: HomeAssistant) {
34 | this._hass = hass;
35 | }
36 |
37 | get stateObj(): any {
38 | return this._hass.states[this._config.entity] as HassEntity;
39 | }
40 |
41 | get domain(): string {
42 | return computeStateDomain(this.stateObj);
43 | }
44 |
45 | get name(): string {
46 | return this._config.name ? this._config.name : this.stateObj?.attributes?.friendly_name ? this.stateObj.attributes.friendly_name : '';
47 | }
48 |
49 | get icon(): string {
50 | if (typeof this._config.icon?.icon === 'string' && this._config.icon?.icon.length) {
51 | return this._config.icon.icon;
52 | }
53 | return this.stateObj.attributes?.icon ? this.stateObj.attributes.icon : domainIcon(this.domain, this.stateObj.state);
54 | }
55 |
56 | get value(): number {
57 | if (this._value) {
58 | return Math.round(this._value / this.step) * this.step;
59 | }
60 | return this.min;
61 | }
62 |
63 | set value(value: number) {
64 | if (value !== this.value) {
65 | this._value = value;
66 | // this._value = Math.round(value / this.step) * this.step;
67 | }
68 | }
69 |
70 | get targetValue(): number {
71 | if (this._targetValue === 0) {
72 | return 0;
73 | }
74 | if (this._targetValue) {
75 | return Math.round(this._targetValue / this.step) * this.step;
76 | }
77 | if (this.value) {
78 | return this.value;
79 | }
80 | return 0;
81 | }
82 |
83 | set targetValue(value: number) {
84 | if (value !== this.targetValue) {
85 | this._targetValue = value;
86 | // this._targetValue = Math.round(value / this.step) * this.step;
87 | }
88 | }
89 |
90 | get label(): string {
91 | return `${this.targetValue}`;
92 | }
93 |
94 | get attributeLabel(): string {
95 | if (this._config.attribute) {
96 | return this.stateObj.attributes[this._config.attribute];
97 | }
98 | return '';
99 | }
100 |
101 | get hidden(): boolean {
102 | return false;
103 | }
104 |
105 | get hasSlider(): boolean {
106 | return true;
107 | }
108 |
109 | get hasToggle(): boolean {
110 | return this._config.slider?.toggle_on_click ?? false;
111 | }
112 |
113 | get toggleValue(): number {
114 | return this.value === this.min ? this.max : this.min;
115 | }
116 |
117 | get state(): string {
118 | return this.stateObj?.state;
119 | }
120 |
121 | get isOff(): boolean {
122 | return this.percentage === 0;
123 | }
124 |
125 | get isUnavailable(): boolean {
126 | return this.state ? this.state === 'unavailable' : true;
127 | }
128 |
129 | get isSliderDisabled(): boolean {
130 | return this.isUnavailable ? this.isUnavailable : this.hasToggle;
131 | }
132 |
133 | get min(): number {
134 | return this._config.slider?.min ?? this._min ?? 0;
135 | }
136 |
137 | get max(): number {
138 | return this._config.slider?.max ?? this._max ?? 100;
139 | }
140 |
141 | get step(): number {
142 | return this._config.slider?.step ?? this._step ?? 5;
143 | }
144 |
145 | get invert(): boolean {
146 | return this._config.slider?.invert ?? this._invert ?? false;
147 | }
148 |
149 | get isValuePercentage(): boolean {
150 | return true;
151 | }
152 |
153 | get percentage(): number {
154 | return Math.round(
155 | ((this.targetValue - (this.invert ? this.max : this.min)) * 100) / (this.max - this.min) * (this.invert ? -1 : 1)
156 | );
157 | }
158 |
159 | get valueFromPercentage(): number {
160 | return percentageToValue(this.percentage, this.min, this.max);
161 | }
162 |
163 | get allowedAttributes(): string[] {
164 | return [];
165 | }
166 |
167 | get style(): Style {
168 | return {
169 | icon: {
170 | filter: this.iconFilter,
171 | color: this.iconColor,
172 | rotateSpeed: this.iconRotateSpeed,
173 | },
174 | slider: {
175 | filter: this.sliderFilter,
176 | color: this.sliderColor,
177 | },
178 | };
179 | }
180 |
181 | get iconFilter(): string {
182 | if (!this._config.icon?.use_state_color || this.percentage === 0) {
183 | return 'brightness(100%)';
184 | }
185 | return `brightness(${(this.percentage + 100) / 2}%)`;
186 | }
187 |
188 | get iconColor(): string {
189 | if (this._config.icon?.use_state_color) {
190 | if (this.stateObj.attributes.hs_color) {
191 | const [hue, sat] = this.stateObj.attributes.hs_color;
192 | if (sat > 10) {
193 | return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
194 | }
195 | } else if (this.percentage > 0) {
196 | return 'var(--paper-item-icon-active-color, #fdd835)'
197 | } else {
198 | return 'var(--paper-item-icon-color, #44739e)'
199 | }
200 | }
201 | return '';
202 | }
203 |
204 | get iconRotateSpeed(): string {
205 | return '0s';
206 | }
207 |
208 | get sliderFilter(): string {
209 | if (!this._config.slider?.use_percentage_bg_opacity || this.percentage === 0 || this._config.slider.background === SliderBackground.GRADIENT) {
210 | return 'brightness(100%)';
211 | }
212 | return `brightness(${(this.percentage + 100) / 2}%)`;
213 | }
214 |
215 | get sliderColor(): string {
216 | if (this._config.slider?.use_state_color) {
217 | if (this.stateObj.attributes.hs_color) {
218 | const [hue, sat] = this.stateObj.attributes.hs_color;
219 | if (sat > 10) {
220 | const color = `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
221 | this._sliderPrevColor = color;
222 | return color;
223 | }
224 | } else if (
225 | this.stateObj.attributes.color_temp &&
226 | this.stateObj.attributes.min_mireds &&
227 | this.stateObj.attributes.max_mireds
228 | ) {
229 | const color = getLightColorBasedOnTemperature(
230 | this.stateObj.attributes.color_temp,
231 | this.stateObj.attributes.min_mireds,
232 | this.stateObj.attributes.max_mireds,
233 | );
234 | this._sliderPrevColor = color;
235 | return color;
236 | } else if (this._sliderPrevColor.startsWith('hsl') || this._sliderPrevColor.startsWith('rgb')) {
237 | return this._sliderPrevColor;
238 | }
239 | }
240 | return 'inherit';
241 | }
242 |
243 | moveSlider(event: any, {left, top, width, height}): number {
244 | let percentage = this.calcMovementPercentage(event, {left, top, width, height});
245 | percentage = this.applyStep(percentage);
246 | percentage = normalize(percentage, 0, 100);
247 | if (!this.isValuePercentage) {
248 | percentage = percentageToValue(percentage, this.min, this.max);
249 | }
250 | return percentage;
251 | }
252 |
253 | calcMovementPercentage(event: any, {left, top, width, height}): number {
254 | let percentage;
255 | switch(this._config.slider?.direction) {
256 | case SliderDirections.LEFT_RIGHT:
257 | percentage = toPercentage(
258 | event.clientX,
259 | left,
260 | width
261 | );
262 | if (this.invert) {
263 | percentage = 100 - percentage;
264 | }
265 | break
266 | case SliderDirections.RIGHT_LEFT:
267 | percentage = toPercentage(
268 | event.clientX,
269 | left,
270 | width
271 | );
272 | if (!this.invert) {
273 | percentage = 100 - percentage;
274 | }
275 | break
276 | case SliderDirections.TOP_BOTTOM:
277 | percentage = toPercentage(
278 | event.clientY,
279 | top,
280 | height
281 | );
282 | if (this.invert) {
283 | percentage = 100 - percentage;
284 | }
285 | break
286 | case SliderDirections.BOTTOM_TOP:
287 | percentage = toPercentage(
288 | event.clientY,
289 | top,
290 | height
291 | );
292 | if (!this.invert) {
293 | percentage = 100 - percentage;
294 | }
295 | break
296 |
297 | }
298 | return percentage;
299 | }
300 |
301 | applyStep(value: number): number {
302 | return Math.round(value / this.step) * this.step;
303 | }
304 |
305 | log(name = '', value: string | number | object = ''): void {
306 | if (this._config.debug) {
307 | console.log(`${this._config.entity}: ${name}`, value)
308 | }
309 | }
310 | }
311 |
--------------------------------------------------------------------------------
/src/controllers/cover-controller.ts:
--------------------------------------------------------------------------------
1 | import { stateIcon } from 'custom-card-helpers';
2 | import { CoverAttributes } from '../types';
3 | import { getEnumValues } from '../utils';
4 | import { Controller } from './controller';
5 |
6 | export class CoverController extends Controller {
7 | _min = 0;
8 | _targetValue;
9 | _invert = true;
10 |
11 | get attribute(): string {
12 | if (this._config.slider?.attribute?.length && this.allowedAttributes.includes(this._config.slider?.attribute)) {
13 | return this._config.slider?.attribute;
14 | }
15 | return CoverAttributes.POSITION;
16 | }
17 |
18 | get icon(): string {
19 | if (typeof this._config.icon?.icon === 'string' && this._config.icon?.icon.length) {
20 | return this._config.icon.icon;
21 | }
22 | return stateIcon(this.stateObj);
23 | }
24 |
25 | get allowedAttributes(): string[] {
26 | return getEnumValues(CoverAttributes);
27 | }
28 | get _value(): number {
29 | switch(this.attribute) {
30 | case CoverAttributes.POSITION:
31 | return this.stateObj?.state === 'closed'
32 | ? 0
33 | : this.stateObj.attributes.current_position;
34 | case CoverAttributes.TILT:
35 | return this.stateObj.attributes.current_tilt_position;
36 | default:
37 | return 0;
38 | }
39 | }
40 |
41 | set _value(value) {
42 | if (!this.hasSlider) {
43 | const service = value > 0 ? 'open_cover' : 'close_cover';
44 | this._hass.callService('cover', service, {
45 | // eslint-disable-next-line @typescript-eslint/camelcase
46 | entity_id: this.stateObj.entity_id
47 | });
48 | } else {
49 | switch(this.attribute) {
50 | case CoverAttributes.POSITION:
51 | this._hass.callService('cover', 'set_cover_position', {
52 | // eslint-disable-next-line @typescript-eslint/camelcase
53 | entity_id: this.stateObj.entity_id,
54 | position: value
55 | });
56 | break;
57 | case CoverAttributes.TILT:
58 | this._hass.callService('cover', 'set_cover_tilt_position', {
59 | // eslint-disable-next-line @typescript-eslint/camelcase
60 | entity_id: this.stateObj.entity_id,
61 | // eslint-disable-next-line @typescript-eslint/camelcase
62 | tilt_position: value
63 | });
64 | break;
65 | default:
66 | }
67 | }
68 | }
69 |
70 | get _step(): number {
71 | return 1;
72 | }
73 |
74 | get label(): string {
75 | const defaultLabel = this._hass.localize(`component.cover.entity_component._.state.${this.state}`);
76 | const closedLabel = this._hass.localize('component.cover.entity_component._.state.closed');
77 | const openLabel = this._hass.localize('component.cover.entity_component._.state.open');
78 | if (!this.hasSlider) {
79 | return defaultLabel;
80 | }
81 | switch(this.attribute) {
82 | case CoverAttributes.POSITION:
83 | if (this.percentage === 0) {
84 | return this.invert ? openLabel : closedLabel;
85 | }
86 | if (this.percentage === 100) {
87 | return this.invert ? closedLabel : openLabel;
88 | }
89 | return `${this.percentage}%`;
90 | case CoverAttributes.TILT:
91 | return `${this.percentage}`;
92 | }
93 | return defaultLabel;
94 | }
95 |
96 | get hasSlider(): boolean {
97 | switch(this.attribute) {
98 | case CoverAttributes.POSITION:
99 | if ('current_position' in this.stateObj.attributes) {
100 | return true;
101 | }
102 | if (
103 | 'supported_features' in this.stateObj.attributes &&
104 | this.stateObj.attributes.supported_features & 4
105 | ) {
106 | return true;
107 | }
108 | break;
109 | case CoverAttributes.TILT:
110 | if ('current_tilt_position' in this.stateObj.attributes) {
111 | return true;
112 | }
113 | if (
114 | 'supported_features' in this.stateObj.attributes &&
115 | this.stateObj.attributes.supported_features & 128
116 | ) {
117 | return true;
118 | }
119 | break;
120 | default:
121 | return false;
122 | }
123 | return false;
124 | }
125 |
126 | get _max(): number {
127 | return this.hasSlider ? 100 : 1;
128 | }
129 |
130 | }
131 |
--------------------------------------------------------------------------------
/src/controllers/fan-controller.ts:
--------------------------------------------------------------------------------
1 | import { STATES_OFF } from 'custom-card-helpers';
2 | import { Controller } from './controller';
3 |
4 | export class FanController extends Controller {
5 | _min = 0;
6 | _targetValue;
7 | _invert = false;
8 |
9 | get _value(): number {
10 | return this.isUnavailable || STATES_OFF.includes(this.state)
11 | ? 0
12 | : this.hasSlider ? this.stateObj.attributes.percentage : 1;
13 | }
14 |
15 | set _value(value) {
16 | const service = value > 0 ? 'turn_on' : 'turn_off';
17 | if (value > 0 && this.hasSlider) {
18 | this._hass.callService('fan', 'set_percentage', {
19 | // eslint-disable-next-line @typescript-eslint/camelcase
20 | entity_id: this.stateObj.entity_id,
21 | percentage: value
22 | });
23 | } else {
24 | this._hass.callService('fan', service, {
25 | // eslint-disable-next-line @typescript-eslint/camelcase
26 | entity_id: this.stateObj.entity_id
27 | });
28 | }
29 | }
30 |
31 | get _step(): number {
32 | return this.hasSlider ? this.stateObj.attributes.percentage_step : 1;
33 | }
34 |
35 | get label(): string {
36 | if (this.percentage > 0) {
37 | if (this.hasSlider) {
38 | return `${this.percentage}%`
39 | } else {
40 | return this._hass.localize('component.fan.entity_component._.state.on');
41 | }
42 | }
43 | return this._hass.localize('component.fan.entity_component._.state.off');
44 | }
45 |
46 | get hasSlider(): boolean {
47 | return 'percentage' in this.stateObj.attributes;
48 | }
49 |
50 | get _max(): number {
51 | return this.hasSlider ? 100 : 1;
52 | }
53 |
54 | get iconRotateSpeed(): string {
55 | let speed = 0;
56 | if (this.hasSlider) {
57 | if (this.percentage > 0) {
58 | speed = 3 - ((this.percentage / 100) * 2);
59 | }
60 | } else {
61 | speed = this._value
62 | }
63 |
64 | return `${speed}s`
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/src/controllers/get-controller.ts:
--------------------------------------------------------------------------------
1 | import { computeDomain } from 'custom-card-helpers';
2 | import { Domain, SliderButtonCardConfig } from '../types';
3 | import { AutomationController } from './automation-controller';
4 | import { ClimateController } from './climate-controller';
5 | import { Controller } from './controller';
6 | import { CoverController } from './cover-controller';
7 | import { FanController } from './fan-controller';
8 | import { InputBooleanController } from './input-boolean-controller';
9 | import { InputNumberController } from './input-number-controller';
10 | import { LightController } from './light-controller';
11 | import { LockController } from './lock-controller';
12 | import { MediaController } from './media-controller';
13 | import { SwitchController } from './switch-controller';
14 | import { NumberController } from './number-controller';
15 |
16 | export class ControllerFactory {
17 | static getInstance(config: SliderButtonCardConfig): Controller {
18 | const domain = computeDomain(config.entity);
19 | const mapping = {
20 | [Domain.LIGHT]: LightController,
21 | [Domain.FAN]: FanController,
22 | [Domain.SWITCH]: SwitchController,
23 | [Domain.AUTOMATION]: AutomationController,
24 | [Domain.COVER]: CoverController,
25 | [Domain.INPUT_BOOLEAN]: InputBooleanController,
26 | [Domain.INPUT_NUMBER]: InputNumberController,
27 | [Domain.MEDIA_PLAYER]: MediaController,
28 | [Domain.NUMBER]: NumberController,
29 | [Domain.CLIMATE]: ClimateController,
30 | [Domain.LOCK]: LockController,
31 | };
32 | if (typeof mapping[domain] === 'undefined') {
33 | throw new Error(`Unsupported entity type: ${domain}`)
34 | }
35 | return new mapping[domain](config);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/controllers/input-boolean-controller.ts:
--------------------------------------------------------------------------------
1 | import { STATES_OFF } from 'custom-card-helpers';
2 | import { Controller } from './controller';
3 |
4 | export class InputBooleanController extends Controller {
5 | _min = 0;
6 | _max = 1;
7 | _targetValue;
8 | _invert = false;
9 |
10 | get _value(): number {
11 | return !STATES_OFF.includes(this.stateObj.state)
12 | ? 1
13 | : 0;
14 | }
15 |
16 | set _value(value) {
17 | const service = value > 0 ? 'turn_on' : 'turn_off';
18 | this._hass.callService('input_boolean', service, {
19 | // eslint-disable-next-line @typescript-eslint/camelcase
20 | entity_id: this.stateObj.entity_id
21 | });
22 | }
23 |
24 | get _step(): number {
25 | return 1;
26 | }
27 |
28 | get label(): string {
29 | if (this.percentage > 0) {
30 | return this._hass.localize('component.input_boolean.entity_component._.state.on');
31 | }
32 | return this._hass.localize('component.input_boolean.entity_component._.state.off');
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/controllers/input-number-controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from './controller';
2 |
3 | export class InputNumberController extends Controller {
4 | _targetValue;
5 | _invert = false;
6 |
7 | get _value(): number {
8 | return this.stateObj.state;
9 | }
10 |
11 | set _value(value) {
12 | this._hass.callService('input_number', 'set_value', {
13 | // eslint-disable-next-line @typescript-eslint/camelcase
14 | entity_id: this.stateObj.entity_id,
15 | value: value,
16 | });
17 | }
18 |
19 | get _min(): number {
20 | return this.stateObj.attributes.min;
21 | }
22 |
23 | get _max(): number {
24 | return this.stateObj.attributes.max;
25 | }
26 |
27 | get isValuePercentage(): boolean {
28 | return false;
29 | }
30 |
31 | get _step(): number {
32 | return this.stateObj.attributes.step;
33 | }
34 |
35 | get label(): string {
36 | return this.stateObj.attributes.unit_of_measurement ? `${this.targetValue} ${this.stateObj.attributes.unit_of_measurement}` : `${this.targetValue}`;
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/controllers/light-controller.ts:
--------------------------------------------------------------------------------
1 | import { STATES_OFF } from 'custom-card-helpers';
2 | import { LightAttributes, LightColorModes } from '../types';
3 | import { getEnumValues, getLightColorBasedOnTemperature } from '../utils';
4 | import { Controller } from './controller';
5 |
6 | const HS_INDEX = {
7 | hue: 0,
8 | saturation: 1
9 | };
10 |
11 | export class LightController extends Controller {
12 |
13 | _step = 1;
14 | _targetValue;
15 | _invert = false;
16 |
17 | get attribute(): string {
18 | const attr = this._config.slider?.attribute as LightAttributes;
19 | let useAttr = LightAttributes.BRIGHTNESS_PCT;
20 | let supported: string[] = [];
21 | if (Array.isArray(this.stateObj?.attributes?.supported_color_modes)) {
22 | supported = this.stateObj?.attributes?.supported_color_modes;
23 | }
24 | if (supported.length === 1 && supported[0] === LightAttributes.ON_OFF) {
25 | useAttr = LightAttributes.ON_OFF;
26 | }
27 | if (attr?.length && this.allowedAttributes.includes(attr)) {
28 |
29 | useAttr = attr;
30 | switch(attr) {
31 | case LightAttributes.COLOR_TEMP:
32 | if (!supported.includes('color_temp')) {
33 | useAttr = LightAttributes.BRIGHTNESS_PCT;
34 | }
35 | break;
36 | case LightAttributes.HUE:
37 | case LightAttributes.SATURATION:
38 | if (!supported.includes('hs')) {
39 | useAttr = LightAttributes.BRIGHTNESS_PCT;
40 | }
41 | break;
42 | }
43 | }
44 | return useAttr;
45 | }
46 |
47 | get allowedAttributes(): string[] {
48 | return getEnumValues(LightAttributes);
49 | }
50 |
51 | get colorMode(): LightColorModes | undefined {
52 | return this.stateObj?.attributes?.color_mode;
53 | }
54 |
55 | get _value(): number {
56 | if (!this.stateObj || STATES_OFF.includes(this.state)) {
57 | return this.isValuePercentage ? 0 : this.min;
58 | }
59 | const attr = this.stateObj.attributes;
60 | switch(this.attribute) {
61 | case LightAttributes.COLOR_TEMP:
62 | return attr.color_temp ? Math.round(attr.color_temp) : this.min;
63 | case LightAttributes.BRIGHTNESS:
64 | return Math.round(attr.brightness);
65 | case LightAttributes.BRIGHTNESS_PCT:
66 | return Math.round((attr.brightness * 100.0) / 255);
67 | case LightAttributes.ON_OFF:
68 | return 1;
69 | case LightAttributes.HUE:
70 | case LightAttributes.SATURATION:
71 | return attr.hs_color
72 | ? Math.round(attr.hs_color[HS_INDEX[this.attribute]])
73 | : 0;
74 | default:
75 | return 0;
76 | }
77 | }
78 |
79 | set _value(value) {
80 | if (!this.stateObj) {
81 | return;
82 | }
83 | let attr = this.attribute;
84 | let _value;
85 | let service = value > 0 ? 'turn_on' : 'turn_off';
86 | let data = {
87 | // eslint-disable-next-line @typescript-eslint/camelcase
88 | entity_id: this.stateObj.entity_id,
89 | }
90 | switch(attr) {
91 | case LightAttributes.BRIGHTNESS:
92 | case LightAttributes.BRIGHTNESS_PCT:
93 | value =
94 | attr === LightAttributes.BRIGHTNESS
95 | ? Math.round(value)
96 | : Math.round((value / 100.0) * 255);
97 | if (!value) {
98 | service = 'turn_off';
99 | } else {
100 | attr = 'brightness';
101 | data = {
102 | ...data,
103 | [attr]: value
104 | }
105 | }
106 | break;
107 | case LightAttributes.HUE:
108 | case LightAttributes.SATURATION:
109 | _value = this.stateObj.attributes.hs_color || [0, 0];
110 | _value[HS_INDEX[attr]] = value;
111 | value = _value;
112 | attr = 'hs_color';
113 | service = 'turn_on';
114 | data = {
115 | ...data,
116 | [attr]: value
117 | }
118 | break;
119 | case LightAttributes.COLOR_TEMP:
120 | attr = 'color_temp';
121 | service = 'turn_on';
122 | data = {
123 | ...data,
124 | [attr]: value
125 | }
126 | break;
127 | }
128 |
129 | this._hass.callService('light', service, {
130 | ...data
131 | });
132 | }
133 |
134 | get _min(): number {
135 | switch(this.attribute) {
136 | case LightAttributes.COLOR_TEMP:
137 | return this.stateObj ? this.stateObj.attributes?.min_mireds ? this.stateObj.attributes.min_mireds : 153 : 153;
138 | default:
139 | return 0;
140 | }
141 | }
142 |
143 | get _max(): number {
144 | switch(this.attribute) {
145 | case LightAttributes.COLOR_TEMP:
146 | return this.stateObj ? this.stateObj.attributes?.max_mireds ? this.stateObj.attributes.max_mireds : 500 : 500;
147 | case LightAttributes.BRIGHTNESS:
148 | return 255;
149 | case LightAttributes.HUE:
150 | return 360;
151 | case LightAttributes.ON_OFF:
152 | return 1;
153 | default:
154 | return 100;
155 | }
156 | }
157 |
158 | get isValuePercentage(): boolean {
159 | switch(this.attribute) {
160 | case LightAttributes.COLOR_TEMP:
161 | case LightAttributes.HUE:
162 | case LightAttributes.BRIGHTNESS:
163 | return false;
164 | default:
165 | return true;
166 | }
167 | }
168 |
169 | get isOff(): boolean {
170 | switch(this.attribute) {
171 | case LightAttributes.COLOR_TEMP:
172 | case LightAttributes.HUE:
173 | case LightAttributes.SATURATION:
174 | case LightAttributes.BRIGHTNESS:
175 | case LightAttributes.ON_OFF:
176 | return STATES_OFF.includes(this.state);
177 | default:
178 | return this.colorMode === LightColorModes.ON_OFF ? STATES_OFF.includes(this.state) : this.percentage === 0;
179 | }
180 | }
181 |
182 | get label(): string {
183 | if (this.isOff) {
184 | return this._hass.localize('component.light.entity_component._.state.off');
185 | }
186 | if (this.colorMode === LightColorModes.ON_OFF) {
187 | return this._hass.localize('component.light.entity_component._.state.on');
188 | }
189 | switch(this.attribute) {
190 | case LightAttributes.ON_OFF:
191 | return this._hass.localize('component.light.entity_component._.state.on');
192 | case LightAttributes.COLOR_TEMP:
193 | case LightAttributes.BRIGHTNESS:
194 | return `${this.targetValue}`;
195 | case LightAttributes.BRIGHTNESS_PCT:
196 | case LightAttributes.SATURATION:
197 | return `${this.targetValue}%`;
198 | case LightAttributes.HUE:
199 | return `${this.targetValue}°`;
200 | default:
201 | return `${this.targetValue}`;
202 | }
203 | }
204 |
205 | get hasToggle(): boolean {
206 | let supported: string[] = [];
207 | if (Array.isArray(this.stateObj?.attributes?.supported_color_modes)) {
208 | supported = this.stateObj?.attributes?.supported_color_modes;
209 | }
210 | if (supported.length === 1 && supported[0] === LightAttributes.ON_OFF) {
211 | return true;
212 | }
213 | return this._config.slider?.toggle_on_click ?? false;
214 | }
215 |
216 | get hasSlider(): boolean {
217 | if (!this.stateObj) {
218 | return false;
219 | }
220 | switch(this.attribute) {
221 | case LightAttributes.ON_OFF:
222 | return false;
223 | case LightAttributes.BRIGHTNESS:
224 | case LightAttributes.BRIGHTNESS_PCT:
225 | if ('brightness' in this.stateObj.attributes) {
226 | return true;
227 | }
228 | return !!('supported_features' in this.stateObj.attributes &&
229 | this.stateObj?.attributes?.supported_features & 1);
230 |
231 | case LightAttributes.COLOR_TEMP:
232 | if ('color_temp' in this.stateObj.attributes) {
233 | return true;
234 | }
235 | return !!('supported_features' in this.stateObj.attributes &&
236 | this.stateObj.attributes.supported_features & 2);
237 |
238 | case LightAttributes.HUE:
239 | case LightAttributes.SATURATION:
240 | if ('hs_color' in this.stateObj.attributes) {
241 | return true;
242 | }
243 | return !!('supported_features' in this.stateObj.attributes &&
244 | this.stateObj.attributes.supported_features & 16);
245 |
246 | default:
247 | return false;
248 | }
249 | }
250 |
251 | get sliderColor(): string {
252 | let returnColor = 'inherit';
253 | if (this._config.slider?.use_state_color) {
254 | if (this.stateObj.attributes.hs_color && this.attribute !== LightAttributes.COLOR_TEMP) {
255 | const [hue, sat] = this.stateObj.attributes.hs_color;
256 | let useHue = hue;
257 | let useSat = sat;
258 | switch(this.attribute) {
259 | case LightAttributes.HUE:
260 | useHue = this.valueFromPercentage;
261 | break;
262 | case LightAttributes.SATURATION:
263 | useSat = this.percentage;
264 | break;
265 | }
266 | if (useSat > 10) {
267 | returnColor = `hsl(${useHue}, 100%, ${100 - useSat / 2}%)`;
268 | this._sliderPrevColor = returnColor;
269 | }
270 | } else if (
271 | this.attribute === LightAttributes.HUE || this.attribute === LightAttributes.SATURATION
272 | ) {
273 | let useHue = 0;
274 | let useSat = 20;
275 | switch(this.attribute) {
276 | case LightAttributes.HUE:
277 | useHue = this.valueFromPercentage;
278 | break;
279 | case LightAttributes.SATURATION:
280 | useSat = this.percentage;
281 | break;
282 | }
283 | if (useSat > 10) {
284 | returnColor = `hsl(${useHue}, 100%, ${100 - useSat / 2}%)`;
285 | this._sliderPrevColor = returnColor;
286 | }
287 | } else if (
288 | this.stateObj.attributes.color_temp &&
289 | this.stateObj.attributes.min_mireds &&
290 | this.stateObj.attributes.max_mireds
291 | ) {
292 | returnColor = getLightColorBasedOnTemperature(
293 | this.attribute === LightAttributes.COLOR_TEMP ? this.valueFromPercentage : this.stateObj.attributes.color_temp,
294 | this.stateObj.attributes.min_mireds,
295 | this.stateObj.attributes.max_mireds
296 | );
297 | this._sliderPrevColor = returnColor;
298 | } else if (this.attribute === LightAttributes.COLOR_TEMP) {
299 | returnColor = getLightColorBasedOnTemperature(
300 | this.valueFromPercentage,
301 | 153,
302 | 500
303 | );
304 | this._sliderPrevColor = returnColor;
305 | } else if (this._sliderPrevColor.startsWith('hsl') || this._sliderPrevColor.startsWith('rgb')) {
306 | returnColor = this._sliderPrevColor;
307 | }
308 | }
309 | return returnColor;
310 | }
311 | }
312 |
--------------------------------------------------------------------------------
/src/controllers/lock-controller.ts:
--------------------------------------------------------------------------------
1 | import { STATES_OFF } from 'custom-card-helpers';
2 | import { Controller } from './controller';
3 |
4 | export class LockController extends Controller {
5 | _min = 0;
6 | _max = 1;
7 | _targetValue;
8 | _invert = false;
9 |
10 | get _value(): number {
11 | return !STATES_OFF.includes(this.stateObj.state)
12 | ? 1
13 | : 0;
14 | }
15 |
16 | set _value(value) {
17 | const service = value > 0 ? 'lock' : 'unlock';
18 | this._hass.callService('lock', service, {
19 | // eslint-disable-next-line @typescript-eslint/camelcase
20 | entity_id: this.stateObj.entity_id
21 | });
22 | }
23 |
24 | get _step(): number {
25 | return 1;
26 | }
27 |
28 | get label(): string {
29 | if (this.percentage > 0) {
30 | return this._hass.localize('component.lock.entity_component._.state.unlocked');
31 | }
32 | return this._hass.localize('component.lock.entity_component._.state.locked');
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/controllers/media-controller.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/camelcase */
2 | import { Controller } from './controller';
3 |
4 | export class MediaController extends Controller {
5 | _min = 0;
6 | _max = 100;
7 | _step = 1;
8 | _targetValue;
9 | _invert = false;
10 |
11 | get _value(): number {
12 | return this.isUnavailable || this.stateObj?.attributes?.is_volume_muted
13 | ? 0
14 | : Math.floor(parseFloat(Number.parseFloat(this.stateObj.attributes.volume_level).toPrecision(2)) * 100.0);
15 | }
16 |
17 | set _value(value) {
18 | value = value / 100.0;
19 | this._hass.callService('media_player', 'volume_set', {
20 | entity_id: this.stateObj.entity_id,
21 | volume_level: value,
22 | });
23 | if (value)
24 | this._hass.callService('media_player', 'volume_mute', {
25 | entity_id: this.stateObj.entity_id,
26 | is_volume_muted: false,
27 | });
28 | }
29 |
30 | get isOff(): boolean {
31 | return this.stateObj.state === 'off';
32 | }
33 |
34 | get label(): string {
35 | if (this.stateObj.attributes.is_volume_muted) return '-';
36 | return !!this.stateObj.attributes.volume_level
37 | ? `${this.percentage}%`
38 | : this._hass.localize(`component.media_player.state._.${this.state}`);
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/src/controllers/number-controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller } from './controller';
2 |
3 | export class NumberController extends Controller {
4 | _targetValue;
5 | _invert = false;
6 |
7 | get _value(): number {
8 | return this.stateObj.state;
9 | }
10 |
11 | set _value(value) {
12 | this._hass.callService('number', 'set_value', {
13 | // eslint-disable-next-line @typescript-eslint/camelcase
14 | entity_id: this.stateObj.entity_id,
15 | value: value,
16 | });
17 | }
18 |
19 | get _min(): number {
20 | return this.stateObj.attributes.min;
21 | }
22 |
23 | get _max(): number {
24 | return this.stateObj.attributes.max;
25 | }
26 |
27 | get isValuePercentage(): boolean {
28 | return false;
29 | }
30 |
31 | get _step(): number {
32 | return this.stateObj.attributes.step;
33 | }
34 |
35 | get label(): string {
36 | return this.stateObj.attributes.unit_of_measurement ? `${this.targetValue} ${this.stateObj.attributes.unit_of_measurement}` : `${this.targetValue}`;
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/controllers/switch-controller.ts:
--------------------------------------------------------------------------------
1 | import { STATES_OFF } from 'custom-card-helpers';
2 | import { Controller } from './controller';
3 |
4 | export class SwitchController extends Controller {
5 | _min = 0;
6 | _max = 1;
7 | _targetValue;
8 | _invert = false;
9 |
10 | get _value(): number {
11 | return !STATES_OFF.includes(this.stateObj.state)
12 | ? 1
13 | : 0;
14 | }
15 |
16 | set _value(value) {
17 | const service = value > 0 ? 'turn_on' : 'turn_off';
18 | this._hass.callService('switch', service, {
19 | // eslint-disable-next-line @typescript-eslint/camelcase
20 | entity_id: this.stateObj.entity_id
21 | });
22 | }
23 |
24 | get _step(): number {
25 | return 1;
26 | }
27 |
28 | get label(): string {
29 | if (this.percentage > 0) {
30 | return this._hass.localize('component.switch.entity_component._.state.on');
31 | }
32 | return this._hass.localize('component.switch.entity_component._.state.off');
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/editor.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | /* eslint-disable @typescript-eslint/camelcase */
3 | import copy from 'fast-copy';
4 | import {
5 | CSSResult,
6 | LitElement,
7 | TemplateResult,
8 | css,
9 | html
10 | } from 'lit';
11 | import { customElement, property, state } from 'lit/decorators.js';
12 |
13 | import { formfieldDefinition } from '../elements/formfield';
14 | import { switchDefinition } from '../elements/switch';
15 | import { textfieldDefinition } from '../elements/textfield';
16 |
17 | import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin';
18 | import { ActionConfig, HomeAssistant, LovelaceCardEditor, computeDomain, fireEvent } from 'custom-card-helpers';
19 | import { localize } from './localize/localize';
20 | import { ActionButtonConfig, ActionButtonConfigDefault, ActionButtonMode, Domain, IconConfig, IconConfigDefault, SliderBackground, SliderButtonCardConfig, SliderConfig, SliderConfigDefault, SliderDirections } from './types';
21 | import { applyPatch, getEnumValues, getSliderDefaultForEntity } from './utils';
22 |
23 | @customElement('slider-button-card-editor')
24 | export class SliderButtonCardEditor extends ScopedRegistryHost(LitElement) implements LovelaceCardEditor {
25 | @property({ attribute: false }) public hass?: HomeAssistant;
26 |
27 | @state() private _config?: SliderButtonCardConfig;
28 | @state() private _helpers?: any;
29 |
30 | private _initialized = false;
31 | private directions = getEnumValues(SliderDirections);
32 | private backgrounds = getEnumValues(SliderBackground);
33 | private actionModes = getEnumValues(ActionButtonMode);
34 |
35 | static elementDefinitions = {
36 | ...formfieldDefinition,
37 | ...switchDefinition,
38 | ...textfieldDefinition,
39 | }
40 |
41 | firstUpdated(): void {
42 | // Use HA elements when using ScopedRegistry. Reference: https://gist.github.com/thomasloven/5f965bd26e5f69876890886c09dd9ba8
43 | this._loadHomeAssistantComponent("ha-entity-picker", { type: "entities", entities: [] });
44 | this._loadHomeAssistantComponent("ha-icon-picker", { type: "entities", entities: [] });
45 | this._loadHomeAssistantComponent("ha-selector", { type: "entities", entities: [] });
46 | }
47 |
48 | async _loadHomeAssistantComponent(component: string, card: {}): Promise {
49 | const registry = (this.shadowRoot as any)?.customElements;
50 | if (!registry || registry.get(component)) {
51 | return;
52 | }
53 |
54 | const ch = await (window as any).loadCardHelpers();
55 | const c = await ch.createCardElement(card);
56 | await c.constructor.getConfigElement();
57 |
58 | registry.define(component, window.customElements.get(component));
59 | }
60 |
61 | public async setConfig(config: SliderButtonCardConfig): Promise {
62 | this._config = config;
63 | }
64 |
65 | protected shouldUpdate(): boolean {
66 | if (!this._initialized) {
67 | this._initialize();
68 | }
69 |
70 | return true;
71 | }
72 |
73 | get _name(): string {
74 | return this._config?.name || '';
75 | }
76 |
77 | get _show_name(): boolean {
78 | return typeof this._config?.show_name === 'undefined' ? true : this._config?.show_name;
79 | }
80 |
81 | get _show_state(): boolean {
82 | return typeof this._config?.show_state === 'undefined' ? true : this._config?.show_state;
83 | }
84 |
85 | get _show_attribute(): boolean {
86 | return typeof this._config?.show_attribute === 'undefined' ? true : this._config?.show_attribute;
87 | }
88 |
89 | get _compact(): boolean {
90 | return typeof this._config?.compact !== 'boolean' ? false : this._config?.compact;
91 | }
92 |
93 | get _entity(): string {
94 | return this._config?.entity || '';
95 | }
96 |
97 | get _attribute(): string {
98 | return this._config?.attribute || '';
99 | }
100 |
101 | get _icon(): IconConfig {
102 | return this._config?.icon || IconConfigDefault;
103 | }
104 |
105 | get _slider(): SliderConfig {
106 | return this._config?.slider || SliderConfigDefault;
107 | }
108 |
109 | get _action_button(): ActionButtonConfig {
110 | return this._config?.action_button || ActionButtonConfigDefault;
111 | }
112 |
113 | get _entityAttributes(): string[] {
114 | if (!this.hass || !this._entity) {
115 | return [];
116 | }
117 | return Object.keys(this.hass.states[this._entity].attributes).sort();
118 | }
119 |
120 | protected _renderOptionSelector(
121 | configValue: string,
122 | options: string[] | { value: string; label: string }[] = [],
123 | label: string,
124 | value: string | ActionConfig | undefined,
125 |
126 | ): TemplateResult | void {
127 | if (!this._config) {
128 | return;
129 | }
130 |
131 | return html`
132 |
146 |
147 | `;
148 | }
149 |
150 | protected render(): TemplateResult | void {
151 | if (!this.hass) {
152 | return html``;
153 | }
154 |
155 | return html`
156 |
157 |
158 |
159 |
160 |
${localize('tabs.general.title')}
161 |
162 |
169 |
170 |
177 | ${this._renderOptionSelector(`attribute`, this._entityAttributes, localize('tabs.general.attribute'), this._attribute)}
178 |
179 |
180 |
185 |
186 |
187 |
192 |
193 |
194 |
199 |
200 |
201 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
${localize('tabs.icon.title')}
214 |
215 |
224 |
225 |
226 |
231 |
232 | ${this.renderStateColor('icon')}
233 |
234 |
245 |
246 |
247 |
248 |
249 |
250 |
${localize('tabs.slider.title')}
251 |
252 |
253 | ${this._renderOptionSelector(
254 | `slider.direction`,
255 | this.directions.map(direction => {
256 | return {'value': direction, 'label': localize(`direction.${direction}`)}
257 | }), localize('tabs.slider.direction'),
258 | this._slider.direction || ''
259 | )}
260 | ${this._renderOptionSelector(
261 | `slider.background`,
262 | this.backgrounds.map(background => {
263 | return {'value': background, 'label': localize(`background.${background}`)}
264 | }), localize('tabs.slider.background'),
265 | this._slider.background || ''
266 | )}
267 |
268 |
269 | ${this.renderBrightness('slider')}
270 | ${this.renderStateColor('slider')}
271 |
272 |
277 |
278 |
279 |
284 |
285 |
286 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
${localize('tabs.action_button.title')}
299 |
300 | ${this._renderOptionSelector(
301 | `action_button.mode`,
302 | this.actionModes.map(mode => {
303 | return {'value': mode, 'label': localize(`mode.${mode}`)}
304 | }), localize('tabs.action_button.mode'),
305 | this._action_button.mode || ''
306 | )}
307 | ${this._action_button.mode === ActionButtonMode.CUSTOM
308 | ? html`
309 |
317 |
318 | `
319 | :
320 | ''}
321 |
322 |
323 |
328 |
329 | ${this._action_button.mode === ActionButtonMode.CUSTOM
330 | ? html`
331 |
332 |
337 |
338 | `
339 | :
340 | ''}
341 |
342 | ${this._action_button.mode === ActionButtonMode.CUSTOM
343 | ? html`
344 |
355 | `
356 | :
357 | ''}
358 |
359 |
360 |
361 |
362 | `;
363 | }
364 |
365 | protected renderBrightness(path: string): TemplateResult | void {
366 | const item = this[`_${path}`];
367 | return html`
368 |
369 |
374 |
375 | `;
376 | }
377 |
378 | protected renderStateColor(path: string): TemplateResult | void {
379 | const item = this[`_${path}`];
380 | return html`
381 |
382 |
387 |
388 | `;
389 | }
390 |
391 | private _initialize(): void {
392 | if (this.hass === undefined) return;
393 | if (this._config === undefined) return;
394 | this._initialized = true;
395 | }
396 |
397 | private _valueChangedSelect(ev): void {
398 | const target = ev.target;
399 | const value = ev.detail.value;
400 | if (!value) {
401 | return;
402 | }
403 | this._changeValue(target.configValue, value);
404 | }
405 |
406 | private _valueChangedEntity(ev): void {
407 | const target = ev.target;
408 | const value = ev.target?.value;
409 | if (!value) {
410 | return;
411 | }
412 | const updateDefaults = computeDomain(value) !== computeDomain(this._config?.entity || 'light.dummy');
413 | this._changeValue(target.configValue, value);
414 | this._changeValue('name', '');
415 | this._changeValue('attribute', '');
416 | this._changeValue('icon.icon', '');
417 | if (updateDefaults) {
418 | const cfg = copy(this._config);
419 | applyPatch(cfg, ['slider'], getSliderDefaultForEntity(value));
420 | this._config = cfg;
421 | fireEvent(this, 'config-changed', { config: this._config });
422 | }
423 | }
424 |
425 | private _valueChanged(ev): void {
426 | const target = ev.target;
427 | const value = ev.target?.value;
428 | this._changeValue(target.configValue, target.checked !== undefined ? target.checked : value);
429 | }
430 |
431 | private _changeValue(configValue: string, value: string | boolean | number): void {
432 | if (!this._config || !this.hass) {
433 | return;
434 | }
435 | if (this[`_${configValue}`] !== undefined && this[`_${configValue}`] === value) {
436 | return;
437 | }
438 | if (configValue) {
439 | const cfg = copy(this._config);
440 | applyPatch(cfg, [...configValue.split('.')], value);
441 | this._config = cfg;
442 | if (value === '') {
443 | delete this._config[configValue];
444 | }
445 | }
446 | fireEvent(this, 'config-changed', { config: this._config });
447 | }
448 |
449 | static get styles(): CSSResult {
450 | return css`
451 | mwc-textfield {
452 | width: 100%;
453 | }
454 | mwc-switch {
455 | padding: 16px 6px;
456 | }
457 | .side-by-side {
458 | display: flex;
459 | flex-flow: row wrap;
460 | }
461 | .side-by-side > * {
462 | padding-right: 8px;
463 | width: 50%;
464 | flex-flow: column wrap;
465 | box-sizing: border-box;
466 | }
467 | .side-by-side > *:last-child {
468 | flex: 1;
469 | padding-right: 0;
470 | }
471 | .suffix {
472 | margin: 0 8px;
473 | }
474 | .group {
475 | padding: 15px;
476 | border: 1px solid var(--primary-text-color)
477 | }
478 | .tabs {
479 | overflow: hidden;
480 | }
481 | .tab {
482 | width: 100%;
483 | color: var(--primary-text-color);
484 | overflow: hidden;
485 | }
486 | .tab-label {
487 | display: flex;
488 | justify-content: space-between;
489 | padding: 1em 1em 1em 0em;
490 | border-bottom: 1px solid var(--secondary-text-color);
491 | font-weight: bold;
492 | cursor: pointer;
493 | }
494 | .tab-label:hover {
495 | /*background: #1a252f;*/
496 | }
497 | .tab-label::after {
498 | content: "❯";
499 | width: 1em;
500 | height: 1em;
501 | text-align: center;
502 | transition: all 0.35s;
503 | }
504 | .tab-content {
505 | max-height: 0;
506 | padding: 0 1em;
507 | background: var(--secondary-background-color);
508 | transition: all 0.35s;
509 | }
510 | input.tab-checkbox {
511 | position: absolute;
512 | opacity: 0;
513 | z-index: -1;
514 | }
515 | input.tab-checkbox:checked + .tab-label {
516 | border-color: var(--accent-color);
517 | }
518 | input.tab-checkbox:checked + .tab-label::after {
519 | transform: rotate(90deg);
520 | }
521 | input.tab-checkbox:checked ~ .tab-content {
522 | max-height: 100vh;
523 | padding: 1em;
524 | }
525 | `;
526 | }
527 | }
528 |
--------------------------------------------------------------------------------
/src/localize/languages/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "version": "v",
4 | "invalid_configuration": "Ungültige Konfiguration",
5 | "show_warning": "Zeige Warnung",
6 | "show_error": "Zeige Fehler"
7 | },
8 | "tabs": {
9 | "general": {
10 | "title": "Allgemein",
11 | "entity": "Entiät (vorgeschrieben)",
12 | "name": "Name (optional)",
13 | "show_name": "Namen zeigen?",
14 | "show_state": "Zustand zeigen?",
15 | "compact": "Kompakt?"
16 | },
17 | "icon": {
18 | "title": "Icon",
19 | "icon": "Icon (optional)",
20 | "show_icon": "Icon zeigen?",
21 | "use_state_color": "Zustandsfarbe verwenden?",
22 | "tap_action": "Tap action"
23 | },
24 | "slider": {
25 | "title": "Schieberegler",
26 | "direction": "Richtung",
27 | "background": "Hintergrund",
28 | "use_brightness": "Helligkeit benutzen?",
29 | "show_track": "Spur anzeigen?",
30 | "toggle_on_click": "Als Schalter benutzen (schieben deaktivieren)",
31 | "force_square": "Quadrat erzwingen?"
32 | },
33 | "action_button": {
34 | "title": "Action-Knopf",
35 | "mode": "Modus",
36 | "icon": "Icon",
37 | "show_button": "Knopf zeigen?",
38 | "show_spinner": "Spinner anzeigen?",
39 | "tap_action": "Tap action"
40 | }
41 | },
42 | "state": {
43 | "off": "Aus",
44 | "on": "An"
45 | },
46 | "direction": {
47 | "left-right": "Links nach Rechts",
48 | "top-bottom": "Oben nach Unten",
49 | "bottom-top": "Unten nach Oben"
50 | },
51 | "background": {
52 | "striped": "gestreift",
53 | "gradient": "Farbverlauf",
54 | "solid": "Einfarbig",
55 | "triangle": "Dreieck",
56 | "custom": "benuzerdefiniert"
57 | },
58 | "mode": {
59 | "toggle": "Umschalter",
60 | "custom": "benuzerdefiniert"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/localize/languages/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "version": "v",
4 | "invalid_configuration": "Invalid configuration",
5 | "show_warning": "Show Warning",
6 | "show_error": "Show Error"
7 | },
8 | "tabs": {
9 | "general": {
10 | "title": "General",
11 | "entity": "Entity (Required)",
12 | "attribute": "Attribute (Optional)",
13 | "name": "Name (Optional)",
14 | "show_name": "Show name?",
15 | "show_state": "Show state?",
16 | "show_attribute": "Show attribute?",
17 | "compact": "Compact?"
18 | },
19 | "icon": {
20 | "title": "Icon",
21 | "icon": "Icon (Optional)",
22 | "show_icon": "Show icon?",
23 | "use_state_color": "Use state color?",
24 | "tap_action": "Tap action"
25 | },
26 | "slider": {
27 | "title": "Slider",
28 | "direction": "Direction",
29 | "background": "Background",
30 | "use_brightness": "Use brightness?",
31 | "show_track": "Show track?",
32 | "toggle_on_click": "Act as a toggle (disable sliding)",
33 | "force_square": "Force square?"
34 | },
35 | "action_button": {
36 | "title": "Action button",
37 | "mode": "Mode",
38 | "icon": "Icon",
39 | "show_button": "Show button?",
40 | "show_spinner": "Show spinner?",
41 | "tap_action": "Tap action"
42 | }
43 | },
44 | "state": {
45 | "off": "Off",
46 | "on": "On"
47 | },
48 | "direction": {
49 | "left-right": "Left to right",
50 | "right-left": "Right to left",
51 | "top-bottom": "Top to bottom",
52 | "bottom-top": "Bottom to top"
53 | },
54 | "background": {
55 | "striped": "Striped",
56 | "gradient": "Gradient",
57 | "solid": "Solid",
58 | "triangle": "Triangle",
59 | "custom": "Custom"
60 | },
61 | "mode": {
62 | "toggle": "Toggle",
63 | "custom": "Custom"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/localize/languages/fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "version": "v",
4 | "invalid_configuration": "Configuration incorrecte",
5 | "show_warning": "Afficher les avertissement",
6 | "show_error": "Afficher les erreurs"
7 | },
8 | "tabs": {
9 | "general": {
10 | "title": "Général",
11 | "entity": "Entité (Obligatoire)",
12 | "name": "Nom (Optionnel)",
13 | "show_name": "Afficher le nom ?",
14 | "show_state": "Afficher l'état ?",
15 | "compact": "Compact ?"
16 | },
17 | "icon": {
18 | "title": "Icône",
19 | "icon": "Icône (Optionnel)",
20 | "show_icon": "Afficher l'icône ?",
21 | "use_state_color": "Afficher la couleur d'état?",
22 | "tap_action": "Action"
23 | },
24 | "slider": {
25 | "title": "Curseur",
26 | "direction": "Direction",
27 | "background": "Fond",
28 | "use_brightness": "Utiliser la luminosité ?",
29 | "show_track": "Afficher le chemin ?",
30 | "toggle_on_click": "Agir comme un bouton (désactive le curseur)",
31 | "force_square": "Forcer carré ?"
32 | },
33 | "action_button": {
34 | "title": "Bouton d'action",
35 | "mode": "Mode",
36 | "icon": "Icône",
37 | "show_button": "Afficher le bouton ?",
38 | "show_spinner": "Afficher spinner ?",
39 | "tap_action": "Action"
40 | }
41 | },
42 | "state": {
43 | "off": "Inactif",
44 | "on": "Actif"
45 | },
46 | "direction": {
47 | "left-right": "gauche à droite",
48 | "top-bottom": "haut à bas",
49 | "bottom-top": "Bas à haut"
50 | },
51 | "background": {
52 | "striped": "Rayures",
53 | "gradient": "Dégradé",
54 | "solid": "Uni",
55 | "triangle": "Triangle",
56 | "custom": "Personnalisé"
57 | },
58 | "mode": {
59 | "toggle": "Bascule",
60 | "custom": "Personnalisé"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/localize/languages/he.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "version": "v",
4 | "invalid_configuration": "תצורה לא חוקית",
5 | "show_warning": "הצג אזהרה",
6 | "show_error": "הצג שגיאה"
7 | },
8 | "tabs": {
9 | "general": {
10 | "title": "כללי",
11 | "entity": "ישיות (נדרש)",
12 | "name": "שם (אופציונלי)",
13 | "show_name": "להציג שם?",
14 | "show_state": "להציג מצב?",
15 | "compact": "קוֹמפָּקטִי?"
16 | },
17 | "icon": {
18 | "title": "סמליל",
19 | "icon": "סמליל (אופציונלי)",
20 | "show_icon": "להציג סמליל?",
21 | "use_state_color": "להשתמש בצבע מצב?",
22 | "tap_action": "פעולה בהקשה"
23 | },
24 | "slider": {
25 | "title": "גלילה",
26 | "direction": "כיוון",
27 | "background": "רקע",
28 | "use_brightness": "להשתמש בבהירות?",
29 | "show_track": "להציג מסלול?",
30 | "toggle_on_click": "פעל כמתג (השבת החלקה)",
31 | "force_square": "כוח מרובע?"
32 | },
33 | "action_button": {
34 | "title": "כפתור פעולה",
35 | "mode": "מצב",
36 | "icon": "סמליל",
37 | "show_button": "להציג כפתור?",
38 | "show_spinner": "להציג ספינר?",
39 | "tap_action": "פעולה בהקשה"
40 | }
41 | },
42 | "state": {
43 | "off": "כבוי",
44 | "on": "פועל"
45 | },
46 | "direction": {
47 | "left-right": "שמאל לימין",
48 | "top-bottom": "מלמעלה למטה",
49 | "bottom-top": "מלמטה למעלה"
50 | },
51 | "background": {
52 | "striped": "מפוספס",
53 | "gradient": "שיפוע",
54 | "solid": "מוצק",
55 | "triangle": "משולש",
56 | "custom": "מותאם אישית"
57 | },
58 | "mode": {
59 | "toggle": "החלפה",
60 | "custom": "מותאם אישית"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/localize/languages/ko.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "version": "v",
4 | "invalid_configuration": "유효하지 않은 설정입니다",
5 | "show_warning": "경고 표시",
6 | "show_error": "에러 표시"
7 | },
8 | "tabs": {
9 | "general": {
10 | "title": "일반",
11 | "entity": "구성 요소 (필수)",
12 | "name": "이름 (옵션)",
13 | "show_name": "이름 표시",
14 | "show_state": "상태 표시",
15 | "compact": "슬림 모드"
16 | },
17 | "icon": {
18 | "title": "아이콘",
19 | "icon": "아이콘 (옵션)",
20 | "show_icon": "아이콘 표시",
21 | "use_state_color": "상태 색상 사용",
22 | "tap_action": "탭 액션"
23 | },
24 | "slider": {
25 | "title": "슬라이더",
26 | "direction": "방향 지정",
27 | "background": "배경",
28 | "use_brightness": "밝기 사용",
29 | "show_track": "범위 표시",
30 | "toggle_on_click": "토글 버튼으로 동작(슬라이더 비활성화)",
31 | "force_square": "정사각형 모양으로 고정"
32 | },
33 | "action_button": {
34 | "title": "액션 버튼",
35 | "mode": "모드",
36 | "icon": "아이콘",
37 | "show_button": "버튼 표시",
38 | "show_spinner": "로딩 스피너 표시",
39 | "tap_action": "탭 액셥"
40 | }
41 | },
42 | "state": {
43 | "off": "꺼짐",
44 | "on": "켜짐"
45 | },
46 | "direction": {
47 | "left-right": "왼쪽에서 오른쪽",
48 | "top-bottom": "위에서 아래",
49 | "bottom-top": "아래에서 위"
50 | },
51 | "background": {
52 | "striped": "줄무늬",
53 | "gradient": "그레디언트",
54 | "solid": "단색",
55 | "triangle": "삼각형",
56 | "custom": "커스텀"
57 | },
58 | "mode": {
59 | "toggle": "토글 모드",
60 | "custom": "커스텀 모드"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/localize/languages/nl.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "version": "v",
4 | "invalid_configuration": "Ongeldige configuratie",
5 | "show_warning": "Toon waarschuwing",
6 | "show_error": "Toon fout"
7 | },
8 | "tabs": {
9 | "general": {
10 | "title": "Algemeen",
11 | "entity": "Entiteit (Verplicht)",
12 | "name": "Naam (Optioneel)",
13 | "show_name": "Toon naam?",
14 | "show_state": "Toon status?",
15 | "compact": "Compact?"
16 | },
17 | "icon": {
18 | "title": "Icoon",
19 | "icon": "Icoon (Optioneel)",
20 | "show_icon": "Toon icoon?",
21 | "use_state_color": "Gebruik status kleur?",
22 | "tap_action": "Tap actie"
23 | },
24 | "slider": {
25 | "title": "Schuifregelaar",
26 | "direction": "Richting",
27 | "background": "Actergrond",
28 | "use_brightness": "Gebruik helderheid?",
29 | "show_track": "Toon spoor?",
30 | "toggle_on_click": "Fungeren als een schakelaar (schuiven uitschakelen)",
31 | "force_square": "Forceer vierkant?"
32 | },
33 | "action_button": {
34 | "title": "Actie button",
35 | "mode": "Modus",
36 | "icon": "Icoon",
37 | "show_button": "Toon button?",
38 | "show_spinner": "Toon spinner?",
39 | "tap_action": "Tap actie"
40 | }
41 | },
42 | "state": {
43 | "off": "Uit",
44 | "on": "Aan"
45 | },
46 | "direction": {
47 | "left-right": "Links naar rechts",
48 | "top-bottom": "Boven naar onder",
49 | "bottom-top": "Onder naar boven"
50 | },
51 | "background": {
52 | "striped": "Gestreept",
53 | "gradient": "Verloop",
54 | "solid": "Vast",
55 | "triangle": "Driehoek",
56 | "custom": "Aangepast"
57 | },
58 | "mode": {
59 | "toggle": "Schakelaar",
60 | "custom": "Aangepast"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/localize/languages/pl.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "version": "v",
4 | "invalid_configuration": "Nieprawidłowa konfiguracja",
5 | "show_warning": "Pokaż ostrzeżenia",
6 | "show_error": "Pokaż błędy"
7 | },
8 | "tabs": {
9 | "general": {
10 | "title": "Ogólne",
11 | "entity": "Encja (Wymagana)",
12 | "name": "Nazwa (Opcjonalna)",
13 | "show_name": "Pokazać nazwę?",
14 | "show_state": "Pokazać stan?",
15 | "compact": "Kompaktowy?"
16 | },
17 | "icon": {
18 | "title": "Ikona",
19 | "icon": "Ikona (Opcjonalna)",
20 | "show_icon": "Pokazać ikonę?",
21 | "use_state_color": "Uzyć kolor stanu?",
22 | "tap_action": "Akcja kliknięcia"
23 | },
24 | "slider": {
25 | "title": "Suwak",
26 | "direction": "Kierunek",
27 | "background": "Tło",
28 | "use_brightness": "Użyć jasności?",
29 | "show_track": "Pokazać ślad?",
30 | "toggle_on_click": "Działaj jako przełącznik (wyłącz przesuwanie)",
31 | "force_square": "Wymusić kwadrat?"
32 | },
33 | "action_button": {
34 | "title": "Przycisk akcji",
35 | "mode": "Tryb",
36 | "icon": "Ikona",
37 | "show_button": "Pokazać przycisk?",
38 | "show_spinner": "Pokazać spinner?",
39 | "tap_action": "Akcja kliknięcia"
40 | }
41 | },
42 | "state": {
43 | "off": "Wyłączony",
44 | "on": "Włączony"
45 | },
46 | "direction": {
47 | "left-right": "Z lewej do prawej",
48 | "top-bottom": "Z góry na dół",
49 | "bottom-top": "Z dołu do góry"
50 | },
51 | "background": {
52 | "striped": "W paski",
53 | "gradient": "Gradient",
54 | "solid": "Pełne tło",
55 | "triangle": "Trójkąt",
56 | "custom": "Ustawienia własne"
57 | },
58 | "mode": {
59 | "toggle": "Przełącznik",
60 | "custom": "Ustawienia własne"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/localize/languages/pt.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "version": "v",
4 | "invalid_configuration": "Configuração Inválida",
5 | "show_warning": "Mostrar Aviso",
6 | "show_error": "Mostrar Erro"
7 | },
8 | "tabs": {
9 | "general": {
10 | "title": "Geral",
11 | "entity": "Entidade (Obrigatório)",
12 | "name": "Nome (Opcional)",
13 | "show_name": "Mostrar Nome?",
14 | "show_state": "Mostrar Estado?",
15 | "compact": "Compactar?"
16 | },
17 | "icon": {
18 | "title": "Ícone",
19 | "icon": "Ícone (Opcional)",
20 | "show_icon": "Mostrar Ícone?",
21 | "use_state_color": "Usar Cor de Estado?",
22 | "tap_action": "Ação de Toque"
23 | },
24 | "slider": {
25 | "title": "Slider",
26 | "direction": "Direção",
27 | "background": "Fundo",
28 | "use_brightness": "Usar Brilho?",
29 | "show_track": "Mostrar Acompanhamento?",
30 | "toggle_on_click": "Atua como um alternador (desative o deslizamento)",
31 | "force_square": "Forçar Quadrado?"
32 | },
33 | "action_button": {
34 | "title": "Botão de Ação",
35 | "mode": "Modo",
36 | "icon": "Ícone",
37 | "show_button": "Mostrar Botão?",
38 | "show_spinner": "Mostrar Spinner?",
39 | "tap_action": "Ação de Toque"
40 | }
41 | },
42 | "state": {
43 | "off": "Desligar",
44 | "on": "Ligar"
45 | },
46 | "direction": {
47 | "left-right": "Esquerda para a Direita",
48 | "top-bottom": "De Cima para Baixo",
49 | "bottom-top": "De Baixo para Cima"
50 | },
51 | "background": {
52 | "striped": "Listrado",
53 | "gradient": "Gradiente",
54 | "solid": "Sólido",
55 | "triangle": "Triângulo",
56 | "custom": "Personalizado"
57 | },
58 | "mode": {
59 | "toggle": "Alternancia",
60 | "custom": "Personalizado"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/localize/languages/ru.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "version": "v",
4 | "invalid_configuration": "Неверная конфигурация",
5 | "show_warning": "Показать предупреждения",
6 | "show_error": "Показать ошибки"
7 | },
8 | "tabs": {
9 | "general": {
10 | "title": "Общие",
11 | "entity": "Объект (обязательно)",
12 | "name": "Имя (Опционально)",
13 | "show_name": "Отображать имя?",
14 | "show_state": "Отображать статус?",
15 | "compact": "Компактный?"
16 | },
17 | "icon": {
18 | "title": "Иконка",
19 | "icon": "Иконка (Опционально)",
20 | "show_icon": "Показать иконку?",
21 | "use_state_color": "Использовать цвет статуса?",
22 | "tap_action": "Действие по нажатию"
23 | },
24 | "slider": {
25 | "title": "Слайдер",
26 | "direction": "Направление",
27 | "background": "Фон",
28 | "use_brightness": "Использовать яркость?",
29 | "show_track": "Показать трек?",
30 | "toggle_on_click": "Действовать как переключатель (отключить скольжение)",
31 | "force_square": "Отображать квадратным?"
32 | },
33 | "action_button": {
34 | "title": "Кнопка действия",
35 | "mode": "Режим",
36 | "icon": "Иконка",
37 | "show_button": "Отобразить кнопку?",
38 | "show_spinner": "Отобразить спиннер?",
39 | "tap_action": "Действие по нажатию"
40 | }
41 | },
42 | "state": {
43 | "off": "Выкл",
44 | "on": "Вкл"
45 | },
46 | "direction": {
47 | "left-right": "Слева направо",
48 | "top-bottom": "Сверху вниз",
49 | "bottom-top": "Снизу вверх"
50 | },
51 | "background": {
52 | "striped": "Полосатый",
53 | "gradient": "Градиент",
54 | "solid": "Сплошной цвет",
55 | "triangle": "Треугольник",
56 | "custom": "Свои настройки"
57 | },
58 | "mode": {
59 | "toggle": "Переключатель",
60 | "custom": "Свои настройки"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/localize/languages/sk.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "version": "v",
4 | "invalid_configuration": "Neplatná konfigurácia",
5 | "show_warning": "Zobraziť warning",
6 | "show_error": "Zobraziť error"
7 | },
8 | "tabs": {
9 | "general": {
10 | "title": "Všeobecné",
11 | "entity": "Entita (požadovaná)",
12 | "attribute": "Atribút (Voliteľné)",
13 | "name": "Názov (voliteľný)",
14 | "show_name": "Zobraziť názov?",
15 | "show_state": "Zobraziť stav?",
16 | "show_attribute": "Zobraziť atribútt?",
17 | "compact": "Kompaktné?"
18 | },
19 | "icon": {
20 | "title": "Ikona",
21 | "icon": "Ikona (voliteľné)",
22 | "show_icon": "Zobraziť ikonu?",
23 | "use_state_color": "Use state color?",
24 | "tap_action": "Klepnite na akciu"
25 | },
26 | "slider": {
27 | "title": "Posuvník",
28 | "direction": "Smer",
29 | "background": "Pozadie",
30 | "use_brightness": "Použiť jas?",
31 | "show_track": "Zobraziť skladbu?",
32 | "toggle_on_click": "Pôsobiť ako prepínač (zakázať posúvanie)",
33 | "force_square": "Silový štvorec?"
34 | },
35 | "action_button": {
36 | "title": "Akčné tlačidlo",
37 | "mode": "Režim",
38 | "icon": "Ikona",
39 | "show_button": "Zobraziť tlačidlo?",
40 | "show_spinner": "Zobraziť číselník?",
41 | "tap_action": "Klepnite na akciu"
42 | }
43 | },
44 | "state": {
45 | "off": "Vypnúť",
46 | "on": "Zapnúť"
47 | },
48 | "direction": {
49 | "left-right": "Zľava doprava",
50 | "right-left": "Zprava do ľava",
51 | "top-bottom": "Zhora nadol",
52 | "bottom-top": "Zdola nahor"
53 | },
54 | "background": {
55 | "striped": "Prúžkované",
56 | "gradient": "Gradient",
57 | "solid": "Pevné",
58 | "triangle": "Trojuholník",
59 | "custom": "Voliteľné"
60 | },
61 | "mode": {
62 | "toggle": "Prepnúť",
63 | "custom": "Voliteľné"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/localize/localize.ts:
--------------------------------------------------------------------------------
1 | import * as en from './languages/en.json';
2 | import * as de from './languages/de.json';
3 | import * as fr from './languages/fr.json';
4 | import * as he from './languages/he.json';
5 | import * as ko from './languages/ko.json';
6 | import * as nl from './languages/nl.json';
7 | import * as pl from './languages/pl.json';
8 | import * as pt from './languages/pt.json';
9 | import * as ru from './languages/ru.json';
10 | import * as sk from './languages/sk.json';
11 |
12 |
13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
14 | const languages: any = {
15 | en: en,
16 | de: de,
17 | fr: fr,
18 | he: he,
19 | ko: ko,
20 | nl: nl,
21 | pl: pl,
22 | pt: pt,
23 | ru: ru,
24 | sk: sk,
25 | };
26 |
27 | export function localize(string: string, search = '', replace = ''): string {
28 | const lang = (localStorage.getItem('selectedLanguage') || 'en').replace(/['"]+/g, '').replace('-', '_');
29 |
30 | let translated: string;
31 |
32 | try {
33 | translated = string.split('.').reduce((o, i) => o[i], languages[lang]);
34 | } catch (e) {
35 | translated = string.split('.').reduce((o, i) => o[i], languages['en']);
36 | }
37 |
38 | if (translated === undefined) translated = string.split('.').reduce((o, i) => o[i], languages['en']);
39 |
40 | if (search !== '' && replace !== '') {
41 | translated = translated.replace(search, replace);
42 | }
43 | return translated;
44 | }
45 |
--------------------------------------------------------------------------------
/src/slider-button-card.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import { ActionHandlerEvent, applyThemesOnElement, computeStateDomain, handleAction, hasConfigOrEntityChanged, HomeAssistant, LovelaceCard, LovelaceCardEditor, STATES_OFF, toggleEntity } from 'custom-card-helpers';
3 | import copy from 'fast-copy';
4 | import { css, CSSResult, customElement, eventOptions, html, LitElement, property, PropertyValues, query, state, TemplateResult } from 'lit-element';
5 | import { classMap } from 'lit-html/directives/class-map';
6 | import { ifDefined } from 'lit-html/directives/if-defined';
7 | import { styleMap } from 'lit-html/directives/style-map';
8 | import { actionHandler } from './action-handler-directive';
9 | import { CARD_VERSION } from './const';
10 | import { Controller } from './controllers/controller';
11 | import { ControllerFactory } from './controllers/get-controller';
12 | import './editor';
13 | import { localize } from './localize/localize';
14 |
15 | import type { SliderButtonCardConfig } from './types';
16 | import { ActionButtonConfigDefault, ActionButtonMode, IconConfigDefault, SliderDirections } from './types';
17 | import { getSliderDefaultForEntity } from './utils';
18 |
19 | /* eslint no-console: 0 */
20 | console.info(
21 | `%c SLIDER-BUTTON-CARD %c ${localize('common.version')}${CARD_VERSION} %c`,
22 | 'background-color: #555;color: #fff;padding: 3px 2px 3px 3px;border: 1px solid #555;border-radius: 3px 0 0 3px;font-family: Roboto,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)',
23 | 'background-color: transparent;color: #555;padding: 3px 3px 3px 2px;border: 1px solid #555; border-radius: 0 3px 3px 0;font-family: Roboto,Verdana,Geneva,sans-serif',
24 | 'background-color: transparent'
25 |
26 | );
27 |
28 | // This puts your card into the UI card picker dialog
29 | (window as any).customCards = (window as any).customCards || [];
30 | (window as any).customCards.push({
31 | type: 'slider-button-card',
32 | name: 'Slider button Card',
33 | description: 'A button card with slider',
34 | preview: true,
35 | });
36 |
37 | @customElement('slider-button-card')
38 | export class SliderButtonCard extends LitElement implements LovelaceCard {
39 | @property({attribute: false}) public hass!: HomeAssistant;
40 | @state() private config!: SliderButtonCardConfig;
41 | @query('.state') stateText;
42 | @query('.button') button;
43 | @query('.action') action;
44 | @query('.slider') slider;
45 | private changing = false;
46 | private changed = false;
47 | private ctrl!: Controller;
48 | private actionTimeout;
49 |
50 | public static async getConfigElement(): Promise {
51 | return document.createElement('slider-button-card-editor');
52 | }
53 |
54 | public static getStubConfig(hass: HomeAssistant, entities: string[]): object {
55 | const entity = entities.find(item => item.startsWith('light')) || '';
56 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
57 | const dummy = hass;
58 | return {
59 | entity: entity,
60 | slider: getSliderDefaultForEntity(entity),
61 | // eslint-disable-next-line @typescript-eslint/camelcase
62 | show_name: true,
63 | // eslint-disable-next-line @typescript-eslint/camelcase
64 | show_state: true,
65 | compact: false,
66 | icon: copy(IconConfigDefault),
67 | // eslint-disable-next-line @typescript-eslint/camelcase
68 | action_button: copy(ActionButtonConfigDefault),
69 | };
70 | }
71 | public getCardSize(): number {
72 | return 0;
73 | }
74 |
75 | public setConfig(config: SliderButtonCardConfig): void {
76 | if (!config) {
77 | throw new Error(localize('common.invalid_configuration'));
78 | }
79 |
80 | if (!config.entity) {
81 | throw new Error(localize('common.invalid_configuration'));
82 | }
83 |
84 | this.config = {
85 | slider: getSliderDefaultForEntity(config.entity),
86 | icon: copy(IconConfigDefault),
87 | // eslint-disable-next-line @typescript-eslint/camelcase
88 | show_name: true,
89 | // eslint-disable-next-line @typescript-eslint/camelcase
90 | show_state: true,
91 | compact: false,
92 | // eslint-disable-next-line @typescript-eslint/camelcase
93 | action_button: copy(ActionButtonConfigDefault),
94 | debug: false,
95 | ...config
96 | };
97 | this.ctrl = ControllerFactory.getInstance(this.config);
98 | }
99 |
100 | protected shouldUpdate(changedProps: PropertyValues): boolean {
101 | if (!this.config) {
102 | return false;
103 | }
104 | const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
105 | if (
106 | !oldHass ||
107 | oldHass.themes !== this.hass.themes ||
108 | oldHass.language !== this.hass.language
109 | ) {
110 | this.ctrl.log('shouldUpdate', 'forced true');
111 | return true;
112 | }
113 | return hasConfigOrEntityChanged(this, changedProps, false);
114 | }
115 |
116 | protected updated(changedProps: PropertyValues): void {
117 | this.updateValue(this.ctrl.value, false);
118 | this.animateActionEnd();
119 | const oldHass = changedProps.get('hass') as HomeAssistant | undefined;
120 | const oldConfig = changedProps.get('config') as
121 | | SliderButtonCardConfig
122 | | undefined;
123 | if (
124 | oldHass?.themes !== this.hass.themes ||
125 | oldConfig?.theme !== this.config.theme
126 | ) {
127 | this.ctrl.log('Theme','updated');
128 | applyThemesOnElement(this, this.hass.themes, this.config.theme);
129 | }
130 | this.ctrl.log('Updated', this.ctrl.value);
131 | }
132 |
133 | protected firstUpdated(_changedProperties: PropertyValues): void {
134 | super.firstUpdated(_changedProperties);
135 | }
136 |
137 | protected render(): TemplateResult | void {
138 | this.ctrl.hass = this.hass;
139 | if (!this.ctrl.stateObj) {
140 | return this._showError(localize('common.show_error'));
141 | }
142 | return html`
143 |
149 |
182 |
183 | `;
184 | }
185 |
186 | private renderText(): TemplateResult {
187 | if (!this.config.show_name && !this.config.show_state && !this.config.show_attribute) {
188 | return html``;
189 | }
190 | return html`
191 |
192 | ${this.config.show_name
193 | ? html`
194 |
${this.ctrl.name}
195 | `
196 | : ''}
197 |
198 |
199 | ${this.config.show_state
200 | ? html`
201 |
202 | ${this.ctrl.isUnavailable
203 | ? html`
204 | ${this.hass.localize('state.default.unavailable')}
205 | ` : html`
206 | ${this.ctrl.label}
207 | `}
208 |
209 | `
210 | : ''}
211 |
212 | ${this.config.show_attribute
213 | ? html`
214 |
215 | ${this.config.show_state && this.ctrl.attributeLabel
216 | ? html ` · `
217 | : ''}
218 | ${this.ctrl.attributeLabel}
219 |
220 | `
221 | : ''}
222 |
223 |
224 | `;
225 | }
226 |
227 | private renderIcon(): TemplateResult {
228 | if (this.config.icon?.show === false) {
229 | return html``;
230 | }
231 | let hasPicture = false;
232 | let backgroundImage = '';
233 | if (this.ctrl.stateObj.attributes.entity_picture) {
234 | backgroundImage = `url(${this.ctrl.stateObj.attributes.entity_picture})`;
235 | hasPicture = true;
236 | }
237 | return html`
238 | this._handleAction(e, this.config.icon)}
240 | .actionHandler=${actionHandler({
241 | hasHold: false,
242 | hasDoubleClick: false,
243 | })}
244 | style=${styleMap({
245 | 'background-image': `${backgroundImage}`,
246 | })}
247 | >
248 |
256 |
257 | `;
258 | }
259 |
260 | private renderAction(): TemplateResult {
261 | if (this.config.action_button?.show === false) {
262 | return html``;
263 | }
264 | if (this.config.action_button?.mode === ActionButtonMode.TOGGLE) {
265 | return html`
266 |
267 |
272 |
273 | `;
274 | }
275 | return html`
276 | this._handleAction(e, this.config.action_button)}
278 | .actionHandler=${actionHandler({
279 | hasHold: false,
280 | hasDoubleClick: false,
281 | })}
282 | >
283 |
287 | ${typeof this.config.action_button?.show_spinner === 'undefined' || this.config.action_button?.show_spinner
288 | ? html`
289 |
290 |
291 |
292 | `
293 | : ''}
294 |
295 | `;
296 | }
297 |
298 | private _handleAction(ev: ActionHandlerEvent, config): void {
299 | if (this.hass && this.config && ev.detail.action) {
300 | if (config.tap_action?.action === 'toggle' && !this.ctrl.isUnavailable) {
301 | this.animateActionStart();
302 | }
303 | handleAction(this, this.hass, {...config, entity: this.config.entity}, ev.detail.action);
304 | }
305 | }
306 |
307 | private async handleClick(ev: Event): Promise {
308 | if (this.ctrl.hasToggle && !this.ctrl.isUnavailable) {
309 | ev.preventDefault();
310 | this.animateActionStart();
311 | this.ctrl.log('Toggle');
312 | await toggleEntity(this.hass, this.config.entity);
313 | // this.setStateValue(this.ctrl.toggleValue);
314 | }
315 | }
316 |
317 | private _toggle(): void {
318 | if (this.hass && this.config) {
319 | // eslint-disable-next-line @typescript-eslint/camelcase
320 | handleAction(this, this.hass, {tap_action: {action: 'toggle'}, entity: this.config.entity}, 'tap');
321 | }
322 | }
323 |
324 | private setStateValue(value: number): void {
325 | this.ctrl.log('setStateValue', value);
326 | this.updateValue(value, false);
327 | this.ctrl.value = value;
328 | this.animateActionStart();
329 | }
330 |
331 | private animateActionStart(): void {
332 | this.animateActionEnd();
333 | if (this.action) {
334 | this.action.classList.add('loading');
335 | }
336 | }
337 |
338 | private animateActionEnd(): void {
339 | if (this.action) {
340 | clearTimeout(this.actionTimeout);
341 | this.actionTimeout = setTimeout(()=> {
342 | this.action.classList.remove('loading');
343 | }, 750)
344 | }
345 | }
346 |
347 | private updateValue(value: number, changing = true): void {
348 | this.changing = changing;
349 | this.changed = !changing;
350 | this.ctrl.log('updateValue', value);
351 | this.ctrl.targetValue = value;
352 | if (!this.button) {
353 | return
354 | }
355 | this.button.classList.remove('off');
356 | if (changing) {
357 | this.button.classList.add('changing');
358 | } else {
359 | this.button.classList.remove('changing');
360 | if (this.ctrl.isOff) {
361 | this.button.classList.add('off');
362 | }
363 | }
364 | if (this.stateText) {
365 | this.stateText.innerHTML = this.ctrl.isUnavailable ? `${this.hass.localize('state.default.unavailable')}` : this.ctrl.label;
366 | }
367 | this.button.style.setProperty('--slider-value', `${this.ctrl.percentage}%`);
368 | this.button.style.setProperty('--slider-bg-filter', this.ctrl.style.slider.filter);
369 | this.button.style.setProperty('--slider-color', this.ctrl.style.slider.color);
370 | this.button.style.setProperty('--icon-filter', this.ctrl.style.icon.filter);
371 | this.button.style.setProperty('--icon-color', this.ctrl.style.icon.color);
372 | this.button.style.setProperty('--icon-rotate-speed', this.ctrl.style.icon.rotateSpeed || '0s');
373 | }
374 |
375 | private _showError(error: string): TemplateResult {
376 | const errorCard = document.createElement('hui-error-card');
377 | errorCard.setConfig({
378 | type: 'error',
379 | error,
380 | origConfig: this.config
381 | });
382 |
383 | return html`
384 | ${errorCard}
385 | `;
386 | }
387 |
388 | private getColorFromVariable(color: string): string {
389 | if (typeof color !== 'undefined' && color.substring(0, 3) === 'var') {
390 | let varColor = window.getComputedStyle(this).getPropertyValue(color.substring(4).slice(0, -1)).trim();
391 | if (!varColor.length) {
392 | varColor = window.getComputedStyle(document.documentElement).getPropertyValue(color.substring(4).slice(0, -1)).trim();
393 | }
394 | return varColor
395 | }
396 | return color;
397 | }
398 |
399 | @eventOptions({passive: true})
400 | private onPointerDown(event: PointerEvent): void {
401 | if (this.config.slider?.direction === SliderDirections.TOP_BOTTOM
402 | || this.config.slider?.direction === SliderDirections.BOTTOM_TOP) {
403 | event.preventDefault();
404 | }
405 | event.stopPropagation();
406 | if (this.ctrl.isSliderDisabled) {
407 | return;
408 | }
409 | this.slider.setPointerCapture(event.pointerId);
410 | }
411 |
412 | @eventOptions({passive: true})
413 | private onPointerUp(event: PointerEvent): void {
414 | if (this.ctrl.isSliderDisabled) {
415 | return;
416 | }
417 |
418 | if (this.config.slider?.direction === SliderDirections.TOP_BOTTOM
419 | || this.config.slider?.direction === SliderDirections.BOTTOM_TOP) {
420 | this.setStateValue(this.ctrl.targetValue);
421 | this.slider.releasePointerCapture(event.pointerId);
422 | }
423 |
424 | if (!this.slider.hasPointerCapture(event.pointerId)) {
425 | return;
426 | }
427 |
428 | this.setStateValue(this.ctrl.targetValue);
429 | this.slider.releasePointerCapture(event.pointerId);
430 | }
431 |
432 | private onPointerCancel(event: PointerEvent): void {
433 | if (this.config.slider?.direction === SliderDirections.TOP_BOTTOM
434 | || this.config.slider?.direction === SliderDirections.BOTTOM_TOP) {
435 | return;
436 | }
437 | this.updateValue(this.ctrl.value, false);
438 | this.slider.releasePointerCapture(event.pointerId);
439 | }
440 |
441 | @eventOptions({passive: true})
442 | private onPointerMove(event: any): void {
443 | if (this.ctrl.isSliderDisabled) {
444 | return;
445 | }
446 | if (!this.slider.hasPointerCapture(event.pointerId)) return;
447 | const {left, top, width, height} = this.slider.getBoundingClientRect();
448 | const percentage = this.ctrl.moveSlider(event, {left, top, width, height});
449 | this.ctrl.log('onPointerMove', percentage);
450 | this.updateValue(percentage);
451 | }
452 |
453 | connectedCallback(): void {
454 | super.connectedCallback();
455 | }
456 |
457 | disconnectedCallback(): void {
458 | super.disconnectedCallback();
459 | }
460 |
461 | static get styles(): CSSResult {
462 | return css`
463 | ha-card {
464 | box-sizing: border-box;
465 | height: 100%;
466 | width: 100%;
467 | min-height: 7rem;
468 | display: flex;
469 | flex-direction: column;
470 | justify-content: space-between;
471 | touch-action: pan-y;
472 | overflow: hidden;
473 | --mdc-icon-size: 2.2em;
474 | }
475 | ha-card[data-mode="top-bottom"],
476 | ha-card[data-mode="bottom-top"] {
477 | touch-action: none;
478 | }
479 | ha-card.square {
480 | aspect-ratio: 1 / 1;
481 | }
482 | ha-card.compact {
483 | min-height: 3rem !important;
484 | }
485 | :host {
486 | --slider-bg-default-color: var(--primary-color, rgb(95, 124, 171));
487 | --slider-bg: var(--slider-color);
488 | --slider-bg-filter: brightness(100%);
489 | --slider-bg-direction: to right;
490 | --slider-track-color: #2b374e;
491 | --slider-tracker-color: transparent;
492 | --slider-value: 0%;
493 | --slider-transition-duration: 0.2s;
494 | /*--label-text-shadow: rgb(255 255 255 / 10%) -1px -1px 1px, rgb(0 0 0 / 50%) 1px 1px 1px;*/
495 | /*--label-color-on: var(--primary-text-color, white);*/
496 | /*--label-color-off: var(--primary-text-color, white);*/
497 | --icon-filter: brightness(100%);
498 | --icon-color: var(--paper-item-icon-color);
499 | --icon-rotate-speed: 0s;
500 | /*--state-color-on: #BAC0C6; */
501 | /*--state-color-off: var(--disabled-text-color);*/
502 | /*--state-text-shadow: rgb(255 255 255 / 10%) -1px -1px 1px, rgb(0 0 0 / 50%) 1px 1px 1px;*/
503 | --btn-bg-color-off: rgba(43,55,78,1);
504 | --btn-bg-color-on: #20293c;
505 | /*--action-icon-color-on: var(--paper-item-icon-color, black);*/
506 | /*--action-icon-color-off: var(--paper-item-icon-color, black);*/
507 | /*--action-spinner-color: var(--label-badge-text-color, white);*/
508 | }
509 | /* --- BUTTON --- */
510 |
511 | .button {
512 | position: relative;
513 | padding: 0.8rem;
514 | box-sizing: border-box;
515 | height: 100%;
516 | min-height: 7rem;
517 | width: 100%;
518 | display: block;
519 | overflow: hidden;
520 | transition: all 0.2s ease-in-out;
521 | touch-action: pan-y;
522 | }
523 | .button[data-mode="top-bottom"],
524 | .button[data-mode="bottom-top"] {
525 | touch-action: none;
526 | }
527 | ha-card.compact .button {
528 | min-height: 3rem !important;
529 | }
530 | .button.off {
531 | background-color: var(--btn-bg-color-off);
532 | }
533 |
534 | /* --- ICON --- */
535 |
536 | .icon {
537 | position: relative;
538 | cursor: pointer;
539 | width: var(--mdc-icon-size, 24px);
540 | height: var(--mdc-icon-size, 24px);
541 | box-sizing: border-box;
542 | padding: 0;
543 | outline: none;
544 | animation: var(--icon-rotate-speed, 0s) linear 0s infinite normal both running rotate;
545 | -webkit-tap-highlight-color: transparent;
546 | }
547 | .icon ha-icon {
548 | filter: var(--icon-filter, brightness(100%));
549 | color: var(--icon-color);
550 | transition: color 0.4s ease-in-out 0s, filter 0.2s linear 0s;
551 | }
552 | .icon.has-picture {
553 | background-size: cover;
554 | border-radius: 50%;
555 | }
556 | .icon.has-picture ha-icon{
557 | display: none;
558 | }
559 | .unavailable .icon ha-icon {
560 | color: var(--disabled-text-color);
561 | }
562 | .compact .icon {
563 | float: left;
564 | }
565 |
566 | /* --- TEXT --- */
567 |
568 | .text {
569 | position: absolute;
570 | bottom: 0;
571 | left: 0;
572 | padding: 0.8rem;
573 | pointer-events: none;
574 | user-select: none;
575 | font-size: 1.1rem;
576 | line-height: 1.3rem;
577 | max-width: calc(100% - 2em);
578 | /*text-shadow: rgb(255 255 255 / 10%) -1px -1px 1px, rgb(0 0 0 / 50%) 1px 1px 1px;*/
579 | }
580 | .compact .text {
581 | position: relative;
582 | top: 0.5rem;
583 | left: 0.5rem;
584 | display: inline-block;
585 | padding: 0;
586 | height: 1.3rem;
587 | width: 100%;
588 | overflow: hidden;
589 | max-width: calc(100% - 4em);
590 | }
591 | .compact.hide-action .text {
592 | max-width: calc(100% - 2em);
593 | }
594 |
595 | /* --- LABEL --- */
596 |
597 | .name {
598 | color: var(--label-color-on, var(--primary-text-color, white));
599 | text-overflow: ellipsis;
600 | overflow: hidden;
601 | white-space: nowrap;
602 | text-shadow: var(--label-text-shadow, none);
603 | }
604 | .off .name {
605 | color: var(--label-color-off, var(--primary-text-color, white));
606 | }
607 | .unavailable.off .name,
608 | .unavailable .name {
609 | color: var(--disabled-text-color);
610 | }
611 | .compact .name {
612 | display: inline-block;
613 | max-width: calc(100% - 3.5em);
614 | }
615 |
616 | /* --- STATE --- */
617 |
618 | .state {
619 | color: var(--state-color-on, var(--label-badge-text-color, white));
620 | text-overflow: ellipsis;
621 | white-space: nowrap;
622 | text-shadow: var(--state-text-shadow);
623 | transition: font-size 0.1s ease-in-out;
624 | }
625 | .changing .state {
626 | font-size: 150%;
627 | }
628 | .off .state {
629 | color: var(--state-color-off, var(--disabled-text-color));
630 | }
631 | .unavailable .state {
632 | color: var(--disabled-text-color);
633 | }
634 | .compact .state {
635 | display: inline-block;
636 | max-width: calc(100% - 0em);
637 | overflow: hidden;
638 | }
639 |
640 | /* --- ATTRIBUTE --- */
641 |
642 | .attribute {
643 | /*
644 | color: var(--state-color-on, var(--label-badge-text-color, white));
645 | text-overflow: ellipsis;
646 | overflow: hidden;
647 | white-space: nowrap;
648 | text-shadow: var(--state-text-shadow);
649 | max-width: calc(50% -2em);
650 | transition: font-size 0.1s ease-in-out;
651 | border: 1px solid red;
652 | */
653 | }
654 |
655 | .compact .attribute {
656 | display: inline-block;
657 | max-width: calc(100% - 0em);
658 | overflow: hidden;
659 | }
660 |
661 | .oneliner {
662 | color: var(--state-color-on, var(--label-badge-text-color, white));
663 | text-overflow: ellipsis;
664 | overflow: hidden;
665 | white-space: nowrap;
666 | max-width: 20px;
667 | width: 20px;
668 | text-shadow: var(--state-text-shadow);
669 | transition: font-size 0.1s ease-in-out;
670 | /*border: 1px solid blue;*/
671 | }
672 | /* --- SLIDER --- */
673 |
674 | .slider {
675 | position: absolute;
676 | top: 0px;
677 | left: 0px;
678 | height: 100%;
679 | width: 100%;
680 | background-color: var( --ha-card-background, var(--card-background-color, var(--btn-bg-color-on, black)) );
681 | cursor: ew-resize;
682 | z-index: 0;
683 | }
684 | .slider[data-mode="bottom-top"] {
685 | cursor: ns-resize;
686 | }
687 | .slider[data-mode="top-bottom"] {
688 | cursor: ns-resize;
689 | }
690 | .slider:active {
691 | cursor: grabbing;
692 | }
693 |
694 | /* --- SLIDER OVERLAY --- */
695 |
696 | .slider .toggle-overlay {
697 | position: absolute;
698 | top: 0px;
699 | left: 0px;
700 | height: 100%;
701 | width: 100%;
702 | cursor: pointer;
703 | opacity: 0;
704 | z-index: 999;
705 | }
706 |
707 | /* --- SLIDER BACKGROUND --- */
708 |
709 | .slider-bg {
710 | position: absolute;
711 | top: 0;
712 | left: 0px;
713 | height: 100%;
714 | width: 100%;
715 | background: var(--slider-bg);
716 | background-size: var(--slider-bg-size, 100% 100%);
717 | background-color: var(--slider-bg-color, transparent);
718 | background-position: var(--slider-bg-position, 0 0);
719 | filter: var(--slider-bg-filter, brightness(100%));
720 | }
721 | .off .slider .slider-bg {
722 | background-color: var( --ha-card-background, var(--card-background-color, var(--btn-bg-color-off, black)) );
723 | }
724 | .slider[data-background="solid"] .slider-bg {
725 | --slider-bg-color: var(--slider-color);
726 | }
727 | .slider[data-background="triangle"] .slider-bg {
728 | --slider-bg-direction: to bottom right;
729 | --slider-bg: linear-gradient(var(--slider-bg-direction), transparent 0%, transparent 50%, var(--slider-color) 50%, var(--slider-color) 100%);
730 | border-right: 0px solid;
731 | }
732 | .slider[data-background="triangle"][data-mode="right-left"] .slider-bg {
733 | --slider-bg-direction: to bottom left;
734 | }
735 | .slider[data-background="triangle"][data-mode="bottom-top"] .slider-bg {
736 | --slider-bg-direction: to top left;
737 | }
738 | .slider[data-background="triangle"][data-mode="top-bottom"] .slider-bg {
739 | --slider-bg-direction: to bottom left;
740 | }
741 | .slider[data-background="custom"] .slider-bg {
742 | --slider-bg: repeating-linear-gradient(-45deg, var(--slider-color) 0, var(--slider-color) 1px, var(--slider-color) 0, transparent 10%);
743 | --slider-bg-size: 30px 30px;
744 | }
745 | .slider[data-background="gradient"] .slider-bg {
746 | --slider-bg: linear-gradient(var(--slider-bg-direction), rgba(0, 0, 0, 0) -10%, var(--slider-color) 100%);
747 | }
748 | .slider[data-background="striped"] .slider-bg {
749 | --slider-bg: linear-gradient(var(--slider-bg-direction), var(--slider-color), var(--slider-color) 50%, transparent 50%, transparent);
750 | --slider-bg-size: 4px 100%;
751 | }
752 | .slider[data-background="striped"][data-mode="bottom-top"] .slider-bg,
753 | .slider[data-background="striped"][data-mode="top-bottom"] .slider-bg {
754 | --slider-bg-size: 100% 4px;
755 | }
756 | .slider[data-mode="right-left"] .slider-bg {
757 | --slider-bg-direction: to left;
758 | }
759 | .slider[data-mode="bottom-top"] .slider-bg {
760 | --slider-bg-direction: to top;
761 | }
762 | .slider[data-mode="top-bottom"] .slider-bg {
763 | --slider-bg-direction: to bottom;
764 | }
765 |
766 | /* --- SLIDER THUMB --- */
767 |
768 | .slider-thumb {
769 | position: relative;
770 | width: 100%;
771 | height: 100%;
772 | transform: translateX(var(--slider-value));
773 | background: transparent;
774 | transition: transform var(--slider-transition-duration) ease-in;
775 | }
776 | .changing .slider .slider-thumb {
777 | transition: none;
778 | }
779 | .slider[data-mode="right-left"] .slider-thumb {
780 | transform: translateX(calc(var(--slider-value) * -1)) !important;
781 | }
782 | .slider[data-mode="top-bottom"] .slider-thumb {
783 | transform: translateY(var(--slider-value)) !important;
784 | }
785 | .slider[data-mode="bottom-top"] .slider-thumb {
786 | transform: translateY(calc(var(--slider-value) * -1)) !important;
787 | }
788 |
789 | .slider-thumb:before {
790 | content: '';
791 | position: absolute;
792 | top: 0;
793 | left: -2px;
794 | height: 100%;
795 | width: 2px;
796 | background: var(--slider-color);
797 | opacity: 0;
798 | transition: opacity 0.2s ease-in-out 0s;
799 | box-shadow: var(--slider-color) 0px 1px 5px 1px;
800 | z-index: 999;
801 | }
802 | .slider[data-mode="top-bottom"] .slider-thumb:before {
803 | top: -2px;
804 | left: 0px;
805 | height: 2px;
806 | width: 100%;
807 | }
808 | .changing .slider-thumb:before {
809 | opacity: 0.5;
810 | }
811 | .off.changing .slider-thumb:before {
812 | opacity: 0;
813 | }
814 |
815 | .slider-thumb:after {
816 | content: '';
817 | position: absolute;
818 | top: 0;
819 | left: 0px;
820 | height: 100%;
821 | width: 100%;
822 | background: var( --ha-card-background, var(--card-background-color, var(--btn-bg-color-on, black)) );
823 | opacity: 1;
824 | }
825 | .slider[data-show-track="true"] .slider-thumb:after {
826 | opacity: 0.9;
827 | }
828 | .off .slider[data-show-track="true"] .slider-thumb:after {
829 | opacity: 1;
830 | }
831 |
832 | /* --- ACTION BUTTON --- */
833 |
834 | .action {
835 | position: relative;
836 | float: right;
837 | width: var(--mdc-icon-size, 24px);
838 | height: var(--mdc-icon-size, 24px);
839 | color: var(--action-icon-color-on, var(--paper-item-icon-color, black));
840 | cursor: pointer;
841 | outline: none;
842 | -webkit-tap-highlight-color: transparent;
843 | }
844 | .action ha-switch {
845 | position: absolute;
846 | right: 0;
847 | top: 5px;
848 | }
849 | .off .action {
850 | color: var(--action-icon-color-off, var(--paper-item-icon-color, black));
851 | }
852 | .unavailable .action {
853 | color: var(--disabled-text-color);
854 | }
855 |
856 |
857 | .circular-loader {
858 | position: absolute;
859 | left: -8px;
860 | top: -8px;
861 | width: calc(var(--mdc-icon-size, 24px) + 16px);
862 | height: calc(var(--mdc-icon-size, 24px) + 16px);
863 | opacity: 0;
864 | transition: opacity 0.2s ease-in-out;
865 | animation: rotate 2s linear infinite;
866 | }
867 | .action.loading .circular-loader {
868 | opacity: 1;
869 | }
870 |
871 | .loader-path {
872 | fill: none;
873 | stroke-width: 2px;
874 | stroke: var(--action-spinner-color, var(--label-badge-text-color, white));
875 | animation: animate-stroke 1.5s ease-in-out infinite both;
876 | stroke-linecap: round;
877 | }
878 |
879 | /* --- MISC --- */
880 |
881 | .unavailable .slider .toggle-overlay,
882 | .unavailable .action,
883 | .unavailable .action ha-switch,
884 | .unavailable .slider {
885 | cursor: not-allowed !important;
886 | }
887 |
888 |
889 | @keyframes rotate {
890 | 100% {
891 | transform: rotate(360deg);
892 | }
893 | }
894 |
895 | @keyframes animate-stroke {
896 | 0% {
897 | stroke-dasharray: 1, 200;
898 | stroke-dashoffset: 0;
899 | }
900 | 50% {
901 | stroke-dasharray: 89, 200;
902 | stroke-dashoffset: -35;
903 | }
904 | 100% {
905 | stroke-dasharray: 89, 200;
906 | stroke-dashoffset: -124;
907 | }
908 | }
909 | `;
910 | }
911 | }
912 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/camelcase */
2 | import { ActionConfig, LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from 'custom-card-helpers';
3 |
4 | declare global {
5 | interface HTMLElementTagNameMap {
6 | 'slider-button-card-editor': LovelaceCardEditor;
7 | 'hui-error-card': LovelaceCard;
8 | }
9 | }
10 |
11 | export interface SliderButtonCardConfig extends LovelaceCardConfig {
12 | type: string;
13 | entity: string;
14 | attribute?: string;
15 | name?: string;
16 | show_name?: boolean;
17 | show_state?: boolean;
18 | show_attribute?: boolean;
19 | icon?: IconConfig;
20 | action_button?: ActionButtonConfig;
21 | slider?: SliderConfig;
22 | theme?: string;
23 | debug?: boolean;
24 | compact?: boolean;
25 | }
26 |
27 | export interface ActionButtonConfig {
28 | mode?: ActionButtonMode;
29 | icon?: string;
30 | show?: boolean;
31 | show_spinner?: boolean;
32 | tap_action?: ActionConfig;
33 | }
34 |
35 | export interface IconConfig {
36 | icon?: string;
37 | show?: boolean;
38 | tap_action?: ActionConfig;
39 | use_state_color?: boolean;
40 | }
41 |
42 | export interface SliderConfig {
43 | min?: number;
44 | max?: number;
45 | step?: number;
46 | attribute?: string;
47 | direction?: SliderDirections;
48 | background: SliderBackground;
49 | use_percentage_bg_opacity?: boolean;
50 | use_state_color?: boolean;
51 | show_track?: boolean;
52 | toggle_on_click?: boolean;
53 | invert?: boolean;
54 | force_square: boolean;
55 | }
56 |
57 | export enum ActionButtonMode {
58 | TOGGLE = 'toggle',
59 | CUSTOM = 'custom',
60 | }
61 |
62 | export enum SliderDirections {
63 | LEFT_RIGHT = 'left-right',
64 | RIGHT_LEFT = 'right-left',
65 | TOP_BOTTOM = 'top-bottom',
66 | BOTTOM_TOP = 'bottom-top',
67 | }
68 |
69 | export enum SliderBackground {
70 | SOLID = 'solid',
71 | GRADIENT = 'gradient',
72 | TRIANGLE = 'triangle',
73 | STRIPED = 'striped',
74 | CUSTOM = 'custom',
75 | }
76 |
77 | export enum Domain {
78 | LIGHT = 'light',
79 | SWITCH = 'switch',
80 | FAN = 'fan',
81 | COVER = 'cover',
82 | INPUT_BOOLEAN = 'input_boolean',
83 | INPUT_NUMBER = 'input_number',
84 | MEDIA_PLAYER = 'media_player',
85 | NUMBER = 'number',
86 | CLIMATE = 'climate',
87 | LOCK = 'lock',
88 | AUTOMATION = 'automation',
89 | }
90 |
91 | export const ActionButtonConfigDefault: ActionButtonConfig = {
92 | mode: ActionButtonMode.TOGGLE,
93 | icon: 'mdi:power',
94 | show: true,
95 | show_spinner: true,
96 | tap_action: {
97 | action: 'toggle'
98 | },
99 | };
100 |
101 | export const IconConfigDefault: IconConfig = {
102 | show: true,
103 | use_state_color: true,
104 | tap_action: {
105 | action: 'more-info'
106 | },
107 | };
108 |
109 | export const SliderConfigDefault: SliderConfig = {
110 | direction: SliderDirections.LEFT_RIGHT,
111 | background: SliderBackground.SOLID,
112 | use_percentage_bg_opacity: false,
113 | use_state_color: false,
114 | show_track: false,
115 | toggle_on_click: false,
116 | force_square: false,
117 | };
118 |
119 | export const SliderConfigDefaultDomain: Map = new Map([
120 | [Domain.LIGHT, {
121 | direction: SliderDirections.LEFT_RIGHT,
122 | background: SliderBackground.GRADIENT,
123 | use_state_color: true,
124 | use_percentage_bg_opacity: false,
125 | show_track: false,
126 | toggle_on_click: false,
127 | force_square: false,
128 | show_attribute: false,
129 | }],
130 | [Domain.FAN, {
131 | direction: SliderDirections.LEFT_RIGHT,
132 | background: SliderBackground.SOLID,
133 | use_state_color: false,
134 | use_percentage_bg_opacity: false,
135 | show_track: false,
136 | toggle_on_click: false,
137 | force_square: false,
138 | show_attribute: false,
139 | }],
140 | [Domain.SWITCH, {
141 | direction: SliderDirections.LEFT_RIGHT,
142 | background: SliderBackground.SOLID,
143 | use_state_color: false,
144 | use_percentage_bg_opacity: false,
145 | show_track: false,
146 | toggle_on_click: true,
147 | force_square: false,
148 | show_attribute: false,
149 | }],
150 | [Domain.AUTOMATION, {
151 | direction: SliderDirections.LEFT_RIGHT,
152 | background: SliderBackground.SOLID,
153 | use_state_color: false,
154 | use_percentage_bg_opacity: false,
155 | show_track: false,
156 | toggle_on_click: true,
157 | force_square: false,
158 | }],
159 | [Domain.COVER, {
160 | direction: SliderDirections.TOP_BOTTOM,
161 | background: SliderBackground.STRIPED,
162 | use_state_color: false,
163 | use_percentage_bg_opacity: false,
164 | toggle_on_click: false,
165 | show_track: false,
166 | force_square: false,
167 | invert: true,
168 | show_attribute: false,
169 | }],
170 | [Domain.INPUT_BOOLEAN, {
171 | direction: SliderDirections.LEFT_RIGHT,
172 | background: SliderBackground.SOLID,
173 | use_state_color: false,
174 | use_percentage_bg_opacity: false,
175 | show_track: false,
176 | toggle_on_click: true,
177 | force_square: false,
178 | show_attribute: false,
179 | }],
180 | [Domain.INPUT_NUMBER, {
181 | direction: SliderDirections.LEFT_RIGHT,
182 | background: SliderBackground.SOLID,
183 | use_state_color: false,
184 | use_percentage_bg_opacity: false,
185 | show_track: false,
186 | toggle_on_click: false,
187 | force_square: false,
188 | }],
189 | [Domain.MEDIA_PLAYER, {
190 | direction: SliderDirections.LEFT_RIGHT,
191 | background: SliderBackground.TRIANGLE,
192 | use_state_color: false,
193 | use_percentage_bg_opacity: false,
194 | show_track: true,
195 | toggle_on_click: false,
196 | force_square: false,
197 | show_attribute: true,
198 | attribute: "media_title",
199 | }],
200 | [Domain.LOCK, {
201 | direction: SliderDirections.LEFT_RIGHT,
202 | background: SliderBackground.SOLID,
203 | use_state_color: false,
204 | use_percentage_bg_opacity: false,
205 | show_track: false,
206 | toggle_on_click: true,
207 | force_square: false,
208 | show_attribute: false,
209 | }],
210 | [Domain.CLIMATE, {
211 | direction: SliderDirections.LEFT_RIGHT,
212 | background: SliderBackground.TRIANGLE,
213 | use_state_color: false,
214 | use_percentage_bg_opacity: false,
215 | show_track: true,
216 | toggle_on_click: false,
217 | force_square: false,
218 | show_attribute: false,
219 | }],
220 | ]);
221 |
222 | export enum LightAttributes {
223 | COLOR_TEMP = 'color_temp',
224 | BRIGHTNESS = 'brightness',
225 | BRIGHTNESS_PCT = 'brightness_pct',
226 | HUE = 'hue',
227 | SATURATION = 'saturation',
228 | ON_OFF = 'onoff',
229 | }
230 |
231 | export enum LightColorModes {
232 | COLOR_TEMP = 'color_temp',
233 | BRIGHTNESS = 'brightness',
234 | HS = 'hs',
235 | ON_OFF = 'onoff',
236 | }
237 |
238 | export enum CoverAttributes {
239 | POSITION = 'position',
240 | TILT = 'tilt',
241 | }
242 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { tinycolor, TinyColor } from '@ctrl/tinycolor';
2 | import { computeDomain } from 'custom-card-helpers';
3 | import copy from 'fast-copy';
4 | import { Domain, SliderConfig, SliderConfigDefault, SliderConfigDefaultDomain } from './types';
5 |
6 | export function getEnumValues(enumeration): string[] {
7 | return Object.keys(enumeration).map(key => enumeration[key]).filter(value => typeof value === 'string');
8 | }
9 |
10 | export const applyPatch = (data, path, value): void => {
11 | if (path.length === 1) {
12 | data[path[0]] = value;
13 | return;
14 | }
15 | if (!data[path[0]]) {
16 | data[path[0]] = {};
17 | }
18 | // eslint-disable-next-line consistent-return
19 | return applyPatch(data[path[0]], path.slice(1), value);
20 | };
21 |
22 | export function getSliderDefaultForEntity(entity: string): SliderConfig {
23 | const domain = computeDomain(entity) || Domain.LIGHT;
24 | const cfg = SliderConfigDefaultDomain.get(domain) || SliderConfigDefault;
25 | return copy(cfg);
26 | }
27 |
28 | export function getLightColorBasedOnTemperature(current: number, min: number, max: number): string {
29 | const high = new TinyColor('rgb(255, 160, 0)'); // orange-ish
30 | const low = new TinyColor('rgb(166, 209, 255)'); // blue-ish
31 | const middle = new TinyColor('white');
32 | const mixAmount = ((current - min) / (max - min)) * 100;
33 | if (mixAmount < 50) {
34 | return tinycolor(low)
35 | .mix(middle, mixAmount * 2)
36 | .toRgbString();
37 | } else {
38 | return tinycolor(middle)
39 | .mix(high, (mixAmount - 50) * 2)
40 | .toRgbString();
41 | }
42 | }
43 | export function toPercentage(value: number, min: number, max: number): number {
44 | return (((value - min) / max) * 100); //.toFixed(2);
45 | }
46 |
47 | export function percentageToValue(percent: number, min: number, max: number): number {
48 | return Math.floor(
49 | (percent * (max - min) / 100 + min)
50 | )
51 | }
52 |
53 | export const normalize = (value: number, min: number, max: number): number => {
54 | if (isNaN(value) || isNaN(min) || isNaN(max)) {
55 | // Not a number, return 0
56 | return 0;
57 | }
58 | if (value > max) return max;
59 | if (value < min) return min;
60 | return value;
61 | };
62 |
63 | export const capitalizeFirst = (s): string => (s && s[0].toUpperCase() + s.slice(1)) || "";
64 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "lib": [
7 | "es2017",
8 | "dom",
9 | "dom.iterable"
10 | ],
11 | "noEmit": true,
12 | "noUnusedParameters": true,
13 | "noImplicitReturns": true,
14 | "noFallthroughCasesInSwitch": true,
15 | "strict": true,
16 | "noImplicitAny": false,
17 | "skipLibCheck": true,
18 | "resolveJsonModule": true,
19 | "experimentalDecorators": true
20 | }
21 | }
--------------------------------------------------------------------------------