Compare commits

...
21 Commits
Author SHA1 Message Date
GeorgeSG e8d87e9afb 1.1.0 2020-11-22 13:47:02 +02:00
GeorgeSG e56dc41dcc Fix import 2020-11-22 13:41:48 +02:00
GeorgeSG 4b5fc79630 Add embedded layout option 2020-11-22 13:40:19 +02:00
GeorgeSG 6113ec9415 Add seconds config to visual editor 2020-11-22 13:23:20 +02:00
GeorgeSG f306d95748 Add optional seconds picker 2020-11-22 13:11:16 +02:00
GeorgeSG fe91b79320 Bump dependencies 2020-11-22 12:35:54 +02:00
GeorgeSG 0b1b87ed6e Merge pull request #8 from GeorgeSG/dependabot/npm_and_yarn/http-proxy-1.18.1
Bump http-proxy from 1.18.0 to 1.18.1
2020-09-12 14:02:34 +03:00
dependabot[bot] 81a0f1a95a Bump http-proxy from 1.18.0 to 1.18.1
Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/http-party/node-http-proxy/releases)
- [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/http-party/node-http-proxy/compare/1.18.0...1.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-11 22:23:04 +00:00
GeorgeSG 526a52f630 1.0.1 2020-07-17 20:24:39 +03:00
GeorgeSG 72d82d7c0a Merge pull request #7 from GeorgeSG/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-07-17 20:24:26 +03:00
dependabot[bot] 21f66a9066 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-17 17:23:17 +00:00
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
22 changed files with 1760 additions and 723 deletions
+1 -2
View File
@@ -9,14 +9,13 @@ on:
- master
jobs:
build:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: |
npm install
npm run lint
npm run build
- name: Test
run: npm run test
+5
View File
@@ -0,0 +1,5 @@
{
"files.exclude": {
"dist/**": true
}
}
+30 -30
View File
@@ -5,7 +5,6 @@
[![GitHub Release][releases-shield]][releases-link]
[![CI][ci-shield]][ci-link]
[![Project Maintenance][maintenance-shield]][maintenance-link]
[![GitHub Activity][commits-shield]][commits-link]
[![License][license-shield]][license-link]
## Overview
@@ -16,7 +15,19 @@ Requires an [Input Datetime](https://www.home-assistant.io/integrations/input_da
## 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
resources:
@@ -32,19 +43,6 @@ Time Picker Card supports Lovelace's Visual Editor. Click the + button to add a
![Visual Editor](https://raw.githubusercontent.com/GeorgeSG/lovelace-time-picker-card/master/examples/visual_editor.png)
### YAML
```yaml
type: 'custom:time-picker-card'
entity: input_datetime.alarm_time
name: 'Choose a time'
hour_mode: 24
hour_step: 1
minute_step: 5
hide:
name: false
```
## Examples
### Default config - card name shown, 24 hour mode
@@ -105,38 +103,42 @@ layout:
| 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` | |
| 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_step | number | **Optional** | Hour change when clicking arrows | `1` |
| minute_step | number | **Optional** | Minute change when clicking arrows | `5` |
| layout | object | **Optional** | Card Layout configuration | `none` |
| hide | object | **Optional** | Hide object | `none` |
## Layout Object
### Layout Object
| Name | Value | Requirement | Description | Default |
| -------------- | ------------------------- | ------------ | -------------------------------------------------------------------------------------------------- | -------- |
| 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` |
| embedded | boolean | **Optional** | Render with embedded style - disables padding, box shadow, and card header | `false` |
## Hide Object
### Hide Object
| Name | Type | Requirement | Description | Default |
| ---- | ------- | ------------ | ------------------- | ------- |
| name | boolean | **Optional** | Hides the card name | `false` |
| Name | Type | Requirement | Description | Default |
| ---- | ------- | ------------ | ------------------- | ------- |
| name | boolean | **Optional** | Hides the card name | `false` |
| seconds | boolean | **Optional** | Hides seconds input | `true` |
## 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,
you can use the following variables in your theme's config file:
| Name | Default | Description |
| ------------------------------------- | ---------------------------- | -------------------------------------- |
| 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-text-color | `white` | Text color |
| time-picker-accent-color | `var(--primary-color)` | AM / PM active color |
| time-picker-off-color | `var(--disabled-text-color)` | AM / PM inactive color |
| Name | Default | Description |
| ------------------------------------- | ------------------------------ | -------------------------------------- |
| 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-text-color | `white` | Text 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-border-radius | `var(--ha-card-border-radius)` | Border radius of the card |
## Meta
@@ -156,8 +158,6 @@ you can use the following variables in your theme's config file:
[ci-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/actions?query=workflow%3ACI
[maintenance-shield]: https://img.shields.io/maintenance/yes/2020.svg
[maintenance-link]: https://github.com/GeorgeSG/lovelace-time-picker-card
[commits-shield]: https://img.shields.io/github/commit-activity/y/GeorgeSG/lovelace-time-picker-card
[commits-link]: https://github.com/GeorgeSG/lovelace-time-picker-card/commits/master
[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
+1004 -574
View File
File diff suppressed because it is too large Load Diff
+24 -23
View File
@@ -1,6 +1,6 @@
{
"name": "time-picker-card",
"version": "0.1.0",
"version": "1.1.0",
"author": "Georgi Gardev <georgi@gardev.com>",
"description": "Lovelace time-picker-card",
"keywords": [
@@ -19,41 +19,42 @@
"build": "npm run lint && npm run rollup",
"lint": "eslint src/*.ts",
"rollup": "rollup -c",
"test": "./node_modules/karma/bin/karma start",
"test": "./node_modules/karma/bin/karma start ./test/karma.conf.js",
"release-patch": "npm version patch && 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"
},
"dependencies": {
"custom-card-helpers": "^1.6.4",
"home-assistant-js-websocket": "^5.1.0",
"lit-element": "^2.3.1",
"lit-html": "^1.2.1"
"custom-card-helpers": "^1.6.6",
"home-assistant-js-websocket": "^5.7.0",
"lit-element": "^2.4.0",
"lit-html": "^1.3.0"
},
"devDependencies": {
"@babel/core": "^7.9.0",
"@open-wc/testing": "^2.5.16",
"@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.1",
"@rollup/plugin-typescript": "^4.0.0",
"@types/chai": "^4.2.11",
"@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/parser": "^2.30.0",
"@babel/core": "^7.12.7",
"@open-wc/testing": "^2.5.32",
"@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^7.1.3",
"@rollup/plugin-typescript": "^4.1.2",
"@types/chai": "^4.2.14",
"@typescript-eslint/eslint-plugin": "^2.34.0",
"@typescript-eslint/parser": "^2.34.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"karma": "^5.0.5",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-prettier": "^3.1.4",
"karma": "^5.2.3",
"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",
"mocha": "^7.2.0",
"prettier": "^2.2.0",
"rollup": "^1.32.1",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-serve": "^1.0.1",
"rollup-plugin-terser": "^5.3.0",
"sinon": "^9.0.2",
"typescript": "^3.8.3"
"rollup-plugin-serve": "^1.1.0",
"rollup-plugin-terser": "^5.3.1",
"sinon": "^9.2.1",
"typescript": "^3.9.7"
}
}
+5 -8
View File
@@ -16,6 +16,7 @@ import { Direction } from '../types';
@customElement('time-unit')
export class TimeUnitComponent extends LitElement {
static readonly EVENT_UPDATE = 'update';
static readonly EVENT_STEP_CHANGE = 'stepChange';
@property() private unit!: TimeUnit;
@@ -27,7 +28,7 @@ export class TimeUnitComponent extends LitElement {
class="time-input"
type="number"
placeholder="MM"
min="0"
min=${this.unit.minValue}
max=${this.unit.maxValue}
.value="${this.unit.toString()}"
@change=${this.onInputChange}
@@ -39,16 +40,12 @@ export class TimeUnitComponent extends LitElement {
onInputChange({ target: { value } }: { target: HTMLInputElement }): void {
this.unit.setStringValue(value);
this.emitUpdate();
const event = new CustomEvent(TimeUnitComponent.EVENT_UPDATE);
this.dispatchEvent(event);
}
onStepChangerClick(direction: Direction): void {
this.unit.stepUpdate(direction);
this.emitUpdate();
}
private emitUpdate(): void {
const event = new CustomEvent(TimeUnitComponent.EVENT_UPDATE);
const event = new CustomEvent(TimeUnitComponent.EVENT_STEP_CHANGE, { detail: { direction } });
this.dispatchEvent(event);
}
+1
View File
@@ -9,6 +9,7 @@ export const ENTITY_DOMAIN = 'input_datetime';
// Config defaults
export const DEFAULT_HOUR_STEP = 1;
export const DEFAULT_MINUTE_STEP = 5;
export const DEFAULT_SECOND_STEP = 5;
export const DEFAULT_LAYOUT_HOUR_MODE = 'double';
export const DEFAULT_LAYOUT_ALIGN_CONTROLS = Layout.AlignControls.CENTER;
export const DEFAULT_LAYOUT_NAME = Layout.Name.HEADER;
+78 -21
View File
@@ -15,12 +15,18 @@ import {
ENTITY_DOMAIN,
DEFAULT_LAYOUT_ALIGN_CONTROLS,
DEFAULT_LAYOUT_NAME,
DEFAULT_SECOND_STEP,
} from './const';
import { TimePickerCardConfig, Layout, HourMode } from './types';
@customElement('time-picker-card-editor')
export class TimePickerCardEditor extends LitElement implements LovelaceCardEditor {
private static readonly NUMBER_PROPERTIES = ['hour_step', 'minute_step', 'hour_mode'];
private static readonly NUMBER_PROPERTIES = [
'hour_step',
'minute_step',
'second_step',
'hour_mode',
];
private static readonly CONFIG_CHANGED_EVENT = 'config-changed';
@property({ type: Object }) hass!: HomeAssistant;
@@ -61,14 +67,14 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
@value-changed=${this.onValueChange}
></paper-input>
<div class="side-by-side">
<ha-switch
style="margin-left: 10px"
.checked="${!Boolean(this.config.hide?.name)}"
.configValue="${'hide_name'}"
@change="${this.onHideNameChange}"
>
<div>
<ha-switch
style="margin-left: 10px"
.checked="${!Boolean(this.config.hide?.name)}"
@change="${this.onHideNameChange}"
></ha-switch>
Show name?
</ha-switch>
</div>
<paper-dropdown-menu
style="width: 100%"
label="Name Position (Optional)"
@@ -101,20 +107,39 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
></paper-input>
</div>
<div class="side-by-side">
<ha-switch
.checked="${this.config.hour_mode === 12}"
.configValue="${'hour_mode'}"
@change="${this.onHourModeChange}"
>
<div>
<ha-switch
style="margin-left: 10px"
.checked="${this.config.hide?.seconds === false}"
@change="${this.onHideSecondsChange}"
></ha-switch>
Show seconds?
</div>
<paper-input
type="number"
label="Second Step (Optional)"
.configValue=${'second_step'}
.value=${this.config.second_step || DEFAULT_SECOND_STEP}
@value-changed=${this.onValueChange}
></paper-input>
</div>
<div class="side-by-side">
<div>
<ha-switch
.checked="${this.config.hour_mode === 12}"
@change="${this.onHourModeChange}"
></ha-switch>
12-Hour mode
</ha-switch>
<ha-switch
.checked="${this.config.layout?.hour_mode === 'single'}"
.configValue="${'hour_mode_layout'}"
@change="${this.onHourModeLayoutChange}"
>
"Single" hour mode layout (in 12-Hour mode only)
</ha-switch>
</div>
${this.config.hour_mode === 12
? html`<div>
<ha-switch
.checked="${this.config.layout?.hour_mode === 'single'}"
@change="${this.onHourModeLayoutChange}"
></ha-switch>
"Single" hour mode layout
</div>`
: ''}
</div>
<div class="side-by-side">
<paper-dropdown-menu
@@ -131,6 +156,22 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
${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}"
@change="${this.onLinkValuesChange}"
></ha-switch>
Link Values
</div>
</div>
<div class="side-by-side">
<div>
<ha-switch
.checked="${this.config.layout?.embedded}"
@change="${this.onEmbeddedChange}"
></ha-switch>
Embedded
</div>
</div>
</div>`;
}
@@ -151,6 +192,11 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
this.dispatch(newConfig);
}
private onHideSecondsChange({ target: { checked } }): void {
const newConfig = { ...this.config, hide: { seconds: !checked } };
this.dispatch(newConfig);
}
private onLayoutAlignChange({ detail: { value } }: CustomEvent): void {
const layout = { ...this.config.layout, align_controls: value };
const newConfig = { ...this.config, layout };
@@ -173,6 +219,16 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
this.dispatch(newConfig);
}
private onLinkValuesChange({ target: { checked } }): void {
const newConfig = { ...this.config, link_values: checked };
this.dispatch(newConfig);
}
private onEmbeddedChange({ target: { checked } }): void {
const newConfig = { ...this.config, layout: { embedded: checked } };
this.dispatch(newConfig);
}
private onValueChange(e: CustomEvent): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const property = (e.target as any).configValue;
@@ -209,6 +265,7 @@ export class TimePickerCardEditor extends LitElement implements LovelaceCardEdit
return css`
ha-switch {
padding: 16px 0;
margin-right: 16px;
}
.side-by-side {
display: flex;
+14 -10
View File
@@ -8,12 +8,16 @@ import { TimeUnit } from './time-unit';
export class Hour extends TimeUnit {
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);
}
get minValue(): number {
return this.hourMode === 12 ? 1 : 0; // there is no 00:00 AM -> it's 12:00 AM.
}
get maxValue(): number {
return this.hourMode || Hour.VALUE_LIMIT;
return this.hourMode === 12 ? 12 : Hour.VALUE_LIMIT - 1;
}
togglePeriod(): void {
@@ -21,15 +25,15 @@ export class Hour extends TimeUnit {
}
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();
}
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 { TimeUnit } from './time-unit';
import { Direction } from '../types';
/**
* * Represents the minute value of a datetime.
@@ -7,14 +8,19 @@ import { TimeUnit } from './time-unit';
export class Minute extends TimeUnit {
private static readonly VALUE_LIMIT = 60;
maxValue = Minute.VALUE_LIMIT;
minValue = 0;
maxValue = Minute.VALUE_LIMIT - 1;
constructor(value: number, step = DEFAULT_MINUTE_STEP) {
super(value, step, Minute.VALUE_LIMIT);
}
protected isValidString(valueStr: string): boolean {
const value = parseInt(valueStr);
return !isNaN(value) && value >= 0 && value <= this._limit;
/**
* Returns true if the minutes will overflow to a different hour when changed in {@param direction}.
* @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;
}
}
+26
View File
@@ -0,0 +1,26 @@
import { DEFAULT_SECOND_STEP } from '../const';
import { TimeUnit } from './time-unit';
import { Direction } from '../types';
/**
* * Represents the second value of a datetime.
*/
export class Second extends TimeUnit {
private static readonly VALUE_LIMIT = 60;
minValue = 0;
maxValue = Second.VALUE_LIMIT - 1;
constructor(value: number, step = DEFAULT_SECOND_STEP) {
super(value, step, Second.VALUE_LIMIT);
}
/**
* Returns true if the seconds will overflow to a different hour when changed in {@param direction}.
* @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 {
/**
* 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;
@@ -40,27 +39,36 @@ export abstract class TimeUnit {
* Updates the value in {@param direction} by the step size specified in the constructoor.
* @param direction
*/
stepUpdate(direction: Direction): void {
direction === Direction.UP ? this.increment() : this.decrement();
stepUpdate(direction: Direction, step: number = this._step): void {
direction === Direction.UP ? this.increment(step) : this.decrement(step);
}
toString(): string {
return this.value < 10 ? `0${this.value}` : this.value.toString();
}
private increment(): void {
this.setValue(this.value + this._step);
private increment(step: number = this._step): void {
this.setValue(this.value + step);
}
private decrement(): void {
this.setValue(this.value - this._step);
private decrement(step: number = this._step): void {
this.setValue(this.value - step);
}
protected setValue(newValue: number): void {
if (isNaN(newValue)) {
return;
}
if (newValue >= this._limit || newValue < 0) {
newValue = (newValue + this._limit) % this._limit;
}
this._value = newValue;
}
protected isValidString(valueStr: string): boolean {
const value = parseInt(valueStr);
return !isNaN(value) && value >= 0 && value < this._limit;
}
}
+37
View File
@@ -0,0 +1,37 @@
import { Direction } from '../types';
import { Hour } from './hour';
import { Minute } from './minute';
import { Second } from './second';
export class Time {
constructor(
public hour: Hour,
public minute: Minute,
public second: Second,
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);
}
secondStep(direction: Direction): void {
if (this._linkValues && this.second.willOverflow(direction)) {
this.minute.stepUpdate(direction, 1);
}
this.second.stepUpdate(direction);
}
get value(): string {
return `${this.hour.value}:${this.minute.value}:${this.second.value}`;
}
}
+1 -5
View File
@@ -16,11 +16,7 @@ export class Partial {
}
static headerName(title: string): TemplateResult {
return html`
<div class="time-picker-header">
${title}
</div>
`;
return html`<div class="time-picker-header">${title}</div>`;
}
static nestedName(name: string, entity: HassEntity): TemplateResult {
+76 -22
View File
@@ -10,21 +10,22 @@ import {
TemplateResult,
} from 'lit-element';
import { ClassInfo, classMap } from 'lit-html/directives/class-map';
import './components/time-period.component';
import './components/time-unit.component';
import {
CARD_SIZE,
CARD_VERSION,
DEFAULT_LAYOUT_ALIGN_CONTROLS,
DEFAULT_LAYOUT_HOUR_MODE,
ENTITY_DOMAIN,
DEFAULT_LAYOUT_ALIGN_CONTROLS,
} from './const';
import './editor';
import { Hour } from './models/hour';
import { Minute } from './models/minute';
import { Second } from './models/second';
import { Time } from './models/time';
import { Partial } from './partials';
import { Period, TimePickerCardConfig, Layout } from './types';
import './components/time-period.component';
import './components/time-unit.component';
import './editor';
import { Layout, Period, TimePickerCardConfig } from './types';
console.info(
`%c TIME-PICKER-CARD \n%c Version ${CARD_VERSION} `,
@@ -43,19 +44,23 @@ window.customCards.push({
export class TimePickerCard extends LitElement implements LovelaceCard {
@property({ type: Object }) hass!: HomeAssistant;
@property() private config!: TimePickerCardConfig;
@property() private hour!: Hour;
@property() private minute!: Minute;
@property() private time!: Time;
@property() private period!: Period;
private get entity(): HassEntity | undefined {
return this.hass.states[this.config.entity];
}
private get isEmbedded(): boolean {
return this.config.layout?.embedded === true;
}
private get hasNameInHeader(): boolean {
return (
Boolean(this.name) &&
Boolean(this.config.hide?.name) === false &&
this.config.layout?.name !== Layout.Name.INSIDE
this.config.layout?.name !== Layout.Name.INSIDE &&
Boolean(this.config.layout?.embedded) === false // embedded layout disables name in header
);
}
@@ -63,7 +68,7 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
return (
Boolean(this.name) &&
Boolean(this.config.hide?.name) === false &&
this.config.layout?.name === Layout.Name.INSIDE
(this.config.layout?.name === Layout.Name.INSIDE || Boolean(this.config.layout?.embedded))
);
}
@@ -79,10 +84,17 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
return this.config.layout?.align_controls ?? DEFAULT_LAYOUT_ALIGN_CONTROLS;
}
private get haCardClass(): ClassInfo {
return {
embedded: this.isEmbedded,
};
}
private get rowClass(): ClassInfo {
return {
'time-picker-row': true,
'with-header-name': this.hasNameInHeader,
embedded: this.isEmbedded,
};
}
@@ -109,22 +121,39 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
);
}
const { hour, minute } = this.entity!.attributes;
this.hour = new Hour(hour, this.config.hour_step, this.config.hour_mode);
this.minute = new Minute(minute, this.config.minute_step);
this.period = this.hour.value >= 12 ? Period.PM : Period.AM;
const { hour, minute, second } = this.entity!.attributes;
const hourInstance = new Hour(hour, this.config.hour_step, this.config.hour_mode);
const minuteInstance = new Minute(minute, this.config.minute_step);
const secondInstance = new Second(second, this.config.second_step);
this.time = new Time(hourInstance, minuteInstance, secondInstance, this.config.link_values);
this.period = hourInstance.value >= 12 ? Period.PM : Period.AM;
return html`
<ha-card>
<ha-card class=${classMap(this.haCardClass)}>
${this.hasNameInHeader ? Partial.headerName(this.name!) : ''}
<div class=${classMap(this.rowClass)}>
${this.hasNameInside ? Partial.nestedName(this.name!, this.entity) : ''}
<div class=${classMap(this.contentClass)}>
<time-unit .unit=${this.hour} @update=${this.callHassService}></time-unit>
<time-unit
.unit=${this.time.hour}
@stepChange=${this.onHourStepChange}
@update=${this.callHassService}
></time-unit>
<div class="time-separator">:</div>
<time-unit .unit=${this.minute} @update=${this.callHassService}></time-unit>
<time-unit
.unit=${this.time.minute}
@stepChange=${this.onMinuteStepChange}
@update=${this.callHassService}
></time-unit>
${this.config.hide?.seconds === false
? html`<div class="time-separator">:</div>
<time-unit
.unit=${this.time.second}
@stepChange=${this.onSecondStepChange}
@update=${this.callHassService}
></time-unit>`
: ''}
${this.shouldShowPeriod
? html`<time-period
.period=${this.period}
@@ -159,7 +188,22 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
}
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();
}
private onSecondStepChange(event: CustomEvent): void {
this.time.secondStep(event.detail.direction);
this.callHassService();
}
@@ -168,11 +212,9 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
throw new Error('Unable to update datetime');
}
const time = `${this.hour.value}:${this.minute.value}:00`;
return this.hass.callService(ENTITY_DOMAIN, 'set_datetime', {
entity_id: this.entity!.entity_id,
time,
time: this.time.value,
});
}
@@ -188,12 +230,20 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
--tpc-text-color: var(--time-picker-text-color, #fff);
--tpc-accent-color: var(--time-picker-accent-color, var(--primary-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));
}
ha-card.embedded {
box-shadow: none;
}
.time-picker-header {
padding: 16px;
color: var(--tpc-text-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;
text-align: center;
}
@@ -205,6 +255,10 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
padding: 16px;
}
.time-picker-row.embedded {
padding: 0;
}
.time-picker-row.with-header-name {
padding: 8px 16px 16px;
}
+4
View File
@@ -3,9 +3,11 @@ import { LovelaceCardConfig } from 'custom-card-helpers';
export interface TimePickerCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
link_values?: boolean;
hour_mode?: HourMode;
hour_step?: number;
minute_step?: number;
second_step?: number;
layout?: TimePickerLayoutConfig;
hide?: TimePickerHideConfig;
}
@@ -16,6 +18,7 @@ export interface TimePickerLayoutConfig {
align_controls?: Layout.AlignControls;
name?: Layout.Name;
hour_mode?: Layout.HourMode;
embedded?: boolean;
}
export namespace Layout {
@@ -35,6 +38,7 @@ export namespace Layout {
export interface TimePickerHideConfig {
name?: boolean;
seconds?: boolean;
}
export enum Direction {
@@ -1,9 +1,9 @@
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 '../../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?: {
+19 -11
View File
@@ -1,10 +1,10 @@
module.exports = function (config) {
config.set({
basePath: 'test/',
return config.set({
basePath: './',
frameworks: ['mocha', 'chai'],
files: ['*.test.ts'],
files: ['**/*.test.ts'],
preprocessors: {
'*.test.ts': ['rollup'],
'**/*.test.ts': ['rollup'],
},
rollupPreprocessor: {
@@ -12,26 +12,34 @@ module.exports = function (config) {
require('@rollup/plugin-node-resolve')(),
require('@rollup/plugin-json')(),
require('@rollup/plugin-typescript')(),
require('rollup-plugin-babel')(),
require('@rollup/plugin-commonjs')({
namedExports: {
chai: ['expect'],
},
}),
],
output: {
format: 'umd',
sourcemap: 'inline',
},
onwarn: function (message) {
if (/Circular dependency/.test(message)) return;
console.warn(message);
},
},
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
reporters: ['dots'],
port: 9876,
colors: true,
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
autoWatch: false,
singleRun: true,
browsers: ['ChromeHeadless'],
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;
});
});
});
+119
View File
@@ -0,0 +1,119 @@
import { Second } from '../../src/models/second';
import { Direction } from '../../src/types';
import { expect } from 'chai';
describe('Second', () => {
let second: Second;
context('with default config', () => {
beforeEach(() => {
second = new Second(2);
});
it('returns value', () => {
expect(second.value).to.equal(2);
});
it('has correct max value', () => {
expect(second.maxValue).to.equal(59);
});
describe('toString', () => {
it('renders values correctly', () => {
second = new Second(0);
expect(second.toString()).to.equal('00');
second = new Second(5);
expect(second.toString()).to.equal('05');
second = new Second(59);
expect(second.toString()).to.equal('59');
});
});
describe('stepUpdate', () => {
it('updates up', () => {
second.stepUpdate(Direction.UP);
expect(second.value).to.equal(7);
});
it('updates down', () => {
second.stepUpdate(Direction.DOWN);
expect(second.value).to.equal(57);
});
it('goes up from 59 to 4', () => {
second = new Second(59);
second.stepUpdate(Direction.UP);
expect(second.value).to.equal(4);
});
it('goes down from 0 to 55', () => {
second = new Second(0);
second.stepUpdate(Direction.DOWN);
expect(second.value).to.equal(55);
});
});
describe('setStringValue', () => {
it('sets value from valid string', () => {
second.setStringValue('3');
expect(second.value).to.equal(3);
});
it("doesn't set value from invalid string", () => {
second.setStringValue('test');
expect(second.value).to.equal(2);
});
it("doesn't set value from outside of scope", () => {
second.setStringValue('60');
expect(second.value).to.equal(2);
second.setStringValue('65');
expect(second.value).to.equal(2);
});
});
});
context('with a different step size', () => {
beforeEach(() => {
second = new Second(2, 15);
});
it('goes up correctly', () => {
second.stepUpdate(Direction.UP);
expect(second.value).to.equal(17);
second.stepUpdate(Direction.UP);
second.stepUpdate(Direction.UP);
second.stepUpdate(Direction.UP);
expect(second.value).to.equal(2);
second.stepUpdate(Direction.UP);
expect(second.value).to.equal(17);
});
it('goes down correclty', () => {
second.stepUpdate(Direction.DOWN);
expect(second.value).to.equal(47);
});
});
describe('will overflow', () => {
it('returns true if the Seconds will overflow an hour up', () => {
second.setStringValue('59');
expect(second.willOverflow(Direction.UP)).to.be.true;
});
it('returns true if the Seconds will overflow an hour down', () => {
second.setStringValue('0');
expect(second.willOverflow(Direction.DOWN)).to.be.true;
});
it('returns true if the Seconds will not overflow', () => {
second.setStringValue('55');
expect(second.willOverflow(Direction.DOWN)).to.be.false;
});
});
});