Skip to content

Commit

Permalink
fix(time): Resolve "Date in past. Will never be fired." error for val…
Browse files Browse the repository at this point in the history
…id dates

- Switched to Cronos.js for improved time handling

feat(time): Added option to allow past date inputs without throwing errors

Closes #1575
  • Loading branch information
zachowj committed Sep 19, 2024
1 parent 1402d06 commit a1e16ee
Show file tree
Hide file tree
Showing 9 changed files with 76 additions and 57 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"axios": "1.7.7",
"bonjour": "3.5.0",
"compare-versions": "6.1.1",
"cron": "3.1.7",
"cronosjs": "^1.7.1",
"debug": "4.3.7",
"flat": "5.0.2",
"geolib": "3.3.4",
Expand Down
25 changes: 3 additions & 22 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 35 additions & 33 deletions src/nodes/time/TimeController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CronJob } from 'cron';
import { CronosTask, scheduleTask } from 'cronosjs';
import selectn from 'selectn';

import ExposeAsMixin from '../../common/controllers/ExposeAsMixin';
Expand All @@ -18,29 +18,27 @@ const DEFAULT_PROPERTY = 'state';

const ExposeAsController = ExposeAsMixin(OutputController<TimeNode>);
export default class TimeController extends ExposeAsController {
#cronjob: CronJob | null = null;
#cronjob: CronosTask | null = null;

#createCronjob(crontab: string | Date) {
#createCronjob(crontab: string) {
this.node.debug(`Creating cronjob: ${crontab}`);
this.#cronjob = CronJob.from({
cronTime: crontab,
onTick: async () => {
this.#cronjob = scheduleTask(
crontab,
async () => {
try {
await this.#onTimer();
} catch (e) {
this.node.error(e);
this.status.setError();
}
},
start: true,
});
{},
);
}

#destoryCronjob() {
if (this.#cronjob != null) {
this.node.debug(
`Destroying cronjob: ${this.#cronjob?.nextDate().toJSDate()}`,
);
this.node.debug(`Destroying cronjob: ${this.#cronjob?.nextRun}`);
this.#cronjob.stop();
this.#cronjob = null;
}
Expand Down Expand Up @@ -97,10 +95,10 @@ export default class TimeController extends ExposeAsController {
return Number(offsetMs);
}

