-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
6,841 additions
and
394 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,5 @@ | ||
node_modules | ||
Dockerfile | ||
*jest* | ||
*.test.js | ||
*.spec.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
const parser = require('cron-parser'); | ||
|
||
module.exports = (name, { | ||
valveControlTopic, | ||
valveControlTemplate = (state) => ({state}), | ||
schedule, | ||
duration, | ||
}) => ({ | ||
start: ({mqtt}) => { | ||
const interval = parser.parseExpression(schedule); | ||
|
||
const update = () => { | ||
const computeWantedStatus = () => { | ||
const now = new Date() | ||
interval.reset(now) | ||
|
||
const prevStart = new Date(interval.prev().getTime()) | ||
const prevEnd = new Date(prevStart.getTime() + duration) | ||
if (prevStart <= now && now < prevEnd) { | ||
return {state: true, timeout: prevEnd - now} | ||
} | ||
|
||
const nextStart = new Date(interval.next().getTime()) | ||
const nextEnd = new Date(nextStart.getTime() + duration) | ||
if (nextStart <= now && now < nextEnd) { | ||
return {state: true, timeout: nextEnd - now} | ||
} | ||
|
||
return {state: false, timeout: nextStart - now} | ||
} | ||
|
||
const status = computeWantedStatus() | ||
|
||
mqtt.publish(valveControlTopic, valveControlTemplate(status.state)) | ||
|
||
setTimeout(update, status.timeout) | ||
} | ||
|
||
update() | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
const {beforeEach, afterEach, describe, test, expect, jest} = require('@jest/globals'); | ||
|
||
beforeEach(() => { | ||
jest.useFakeTimers() | ||
}) | ||
|
||
const setup = async ({schedule, duration = 1000}) => { | ||
const irrigation = require('./irrigation')('test-irrigation', { | ||
valveControlTopic: 'valve-control', | ||
valveControlTemplate: (status) => status, | ||
schedule, | ||
duration, | ||
}) | ||
const publish = jest.fn() | ||
const mqtt = {publish} | ||
|
||
// start the bot | ||
await irrigation.start({mqtt}) | ||
|
||
return ({mqtt, irrigation}) | ||
} | ||
|
||
describe('irrigation', () => { | ||
test('should turn on irrigation when at the start of interval', async () => { | ||
const schedule = '0 0 0 * * *' | ||
jest.setSystemTime(new Date('2021-06-01T00:00:00Z')) | ||
const {mqtt} = await setup({schedule}) | ||
|
||
// should immediately turn on the valve | ||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'on') | ||
}) | ||
|
||
test('should turn on irrigation when at the middle of interval', async () => { | ||
const schedule = '0 0 0 * * *' | ||
const duration = 10000 | ||
jest.setSystemTime(new Date('2021-06-01T00:00:05Z')) | ||
const {mqtt} = await setup({schedule, duration}) | ||
|
||
// should immediately turn on the valve | ||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'on') | ||
}) | ||
|
||
test('should turn off irrigation when at the end of interval', async () => { | ||
const schedule = '0 0 0 * * *' | ||
const duration = 10000 | ||
jest.setSystemTime(new Date('2021-06-01T00:00:10Z')) | ||
const {mqtt} = await setup({schedule, duration}) | ||
|
||
// should immediately turn on the valve | ||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'off') | ||
}) | ||
|
||
test('should turn off irrigation when outside of interval', async () => { | ||
const schedule = '0 0 0 * * *' | ||
const duration = 10000 | ||
jest.setSystemTime(new Date('2021-06-01T00:01:00Z')) | ||
const {mqtt} = await setup({schedule, duration}) | ||
|
||
// should immediately turn on the valve | ||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'off') | ||
}) | ||
|
||
test('should turn on irrigation when reach start of interval', async () => { | ||
const schedule = '1 0 0 * * *' | ||
const duration = 10000 | ||
jest.setSystemTime(new Date('2021-06-01T00:00:00Z')) | ||
const {mqtt} = await setup({schedule, duration}) | ||
|
||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'off') | ||
|
||
mqtt.publish.mockClear() | ||
jest.advanceTimersByTime(1000) | ||
|
||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'on') | ||
}) | ||
|
||
test('should turn on irrigation when reach mid of interval', async () => { | ||
const schedule = '1 0 0 * * *' | ||
const duration = 10000 | ||
jest.setSystemTime(new Date('2021-06-01T00:00:00Z')) | ||
const {mqtt} = await setup({schedule, duration}) | ||
|
||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'off') | ||
|
||
mqtt.publish.mockClear() | ||
jest.advanceTimersByTime(6000) | ||
|
||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'on') | ||
}) | ||
|
||
test('should turn off irrigation when reach end of interval', async () => { | ||
const schedule = '1 0 0 * * *' | ||
const duration = 10000 | ||
jest.setSystemTime(new Date('2021-06-01T00:00:00Z')) | ||
const {mqtt} = await setup({schedule, duration}) | ||
|
||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'off') | ||
|
||
mqtt.publish.mockClear() | ||
jest.advanceTimersByTime(11000) | ||
|
||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'off') | ||
}) | ||
|
||
test('should turn off irrigation when reach end of interval from start', async () => { | ||
const schedule = '0 0 0 * * *' | ||
const duration = 10000 | ||
jest.setSystemTime(new Date('2021-06-01T00:00:00Z')) | ||
const {mqtt} = await setup({schedule, duration}) | ||
|
||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'on') | ||
|
||
mqtt.publish.mockClear() | ||
jest.advanceTimersByTime(10000) | ||
|
||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'off') | ||
}) | ||
|
||
test('should turn off irrigation when reach beyond end of interval from start', async () => { | ||
const schedule = '0 0 0 * * *' | ||
const duration = 10000 | ||
jest.setSystemTime(new Date('2021-06-01T00:00:00Z')) | ||
const {mqtt} = await setup({schedule, duration}) | ||
|
||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'on') | ||
|
||
mqtt.publish.mockClear() | ||
jest.advanceTimersByTime(15000) | ||
|
||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'off') | ||
}) | ||
|
||
test('should turn on irrigation when reach next start of interval from start', async () => { | ||
const schedule = '0 0 0 * * *' | ||
const duration = 10000 | ||
jest.setSystemTime(new Date('2021-06-01T00:00:00Z')) | ||
const {mqtt} = await setup({schedule, duration}) | ||
|
||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'on') | ||
|
||
mqtt.publish.mockClear() | ||
jest.advanceTimersByTime(24*60*60*1000) | ||
|
||
expect(mqtt.publish).toHaveBeenCalledWith('valve-control', 'on') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
const {beforeEach, describe, jest, test, expect} = require('@jest/globals') | ||
|
||
beforeEach(() => { | ||
jest.useFakeTimers() | ||
}) | ||
|
||
describe('timeout-emit', () => { | ||
test('should emit after timeout after receiving message', async () => { | ||
const timeoutEmit = require('./timeout-emit')('test-timeout-emit', { | ||
listenTopic: 'test-topic', | ||
timeout: 1000, | ||
emitTopic: 'emit-topic', | ||
emitValue: 'timeout' | ||
}) | ||
const subscribe = jest.fn() | ||
const publish = jest.fn() | ||
const mqtt = {subscribe, publish} | ||
|
||
// start the bot | ||
await timeoutEmit.start({ mqtt }) | ||
expect(subscribe).toHaveBeenCalledWith('test-topic', expect.any(Function)) | ||
|
||
// check for timeout after start | ||
jest.advanceTimersByTime(1000) | ||
expect(publish).not.toHaveBeenCalled() | ||
|
||
// receive a message | ||
subscribe.mock.calls[0][1]('payload') | ||
expect(publish).not.toHaveBeenCalled() | ||
|
||
// check for timeout after message | ||
jest.advanceTimersByTime(1000) | ||
expect(publish).toHaveBeenCalledWith('emit-topic', 'timeout') | ||
|
||
// check for timeout after emit | ||
publish.mockClear() | ||
jest.advanceTimersByTime(1000) | ||
expect(publish).not.toHaveBeenCalled() | ||
}) | ||
|
||
test('should emit after timout after first received message within timeout', async () => { | ||
const timeoutEmit = require('./timeout-emit')('test-timeout-emit', { | ||
listenTopic: 'test-topic', | ||
timeout: 1000, | ||
emitTopic: 'emit-topic', | ||
emitValue: 'timeout' | ||
}) | ||
const subscribe = jest.fn() | ||
const publish = jest.fn() | ||
const mqtt = {subscribe, publish} | ||
|
||
// start the bot | ||
await timeoutEmit.start({ mqtt }) | ||
expect(subscribe).toHaveBeenCalledWith('test-topic', expect.any(Function)) | ||
|
||
// receive a message | ||
subscribe.mock.calls[0][1]('payload') | ||
expect(publish).not.toHaveBeenCalled() | ||
|
||
// advance with half of the timeout | ||
jest.advanceTimersByTime(500) | ||
expect(publish).not.toHaveBeenCalled() | ||
|
||
// receive a message | ||
subscribe.mock.calls[0][1]('payload') | ||
expect(publish).not.toHaveBeenCalled() | ||
|
||
// advance with half of the timeout | ||
jest.advanceTimersByTime(500) | ||
expect(publish).toHaveBeenCalledWith('emit-topic', 'timeout') | ||
|
||
// check for timeout after emit | ||
publish.mockClear() | ||
jest.advanceTimersByTime(1000) | ||
expect(publish).not.toHaveBeenCalled() | ||
}) | ||
|
||
test('should emit after timeout after receiving message with filter', async () => { | ||
const timeoutEmit = require('./timeout-emit')('test-timeout-emit', { | ||
listenTopic: 'test-topic', | ||
listenFilter: (payload) => payload === 'valid', | ||
timeout: 1000, | ||
emitTopic: 'emit-topic', | ||
emitValue: 'timeout' | ||
}) | ||
const subscribe = jest.fn() | ||
const publish = jest.fn() | ||
const mqtt = {subscribe, publish} | ||
|
||
// start the bot | ||
await timeoutEmit.start({ mqtt }) | ||
expect(subscribe).toHaveBeenCalledWith('test-topic', expect.any(Function)) | ||
|
||
// receive an invalid message | ||
subscribe.mock.calls[0][1]('invalid') | ||
expect(publish).not.toHaveBeenCalled() | ||
|
||
// check for timeout after invalid message | ||
jest.advanceTimersByTime(1000) | ||
expect(publish).not.toHaveBeenCalled() | ||
|
||
// receive a valid message | ||
subscribe.mock.calls[0][1]('valid') | ||
expect(publish).not.toHaveBeenCalled() | ||
|
||
// check for timeout after message | ||
jest.advanceTimersByTime(1000) | ||
expect(publish).toHaveBeenCalledWith('emit-topic', 'timeout') | ||
|
||
// check for timeout after emit | ||
publish.mockClear() | ||
jest.advanceTimersByTime(1000) | ||
expect(publish).not.toHaveBeenCalled() | ||
}) | ||
|
||
test('should not emit after receiving message with filter false', async () => { | ||
const timeoutEmit = require('./timeout-emit')('test-timeout-emit', { | ||
listenTopic: 'test-topic', | ||
listenFilter: (payload) => payload === 'valid', | ||
timeout: 1000, | ||
emitTopic: 'emit-topic', | ||
emitValue: 'timeout' | ||
}) | ||
const subscribe = jest.fn() | ||
const publish = jest.fn() | ||
const mqtt = {subscribe, publish} | ||
|
||
// start the bot | ||
await timeoutEmit.start({ mqtt }) | ||
expect(subscribe).toHaveBeenCalledWith('test-topic', expect.any(Function)) | ||
|
||
// receive an invalid message | ||
subscribe.mock.calls[0][1]('valid') | ||
expect(publish).not.toHaveBeenCalled() | ||
|
||
// check for timeout after invalid message | ||
jest.advanceTimersByTime(500) | ||
expect(publish).not.toHaveBeenCalled() | ||
|
||
// receive an invalid message | ||
subscribe.mock.calls[0][1]('invalid') | ||
expect(publish).not.toHaveBeenCalled() | ||
|
||
// check for timeout after invalid message | ||
jest.advanceTimersByTime(1000) | ||
expect(publish).not.toHaveBeenCalled() | ||
}) | ||
}) |
Oops, something went wrong.