├── hacs.json ├── README.md └── set-timer-popup-card.js /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "set-timer-popup-card", 3 | "render_readme": true, 4 | "filename": "set-timer-popup-card.js" 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # set-timer-popup-card 2 | 3 | Lovelace card intended for use with [Switch timer](https://github.com/gh0stblizz4rd/ha-switch-timer) integration. Schedule a timer by using a [browser_mod](https://github.com/thomasloven/hass-browser_mod) popup. 4 | 5 |

6 | 7 |

8 | 9 | This card is designed to be placed inside a [browser_mod](https://github.com/thomasloven/hass-browser_mod) popup. The card will close the popup after 500 milliseconds automatically when a timer is scheduled (Requires your browser to be registered in browser_mod config panel). 10 | 11 | **Example browser_mod.popup call to display the card** 12 | ``` 13 | action: browser_mod.popup 14 | data: 15 | title: Popup title 16 | target: 17 | device_id: this 18 | content: 19 | type: custom:set-timer-popup-card 20 | entity: switch_timer.sample_light 21 | ``` 22 | This popup service can be set as `hold_action` or `tap_action` for a frontend card. 23 |

24 | 25 |

26 | 27 |

28 | 29 | ### Installation using [HACS](https://hacs.xyz/) 30 | 1. Go to the main screen of HACS and select custom repositories option in the top right corner menu. 31 |

32 | 33 |

34 | 2. Enter enter the URL of this repository, select type as Dashboard and click add. 35 |

36 | 37 |

38 | 3. Search for set-timer-popup-card in HACS and download the card. 39 |

40 | 41 |

42 | 4. After downloading the card, make sure to click the reload button in the prompt. The card is ready to be used. 43 |

44 | 45 |

46 | 47 | Enjoy! ;) 48 | 49 | 50 | ## Issues & support 51 | If you encounter any problems with the card, feel free to contact me by email romansa772@aol.com or open an issue in this repository. If you wish to support my work you can make a donation [here](https://paypal.me/romansaudzeris?country.x=LV&locale.x=en_US) or just give this repo a star. 52 | -------------------------------------------------------------------------------- /set-timer-popup-card.js: -------------------------------------------------------------------------------- 1 | const LitElement = Object.getPrototypeOf( 2 | customElements.get("ha-panel-lovelace") 3 | ); 4 | const html = LitElement.prototype.html; 5 | const css = LitElement.prototype.css; 6 | 7 | class SetTimerCard extends LitElement { 8 | set hass(hass) { 9 | this._hass = hass; 10 | this.entityState = this._hass.states[this.entity].state; 11 | } 12 | 13 | constructor() { 14 | super(); 15 | this.hoursColumnMoveIndex = 0; 16 | this.minutesColumnMoveIndex = 0; 17 | this.secondsColumnMoveIndex = 0; 18 | this.hoursMaxMoveIndex = 24; 19 | this.minutesMaxMoveIndex = 60; 20 | this.secondsMaxMoveIndex = 60; 21 | this.timerAction = ""; 22 | } 23 | 24 | static styles = css` 25 | .set-timer-card { 26 | overflow: hidden; 27 | height: 100%; 28 | } 29 | 30 | .container ha-card { 31 | border: none !important; 32 | padding: 12px; 33 | } 34 | 35 | .timer-input-card { 36 | display: flex; 37 | align-items: center; 38 | flex-direction: column; 39 | gap: 15px; 40 | border: none !important; 41 | } 42 | 43 | .timer-input-wrapper { 44 | display: flex; 45 | justify-content: center; 46 | flex-direction: column; 47 | align-items: center; 48 | gap: 8px; 49 | } 50 | 51 | .dimmed { 52 | opacity: 0.9; 53 | } 54 | 55 | .timer-setting-text { 56 | font-size: 17px; 57 | } 58 | 59 | .column-titles { 60 | display: flex; 61 | } 62 | 63 | .column-title { 64 | width: 90px; 65 | text-align: center; 66 | font-family: Arial, sans-serif; 67 | } 68 | 69 | .timer-columns-wrapper { 70 | width: fit-content; 71 | display: flex; 72 | align-items: center; 73 | } 74 | 75 | .timer-digit-column-wrapper { 76 | mask-image: linear-gradient( 77 | to bottom, 78 | rgba(0, 0, 0, 0), 79 | rgba(0, 0, 0, 1) 40%, 80 | rgba(0, 0, 0, 1) 60%, 81 | rgba(0, 0, 0, 0) 82 | ); 83 | z-index: 2; 84 | } 85 | 86 | .timer-digit-column { 87 | display: flex; 88 | flex-direction: column; 89 | height: 130px; 90 | font-size: 40px; 91 | font-family: Arial, sans-serif; 92 | transition: transform 100ms ease; 93 | } 94 | 95 | .timer-digit { 96 | text-align: center; 97 | min-width: 85px; 98 | min-height: 55px; 99 | } 100 | 101 | .digit-seperator { 102 | width: 4px; 103 | height: 130px; 104 | background-color: var(--primary-text-color); 105 | } 106 | 107 | .timer-action-selector { 108 | display: flex; 109 | align-items: center; 110 | z-index: 5; 111 | gap: 8px; 112 | } 113 | 114 | .timer-action { 115 | padding: 4px 6px; 116 | } 117 | 118 | .pointer-cursor { 119 | cursor: pointer; 120 | } 121 | 122 | .timer-action-active { 123 | color: var(--primary-background-color); 124 | background-color: var(--primary-text-color); 125 | border-radius: 17px; 126 | } 127 | .set-timer-button { 128 | padding: 10px 16px; 129 | background-color: var(--primary-color); 130 | color: white; 131 | border: none; 132 | border-radius: 4px; 133 | z-index: 5; 134 | cursor: pointer; 135 | font-size: 1rem; 136 | } 137 | `; 138 | render() { 139 | let actionClassList; 140 | if (this.entityState == "set") { 141 | actionClassList = "timer-action"; 142 | } else if (this.entityState == "idle") { 143 | actionClassList = "timer-action pointer-cursor"; 144 | } 145 | 146 | return html` 147 | 148 |
149 |
150 |
153 | 154 | 155 |
156 | Hours 157 | Minutes 158 | Seconds 159 |
160 |
161 |
166 |
167 |
168 |
00
169 |
01
170 |
02
171 |
03
172 |
04
173 |
05
174 |
06
175 |
07
176 |
08
177 |
09
178 |
10
179 |
11
180 |
12
181 |
13
182 |
14
183 |
15
184 |
16
185 |
17
186 |
18
187 |
19
188 |
20
189 |
21
190 |
22
191 |
23
192 |
193 |
194 |
195 |
200 |
201 |
202 |
00
203 |
01
204 |
02
205 |
03
206 |
04
207 |
05
208 |
06
209 |
07
210 |
08
211 |
09
212 |
10
213 |
11
214 |
12
215 |
13
216 |
14
217 |
15
218 |
16
219 |
17
220 |
18
221 |
19
222 |
20
223 |
21
224 |
22
225 |
23
226 |
24
227 |
25
228 |
26
229 |
27
230 |
28
231 |
29
232 |
30
233 |
31
234 |
32
235 |
33
236 |
34
237 |
35
238 |
36
239 |
37
240 |
38
241 |
39
242 |
40
243 |
41
244 |
42
245 |
43
246 |
44
247 |
45
248 |
46
249 |
47
250 |
48
251 |
49
252 |
50
253 |
51
254 |
52
255 |
53
256 |
54
257 |
55
258 |
56
259 |
57
260 |
58
261 |
59
262 |
263 |
264 |
265 |
270 |
271 |
272 |
00
273 |
01
274 |
02
275 |
03
276 |
04
277 |
05
278 |
06
279 |
07
280 |
08
281 |
09
282 |
10
283 |
11
284 |
12
285 |
13
286 |
14
287 |
15
288 |
16
289 |
17
290 |
18
291 |
19
292 |
20
293 |
21
294 |
22
295 |
23
296 |
24
297 |
25
298 |
26
299 |
27
300 |
28
301 |
29
302 |
30
303 |
31
304 |
32
305 |
33
306 |
34
307 |
35
308 |
36
309 |
37
310 |
38
311 |
39
312 |
40
313 |
41
314 |
42
315 |
43
316 |
44
317 |
45
318 |
46
319 |
47
320 |
48
321 |
49
322 |
50
323 |
51
324 |
52
325 |
53
326 |
54
327 |
55
328 |
56
329 |
57
330 |
58
331 |
59
332 |
333 |
334 |
336 |
337 |
340 | Turn on 347 | Turn off 352 | Toggle 357 |
358 | 361 |
362 | `; 363 | } 364 | 365 | _handleTouchStart(event) { 366 | event.preventDefault(); 367 | const touch = event.touches[0]; 368 | this.startY = touch.clientY; 369 | this.lastMoveDeltaY = 0; 370 | } 371 | 372 | _handleTouchMove(event) { 373 | event.preventDefault(); 374 | 375 | if (this.entityState == "idle") { 376 | const touch = event.changedTouches[0]; 377 | const endY = touch.clientY; 378 | const deltaY = this.startY - endY; 379 | let scrollDirectionUpward; 380 | 381 | if (deltaY > 0) { 382 | scrollDirectionUpward = true; 383 | } else if (deltaY < 0) { 384 | scrollDirectionUpward = false; 385 | } 386 | 387 | if (Math.abs(deltaY) - this.lastMoveDeltaY >= 20) { 388 | this.swipeColumn(scrollDirectionUpward, event.currentTarget.id); 389 | this.lastMoveDeltaY = Math.abs(deltaY); 390 | } 391 | } 392 | } 393 | 394 | _startIntervalUpdater() { 395 | this.timerUpdateInterval = window.setInterval(() => { 396 | this._updateRemaningTime( 397 | this._hass.states[this.entity].attributes.finishing_at 398 | ); 399 | }, 500); 400 | } 401 | 402 | _stopIntervalUpdater() { 403 | window.clearInterval(this.timerUpdateInterval); 404 | window.timerUpdateInterval = null; 405 | } 406 | 407 | connectedCallback() { 408 | super.connectedCallback(); 409 | 410 | if (this.entityState == "set") { 411 | this._startIntervalUpdater(); 412 | } 413 | } 414 | 415 | disconnectedCallback() { 416 | super.disconnectedCallback(); 417 | window.clearInterval(this.timerUpdateInterval); 418 | } 419 | 420 | _updateRemaningTime(finishingAt) { 421 | const finishingTime = new Date(finishingAt); 422 | const remainingMs = finishingTime - new Date(); 423 | const remainingH = Math.floor(remainingMs / (1000 * 60 * 60)); 424 | const remainingM = Math.floor(remainingMs / (1000 * 60)); 425 | const remainingS = Math.floor(remainingMs / 1000); 426 | const remainingTime = [ 427 | remainingH, 428 | remainingM - remainingH * 60, 429 | remainingS - remainingM * 60, 430 | ]; 431 | 432 | if (this.entityState == "idle") { 433 | this._stopIntervalUpdater(); 434 | this.hoursColumnMoveIndex = 0; 435 | this.minutesColumnMoveIndex = 0; 436 | this.secondsColumnMoveIndex = 0; 437 | 438 | this._moveTimerColumn(this.hoursColumnMoveIndex, "hours-column"); 439 | this._moveTimerColumn(this.minutesColumnMoveIndex, "minutes-column"); 440 | this._moveTimerColumn(this.secondsColumnMoveIndex, "seconds-column"); 441 | this.requestUpdate(); 442 | return null; 443 | } 444 | 445 | this.hoursColumnMoveIndex = remainingTime[0]; 446 | this.minutesColumnMoveIndex = remainingTime[1]; 447 | this.secondsColumnMoveIndex = remainingTime[2]; 448 | 449 | this._moveTimerColumn(this.hoursColumnMoveIndex, "hours-column"); 450 | this._moveTimerColumn(this.minutesColumnMoveIndex, "minutes-column"); 451 | this._moveTimerColumn(this.secondsColumnMoveIndex, "seconds-column"); 452 | } 453 | 454 | _handleScroll(event) { 455 | if (this.entityState == "idle") { 456 | const columnWrapper = event.currentTarget; 457 | const columnWrapperId = columnWrapper.id; 458 | let indexChange; 459 | 460 | event.preventDefault(); 461 | 462 | if (event.deltaY > 0) { 463 | indexChange = 1; 464 | } else if (event.deltaY < 0) { 465 | indexChange = -1; 466 | } 467 | 468 | let newIndex; 469 | switch (columnWrapperId) { 470 | case "hours-column": 471 | newIndex = this.hoursColumnMoveIndex + indexChange; 472 | 473 | if (newIndex < this.hoursMaxMoveIndex && newIndex >= 0) { 474 | this.hoursColumnMoveIndex = newIndex; 475 | this._moveTimerColumn(this.hoursColumnMoveIndex, columnWrapperId); 476 | } 477 | break; 478 | case "minutes-column": 479 | newIndex = this.minutesColumnMoveIndex + indexChange; 480 | if (newIndex < this.minutesMaxMoveIndex && newIndex >= 0) { 481 | this.minutesColumnMoveIndex = newIndex; 482 | this._moveTimerColumn(this.minutesColumnMoveIndex, columnWrapperId); 483 | } 484 | break; 485 | case "seconds-column": 486 | newIndex = this.secondsColumnMoveIndex + indexChange; 487 | if (newIndex < this.secondsMaxMoveIndex && newIndex >= 0) { 488 | this.secondsColumnMoveIndex = newIndex; 489 | this._moveTimerColumn(this.secondsColumnMoveIndex, columnWrapperId); 490 | } 491 | break; 492 | } 493 | } 494 | } 495 | 496 | swipeColumn(upwardDirection, columnWrapperId) { 497 | let indexChange; 498 | let newIndex; 499 | 500 | if (upwardDirection) { 501 | indexChange = 1; 502 | } else if (!upwardDirection) { 503 | indexChange = -1; 504 | } 505 | 506 | switch (columnWrapperId) { 507 | case "hours-column": 508 | newIndex = this.hoursColumnMoveIndex + indexChange; 509 | if (newIndex < this.hoursMaxMoveIndex && newIndex >= 0) { 510 | this.hoursColumnMoveIndex = newIndex; 511 | this._moveTimerColumn(this.hoursColumnMoveIndex, columnWrapperId); 512 | } 513 | break; 514 | case "minutes-column": 515 | newIndex = this.minutesColumnMoveIndex + indexChange; 516 | if (newIndex < this.minutesMaxMoveIndex && newIndex >= 0) { 517 | this.minutesColumnMoveIndex = newIndex; 518 | this._moveTimerColumn(this.minutesColumnMoveIndex, columnWrapperId); 519 | } 520 | break; 521 | case "seconds-column": 522 | newIndex = this.secondsColumnMoveIndex + indexChange; 523 | if (newIndex < this.secondsMaxMoveIndex && newIndex >= 0) { 524 | this.secondsColumnMoveIndex = newIndex; 525 | this._moveTimerColumn(this.secondsColumnMoveIndex, columnWrapperId); 526 | } 527 | break; 528 | } 529 | } 530 | 531 | _moveTimerColumn(columnMoveIndex, columnWrapperId) { 532 | const columnWrapper = this.shadowRoot.querySelector( 533 | `#${columnWrapperId} .timer-digit-column` 534 | ); 535 | columnWrapper.style.transform = `translateY(-${columnMoveIndex * 55}px)`; 536 | } 537 | 538 | _setTimerAction(clickEvent) { 539 | if (this.entityState == "idle") { 540 | this.renderRoot.querySelectorAll(".timer-action").forEach((button) => { 541 | button.classList.remove("timer-action-active"); 542 | }); 543 | const button = clickEvent.currentTarget; 544 | button.classList.add("timer-action-active"); 545 | this.timerAction = button.innerHTML; 546 | } 547 | } 548 | 549 | _submitAction(clickEvent) { 550 | if (this.entityState == "idle") { 551 | this._hass.callService("switch_timer", "set_timer", { 552 | entity_id: this.entity, 553 | action: this.timerAction, 554 | duration: `${String(this.hoursColumnMoveIndex).padStart( 555 | 2, 556 | "0" 557 | )}:${String(this.minutesColumnMoveIndex).padStart(2, "0")}:${String( 558 | this.secondsColumnMoveIndex 559 | ).padStart(2, "0")}`, 560 | }); 561 | 562 | setTimeout(() => { 563 | this.requestUpdate(); 564 | this._startIntervalUpdater(); 565 | }, 200); 566 | setTimeout(() => { 567 | this._hass.callService("browser_mod", "close_popup", { 568 | target: "this", 569 | }); 570 | }, 1500); 571 | } else if (this.entityState == "set") { 572 | this._stopIntervalUpdater(); 573 | this._hass.callService("switch_timer", "cancel_timer", { 574 | entity_id: this.entity, 575 | }); 576 | 577 | this.hoursColumnMoveIndex = 0; 578 | this.minutesColumnMoveIndex = 0; 579 | this.secondsColumnMoveIndex = 0; 580 | this._moveTimerColumn(this.hoursColumnMoveIndex, "hours-column"); 581 | this._moveTimerColumn(this.minutesColumnMoveIndex, "minutes-column"); 582 | this._moveTimerColumn(this.secondsColumnMoveIndex, "seconds-column"); 583 | setTimeout(() => { 584 | this.requestUpdate(); 585 | }, 200); 586 | } 587 | } 588 | setConfig(config) { 589 | if (!config.entity) { 590 | throw new Error("No timer entity supplied"); 591 | } else if (!config.entity.startsWith("switch_timer.")) { 592 | throw new Error( 593 | "The supplied entity is not a valid 'switch_timer' entity" 594 | ); 595 | } 596 | this.entity = config.entity; 597 | } 598 | 599 | getCardSize() { 600 | return 3; 601 | } 602 | } 603 | customElements.define("set-timer-popup-card", SetTimerCard); 604 | --------------------------------------------------------------------------------