#getRandomOffset(crontab: Date, offset: number) {
#getRandomOffset(date: Date, offset: number) {
// if not repeating stay ahead of current time
if (!this.node.config.repeatDaily && Math.sign(offset) === -1) {
const cronTimestamp = crontab.getTime();
const cronTimestamp = date.getTime();
const maxOffset =
Math.max(Date.now(), cronTimestamp + offset) - cronTimestamp;
return maxOffset * Math.random();
Expand Down Expand Up @@ -151,9 +149,7 @@ export default class TimeController extends ExposeAsController {
if (this.node.config.repeatDaily) {
const sentTime = this.#formatDate(now);
// convert luxon to date
const nextTime = this.#formatDate(
this.#cronjob?.nextDate().toJSDate(),
);
const nextTime = this.#formatDate(this.#cronjob?.nextRun);
this.status.setSuccess([
'ha-time.status.sent_and_next',
{
Expand All @@ -176,7 +172,7 @@ export default class TimeController extends ExposeAsController {
const property = this.node.config.property || DEFAULT_PROPERTY;
const entity = this.#getEntity();
const dateString = selectn(property, entity);
let crontab;
let date: Date | undefined;
let offset;

this.#destoryCronjob();
Expand All @@ -194,40 +190,46 @@ export default class TimeController extends ExposeAsController {
'ha-time.status.invalid_date',
);
}
crontab = new Date(dateString);
date = new Date(dateString);
} else {
crontab = new Date();
crontab.setHours(digits.hour);
crontab.setMinutes(digits.minutes);
crontab.setSeconds(digits.seconds);
date = new Date();
date.setHours(digits.hour);
date.setMinutes(digits.minutes);
date.setSeconds(digits.seconds);
}

// plus minus offset
if (offset !== 0) {
if (this.node.config.randomOffset) {
offset = this.#getRandomOffset(crontab, offset);
offset = this.#getRandomOffset(date, offset);
}
const timestamp = crontab.getTime() + offset;
crontab.setTime(timestamp);
const timestamp = date.getTime() + offset;
date.setTime(timestamp);
}

let crontab = `${date.getSeconds()} ${date.getMinutes()} ${date.getHours()} ${date.getDate()} ${
date.getMonth() + 1
} *`;

// Create repeating crontab string
if (this.node.config.repeatDaily) {
const days = this.#getDays();
crontab = `${crontab.getSeconds()} ${crontab.getMinutes()} ${crontab.getHours()} * * ${days}`;
} else if (crontab.getTime() < Date.now()) {
this.node.warn(
RED._('ha-time.error.in_the_past', {
date: dateString,
}),
);
crontab = `${date.getSeconds()} ${date.getMinutes()} ${date.getHours()} * * ${days}`;
} else if (date.getTime() < Date.now()) {
if (!this.node.config.ignorePastDate) {
this.node.warn(
RED._('ha-time.error.in_the_past', {
date: dateString,
}),
);
}
this.status.setFailed('ha-time.status.in_the_past');
return;
}

this.#createCronjob(crontab);

const nextTime = this.#formatDate(this.#cronjob?.nextDate().toJSDate());
const nextTime = this.#formatDate(this.#cronjob?.nextRun);
this.status.setText(RED._('ha-time.status.next_at', { nextTime }));
}
}
7 changes: 7 additions & 0 deletions src/nodes/time/editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@
</div>
</div>

<div class="form-row checkbox-option">
<label>
<input type="checkbox" id="node-input-ignorePastDate" />
<span data-i18n="ha-time.label.ignore_past_date"></span>
</label>
</div>

<div class="form-row"><ol id="custom-outputs"></ol></div>

<div class="form-row" id="exposed-as-row">
Expand Down
2 changes: 2 additions & 0 deletions src/nodes/time/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface TimeEditorNodeProperties extends EditorNodeProperties {
friday: boolean;
saturday: boolean;
exposeAsEntityConfig: string;
ignorePastDate: boolean;

// deprecated but still needed for imports of old flows
debugenabled: undefined;
Expand Down Expand Up @@ -104,6 +105,7 @@ const TimeEditor: EditorNodeDef<TimeEditorNodeProperties> = {
thursday: { value: true },
friday: { value: true },
saturday: { value: true },
ignorePastDate: { value: true },

// deprecated but still needed for imports of old flows
debugenabled: { value: undefined },
Expand Down
1 change: 1 addition & 0 deletions src/nodes/time/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface TimeNodeProperties extends BaseNodeProperties {
thursday: boolean;
friday: boolean;
saturday: boolean;
ignorePastDate: boolean;
outputProperties: OutputProperty[];
exposeAsEntityConfig: string;
}
Expand Down
1 change: 1 addition & 0 deletions src/nodes/time/locale.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"label": {
"entity_id": "Entity ID",
"friday": "Friday",
"ignore_past_date": "Ignore past date errors",
"monday": "Monday",
"name": "Name",
"offset": "Offset",
Expand Down
12 changes: 12 additions & 0 deletions src/nodes/time/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ export default [
newSchema.exposeToHomeAssistant = undefined;
newSchema.haConfig = undefined;

return newSchema;
},
},
{
version: 4,
up: (schema: any) => {
const newSchema = {
...schema,
version: 4,
ignorePastDate: false,
};

return newSchema;
},
},
Expand Down
15 changes: 14 additions & 1 deletion test/migrations/time.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ const VERSION_3 = {
exposeToHomeAssistant: undefined,
haConfig: undefined,
};
const VERSION_4 = {
...VERSION_3,
version: 4,
ignorePastDate: false,
};

describe('Migrations - Time Node', function () {
describe('Version 0', function () {
Expand Down Expand Up @@ -154,8 +159,16 @@ describe('Migrations - Time Node', function () {
});
});

describe('Version 4', function () {
it('should update version 3 to version 4', function () {
const migrate = migrations.find((m) => m.version === 4);
const migratedSchema = migrate?.up(VERSION_3);
expect(migratedSchema).toEqual(VERSION_4);
});
});

it('should update an undefined version to current version', function () {
const migratedSchema = migrate(VERSION_UNDEFINED);
expect(migratedSchema).toEqual(VERSION_3);
expect(migratedSchema).toEqual(VERSION_4);
});
});

0 comments on commit a1e16ee

Please sign in to comment.