diff --git a/src/models/Hour.ts b/src/models/Hour.ts
new file mode 100644
index 0000000..38c3743
--- /dev/null
+++ b/src/models/Hour.ts
@@ -0,0 +1,10 @@
+import { TimeUnit } from './TimeUnit';
+
+export class Hour extends TimeUnit {
+ private static readonly DEFAULT_STEP = 1;
+ private static readonly MAX = 24;
+
+ constructor(value: number, step = Hour.DEFAULT_STEP) {
+ super(value, step, Hour.MAX);
+ }
+}
diff --git a/src/models/Minute.ts b/src/models/Minute.ts
new file mode 100644
index 0000000..4ff181a
--- /dev/null
+++ b/src/models/Minute.ts
@@ -0,0 +1,10 @@
+import { TimeUnit } from './TimeUnit';
+
+export class Minute extends TimeUnit {
+ private static readonly DEFAULT_STEP = 5;
+ private static readonly MAX = 50;
+
+ constructor(value: number, step = Minute.DEFAULT_STEP) {
+ super(value, step, Minute.MAX);
+ }
+}
diff --git a/src/models/TimeUnit.ts b/src/models/TimeUnit.ts
new file mode 100644
index 0000000..78b87b9
--- /dev/null
+++ b/src/models/TimeUnit.ts
@@ -0,0 +1,44 @@
+import { Direction } from '../types';
+
+export abstract class TimeUnit {
+ constructor(private _value: number, private _step: number, private _limit: number) {}
+
+ get value(): number {
+ return this._value;
+ }
+
+ setStringValue(stringValue: string): void {
+ if (this.isValidString(stringValue)) {
+ this.setValue(parseInt(stringValue));
+ }
+ }
+
+ stepUpdate(direction: Direction): void {
+ direction === Direction.UP ? this.increment() : this.decrement();
+ }
+
+ increment(): void {
+ this.setValue(this.value + this._step);
+ }
+
+ decrement(): void {
+ this.setValue(this.value - this._step);
+ }
+
+ toString(): string {
+ return this.value < 10 ? `0${this.value}` : this.value.toString();
+ }
+
+ private isValidString(valueStr: string): boolean {
+ const value = parseInt(valueStr);
+ return !isNaN(value) && value >= 0 && value <= this._limit;
+ }
+
+ private setValue(newValue: number): void {
+ if (newValue >= this._limit || newValue < 0) {
+ newValue = (newValue + this._limit) % this._limit;
+ }
+
+ this._value = newValue;
+ }
+}
diff --git a/src/models/index.ts b/src/models/index.ts
new file mode 100644
index 0000000..e973ccf
--- /dev/null
+++ b/src/models/index.ts
@@ -0,0 +1,3 @@
+export { TimeUnit } from './TimeUnit';
+export { Hour } from './Hour';
+export { Minute } from './Minute';
diff --git a/src/partials.ts b/src/partials.ts
new file mode 100644
index 0000000..42e49ca
--- /dev/null
+++ b/src/partials.ts
@@ -0,0 +1,9 @@
+import { html, TemplateResult } from 'lit-element';
+import { TimePickerCardConfig } from './types';
+
+export class Partial {
+ static error(error: string, origConfig: TimePickerCardConfig): TemplateResult {
+ const config = { error, origConfig };
+ return html``;
+ }
+}
diff --git a/src/styles.ts b/src/styles.ts
index 92d24b3..28eef57 100644
--- a/src/styles.ts
+++ b/src/styles.ts
@@ -1,8 +1,64 @@
import { css } from 'lit-element';
export const styles = css`
- .card-warning {
- background-color: var(--google-red-500, #ef5350);
- color: #fff;
+ .time-picker-ha-card {
+ padding: 16px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .time-form {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 0 8px;
+ }
+
+ .time-picker-icon-up,
+ .time-picker-icon-down {
+ cursor: pointer;
+ }
+
+ .time-picker-icon-up {
+ padding: 8px;
+ }
+
+ .time-picker-icon-down {
+ margin-top: 8px;
+ padding: 8px;
+ }
+
+ .time-input {
+ width: 30px;
+ padding: 8px;
+ background: var(--card-background-color, transparent);
+ border: 0;
+ color: var(--text-color, #fff);
+ border-bottom: 1px solid var(--text-color, #fff);
+ text-align: center;
+ font-size: 1em;
+ }
+
+ input[type='number']::-webkit-inner-spin-button,
+ input[type='number']::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ input[type='number'] {
+ -moz-appearance: textfield;
+ }
+
+ input[type='number']:focus {
+ outline: none;
+ }
+
+ input[type='number']:invalid {
+ box-shadow: none;
+ outline: none;
+ border: 0;
+ border-bottom: 2px solid red;
}
`;
diff --git a/src/time-picker-card.ts b/src/time-picker-card.ts
index 40a1db8..8a07b3e 100644
--- a/src/time-picker-card.ts
+++ b/src/time-picker-card.ts
@@ -1,8 +1,11 @@
import { HomeAssistant } from 'custom-card-helpers';
+import { HassEntity } from 'home-assistant-js-websocket';
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from 'lit-element';
import { CARD_VERSION } from './const';
+import { Hour, Minute, TimeUnit } from './models';
+import { Partial } from './partials';
import { styles } from './styles';
-import { TimePickerCardConfig } from './types';
+import { Direction, TimePickerCardConfig } from './types';
console.info(
`%c TIME-PICKER-CARD \n%c Version ${CARD_VERSION} `,
@@ -14,13 +17,69 @@ console.info(
export class TimePickerCard extends LitElement {
@property() private hass?: HomeAssistant;
@property() private config?: TimePickerCardConfig;
+ @property() private hour?: Hour;
+ @property() private minute?: Minute;
+
+ private get entity(): HassEntity | undefined {
+ if (!this.config) {
+ return;
+ }
+
+ return this.hass?.states[this.config!.entity];
+ }
render(): TemplateResult | null {
if (!this.config || !this.hass) {
return null;
}
- return html`TimePickerCard`;
+ if (!this.entity?.entity_id.startsWith('input_datetime')) {
+ return Partial.error('You must set an input_datetime entity', this.config);
+ }
+
+ if (!this.entity.attributes.has_time) {
+ return Partial.error(
+ 'You must set an input_datetime entity that sets has_time: true',
+ this.config
+ );
+ }
+
+ this.hour = new Hour(this.entity?.attributes.hour ?? 0, this.config.hour_step);
+ this.minute = new Minute(this.entity?.attributes.minute ?? 0, this.config.minute_step);
+
+ return html`
+
+
+ ${this.renderStepChanger(Direction.UP, this.hour)}
+
+ ${this.renderStepChanger(Direction.DOWN, this.hour)}
+
+
+ :
+
+
+ ${this.renderStepChanger(Direction.UP, this.minute)}
+
+ ${this.renderStepChanger(Direction.DOWN, this.minute)}
+
+
+ `;
}
setConfig(config): void {
@@ -29,7 +88,7 @@ export class TimePickerCard extends LitElement {
}
if (!config.entity) {
- throw new Error('You need to set an entity');
+ throw new Error('You must set an entity');
}
this.config = config;
@@ -39,6 +98,45 @@ export class TimePickerCard extends LitElement {
return 3;
}
+ onHourChange({ target: { value } }: { target: HTMLInputElement }): void {
+ this.hour!.setStringValue(value);
+ this.callService();
+ }
+
+ onMinuteChange({ target: { value } }: { target: HTMLInputElement }): void {
+ this.minute!.setStringValue(value);
+ this.callService();
+ }
+
+ private renderStepChanger(direction: Direction, unit: TimeUnit): TemplateResult {
+ const onIconClick = (): void => {
+ unit.stepUpdate(direction);
+ this.callService();
+ };
+
+ const className = `time-picker-icon-${direction}`;
+
+ return html`
+
+
+
+
+ `;
+ }
+
+ private callService(): Promise {
+ if (!this.hass) {
+ throw new Error('Unable to update datetime');
+ }
+
+ const time = `${this.hour!.value}:${this.minute!.value}:00`;
+
+ return this.hass.callService('input_datetime', 'set_datetime', {
+ entity_id: this.entity?.entity_id,
+ time,
+ });
+ }
+
static get styles(): CSSResult {
return styles;
}
diff --git a/src/types.ts b/src/types.ts
index 95415be..ce98d78 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,3 +1,10 @@
export interface TimePickerCardConfig {
entity: string;
+ hour_step?: number;
+ minute_step?: number;
+}
+
+export enum Direction {
+ UP = 'up',
+ DOWN = 'down',
}