Skip to content

Commit

Permalink
add irrigation schedule
Browse files Browse the repository at this point in the history
  • Loading branch information
groupsky committed Apr 21, 2024
1 parent fc8327c commit 42cac82
Show file tree
Hide file tree
Showing 9 changed files with 6,841 additions and 394 deletions.
37 changes: 37 additions & 0 deletions config/automations/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,43 @@ module.exports = {
emitTopic: `${featuresPrefix}/relay/irrigation_grass_pergola/set`,
emitValue: { state: false }
},

irrigationFlowerGroundSchedule: {
type: 'irrigation',
schedule: '0 */10 8-18 * * *',
duration: 5*60*1000,
valveControlTopic: `${featuresPrefix}/irrigation_flower_ground/set`,
},
irrigationFlowerPotsSchedule: {
type: 'irrigation',
schedule: '0 25,55 6-22 * * *',
duration: 2*60*1000,
valveControlTopic: `${featuresPrefix}/irrigation_flower_pots/set`,
},
irrigationGrassPergolaSchedule: {
type: 'irrigation',
schedule: '0 0 2 * * *',
duration: 20*60*1000,
valveControlTopic: `${featuresPrefix}/irrigation_grass_pergola/set`,
},
irrigationGrassNorthWestSchedule: {
type: 'irrigation',
schedule: '0 20 2 * * *',
duration: 20*60*1000,
valveControlTopic: `${featuresPrefix}/irrigation_grass_north_west/set`,
},
irrigationGrassWestCenterSchedule: {
type: 'irrigation',
schedule: '0 40 2 * * *',
duration: 20*60*1000,
valveControlTopic: `${featuresPrefix}/irrigation_grass_west_center/set`,
},
irrigationGrassNorthWestSchedule2: {
type: 'irrigation',
schedule: '0 0 3 * * *',
duration: 20*60*1000,
valveControlTopic: `${featuresPrefix}/irrigation_grass_north_west/set`,
},
},
gates: {
mqtt: {
Expand Down
3 changes: 3 additions & 0 deletions docker/automations/.dockerignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
node_modules
Dockerfile
*jest*
*.test.js
*.spec.js
41 changes: 41 additions & 0 deletions docker/automations/bots/irrigation.js
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()
}
})
146 changes: 146 additions & 0 deletions docker/automations/bots/irrigation.test.js
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')
})
})
148 changes: 148 additions & 0 deletions docker/automations/bots/timeout-emit.test.js
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()
})
})

0 comments on commit 42cac82

Please sign in to comment.