Skip to content

Commit

Permalink
Fix #1331 : In scene, add OR condition to check-time action (#1425)
Browse files Browse the repository at this point in the history
* In scene, add OR condition to check-time action

* Add more logs

* Fix comment
  • Loading branch information
Pierre-Gilles authored Feb 1, 2022
1 parent f499d43 commit cfee2ac
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 114 deletions.
41 changes: 34 additions & 7 deletions server/lib/scene/scene.actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,30 +149,57 @@ const actionsFunc = {
},
[ACTIONS.CONDITION.CHECK_TIME]: async (self, action, scope) => {
const now = dayjs.tz(dayjs(), self.timezone);
let beforeDate;
let afterDate;
let isBeforeCondition = true;
let isAfterCondition = true;

if (action.before) {
const beforeDate = dayjs.tz(`${now.format('YYYY-MM-DD')} ${action.before}`, self.timezone);
const isBeforeCondition = now.isBefore(beforeDate);
beforeDate = dayjs.tz(`${now.format('YYYY-MM-DD')} ${action.before}`, self.timezone);
isBeforeCondition = now.isBefore(beforeDate);
if (!isBeforeCondition) {
logger.debug(
`Check time before: ${now.format('HH:mm')} > ${beforeDate.format('HH:mm')} condition is not verified.`,
`Check time before: ${now.format('HH:mm')} < ${beforeDate.format('HH:mm')} condition is not verified.`,
);
throw new AbortScene('CONDITION_IS_BEFORE_HOUR_NOT_VERIFIED');
} else {
logger.debug(`Check time before: ${now.format('HH:mm')} < ${beforeDate.format('HH:mm')} condition is valid.`);
}
}
if (action.after) {
const afterDate = dayjs.tz(`${now.format('YYYY-MM-DD')} ${action.after}`, self.timezone);
const isAfterCondition = now.isAfter(afterDate);
afterDate = dayjs.tz(`${now.format('YYYY-MM-DD')} ${action.after}`, self.timezone);
isAfterCondition = now.isAfter(afterDate);
if (!isAfterCondition) {
logger.debug(
`Check time after: ${now.format('HH:mm')} > ${afterDate.format('HH:mm')} condition is not verified.`,
);
throw new AbortScene('CONDITION_IS_AFTER_HOUR_NOT_VERIFIED');
} else {
logger.debug(`Check time after: ${now.format('HH:mm')} > ${afterDate.format('HH:mm')} condition is valid.`);
}
}

// if the afterDate is not before the beforeDate
// It means the user is trying to do a cross-day time check
// Example: AFTER 23:00 and BEFORE 8:00.
// This means H > 23 OR h < 8
// Putting a AND has no sense because it'll simply not work
// Example: H > 23 AND H < 8 is always wrong.
if (action.before && action.after && !afterDate.isBefore(beforeDate)) {
// So the condition is a OR in this case
const conditionVerified = isBeforeCondition || isAfterCondition;
if (!conditionVerified) {
throw new AbortScene('CONDITION_BEFORE_OR_AFTER_NOT_VERIFIED');
} else {
logger.debug(`Check time: Condition OR verified.`);
}
} else {
// Otherwise, the condition is a AND
const conditionVerified = isBeforeCondition && isAfterCondition;
if (!conditionVerified) {
throw new AbortScene('CONDITION_BEFORE_AND_AFTER_NOT_VERIFIED');
} else {
logger.debug(`Check time: Condition AND verified.`);
}
}
if (action.days_of_the_week) {
const currentDayOfTheWeek = now.format('dddd').toLowerCase();
const isCurrentDayInCondition = action.days_of_the_week.indexOf(currentDayOfTheWeek) !== -1;
Expand Down
181 changes: 181 additions & 0 deletions server/test/lib/scene/actions/scene.action.checkTime.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
const { assert, fake, useFakeTimers } = require('sinon');
const chaiAssert = require('chai').assert;
const dayjs = require('dayjs');
const EventEmitter = require('events');
const { ACTIONS } = require('../../../../utils/constants');
const { AbortScene } = require('../../../../utils/coreErrors');
const { executeActions } = require('../../../../lib/scene/scene.executeActions');

const StateManager = require('../../../../lib/state');

const event = new EventEmitter();

describe('scene.action.checkTime', () => {
it('should execute condition.check-time, and send message because condition is true', async () => {
const stateManager = new StateManager(event);
const message = {
sendToUser: fake.resolves(null),
};
const scope = {};
const todayAt12 = dayjs()
.hour(12)
.minute(0);
const fiveMinutesAgo = todayAt12.subtract(5, 'minute');
const inFiveMinutes = todayAt12.add(5, 'minute');
const clock = useFakeTimers(todayAt12.valueOf());
await executeActions(
{ stateManager, event, message },
[
[
{
type: ACTIONS.CONDITION.CHECK_TIME,
after: fiveMinutesAgo.format('HH:mm'),
before: inFiveMinutes.format('HH:mm'),
days_of_the_week: [todayAt12.format('dddd').toLowerCase()],
},
],
[
{
type: ACTIONS.MESSAGE.SEND,
user: 'pepper',
text: 'hello',
},
],
],
scope,
);
assert.calledWith(message.sendToUser, 'pepper', 'hello');
clock.restore();
});
it('should abort scene because condition is not verified', async () => {
const stateManager = new StateManager(event);
const scope = {};
const todayAt12 = dayjs()
.hour(12)
.minute(0);
const fiveMinutesAgo = todayAt12.subtract(5, 'minute');
const clock = useFakeTimers(todayAt12.valueOf());
const promise = executeActions(
{ stateManager, event },
[
[
{
type: ACTIONS.CONDITION.CHECK_TIME,
before: fiveMinutesAgo.format('HH:mm'),
},
],
],
scope,
);
await chaiAssert.isRejected(promise, AbortScene);
clock.restore();
});
it('should abort scene because condition is not verified', async () => {
const stateManager = new StateManager(event);
const scope = {};
const todayAt12 = dayjs()
.hour(12)
.minute(0);
const inFiveMinutes = todayAt12.add(5, 'minute');
const clock = useFakeTimers(todayAt12.valueOf());
const promise = executeActions(
{ stateManager, event },
[
[
{
type: ACTIONS.CONDITION.CHECK_TIME,
after: inFiveMinutes.format('HH:mm'),
},
],
],
scope,
);
await chaiAssert.isRejected(promise, AbortScene);
clock.restore();
});
it('should abort scene because condition is not verified', async () => {
const stateManager = new StateManager(event);
const scope = {};
const todayAt19 = dayjs()
.hour(19)
.minute(0);
const eightPm = dayjs()
.hour(20)
.minute(0);
const nineAm = dayjs()
.hour(9)
.minute(0);
const clock = useFakeTimers(todayAt19.valueOf());
const promise = executeActions(
{ stateManager, event },
[
[
{
type: ACTIONS.CONDITION.CHECK_TIME,
after: eightPm.format('HH:mm'),
before: nineAm.format('HH:mm'),
},
],
],
scope,
);
await chaiAssert.isRejected(promise, AbortScene);
clock.restore();
});
it('should not abort scene because condition is verified', async () => {
const stateManager = new StateManager(event);
const scope = {};
const todayAt20 = dayjs()
.hour(20)
.minute(0);
const sevenPm = dayjs()
.hour(19)
.minute(0);
const nineAm = dayjs()
.hour(9)
.minute(0);
const clock = useFakeTimers(todayAt20.valueOf());
try {
await executeActions(
{ stateManager, event },
[
[
{
type: ACTIONS.CONDITION.CHECK_TIME,
after: sevenPm.format('HH:mm'),
before: nineAm.format('HH:mm'),
},
],
],
scope,
);
} catch (e) {
console.log(e);
throw e;
}
clock.restore();
});
it('should abort scene because condition is not verified', async () => {
const stateManager = new StateManager(event);
const scope = {};
const todayAt12 = dayjs()
.hour(12)
.minute(0);
const tomorrow = todayAt12.add(1, 'day');
const clock = useFakeTimers(todayAt12.valueOf());
const promise = executeActions(
{ stateManager, event },
[
[
{
type: ACTIONS.CONDITION.CHECK_TIME,
days_of_the_week: [tomorrow.format('dddd').toLowerCase()],
},
],
],
scope,
);
await chaiAssert.isRejected(promise, AbortScene);
clock.restore();
});
});
111 changes: 4 additions & 107 deletions server/test/lib/scene/scene.executeActions.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
const { assert, fake, useFakeTimers } = require('sinon');
const { assert, fake } = require('sinon');
const chaiAssert = require('chai').assert;
const { expect } = require('chai');
const dayjs = require('dayjs');
const EventEmitter = require('events');
const cloneDeep = require('lodash.clonedeep');
const { ACTIONS } = require('../../../utils/constants');
Expand All @@ -12,6 +11,9 @@ const StateManager = require('../../../lib/state');

const event = new EventEmitter();

// WE ARE SLOWLY MOVING ALL TESTS FROM THIS BIG FILE
// TO A SMALLER SET OF FILE IN THE "ACTIONS" FOLDER.

describe('scene.executeActions', () => {
it('should execute light turn on', async () => {
const device = {
Expand Down Expand Up @@ -619,111 +621,6 @@ describe('scene.executeActions', () => {
);
assert.calledWith(message.sendToUser, 'pepper', 'Temperature in the living room is 15 °C.');
});
it('should execute condition.check-time, and send message because condition is true', async () => {
const stateManager = new StateManager(event);
const message = {
sendToUser: fake.resolves(null),
};
const scope = {};
const todayAt12 = dayjs()
.hour(12)
.minute(0);
const fiveMinutesAgo = todayAt12.subtract(5, 'minute');
const inFiveMinutes = todayAt12.add(5, 'minute');
const clock = useFakeTimers(todayAt12.valueOf());
await executeActions(
{ stateManager, event, message },
[
[
{
type: ACTIONS.CONDITION.CHECK_TIME,
after: fiveMinutesAgo.format('hh:mm'),
before: inFiveMinutes.format('hh:mm'),
days_of_the_week: [todayAt12.format('dddd').toLowerCase()],
},
],
[
{
type: ACTIONS.MESSAGE.SEND,
user: 'pepper',
text: 'hello',
},
],
],
scope,
);
assert.calledWith(message.sendToUser, 'pepper', 'hello');
clock.restore();
});
it('should abort scene because condition is not verified', async () => {
const stateManager = new StateManager(event);
const scope = {};
const todayAt12 = dayjs()
.hour(12)
.minute(0);
const fiveMinutesAgo = todayAt12.subtract(5, 'minute');
const clock = useFakeTimers(todayAt12.valueOf());
const promise = executeActions(
{ stateManager, event },
[
[
{
type: ACTIONS.CONDITION.CHECK_TIME,
before: fiveMinutesAgo.format('hh:mm'),
},
],
],
scope,
);
await chaiAssert.isRejected(promise, AbortScene);
clock.restore();
});
it('should abort scene because condition is not verified', async () => {
const stateManager = new StateManager(event);
const scope = {};
const todayAt12 = dayjs()
.hour(12)
.minute(0);
const inFiveMinutes = todayAt12.add(5, 'minute');
const clock = useFakeTimers(todayAt12.valueOf());
const promise = executeActions(
{ stateManager, event },
[
[
{
type: ACTIONS.CONDITION.CHECK_TIME,
after: inFiveMinutes.format('hh:mm'),
},
],
],
scope,
);
await chaiAssert.isRejected(promise, AbortScene);
clock.restore();
});
it('should abort scene because condition is not verified', async () => {
const stateManager = new StateManager(event);
const scope = {};
const todayAt12 = dayjs()
.hour(12)
.minute(0);
const tomorrow = todayAt12.add(1, 'day');
const clock = useFakeTimers(todayAt12.valueOf());
const promise = executeActions(
{ stateManager, event },
[
[
{
type: ACTIONS.CONDITION.CHECK_TIME,
days_of_the_week: [tomorrow.format('dddd').toLowerCase()],
},
],
],
scope,
);
await chaiAssert.isRejected(promise, AbortScene);
clock.restore();
});

it('should execute action scene.start', async () => {
const stateManager = new StateManager(event);
Expand Down

0 comments on commit cfee2ac

Please sign in to comment.