mirror of
https://github.com/GeorgeSG/lovelace-time-picker-card.git
synced 2026-09-06 02:18:44 +00:00
Initial implementation
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { TimeUnit } from './TimeUnit';
|
||||
export { Hour } from './Hour';
|
||||
export { Minute } from './Minute';
|
||||
@@ -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`<hui-error-card ._config="${config}"></hui-error-card>`;
|
||||
}
|
||||
}
|
||||
+59
-3
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
+101
-3
@@ -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`<ha-card>TimePickerCard</ha-card>`;
|
||||
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`
|
||||
<ha-card class="time-picker-ha-card">
|
||||
<div class="time-form">
|
||||
${this.renderStepChanger(Direction.UP, this.hour)}
|
||||
<input
|
||||
class="time-input"
|
||||
type="number"
|
||||
placeholder="HH"
|
||||
min="0"
|
||||
max="24"
|
||||
.value=${this.hour.toString()}
|
||||
@change=${this.onHourChange}
|
||||
/>
|
||||
${this.renderStepChanger(Direction.DOWN, this.hour)}
|
||||
</div>
|
||||
<div class="time-separator">
|
||||
:
|
||||
</div>
|
||||
<div class="time-form">
|
||||
${this.renderStepChanger(Direction.UP, this.minute)}
|
||||
<input
|
||||
class="time-input"
|
||||
type="number"
|
||||
placeholder="MM"
|
||||
min="0"
|
||||
max="60"
|
||||
.value="${this.minute.toString()}"
|
||||
@change=${this.onMinuteChange}
|
||||
/>
|
||||
${this.renderStepChanger(Direction.DOWN, this.minute)}
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
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`
|
||||
<div class=${className} @click=${onIconClick}>
|
||||
<ha-icon .icon="mdi:arrow-${direction}"></ha-icon>
|
||||
<mwc-ripple id="ripple"></mwc-ripple>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private callService(): Promise<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
export interface TimePickerCardConfig {
|
||||
entity: string;
|
||||
hour_step?: number;
|
||||
minute_step?: number;
|
||||
}
|
||||
|
||||
export enum Direction {
|
||||
UP = 'up',
|
||||
DOWN = 'down',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user