irrigation/src/irrigation.js

291 lines
9.5 KiB
JavaScript

// native
import { sync as getConfig } from 'load-yaml-file'
import delay from 'delay'
import deepFreeze from 'deep-freeze'
import clone from 'clone'
import { all as merge } from 'deepmerge'
// import to from 'await-to-js'
// UCI
import RxClass from '@uci-utils/rx-class'
import { Gpio } from '@uci/gpio'
import { Runner, Schedule } from '@uci-utils/scheduler'
import { init as initHass } from './ha.js'
import { PUMP, ZONE, RUNNER } from './defaults.js'
// TODO change to dynamic imports
// import Base from '@uci/base' // TODO add Hass websocket client to Base
// TODO extend the class conditionally with uci base
class Irrigation extends RxClass {
constructor(opts) {
super()
this.config = deepFreeze((typeof opts ==='string') ? getConfig(opts) : opts)
// console.log(this.config)
this.deployed = this.config.deployed==null ? 'true' : this.config.deployed // assume deployed by default
this.settings = {
zones:{},
pump: clone(this.config.pump.settings)
}
this.zones = clone(this.config.zones)
Object.keys(this.zones).forEach(id => {
const zone = this.zones[id]
zone.id = id
zone.name = zone.name || zone.id
this.zoneResetSettings(zone)
delete zone.settings
this.set(['zones',zone.id,'state'],'off')
})
console.log('done setting up zones')
this.pump = clone(this.config.pump)
this.settings.pump = merge([{},PUMP,this.pump.settings])
this.set('pump.state','off')
this.deltas = this.config.deltas
console.log('done setting up pump')
const rOpts = {
name: this.config.name ||'irrigation',
schedules: Object.values(this.zones).map(zone => {
const zsets = this.get(['settings.zones',zone.id,'schedule'])
const sch = new Schedule ({ name: zone.name, id:zone.id, settings:zsets})
this.$del(['settings.zones',zone.id,'schedule'],true)
return sch
})
}
this.runner = new Runner (Object.assign(rOpts,RUNNER))
this.rxAdd('settings',{traverse:true}) // TODO add store subscription here
// console.log('runner', this.runner)
console.log('schedules and runner created')
console.log('enabled schedules', this.runner.schedules.map(sch=>sch.id))
if (this.deployed) {
console.log('deployed, setting up gpios')
this.pump.gpio = new Gpio(this.pump.gpio_pin_num,{activeLow:true})
Object.values(this.zones).forEach(zone =>{
zone.gpio = new Gpio(zone.gpio_pin_num,{activeLow:true})
})
}
}
async init() {
let init = false
await this.pumpGPIO('off')
if (this.config.hass) await initHass.call(this)
// set up zone schedules and init
Object.values(this.zones).forEach(zone => {
const sch = this.runner.getSchedule(zone.id)
// console.log(this.runner.getSchedule(zone.id).get('settings'))
// this.rxSubscribe(['settings.zones',zone.id,'schedule.enabled'],'abort',value => {
// console.log(zone.id,'zone schedule enabled change',value)
// if (init) {
// if (value==='off') {
// console.log('removing any active schedules', sch.id)
// this.runner.removeActive(sch.id)
// }
// }
// })
// this.zoneSchCountdown(zone.id)
sch.registerStartAction(start,
this.get(['settings.zones',zone.id,'duration']),
this.zoneOn.bind(this,zone,'on','auto'),
this.zoneOn.bind(this,zone,'off','auto')
) // passing zone here make it available in action
sch.registerStopAction(stop,this.zoneOn.bind(this,zone,'off','auto'))
this.zoneOff(zone)
})
// this.runner.rxSubscribe('countdown','runner',value => { console.log('trigger #', this.runner._toID,'in', value)})
// this.runner.rxSubscribe('running','runner',list => { console.log('running schedules:',list)})
// this.runner.rxSubscribe('queue','runner',list => { console.log('queued schedules:',list)})
// this.runner.rxSubscribe('queueCount','runner',value => { console.log('queue schedule count', value)})
// this.runner.rxSubscribe('runningCount','runner',value => { console.log('running schedule count', value)})
console.log('Irrigation Initialization Done')
init = true
} // end init
getZone (id) {
return this.zones[id]
}
zoneResetSettings(zone) {
if (typeof zone ==='string') zone = this.getZone(zone)
this.settings.zones[zone.id] = merge([{},ZONE,
this.config.zone_settings_default || {},
zone.settings,
this.config.zone_settings_common || {}
])
// console.log(zone.id,this.getZoneProp(zone,'settings'))
}
setZoneProp(zone,path,value) {
if (typeof zone !=='string') zone=zone.id
return this.set(['zones',zone,path],value)
}
getZoneProp(zone,path) {
if (typeof zone !=='string') zone=zone.id
return this.get(['zones',zone,path])
}
async pumpPrime (prime=3) {
// console.log('priming pump')
// await this.pump.gpio.on()
await this.pumpGPIO('on')
await delay(prime*1000) // let pump get to pressure
// console.log('waited for prime',prime,'secs')
// await this.pump.gpio.off()
await this.pumpGPIO('off')
}
async pumpDripMode (drip) {
console.log('-----using drip pump cycling----')
this.pump.dripMode = true
await this.pumpPrime(this.pump.prime)
const pause = drip === true ? (this.pump.settings.drip || 60) : drip
// console.log('pump is primed pausing for',pause,'secs')
this.pump.dripInt = setInterval(async ()=>{
await this.pumpPrime(this.pump.prime)
// console.log('pump is primed again pausing for',pause,'secs')
},pause*1000)
}
async pumpToggle() {
return await this.pumpGPIO('toggle')
}
async pumpOff () {
return await this.pumpGPIO('off')
}
async pumpOn () {
return await this.pumpGPIO('on')
}
async pumpGPIO(state) {
state == null ? 'on' : state
if (state === true) state='on'
if (state === false) state='off'
if (this.deployed) {
// await this.pump.gpio.read()
// if (this.pump.gpio.state !== state ) {
let res = await this.pump.gpio[state]()
// console.log('pump state change to',res)
this.pump.state = this.pump.gpio.state
this.pump.active = this.pump.gpio.value
// console.log('change of pump state',this.pump.state)
return res
// }
}
else console.log('mock pump state change', state)
}
// Todo think of better way to handle automatic zone change vs manual
async zoneOn (zone,state,auto) {
const zset = this.settings.zones[zone.id]
const pset = this.settings.pump
// console.log(zone.id,'auto,zone.auto,state',auto,zone.auto,state)
if (typeof zone === 'string') zone = this.zones[zone]
state = state == null ? 'on' : state
if (state === true) state='on'
if (state === false ) state='off'
if (!zone.auto) zone.auto = state == 'on' && auto==='auto'
// console.log('zone.auto',zone.auto)
if (this.deployed) {
await zone.gpio.read()
if (zone.gpio.state !== state ) {
const cstate = await zone.gpio[state]()?'on':'off'
if(cstate !== state) {
console.log('ERROR: zone state request was not realized, aborting!!')
return {error:'ERROR: zone state request was not realized, aborting!!'}
}
// console.log(zone.id, 'new zone state',state)
zone.state = state
zone.active = zone.gpio.value
}
}
if (pset.use && !(zset.pump === 'off')) { // unless specifically turned off use pump
// console.log('handling pump with zone state')
if (zset.drip) {
if (!this.pump.dripMode && state==='on') this.pumpDripMode(zset.drip)
if (state==='off') {
// console.log(zone.id,'pump off: clearing drip mode')
clearInterval(this.pump.dripInt)
this.pump.dripMode = false
// console.log('--------clearing drip cycling mode------------')
await this.pumpGPIO('off')
}
} else await this.pumpGPIO(state)
}
// console.log(zone.name,zone.id,'is', state)
if (!zone.auto && state==='on') {
const duration = zset.duration || 10
console.log('MANUAL - setting timeout to turn off in',duration,'minutes')
zone._offTO = setTimeout( () => {
console.log(zone.name, zone.id, 'run of ', duration,' is over')
this.zoneOn(zone,'off')
},
duration*60000)
} else clearTimeout(zone._offTO)
if (state ==='off' && auto==='auto') zone.auto = false
}
zoneOff(zone) {
this.zoneOn(zone,'off')
}
} // end class
export default Irrigation
export { Irrigation }
// zone schedule start action
function start (duration,on,off,activeSch) {
// console.log('start',duration,on,off,activeSch.name)
return new Promise(resolve => {
console.log(activeSch.name,'running for',duration,'minutes')
console.log(new Date().toString())
on()
// const tick = setInterval(()=>{
// console.log(activeSch.runID,'duration tick')
// }, 1000)
this.once('abort:'+activeSch.runID,()=> {
clearTimeout(run)
// clearInterval(tick)
off()
console.log('aborting run>>>', activeSch.runID)
})
const run = setTimeout(()=>{
console.log('stopping>>>',activeSch.runID)
// clearInterval(tick)
off()
this.removeAllListeners('abort:'+activeSch.runID)
resolve()
}, duration*1000 * 60)
})
}
// zone stop action
function stop (stop,activeSch) {
console.log('aborting run for action id',activeSch.runID, stop)
this.emit('abort:'+activeSch.runID)
}
// const objectMap = (obj, fn) =>
// Object.fromEntries(
// Object.entries(obj).map(
// ([k, v], i) => [k, fn(v, k, i)]
// )
// )