Compare commits

...
22 Commits
Author SHA1 Message Date
GeorgeSG fe93d385e9 1.0.0 2020-07-17 20:22:38 +03:00
GeorgeSG 05e0952624 Implements #5: Add link_values option 2020-07-17 20:20:42 +03:00
GeorgeSG 3c10a9ab44 Fixes #6: Inherit border-radius from theme. Add variable for border radius 2020-07-17 19:09:34 +03:00
GeorgeSG 9a2a57af5c Update README 2020-05-18 22:22:32 +03:00
GeorgeSG 74949a3df2 0.1.1 2020-05-10 00:22:45 +03:00
GeorgeSG 224ee3b043 Remove commit activity from README 2020-05-10 00:18:38 +03:00
GeorgeSG 2e604a66d6 Update README 2020-05-10 00:17:43 +03:00
GeorgeSG b7ad71d979 Rename CI task. Don't lint twice 2020-05-10 00:15:20 +03:00
GeorgeSG 79418592c9 Fix 12AM representation. Add model tests 2020-05-10 00:13:29 +03:00
GeorgeSG 0297c9aba4 Update README 2020-05-09 20:42:35 +03:00
GeorgeSG 588cead35d 0.1.0 2020-05-09 20:39:14 +03:00
GeorgeSG 04462e096c Update README 2020-05-09 20:38:29 +03:00
GeorgeSG 03312aa5f7 Add new screenshot and reorganize README. Add code examples 2020-05-09 20:37:23 +03:00
GeorgeSG 4488b303d2 Add name position option. Refactor types 2020-05-09 20:24:37 +03:00
GeorgeSG 491e571aab Add layout.align option 2020-05-09 19:39:36 +03:00
GeorgeSG ae7dfb7668 Add test step in ci 2020-05-09 16:20:11 +03:00
GeorgeSG cc22b482b4 Add some tests 2020-05-09 16:17:53 +03:00
GeorgeSG 812e7f3016 Bump custom-card-helpers. Refactor types 2020-05-09 13:40:26 +03:00
GeorgeSG 20cba88ef2 Update README - add full URLs to images 2020-05-09 13:17:44 +03:00
GeorgeSG 5b6c9d0af0 Update README - add downloads shield 2020-05-09 12:58:27 +03:00
GeorgeSG 993fda7d34 Update README - HACS default 2020-05-09 12:50:11 +03:00
GeorgeSG df9de8bd00 Refactor editor datetimeEntities 2020-05-08 00:58:58 +03:00
25 changed files with 3163 additions and 218 deletions
+3 -2
View File
@@ -13,7 +13,8 @@
"rules": { "rules": {
"@typescript-eslint/camelcase": 0, "@typescript-eslint/camelcase": 0,
"@typescript-eslint/no-non-null-assertion": 0, "@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/explicit-function-return-type": [1, { "allowExpressions": true }] "@typescript-eslint/explicit-function-return-type": [1, { "allowExpressions": true }],
"@typescript-eslint/no-namespace": 0
}, },
"ignorePatterns": ["dist/", "node_modules/"] "ignorePatterns": ["dist/", "node_modules/", "*.js"]
} }
+3 -2
View File
@@ -9,12 +9,13 @@ on:
- master - master
jobs: jobs:
build: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Build - name: Build
run: | run: |
npm install npm install
npm run lint
npm run build npm run build
- name: Test
run: npm run test
+5
View File
@@ -0,0 +1,5 @@
{
"files.exclude": {
"dist/**": true
}
}
+101 -60
View File
@@ -1,11 +1,11 @@
# Time Picker Card by [@georgesg](https://www.github.com/georgesg) # Time Picker Card
[![hacs_badge][hacs-shield]][hacs] [![HACS][hacs-shield]][hacs-link]
[![GitHub Release][releases-shield]][releases] [![Downloads][downloads-shield]][downloads-link]
![CI](https://github.com/GeorgeSG/lovelace-time-picker-card/workflows/CI/badge.svg) [![GitHub Release][releases-shield]][releases-link]
[![Project Maintenance][maintenance-shield]][maintenance] [![CI][ci-shield]][ci-link]
[![GitHub Activity][commits-shield]][commits] [![Project Maintenance][maintenance-shield]][maintenance-link]
[![License][license-shield]][license] [![License][license-shield]][license-link]
## Overview ## Overview
@@ -13,27 +13,21 @@ This is a Time Picker Card for [Home Assistant](https://www.home-assistant.io/)'
Requires an [Input Datetime](https://www.home-assistant.io/integrations/input_datetime/) that has time (`has_time: true`). Requires an [Input Datetime](https://www.home-assistant.io/integrations/input_datetime/) that has time (`has_time: true`).
## Screenshots
### Default config - card name shown, 24 hour mode
![Default theme with card name](examples/default_with_name.png)
### Custom config - hidden card name, 12 hour mode
![Default theme with no card name](examples/default_without_name.png)
### Custom config - hidden card name, 12 hour mode with a "single" hour mode picker
![Default theme with single hour mode](examples/single_hour_mode.png)
### Using a custom theme
![Custom theme](examples/custom.png)
## Installation ## Installation
Install using [HACS](https://hacs.xyz) or follow this [guide](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins) ### HACS
Install using [HACS](https://hacs.xyz) and add the following to your config:
```yaml
resources:
- url: /hacsfiles/lovelace-time-picker-card/time-picker-card.js
type: module
```
### Manual
Download time-picker-card.js from the [latest realease](https://github.com/GeorgeSG/lovelace-time-picker-card/releases/latest) and place it in your `config/www` folder. Add the following to your config:
```yaml ```yaml
resources: resources:
@@ -47,22 +41,61 @@ resources:
Time Picker Card supports Lovelace's Visual Editor. Click the + button to add a card and search for time picker. Time Picker Card supports Lovelace's Visual Editor. Click the + button to add a card and search for time picker.
![Visual Editor](examples/visual_editor.png) ![Visual Editor](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/visual_editor.png)
## Examples
### YAML ### Default config - card name shown, 24 hour mode
![Default theme with card name](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/default_with_name.png)
```yaml ```yaml
type: 'custom:time-picker-card' type: 'custom:time-picker-card'
entity: input_datetime.alarm_time entity: input_datetime.alarm_time
name: 'Choose a time'
hour_mode: 24
hour_step: 1
minute_step: 5
hide:
name: false
``` ```
### Custom config - hidden card name, 12 hour mode
![Default theme with no card name](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/default_without_name.png)
```yaml
type: 'custom:time-picker-card'
entity: input_datetime.alarm_time
hour_mode: 12
hide:
name: true
```
### Custom config - hidden card name, 12 hour mode with a "single" hour mode picker
![Default theme with single hour mode](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/single_hour_mode.png)
```yaml
type: 'custom:time-picker-card'
entity: input_datetime.alarm_time
hour_mode: 12
layout:
hour_mode: single
hide:
name: true
```
### Custom config - card name inside card and controls aligned right
![Default theme with single hour mode](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/name_inside.png)
```yaml
type: 'custom:time-picker-card'
entity: input_datetime.alarm_time
layout:
name: inside
align_controls: right
```
### With a custom lovelace theme
![Custom theme](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/custom.png)
## Options ## Options
| Name | Type | Requirement | Description | Default | | Name | Type | Requirement | Description | Default |
@@ -70,54 +103,62 @@ hide:
| type | string | **Required** | `custom:time-picker-card` | | | type | string | **Required** | `custom:time-picker-card` | |
| entity | string | **Required** | [Input Datetime](https://www.home-assistant.io/integrations/input_datetime/) entity with `has_time: true` | | | entity | string | **Required** | [Input Datetime](https://www.home-assistant.io/integrations/input_datetime/) entity with `has_time: true` | |
| name | string | **Optional** | Card name | Entity's `friendly_name` | | name | string | **Optional** | Card name | Entity's `friendly_name` |
| link_values | boolean | **Optional** | If enabled, will change hour when minutes overflow. E.g. will go from 11:55 to 12:00, instead of 11:00 | `false` |
| hour_mode | `12` or `24` | **Optional** | Hour format. If `12`, card will show AM/PM picker | `24` | | hour_mode | `12` or `24` | **Optional** | Hour format. If `12`, card will show AM/PM picker | `24` |
| hour_step | number | **Optional** | Hour change when clicking arrows | `1` | | hour_step | number | **Optional** | Hour change when clicking arrows | `1` |
| minute_step | number | **Optional** | Minute change when clicking arrows | `5` | | minute_step | number | **Optional** | Minute change when clicking arrows | `5` |
| layout | object | **Optional** | Card Layout configuration | `none` | | layout | object | **Optional** | Card Layout configuration | `none` |
| hide | object | **Optional** | Hide object | `none` | | hide | object | **Optional** | Hide object | `none` |
## Layout Object ### Layout Object
| Name | Type | Requirement | Description | Default | | Name | Value | Requirement | Description | Default |
| --------- | -------------------- | ------------ | -------------------------------------------------------------------------------------------------- | -------- | | -------------- | ------------------------- | ------------ | -------------------------------------------------------------------------------------------------- | -------- |
| hour_mode | `single` or `double` | **Optional** | Whether to show both AM/PM or just the current mode. In `single` mode, tap the value to change it. | `double` | | hour_mode | `single`, `double` | **Optional** | Whether to show both AM/PM or just the current mode. In `single` mode, tap the value to change it. | `double` |
| align_controls | `left`, `center`, `right` | **Optional** | Horizontal alignment of the controls | `center` |
| name | `header`, `inside` | **Optional** | Whether to show the name as a header or inside the card | `header` |
## Hide Object ### Hide Object
| Name | Type | Requirement | Description | Default | | Name | Type | Requirement | Description | Default |
| ---- | ------- | ------------ | ------------------- | ------- | | ---- | ------- | ------------ | ------------------- | ------- |
| name | boolean | **Optional** | Hides the card name | `false` | | name | boolean | **Optional** | Hides the card name | `false` |
## Theme Variables ### Theme Variables
Time Picker Card will automatically pick up colors from your lovelace theme, but if you want to customize some of them, Time Picker Card will automatically pick up colors from your lovelace theme, but if you want to customize some of them,
you can use the following variables in your theme's config file: you can use the following variables in your theme's config file:
| Name | Default | Description | | Name | Default | Description |
| ------------------------------------- | ---------------------------- | -------------------------------------- | | ------------------------------------- | ------------------------------ | -------------------------------------- |
| time-picker-elements-background-color | `var(--primary-color)` | Background color for header and inputs | | time-picker-elements-background-color | `var(--primary-color)` | Background color for header and inputs |
| time-picker-icon-color | `var(--primary-text-color)` | Arrow color | | time-picker-icon-color | `var(--primary-text-color)` | Arrow color |
| time-picker-text-color | `white` | Text color | | time-picker-text-color | `white` | Text color |
| time-picker-accent-color | `var(--primary-color)` | AM / PM active color | | time-picker-accent-color | `var(--primary-color)` | AM / PM active color |
| time-picker-off-color | `var(--disabled-text-color)` | AM / PM inactive color | | time-picker-off-color | `var(--disabled-text-color)` | AM / PM inactive color |
| time-picker-border-radius | `var(--ha-card-border-radius)` | Border radius of the card |
## Meta ## Meta
**Georgi Gardev** **Georgi Gardev**
- [gar.dev](https://gar.dev) - [gar.dev](https://gar.dev)
- [![GitHub][github-icon]](https://github.com/GeorgeSG/) [GeorgeSG](https://github.com/GeorgeSG/) - [![GitHub][github-icon]][github-link] [GeorgeSG][github-link]
- [![Twitter][twitter-icon]](https://twitter.com/georgesg92) [@georgesg92](https://twitter.com/georgesg92) - [![Twitter][twitter-icon]][twitter-link] [@georgesg92][twitter-link]
[commits-shield]: https://img.shields.io/github/commit-activity/y/GeorgeSG/lovelace-time-picker-card?style=flat-square [hacs-shield]: https://img.shields.io/badge/HACS-Default-brightgreen.svg
[commits]: https://github.com/GeorgeSG/lovelace-time-picker-card/commits/master [hacs-link]: https://github.com/custom-components/hacs
[license-shield]: https://img.shields.io/github/license/GeorgeSG/lovelace-time-picker-card?style=flat-square [downloads-shield]: https://img.shields.io/github/downloads/GeorgeSG/lovelace-time-picker-card/latest/total?color=brightgreen&logo=github
[license]: https://github.com/GeorgeSG/lovelace-time-picker-card/blob/master/LICENSE [downloads-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/releases
[maintenance-shield]: https://img.shields.io/maintenance/yes/2020.svg?style=flat-square [releases-shield]: https://img.shields.io/github/release/GeorgeSG/lovelace-time-picker-card.svg
[maintenance]: https://github.com/GeorgeSG/lovelace-time-picker-card [releases-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/releases
[releases-shield]: https://img.shields.io/github/release/GeorgeSG/lovelace-time-picker-card.svg?style=flat-square [ci-shield]: https://img.shields.io/github/workflow/status/GeorgeSG/lovelace-time-picker-card/CI?label=CI&logo=github&
[releases]: https://github.com/GeorgeSG/lovelace-time-picker-card/releases [ci-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/actions?query=workflow%3ACI
[hacs-shield]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=flat-square [maintenance-shield]: https://img.shields.io/maintenance/yes/2020.svg
[hacs]: https://github.com/custom-components/hacs [maintenance-link]: https://github.com/GeorgeSG/lovelace-time-picker-card
[twitter-icon]: http://i.imgur.com/wWzX9uB.png [license-shield]: https://img.shields.io/github/license/GeorgeSG/lovelace-time-picker-card?color=brightgreen
[license-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/blob/master/LICENSE
[github-icon]: http://i.imgur.com/9I6NRUm.png [github-icon]: http://i.imgur.com/9I6NRUm.png
[github-link]: https://github.com/GeorgeSG/
[twitter-icon]: http://i.imgur.com/wWzX9uB.png
[twitter-link]: https://twitter.com/georgesg92
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

+2233 -27
View File
File diff suppressed because it is too large Load Diff
+13 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "time-picker-card", "name": "time-picker-card",
"version": "0.0.5", "version": "1.0.0",
"author": "Georgi Gardev <georgi@gardev.com>", "author": "Georgi Gardev <georgi@gardev.com>",
"description": "Lovelace time-picker-card", "description": "Lovelace time-picker-card",
"keywords": [ "keywords": [
@@ -19,31 +19,42 @@
"build": "npm run lint && npm run rollup", "build": "npm run lint && npm run rollup",
"lint": "eslint src/*.ts", "lint": "eslint src/*.ts",
"rollup": "rollup -c", "rollup": "rollup -c",
"test": "./node_modules/karma/bin/karma start ./test/karma.conf.js",
"release-patch": "npm version patch && git push origin master --tags", "release-patch": "npm version patch && git push origin master --tags",
"release-minor": "npm version minor && git push origin master --tags", "release-minor": "npm version minor && git push origin master --tags",
"release-major": "npm version major && git push origin master --tags" "release-major": "npm version major && git push origin master --tags"
}, },
"dependencies": { "dependencies": {
"custom-card-helpers": "^1.5.0", "custom-card-helpers": "^1.6.4",
"home-assistant-js-websocket": "^5.1.0", "home-assistant-js-websocket": "^5.1.0",
"lit-element": "^2.3.1", "lit-element": "^2.3.1",
"lit-html": "^1.2.1" "lit-html": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.9.0", "@babel/core": "^7.9.0",
"@open-wc/testing": "^2.5.16",
"@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-json": "^4.0.3", "@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.1", "@rollup/plugin-node-resolve": "^7.1.1",
"@rollup/plugin-typescript": "^4.0.0", "@rollup/plugin-typescript": "^4.0.0",
"@types/chai": "^4.2.11",
"@typescript-eslint/eslint-plugin": "^2.30.0", "@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/parser": "^2.30.0", "@typescript-eslint/parser": "^2.30.0",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3", "eslint-plugin-prettier": "^3.1.3",
"karma": "^5.0.5",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^3.1.0",
"karma-mocha": "^2.0.1",
"karma-rollup-preprocessor": "^7.0.5",
"mocha": "^7.1.2",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"rollup": "^1.32.1", "rollup": "^1.32.1",
"rollup-plugin-babel": "^4.4.0", "rollup-plugin-babel": "^4.4.0",
"rollup-plugin-serve": "^1.0.1", "rollup-plugin-serve": "^1.0.1",
"rollup-plugin-terser": "^5.3.0", "rollup-plugin-terser": "^5.3.0",
"sinon": "^9.0.2",
"typescript": "^3.8.3" "typescript": "^3.8.3"
} }
} }
+2 -2
View File
@@ -8,7 +8,7 @@ import {
TemplateResult, TemplateResult,
} from 'lit-element'; } from 'lit-element';
import { ClassInfo, classMap } from 'lit-html/directives/class-map'; import { ClassInfo, classMap } from 'lit-html/directives/class-map';
import { Period, HourModeLayout } from '../types'; import { Layout, Period } from '../types';
/** /**
* Renders a Time Period selector with an input for a value and two arrows for step-chaning * Renders a Time Period selector with an input for a value and two arrows for step-chaning
@@ -19,7 +19,7 @@ export class TimePeriodComponent extends LitElement {
static readonly EVENT_TOGGLE = 'toggle'; static readonly EVENT_TOGGLE = 'toggle';
@property() private period!: Period; @property() private period!: Period;
@property() private mode!: HourModeLayout; @property() private mode!: Layout.HourMode;
private get amClass(): ClassInfo { private get amClass(): ClassInfo {
return { 'time-period': true, active: this.period === Period.AM }; return { 'time-period': true, active: this.period === Period.AM };
+5 -8
View File
@@ -16,6 +16,7 @@ import { Direction } from '../types';
@customElement('time-unit') @customElement('time-unit')
export class TimeUnitComponent extends LitElement { export class TimeUnitComponent extends LitElement {
static readonly EVENT_UPDATE = 'update'; static readonly EVENT_UPDATE = 'update';
static readonly EVENT_STEP_CHANGE = 'stepChange';
@property() private unit!: TimeUnit; @property() private unit!: TimeUnit;
@@ -27,7 +28,7 @@ export class TimeUnitComponent extends LitElement {
class="time-input" class="time-input"
type="number" type="number"
placeholder="MM" placeholder="MM"
min="0" min=${this.unit.minValue}
max=${this.unit.maxValue} max=${this.unit.maxValue}
.value="${this.unit.toString()}" .value="${this.unit.toString()}"
@change=${this.onInputChange} @change=${this.onInputChange}
@@ -39,16 +40,12 @@ export class TimeUnitComponent extends LitElement {
onInputChange({ target: { value } }: { target: HTMLInputElement }): void { onInputChange({ target: { value } }: { target: HTMLInputElement }): void {
this.unit.setStringValue(value); this.unit.setStringValue(value);
this.emitUpdate(); const event = new CustomEvent(TimeUnitComponent.EVENT_UPDATE);
this.dispatchEvent(event);
} }
onStepChangerClick(direction: Direction): void { onStepChangerClick(direction: Direction): void {
this.unit.stepUpdate(direction); const event = new CustomEvent(TimeUnitComponent.EVENT_STEP_CHANGE, { detail: { direction } });
this.emitUpdate();
}
private emitUpdate(): void {
const event = new CustomEvent(TimeUnitComponent.EVENT_UPDATE);
this.dispatchEvent(event); this.dispatchEvent(event);
} }
+3
View File
@@ -1,4 +1,5 @@
import * as pkg from '../package.json'; import * as pkg from '../package.json';
import { Layout } from './types';
export const CARD_VERSION = pkg.version; export const CARD_VERSION = pkg.version;
export const CARD_SIZE = 3; export const CARD_SIZE = 3;
@@ -9,3 +10,5 @@ export const ENTITY_DOMAIN = 'input_datetime';
export const DEFAULT_HOUR_STEP = 1; export const DEFAULT_HOUR_STEP = 1;
export const DEFAULT_MINUTE_STEP = 5; export const DEFAULT_MINUTE_STEP = 5;
export const DEFAULT_LAYOUT_HOUR_MODE = 'double'; export const DEFAULT_LAYOUT_HOUR_MODE = 'double';
export const DEFAULT_LAYOUT_ALIGN_CONTROLS = Layout.AlignControls.CENTER;
export const DEFAULT_LAYOUT_NAME = Layout.Name.HEADER;
+110 -51
View File
@@ -1,4 +1,4 @@
import { computeDomain, HomeAssistant, LovelaceCardConfig } from 'custom-card-helpers'; import { computeDomain, HomeAssistant, LovelaceCardEditor } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket'; import { HassEntity } from 'home-assistant-js-websocket';
import { import {
css, css,
@@ -9,15 +9,14 @@ import {
property, property,
TemplateResult, TemplateResult,
} from 'lit-element'; } from 'lit-element';
import { DEFAULT_HOUR_STEP, DEFAULT_MINUTE_STEP, ENTITY_DOMAIN } from './const'; import {
import { HourMode, HourModeLayout, TimePickerCardConfig } from './types'; DEFAULT_HOUR_STEP,
DEFAULT_MINUTE_STEP,
// TODO: Remove this once there's a new release of custom-card-helpers that exports it. ENTITY_DOMAIN,
export interface LovelaceCardEditor extends HTMLElement { DEFAULT_LAYOUT_ALIGN_CONTROLS,
hass?: HomeAssistant; DEFAULT_LAYOUT_NAME,
lovelace?: unknown; } from './const';
setConfig(config: LovelaceCardConfig): void; import { TimePickerCardConfig, Layout, HourMode } from './types';
}
@customElement('time-picker-card-editor') @customElement('time-picker-card-editor')
export class TimePickerCardEditor extends LitElement implements LovelaceCardEditor { export class TimePickerCardEditor extends LitElement implements LovelaceCardEditor {
@@ -32,15 +31,11 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
} }
private get datetimeEntities(): Array<string> { private get datetimeEntities(): Array<string> {
// Find all input_datetime entities return Object.keys(this.hass.states).filter(
const datetimeEntities = Object.keys(this.hass.states) (entityId) =>
.filter((entityId) => computeDomain(entityId) === ENTITY_DOMAIN) computeDomain(entityId) === ENTITY_DOMAIN &&
.map((entityId) => this.hass.states[entityId]); this.hass.states[entityId].attributes.has_time === true
);
// Return only the datetime entities that have time.
return datetimeEntities
.filter((entity) => entity.attributes.has_time === true)
.map((entity) => entity.entity_id);
} }
render(): TemplateResult { render(): TemplateResult {
@@ -58,22 +53,37 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
${this.datetimeEntities.map((entity) => html`<paper-item>${entity}</paper-item>`)} ${this.datetimeEntities.map((entity) => html`<paper-item>${entity}</paper-item>`)}
</paper-listbox> </paper-listbox>
</paper-dropdown-menu> </paper-dropdown-menu>
<paper-input
label="Name (Optional)"
.configValue=${'name'}
.value=${this.config.name}
.placeholder=${this.entity?.attributes.friendly_name}
@value-changed=${this.onValueChange}
></paper-input>
<div class="side-by-side"> <div class="side-by-side">
<paper-input <div>
label="Name (Optional)" <ha-switch
.configValue=${'name'} style="margin-left: 10px"
.value=${this.config.name} .checked="${!Boolean(this.config.hide?.name)}"
.placeholder=${this.entity?.attributes.friendly_name} .configValue="${'hide_name'}"
@value-changed=${this.onValueChange} @change="${this.onHideNameChange}"
></paper-input> ></ha-switch>
<ha-switch
style="margin-left: 10px"
.checked="${!Boolean(this.config.hide?.name)}"
.configValue="${'hide_name'}"
@change="${this.onHideNameChange}"
>
Show name? Show name?
</ha-switch> </div>
<paper-dropdown-menu
style="width: 100%"
label="Name Position (Optional)"
@value-changed=${this.onLayoutNameChange}
>
<paper-listbox
slot="dropdown-content"
.selected=${Object.values(Layout.Name).indexOf(
this.config.layout?.name ?? DEFAULT_LAYOUT_NAME
)}
>
${Object.values(Layout.Name).map((name) => html`<paper-item>${name}</paper-item>`)}
</paper-listbox>
</paper-dropdown-menu>
</div> </div>
<div class="side-by-side"> <div class="side-by-side">
<paper-input <paper-input
@@ -92,20 +102,48 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
></paper-input> ></paper-input>
</div> </div>
<div class="side-by-side"> <div class="side-by-side">
<ha-switch <div>
.checked="${this.config.hour_mode === 12}" <ha-switch
.configValue="${'hour_mode'}" .checked="${this.config.hour_mode === 12}"
@change="${this.onHourModeChange}" .configValue="${'hour_mode'}"
> @change="${this.onHourModeChange}"
></ha-switch>
12-Hour mode 12-Hour mode
</ha-switch> </div>
<ha-switch ${this.config.hour_mode === 12
.checked="${this.config.layout?.hour_mode === 'single'}" ? html`<div>
.configValue="${'hour_mode_layout'}" <ha-switch
@change="${this.onHourModeLayoutChange}" .checked="${this.config.layout?.hour_mode === 'single'}"
.configValue="${'hour_mode_layout'}"
@change="${this.onHourModeLayoutChange}"
></ha-switch>
"Single" hour mode layout
</div>`
: ''}
</div>
<div class="side-by-side">
<paper-dropdown-menu
style="width: 100%"
label="Align Controls (Optional)"
@value-changed=${this.onLayoutAlignChange}
> >
"Single" hour mode layout (in 12-Hour mode only) <paper-listbox
</ha-switch> slot="dropdown-content"
.selected=${Object.values(Layout.AlignControls).indexOf(
this.config.layout?.align_controls ?? DEFAULT_LAYOUT_ALIGN_CONTROLS
)}
>
${Object.values(Layout.AlignControls).map((a) => html`<paper-item>${a}</paper-item>`)}
</paper-listbox>
</paper-dropdown-menu>
<div>
<ha-switch
.checked="${this.config.link_values}"
.configValue="${'link_values'}"
@change="${this.onLinkValuesChange}"
></ha-switch>
Link Values
</div>
</div> </div>
</div>`; </div>`;
} }
@@ -121,15 +159,35 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
this.dispatch(newConfig); this.dispatch(newConfig);
} }
private onHourModeLayoutChange({ target: { checked } }): void { private onHideNameChange({ target: { checked } }): void {
const hourModeLayout: HourModeLayout = checked ? 'single' : 'double'; const newConfig = { ...this.config, hide: { name: !checked } };
const newConfig = { ...this.config, layout: { hour_mode: hourModeLayout } };
this.dispatch(newConfig); this.dispatch(newConfig);
} }
private onHideNameChange({ target: { checked } }): void { private onLayoutAlignChange({ detail: { value } }: CustomEvent): void {
const newConfig = { ...this.config, hide: { name: !checked } }; const layout = { ...this.config.layout, align_controls: value };
const newConfig = { ...this.config, layout };
this.dispatch(newConfig);
}
private onLayoutNameChange({ detail: { value } }: CustomEvent): void {
const layout = { ...this.config.layout, name: value };
const newConfig = { ...this.config, layout };
this.dispatch(newConfig);
}
private onHourModeLayoutChange({ target: { checked } }): void {
const hourModeLayout: Layout.HourMode = checked ? 'single' : 'double';
const newConfig = {
...this.config,
layout: { ...this.config.layout, hour_mode: hourModeLayout },
};
this.dispatch(newConfig);
}
private onLinkValuesChange({ target: { checked } }): void {
const newConfig = { ...this.config, link_values: checked };
this.dispatch(newConfig); this.dispatch(newConfig);
} }
@@ -169,6 +227,7 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
return css` return css`
ha-switch { ha-switch {
padding: 16px 0; padding: 16px 0;
margin-right: 16px;
} }
.side-by-side { .side-by-side {
display: flex; display: flex;
+14 -10
View File
@@ -8,12 +8,16 @@ import { TimeUnit } from './time-unit';
export class Hour extends TimeUnit { export class Hour extends TimeUnit {
private static readonly VALUE_LIMIT = 24; private static readonly VALUE_LIMIT = 24;
constructor(value: number, step = DEFAULT_HOUR_STEP, private hourMode: HourMode) { constructor(value: number, step = DEFAULT_HOUR_STEP, private hourMode?: HourMode) {
super(value, step, Hour.VALUE_LIMIT); super(value, step, Hour.VALUE_LIMIT);
} }
get minValue(): number {
return this.hourMode === 12 ? 1 : 0; // there is no 00:00 AM -> it's 12:00 AM.
}
get maxValue(): number { get maxValue(): number {
return this.hourMode || Hour.VALUE_LIMIT; return this.hourMode === 12 ? 12 : Hour.VALUE_LIMIT - 1;
} }
togglePeriod(): void { togglePeriod(): void {
@@ -21,15 +25,15 @@ export class Hour extends TimeUnit {
} }
toString(): string { toString(): string {
const value = this.hourMode === 12 ? (this.value + 12) % 12 : this.value; let value;
if (this.hourMode === 12) {
value = (this.value + 12) % 12;
value = value === 0 ? 12 : value;
} else {
value = this.value;
}
return value < 10 ? `0${value}` : value.toString(); return value < 10 ? `0${value}` : value.toString();
} }
protected isValidString(valueStr: string): boolean {
const value = parseInt(valueStr);
const limit = this.hourMode || this._limit;
return !isNaN(value) && value >= 0 && value <= limit;
}
} }
+10 -4
View File
@@ -1,5 +1,6 @@
import { DEFAULT_MINUTE_STEP } from '../const'; import { DEFAULT_MINUTE_STEP } from '../const';
import { TimeUnit } from './time-unit'; import { TimeUnit } from './time-unit';
import { Direction } from '../types';
/** /**
* * Represents the minute value of a datetime. * * Represents the minute value of a datetime.
@@ -7,14 +8,19 @@ import { TimeUnit } from './time-unit';
export class Minute extends TimeUnit { export class Minute extends TimeUnit {
private static readonly VALUE_LIMIT = 60; private static readonly VALUE_LIMIT = 60;
maxValue = Minute.VALUE_LIMIT; minValue = 0;
maxValue = Minute.VALUE_LIMIT - 1;
constructor(value: number, step = DEFAULT_MINUTE_STEP) { constructor(value: number, step = DEFAULT_MINUTE_STEP) {
super(value, step, Minute.VALUE_LIMIT); super(value, step, Minute.VALUE_LIMIT);
} }
protected isValidString(valueStr: string): boolean { /**
const value = parseInt(valueStr); * Returns true if the minutes will overflow to a different hour when changed in {@param direction}.
return !isNaN(value) && value >= 0 && value <= this._limit; * @param direction
*/
willOverflow(direction: Direction): boolean {
const newValue = direction === Direction.UP ? this.value + this._step : this.value - this._step;
return newValue >= this._limit || newValue < 0;
} }
} }
+18 -10
View File
@@ -5,12 +5,11 @@ import { Direction } from '../types';
*/ */
export abstract class TimeUnit { export abstract class TimeUnit {
/** /**
* Whether {@param valueStr} can be set as a value of this instance. * The min allowed UI value for this instance.
*/ */
protected abstract isValidString(valueStr: string): boolean; abstract minValue: number;
/** /**
* The max allowed value for this instance. Used for UI validation. * The max allowed value for this instance.
*/ */
abstract maxValue: number; abstract maxValue: number;
@@ -40,27 +39,36 @@ export abstract class TimeUnit {
* Updates the value in {@param direction} by the step size specified in the constructoor. * Updates the value in {@param direction} by the step size specified in the constructoor.
* @param direction * @param direction
*/ */
stepUpdate(direction: Direction): void { stepUpdate(direction: Direction, step: number = this._step): void {
direction === Direction.UP ? this.increment() : this.decrement(); direction === Direction.UP ? this.increment(step) : this.decrement(step);
} }
toString(): string { toString(): string {
return this.value < 10 ? `0${this.value}` : this.value.toString(); return this.value < 10 ? `0${this.value}` : this.value.toString();
} }
private increment(): void { private increment(step: number = this._step): void {
this.setValue(this.value + this._step); this.setValue(this.value + step);
} }
private decrement(): void { private decrement(step: number = this._step): void {
this.setValue(this.value - this._step); this.setValue(this.value - step);
} }
protected setValue(newValue: number): void { protected setValue(newValue: number): void {
if (isNaN(newValue)) {
return;
}
if (newValue >= this._limit || newValue < 0) { if (newValue >= this._limit || newValue < 0) {
newValue = (newValue + this._limit) % this._limit; newValue = (newValue + this._limit) % this._limit;
} }
this._value = newValue; this._value = newValue;
} }
protected isValidString(valueStr: string): boolean {
const value = parseInt(valueStr);
return !isNaN(value) && value >= 0 && value < this._limit;
}
} }
+23
View File
@@ -0,0 +1,23 @@
import { Hour } from './hour';
import { Minute } from './minute';
import { Direction } from '../types';
export class Time {
constructor(public hour: Hour, public minute: Minute, private _linkValues: boolean = false) {}
hourStep(direction: Direction): void {
this.hour.stepUpdate(direction);
}
minuteStep(direction: Direction): void {
if (this._linkValues && this.minute.willOverflow(direction)) {
this.hour.stepUpdate(direction, 1);
}
this.minute.stepUpdate(direction);
}
get value(): string {
return `${this.hour.value}:${this.minute.value}`;
}
}
+8 -2
View File
@@ -1,6 +1,7 @@
import { html, TemplateResult } from 'lit-element'; import { html, TemplateResult } from 'lit-element';
import { TimePickerCardConfig } from './types'; import { TimePickerCardConfig } from './types';
import { LovelaceCard } from 'custom-card-helpers'; import { LovelaceCard } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
export class Partial { export class Partial {
static error(error: string, origConfig: TimePickerCardConfig): TemplateResult { static error(error: string, origConfig: TimePickerCardConfig): TemplateResult {
@@ -14,11 +15,16 @@ export class Partial {
return html`${errorCard}`; return html`${errorCard}`;
} }
static header(title: string, icon?: string): TemplateResult | null { static headerName(title: string): TemplateResult {
return html` return html`
<div class="time-picker-header"> <div class="time-picker-header">
${icon ? html`<ha-icon class="icon" icon=${icon}></ha-icon>` : ''}${title} ${title}
</div> </div>
`; `;
} }
static nestedName(name: string, entity: HassEntity): TemplateResult {
return html`<state-badge .stateObj=${entity}></state-badge>
<div class="entity-name-inside">${name}</div>`;
}
} }
+121 -34
View File
@@ -9,14 +9,23 @@ import {
property, property,
TemplateResult, TemplateResult,
} from 'lit-element'; } from 'lit-element';
import { ClassInfo, classMap } from 'lit-html/directives/class-map';
import {
CARD_SIZE,
CARD_VERSION,
DEFAULT_LAYOUT_HOUR_MODE,
ENTITY_DOMAIN,
DEFAULT_LAYOUT_ALIGN_CONTROLS,
} from './const';
import { Hour } from './models/hour';
import { Minute } from './models/minute';
import { Time } from './models/time';
import { Partial } from './partials';
import { Period, TimePickerCardConfig, Layout } from './types';
import './components/time-period.component'; import './components/time-period.component';
import './components/time-unit.component'; import './components/time-unit.component';
import './editor'; import './editor';
import { CARD_SIZE, CARD_VERSION, DEFAULT_LAYOUT_HOUR_MODE, ENTITY_DOMAIN } from './const';
import { Hour } from './models/hour';
import { Minute } from './models/minute';
import { Partial } from './partials';
import { Period, TimePickerCardConfig } from './types';
console.info( console.info(
`%c TIME-PICKER-CARD \n%c Version ${CARD_VERSION} `, `%c TIME-PICKER-CARD \n%c Version ${CARD_VERSION} `,
@@ -24,29 +33,38 @@ console.info(
'color: white; font-weight: bold; background: dimgray' 'color: white; font-weight: bold; background: dimgray'
); );
/* eslint-disable @typescript-eslint/no-explicit-any */ window.customCards = window.customCards || [];
(window as any).customCards = (window as any).customCards || []; window.customCards.push({
(window as any).customCards.push({
type: 'time-picker-card', type: 'time-picker-card',
name: 'Time Picker Card', name: 'Time Picker Card',
description: 'A Time Picker card for setting the time value of Input Datetime entities.', description: 'A Time Picker card for setting the time value of Input Datetime entities.',
}); });
/* eslint-enable @typescript-eslint/no-explicit-any */
@customElement('time-picker-card') @customElement('time-picker-card')
export class TimePickerCard extends LitElement implements LovelaceCard { export class TimePickerCard extends LitElement implements LovelaceCard {
@property({ type: Object }) hass!: HomeAssistant; @property({ type: Object }) hass!: HomeAssistant;
@property() private config!: TimePickerCardConfig; @property() private config!: TimePickerCardConfig;
@property() private hour!: Hour; @property() private time!: Time;
@property() private minute!: Minute;
@property() private period!: Period; @property() private period!: Period;
private get entity(): HassEntity | undefined { private get entity(): HassEntity | undefined {
return this.hass.states[this.config.entity]; return this.hass.states[this.config.entity];
} }
private get shouldShowName(): boolean { private get hasNameInHeader(): boolean {
return Boolean(this.config.hide?.name) === false && Boolean(this.name); return (
Boolean(this.name) &&
Boolean(this.config.hide?.name) === false &&
this.config.layout?.name !== Layout.Name.INSIDE
);
}
private get hasNameInside(): boolean {
return (
Boolean(this.name) &&
Boolean(this.config.hide?.name) === false &&
this.config.layout?.name === Layout.Name.INSIDE
);
} }
private get name(): string | undefined { private get name(): string | undefined {
@@ -57,6 +75,24 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
return this.config.hour_mode === 12; return this.config.hour_mode === 12;
} }
private get layoutAlign(): Layout.AlignControls {
return this.config.layout?.align_controls ?? DEFAULT_LAYOUT_ALIGN_CONTROLS;
}
private get rowClass(): ClassInfo {
return {
'time-picker-row': true,
'with-header-name': this.hasNameInHeader,
};
}
private get contentClass(): ClassInfo {
return {
'time-picker-content': true,
[`layout-${this.layoutAlign}`]: true,
};
}
render(): TemplateResult | null { render(): TemplateResult | null {
if (!this.entity) { if (!this.entity) {
return Partial.error('Entity not found', this.config); return Partial.error('Entity not found', this.config);
@@ -74,25 +110,38 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
} }
const { hour, minute } = this.entity!.attributes; const { hour, minute } = this.entity!.attributes;
this.hour = new Hour(hour, this.config.hour_step, this.config.hour_mode); const hourInstance = new Hour(hour, this.config.hour_step, this.config.hour_mode);
this.minute = new Minute(minute, this.config.minute_step); const minuteInstance = new Minute(minute, this.config.minute_step);
this.period = this.hour.value >= 12 ? Period.PM : Period.AM; this.time = new Time(hourInstance, minuteInstance, this.config.link_values);
this.period = hourInstance.value >= 12 ? Period.PM : Period.AM;
return html` return html`
<ha-card> <ha-card>
${this.shouldShowName ? Partial.header(this.name!) : ''} ${this.hasNameInHeader ? Partial.headerName(this.name!) : ''}
<div class="time-picker-content"> <div class=${classMap(this.rowClass)}>
<time-unit .unit=${this.hour} @update=${this.callHassService}></time-unit> ${this.hasNameInside ? Partial.nestedName(this.name!, this.entity) : ''}
<div class="time-separator">:</div>
<time-unit .unit=${this.minute} @update=${this.callHassService}></time-unit>
${this.shouldShowPeriod <div class=${classMap(this.contentClass)}>
? html`<time-period <time-unit
.period=${this.period} .unit=${this.time.hour}
.mode=${this.config.layout?.hour_mode ?? DEFAULT_LAYOUT_HOUR_MODE} @stepChange=${this.onHourStepChange}
@toggle=${this.onPeriodToggle} @update=${this.callHassService}
></time-period>` ></time-unit>
: ''} <div class="time-separator">:</div>
<time-unit
.unit=${this.time.minute}
@stepChange=${this.onMinuteStepChange}
@update=${this.callHassService}
></time-unit>
${this.shouldShowPeriod
? html`<time-period
.period=${this.period}
.mode=${this.config.layout?.hour_mode ?? DEFAULT_LAYOUT_HOUR_MODE}
@toggle=${this.onPeriodToggle}
></time-period>`
: ''}
</div>
</div> </div>
</ha-card> </ha-card>
`; `;
@@ -119,7 +168,17 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
} }
private onPeriodToggle(): void { private onPeriodToggle(): void {
this.hour.togglePeriod(); this.time.hour.togglePeriod();
this.callHassService();
}
private onHourStepChange(event: CustomEvent): void {
this.time.hourStep(event.detail.direction);
this.callHassService();
}
private onMinuteStepChange(event: CustomEvent): void {
this.time.minuteStep(event.detail.direction);
this.callHassService(); this.callHassService();
} }
@@ -128,11 +187,9 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
throw new Error('Unable to update datetime'); throw new Error('Unable to update datetime');
} }
const time = `${this.hour.value}:${this.minute.value}:00`;
return this.hass.callService(ENTITY_DOMAIN, 'set_datetime', { return this.hass.callService(ENTITY_DOMAIN, 'set_datetime', {
entity_id: this.entity!.entity_id, entity_id: this.entity!.entity_id,
time, time: this.time.value,
}); });
} }
@@ -148,23 +205,53 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
--tpc-text-color: var(--time-picker-text-color, #fff); --tpc-text-color: var(--time-picker-text-color, #fff);
--tpc-accent-color: var(--time-picker-accent-color, var(--primary-color)); --tpc-accent-color: var(--time-picker-accent-color, var(--primary-color));
--tpc-off-color: var(--time-picker-off-color, var(--disabled-text-color)); --tpc-off-color: var(--time-picker-off-color, var(--disabled-text-color));
--tpc-border-radius: var(--time-picker-border-radius, var(--ha-card-border-radius, 4px));
} }
.time-picker-header { .time-picker-header {
padding: 16px; padding: 16px;
color: var(--tpc-text-color); color: var(--tpc-text-color);
background-color: var(--tpc-elements-background-color); background-color: var(--tpc-elements-background-color);
border-top-left-radius: var(--tpc-border-radius);
border-top-right-radius: var(--tpc-border-radius);
font-size: 1em; font-size: 1em;
text-align: center; text-align: center;
} }
.time-picker-content { .time-picker-row {
padding: 8px 16px 16px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 16px;
}
.time-picker-row.with-header-name {
padding: 8px 16px 16px;
}
.time-picker-content {
display: flex;
flex-direction: row;
align-items: center;
flex: 1 0 auto;
}
.time-picker-content.layout-left {
justify-content: flex-start;
}
.time-picker-content.layout-center {
justify-content: center; justify-content: center;
} }
.time-picker-content.layout-right {
justify-content: flex-end;
}
.entity-name-inside {
margin-left: 16px;
}
`; `;
} }
+19 -3
View File
@@ -3,6 +3,7 @@ import { LovelaceCardConfig } from 'custom-card-helpers';
export interface TimePickerCardConfig extends LovelaceCardConfig { export interface TimePickerCardConfig extends LovelaceCardConfig {
entity: string; entity: string;
name?: string; name?: string;
link_values?: boolean;
hour_mode?: HourMode; hour_mode?: HourMode;
hour_step?: number; hour_step?: number;
minute_step?: number; minute_step?: number;
@@ -13,13 +14,28 @@ export interface TimePickerCardConfig extends LovelaceCardConfig {
export type HourMode = 12 | 24 | undefined; export type HourMode = 12 | 24 | undefined;
export interface TimePickerLayoutConfig { export interface TimePickerLayoutConfig {
hour_mode: HourModeLayout; align_controls?: Layout.AlignControls;
name?: Layout.Name;
hour_mode?: Layout.HourMode;
} }
export type HourModeLayout = 'single' | 'double'; export namespace Layout {
export type HourMode = 'single' | 'double';
export enum AlignControls {
LEFT = 'left',
CENTER = 'center',
RIGHT = 'right',
}
export enum Name {
HEADER = 'header',
INSIDE = 'inside',
}
}
export interface TimePickerHideConfig { export interface TimePickerHideConfig {
name: boolean; name?: boolean;
} }
export enum Direction { export enum Direction {
+12
View File
@@ -0,0 +1,12 @@
export interface CustomCardEntry {
type: string;
name?: string;
description?: string;
preview?: boolean;
}
declare global {
interface Window {
customCards: Array<CustomCardEntry>;
}
}
+17
View File
@@ -0,0 +1,17 @@
import { LovelaceCard } from 'custom-card-helpers';
import { customElement, html, LitElement, TemplateResult } from 'lit-element';
@customElement('hui-error-card')
export class HuiErrorCard extends LitElement implements LovelaceCard {
setConfig(): void {
return;
}
render(): TemplateResult {
return html``;
}
getCardSize(): number {
return 1;
}
}
+111
View File
@@ -0,0 +1,111 @@
import { expect, fixture, html } from '@open-wc/testing';
import { HomeAssistant } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
import '../../src/time-picker-card';
import { TimePickerCard } from '../../src/time-picker-card';
import { TimePickerCardConfig, HourMode } from '../../src/types';
import './test-error-card';
async function render(config?: {
entityId?: string;
friendlyName?: string;
hasTime?: boolean;
hourMode?: HourMode;
hideName?: boolean;
}): Promise<TimePickerCard> {
const { entityId, friendlyName, hasTime, hourMode, hideName } = Object.assign(
{
entityId: 'input_datetime.wake_time',
friendlyName: 'Wake Time',
hasTime: true,
hideName: false,
},
config
);
const testEntity: HassEntity = {
entity_id: entityId,
attributes: {
friendly_name: friendlyName,
has_time: hasTime,
},
state: '00:10:00',
last_changed: '',
last_updated: '',
context: {
id: entityId,
user_id: null,
},
};
const hass: Partial<HomeAssistant> = {
states: {
[entityId]: testEntity,
},
};
const testConfig: TimePickerCardConfig = {
type: 'custom:time-picker-card',
entity: entityId,
hour_mode: hourMode,
hide: {
name: hideName,
},
};
const card = await fixture<TimePickerCard>(
html`<time-picker-card .hass=${hass} .config=${testConfig}></time-picker-card>`
);
card.setConfig(testConfig);
return card;
}
function errorCard(): string {
return html`<hui-error-card></hui-error-card>`.getHTML();
}
describe('test', () => {
it('renders', async () => {
const card = await render();
expect(card).shadowDom.to.match('time-picker-card');
});
it('errors if entity is not an input_datetime', async () => {
const card = await render({ entityId: 'sensor.test_sensor' });
expect(card).shadowDom.to.equal(errorCard());
});
it("errors if entity doesn't have time", async () => {
const card = await render({ hasTime: false });
expect(card).shadowDom.to.equal(errorCard());
});
it('renders card name', async () => {
const card = await render();
expect(card.shadowRoot?.innerHTML).to.match(/Wake Time/);
});
it('hides card name', async () => {
const card = await render({ hideName: true });
expect(card.shadowRoot?.innerHTML).not.to.match(/Wake Time/);
});
it('renders time units', async () => {
const card = await render();
expect(card.shadowRoot?.innerHTML.match(/<time-unit>/g)!.length).to.equal(2);
});
describe('time-period', () => {
it('is not rendered by default', async () => {
const card = await render();
expect(card.shadowRoot?.innerHTML.match(/<time-period>/g)).to.be.null;
});
it('renders in 12-hour mode', async () => {
const card = await render({ hourMode: 12 });
expect(card.shadowRoot?.innerHTML.match(/<time-period>/g)!.length).to.equal(1);
});
});
});
+45
View File
@@ -0,0 +1,45 @@
module.exports = function (config) {
return config.set({
basePath: './',
frameworks: ['mocha', 'chai'],
files: ['**/*.test.ts'],
preprocessors: {
'**/*.test.ts': ['rollup'],
},
rollupPreprocessor: {
plugins: [
require('@rollup/plugin-node-resolve')(),
require('@rollup/plugin-json')(),
require('@rollup/plugin-typescript')(),
require('@rollup/plugin-commonjs')({
namedExports: {
chai: ['expect'],
},
}),
],
output: {
format: 'umd',
sourcemap: 'inline',
},
onwarn: function (message) {
if (/Circular dependency/.test(message)) return;
console.warn(message);
},
},
reporters: ['dots'],
port: 9876,
colors: true,
autoWatch: false,
singleRun: true,
concurrency: Infinity,
browsers: ['ChromeHeadless'],
client: {
captureConsole: false,
},
});
};
+166
View File
@@ -0,0 +1,166 @@
import { Hour } from '../../src/models/hour';
import { Direction } from '../../src/types';
import { DEFAULT_HOUR_STEP } from '../../src/const';
import { expect } from 'chai';
describe('Hour', () => {
let hour: Hour;
context('with default config', () => {
beforeEach(() => {
hour = new Hour(2);
});
it('returns value', () => {
expect(hour.value).to.equal(2);
});
it('has correct max value', () => {
expect(hour.maxValue).to.equal(23);
});
describe('toString', () => {
it('returns formatted string', () => {
expect(hour.toString()).to.equal('02');
});
it('renders values correctly', () => {
hour = new Hour(0);
expect(hour.toString()).to.equal('00');
hour = new Hour(6);
expect(hour.toString()).to.equal('06');
hour = new Hour(12);
expect(hour.toString()).to.equal('12');
hour = new Hour(18);
expect(hour.toString()).to.equal('18');
hour = new Hour(23);
expect(hour.toString()).to.equal('23');
});
});
describe('stepUpdate', () => {
it('updates up', () => {
hour.stepUpdate(Direction.UP);
expect(hour.value).to.equal(3);
});
it('updates down', () => {
hour.stepUpdate(Direction.DOWN);
expect(hour.value).to.equal(1);
});
it('goes up from 23 to 0', () => {
hour = new Hour(23);
hour.stepUpdate(Direction.UP);
expect(hour.value).to.equal(0);
});
it('goes down from 0 to 23', () => {
hour = new Hour(0);
hour.stepUpdate(Direction.DOWN);
expect(hour.value).to.equal(23);
});
});
describe('setStringValue', () => {
it('sets value from valid string', () => {
hour.setStringValue('3');
expect(hour.value).to.equal(3);
});
it("doesn't set value from invalid string", () => {
hour.setStringValue('test');
expect(hour.value).to.equal(2);
});
it("doesn't set value from outside of scope", () => {
hour.setStringValue('24');
expect(hour.value).to.equal(2);
});
});
});
context('with a different step size', () => {
beforeEach(() => {
hour = new Hour(2, 5);
});
it('goes up correctly', () => {
hour.stepUpdate(Direction.UP);
expect(hour.value).to.equal(7);
hour.stepUpdate(Direction.UP);
hour.stepUpdate(Direction.UP);
hour.stepUpdate(Direction.UP);
hour.stepUpdate(Direction.UP);
expect(hour.value).to.equal(3);
});
it('goes down correclty', () => {
hour.stepUpdate(Direction.DOWN);
expect(hour.value).to.equal(21);
});
});
context('in 12 hour mode', () => {
beforeEach(() => {
hour = new Hour(2, DEFAULT_HOUR_STEP, 12);
});
it('has correct max value', () => {
expect(hour.maxValue).to.equal(12);
});
describe('togglePeriod', () => {
it('changes values correclty', () => {
hour = new Hour(2, DEFAULT_HOUR_STEP, 12);
hour.togglePeriod();
expect(hour.value).to.equal(14);
hour = new Hour(0, DEFAULT_HOUR_STEP, 12);
hour.togglePeriod();
expect(hour.value).to.equal(12);
hour = new Hour(23, DEFAULT_HOUR_STEP, 12);
hour.togglePeriod();
expect(hour.value).to.equal(11);
});
it('has no side effects', () => {
const value = hour.value;
hour.togglePeriod();
hour.togglePeriod();
expect(hour.value).to.equal(value);
});
});
describe('toString', () => {
it('renders values above 12 orrectly', () => {
hour = new Hour(13, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).to.equal('01');
});
it('renders values correctly', () => {
hour = new Hour(0, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).to.equal('12');
hour = new Hour(6, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).to.equal('06');
hour = new Hour(12, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).to.equal('12');
hour = new Hour(18, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).to.equal('06');
hour = new Hour(23, DEFAULT_HOUR_STEP, 12);
expect(hour.toString()).to.equal('11');
});
});
});
});
+119
View File
@@ -0,0 +1,119 @@
import { Minute } from '../../src/models/minute';
import { Direction } from '../../src/types';
import { expect } from 'chai';
describe('Minute', () => {
let minute: Minute;
context('with default config', () => {
beforeEach(() => {
minute = new Minute(2);
});
it('returns value', () => {
expect(minute.value).to.equal(2);
});
it('has correct max value', () => {
expect(minute.maxValue).to.equal(59);
});
describe('toString', () => {
it('renders values correctly', () => {
minute = new Minute(0);
expect(minute.toString()).to.equal('00');
minute = new Minute(5);
expect(minute.toString()).to.equal('05');
minute = new Minute(59);
expect(minute.toString()).to.equal('59');
});
});
describe('stepUpdate', () => {
it('updates up', () => {
minute.stepUpdate(Direction.UP);
expect(minute.value).to.equal(7);
});
it('updates down', () => {
minute.stepUpdate(Direction.DOWN);
expect(minute.value).to.equal(57);
});
it('goes up from 59 to 4', () => {
minute = new Minute(59);
minute.stepUpdate(Direction.UP);
expect(minute.value).to.equal(4);
});
it('goes down from 0 to 55', () => {
minute = new Minute(0);
minute.stepUpdate(Direction.DOWN);
expect(minute.value).to.equal(55);
});
});
describe('setStringValue', () => {
it('sets value from valid string', () => {
minute.setStringValue('3');
expect(minute.value).to.equal(3);
});
it("doesn't set value from invalid string", () => {
minute.setStringValue('test');
expect(minute.value).to.equal(2);
});
it("doesn't set value from outside of scope", () => {
minute.setStringValue('60');
expect(minute.value).to.equal(2);
minute.setStringValue('65');
expect(minute.value).to.equal(2);
});
});
});
context('with a different step size', () => {
beforeEach(() => {
minute = new Minute(2, 15);
});
it('goes up correctly', () => {
minute.stepUpdate(Direction.UP);
expect(minute.value).to.equal(17);
minute.stepUpdate(Direction.UP);
minute.stepUpdate(Direction.UP);
minute.stepUpdate(Direction.UP);
expect(minute.value).to.equal(2);
minute.stepUpdate(Direction.UP);
expect(minute.value).to.equal(17);
});
it('goes down correclty', () => {
minute.stepUpdate(Direction.DOWN);
expect(minute.value).to.equal(47);
});
});
describe('will overflow', () => {
it('returns true if the minutes will overflow an hour up', () => {
minute.setStringValue('59');
expect(minute.willOverflow(Direction.UP)).to.be.true;
});
it('returns true if the minutes will overflow an hour down', () => {
minute.setStringValue('0');
expect(minute.willOverflow(Direction.DOWN)).to.be.true;
});
it('returns true if the minutes will not overflow', () => {
minute.setStringValue('55');
expect(minute.willOverflow(Direction.DOWN)).to.be.false;
});
});
});
+2 -1
View File
@@ -12,6 +12,7 @@
"noImplicitAny": false, "noImplicitAny": false,
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"experimentalDecorators": true "experimentalDecorators": true,
"allowSyntheticDefaultImports": true // for importing sinon in tests
} }
} }