diff --git a/README.md b/README.md index dfdef51..5714eae 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,10 @@ layout: ### 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 diff --git a/src/const.ts b/src/const.ts index 4e0ee49..57db70d 100644 --- a/src/const.ts +++ b/src/const.ts @@ -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; diff --git a/src/models/second.ts b/src/models/second.ts new file mode 100644 index 0000000..8ac7d4b --- /dev/null +++ b/src/models/second.ts @@ -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; + } +} diff --git a/src/models/time.ts b/src/models/time.ts index afe91f9..1e99582 100644 --- a/src/models/time.ts +++ b/src/models/time.ts @@ -1,9 +1,15 @@ +import { Direction } from '../types'; import { Hour } from './hour'; import { Minute } from './minute'; -import { Direction } from '../types'; +import { Second } from './second'; export class Time { - constructor(public hour: Hour, public minute: Minute, private _linkValues: boolean = false) {} + constructor( + public hour: Hour, + public minute: Minute, + public second: Second, + private _linkValues: boolean = false + ) {} hourStep(direction: Direction): void { this.hour.stepUpdate(direction); @@ -17,7 +23,15 @@ export class Time { 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}`; + return `${this.hour.value}:${this.minute.value}:${this.second.value}`; } } diff --git a/src/partials.ts b/src/partials.ts index 572ba3a..e0dc1ed 100644 --- a/src/partials.ts +++ b/src/partials.ts @@ -16,11 +16,7 @@ export class Partial { } static headerName(title: string): TemplateResult { - return html` -
- ${title} -
- `; + return html`
${title}
`; } static nestedName(name: string, entity: HassEntity): TemplateResult { diff --git a/src/time-picker-card.ts b/src/time-picker-card.ts index 6aedf6a..85f0a66 100644 --- a/src/time-picker-card.ts +++ b/src/time-picker-card.ts @@ -10,22 +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} `, @@ -109,10 +109,11 @@ export class TimePickerCard extends LitElement implements LovelaceCard { ); } - const { hour, minute } = this.entity!.attributes; + 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); - this.time = new Time(hourInstance, minuteInstance, this.config.link_values); + 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` @@ -133,7 +134,14 @@ export class TimePickerCard extends LitElement implements LovelaceCard { @stepChange=${this.onMinuteStepChange} @update=${this.callHassService} > - + ${this.config.hide?.seconds === false + ? html`
:
+ ` + : ''} ${this.shouldShowPeriod ? html` { if (!this.hass) { throw new Error('Unable to update datetime'); diff --git a/src/types.ts b/src/types.ts index 41e91f9..6ddef6c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ export interface TimePickerCardConfig extends LovelaceCardConfig { hour_mode?: HourMode; hour_step?: number; minute_step?: number; + second_step?: number; layout?: TimePickerLayoutConfig; hide?: TimePickerHideConfig; } @@ -36,6 +37,7 @@ export namespace Layout { export interface TimePickerHideConfig { name?: boolean; + seconds?: boolean; } export enum Direction { diff --git a/test/models/second.test.ts b/test/models/second.test.ts new file mode 100644 index 0000000..7691210 --- /dev/null +++ b/test/models/second.test.ts @@ -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; + }); + }); +});