// 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('starting',activeSch.runID) console.log('---watering for', duration,'minutes') 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)] // ) // )