First working version

supports using Home Assistant as a UI
  does not currently support persistence for setting changes (from HA), needs changes to rx-class
  does not support aborting a run (need changes to scheduler
master
David Kebler 2020-06-19 10:04:11 -07:00
parent 477c7d8611
commit 5576ddea54
18 changed files with 992 additions and 0 deletions

33
.eslintrc.js Normal file
View File

@ -0,0 +1,33 @@
module.exports = {
"ecmaFeatures": {
"modules": true,
"spread" : true,
"restParams" : true
},
"env": {
"es6": true,
"node": true,
"mocha": true
},
"parserOptions": {
"ecmaVersion": 2017
,"sourceType": "module"
},
"extends": "eslint:recommended",
"rules": {
"indent": [
"error",
2
],
"no-console": 0,
"semi": ["error", "never"],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
]
}
}

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/node_modules/
/.yalc/
/irrigation.yml

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Irrigation Application
This nodejs application uses [uci](https://git.kebler.net/UCOMmandIt) libraries to control a bank of 8 gpios/relays on an SBC. It contains a home assistant websocket client module to allows HomeAssitant to act as a front end for the application.
Configruation is done via a yaml file (irrigation.yml). See src/defaults.js as well.

26
dev/water-rx-class.conf Normal file
View File

@ -0,0 +1,26 @@
#!/bin/bash
WATCH_DIR='/mnt/AllData/hacking/active-dev-repos/uci/lib/uci-utils/rx-class'
# Watch-related options
# WATCH_EXCLUDE='(\.git|___jb_|\syncd|\node_modules|.yalc)'
# Whether to enable verbosity. If enabled, change events are output.
WATCH_VERBOSE=0
# SSH connection settings
# using configs from .ssh/config file
SSH_USER=sysadmin
SSH_HOST=water
# Sync options
TARGET_DIR="/opt/irrigation/node_modules/@uci-utils/rx-class"
# space delimited string of exclusions
RSYNC_EXCLUDE=" \
--exclude node_modules \
"
# --exclude build/Release/ \
# --exclude deploy \
# --exclude archive \
# --exclude .yalc \
# --exclude yalc.* \
RSYNC_OPTIONS="-Cra --out-format='[%t]--%n' --delete "

25
dev/water.conf Normal file
View File

@ -0,0 +1,25 @@
#!/bin/bash
WATCH_DIR='/mnt/AllData/hacking/active-dev-repos/irrigation'
# Watch-related options
WATCH_EXCLUDE='(\.git|___jb_|\syncd|\node_modules|.yalc)'
# Whether to enable verbosity. If enabled, change events are output.
WATCH_VERBOSE=0
# SSH connection settings
# using configs from .ssh/config file
SSH_USER=sysadmin
SSH_HOST=water
# Sync options
TARGET_DIR="/opt/irrigation"
#space delimited string of exclusions
RSYNC_EXCLUDE=" \
--exclude node_modules \
--exclude build/Release/ \
--exclude deploy \
--exclude archive \
--exclude .yalc \
--exclude yalc.* \
"
RSYNC_OPTIONS="-Cra --out-format='[%t]--%n' --delete "

26
dev/watergpio.conf Normal file
View File

@ -0,0 +1,26 @@
#!/bin/bash
WATCH_DIR='/mnt/AllData/hacking/active-dev-repos/uci/lib/uci-gpio'
# Watch-related options
# WATCH_EXCLUDE='(\.git|___jb_|\syncd|\node_modules|.yalc)'
# Whether to enable verbosity. If enabled, change events are output.
WATCH_VERBOSE=0
# SSH connection settings
# using configs from .ssh/config file
SSH_USER=sysadmin
SSH_HOST=water
# Sync options
TARGET_DIR="/opt/irrigation/node_modules/@uci/gpio"
# space delimited string of exclusions
RSYNC_EXCLUDE=" \
--exclude node_modules \
"
# --exclude build/Release/ \
# --exclude deploy \
# --exclude archive \
# --exclude .yalc \
# --exclude yalc.* \
RSYNC_OPTIONS="-Cra --out-format='[%t]--%n' --delete "

28
dev/waterha.conf Normal file
View File

@ -0,0 +1,28 @@
#!/bin/bash
WATCH_DIR='/mnt/AllData/hacking/active-dev-repos/uci/lib/uci-ha'
# Watch-related options
# WATCH_EXCLUDE='(\.git|___jb_|\syncd|\node_modules|.yalc)'
# Whether to enable verbosity. If enabled, change events are output.
WATCH_VERBOSE=0
# SSH connection settings
# using configs from .ssh/config file
SSH_USER=sysadmin
SSH_HOST=water
# Sync options
TARGET_DIR="/opt/irrigation/node_modules/@uci/ha"
# space delimited string of exclusions
RSYNC_EXCLUDE=" \
--exclude node_modules \
--exclude archive \
--exclude examples \
"
# --exclude build/Release/ \
# --exclude deploy \
# --exclude .yalc \
# --exclude yalc.* \
# "
RSYNC_OPTIONS="-Cra --out-format='[%t]--%n' --delete "

26
dev/watersch.conf Normal file
View File

@ -0,0 +1,26 @@
#!/bin/bash
WATCH_DIR='/mnt/AllData/hacking/active-dev-repos/scheduler'
# Watch-related options
# WATCH_EXCLUDE='(\.git|___jb_|\syncd|\node_modules|.yalc)'
# Whether to enable verbosity. If enabled, change events are output.
WATCH_VERBOSE=0
# SSH connection settings
# using configs from .ssh/config file
SSH_USER=sysadmin
SSH_HOST=water
# Sync options
TARGET_DIR="/opt/irrigation/node_modules/@uci-utils/scheduler"
# space delimited string of exclusions
RSYNC_EXCLUDE=" \
--exclude node_modules \
"
# --exclude build/Release/ \
# --exclude deploy \
# --exclude archive \
# --exclude .yalc \
# --exclude yalc.* \
RSYNC_OPTIONS="-Cra --out-format='[%t]--%n' --delete "

96
example.irrigation.yml Normal file
View File

@ -0,0 +1,96 @@
name: Irrigation System
enabled: true
# reset_settings: true # uncomment to force zone settings to overwrite those in store
# deployed: false # uncomment for testing
hass: # home assistant websocket (client) access
namespace: irrigation # will be prefix on HA entity id
pingpong: true # must use pingpong disconnect/reconnect monitoring
host: '' # ip or host name
access_token: '' # generate token in HA
pump:
relay: 1
name: WellPump
gpio_pin_num: 80
settings:
# prime: 5 # secs pump is primed in drip mode, default: 5
drip: 30 # drip mode pause in secs, default: 60
# zone_settings_default: # overrides built in defaults
# zone_settings_common: # applied last will override defaults and zone settings
# schedule:
# timing:
# delta: 24
# runner:
# enabled:
zones:
zone_1:
relay: 2
name: Front Yard North
gpio_pin_num: 73
settings:
schedule:
enabled: false
timing:
delta: 48
zone_2:
relay: 3
name: Front Yard South
gpio_pin_num: 69
settings:
schedule:
enabled: false
timing:
delta: 48
zone_3:
relay: 4
name: Front Yard Beds/Back Yard East Spigot
gpio_pin_num: 230
settings:
duration: 10
schedule:
timing:
hour: 8
zone_4:
relay: 5
name: Back Yard Garage Spigot
gpio_pin_num: 229
settings:
duration: 5
schedule:
enabled: false
zone_5:
relay: 6
name: Garden North & West
gpio_pin_num: 75
settings:
schedule:
timing:
hour: 7
enabled: true
drip: true # pauses pump for secs
duration: 10
zone_6:
relay: 7
name: Garden South
gpio_pin_num: 74
settings:
schedule:
timing:
hour: 7
enabled: true
drip: true # pauses pump true = use default
duration: 10
zone_7:
relay: 8
name: Back Yard West Spigot
gpio_pin_num: 70
settings:
schedule:
enabled: false
deltas:
3: 3 Hours
6: 6 Hours
12: 12 Hours
24: Day
48: Every Other Day
72: Every Third Day
168: Once a Week

17
index.js Normal file
View File

@ -0,0 +1,17 @@
import Irrigation from './src/irrigation.js'
let opts = 'irrigation.yml'
console.log('starting irrigation')
const irrigation = new Irrigation(opts)
;
(async () => {
await irrigation.init()
irrigation.runner.enabled=true
})().catch(err => {
console.error('FATAL: UNABLE TO START SYSTEM!\n',err)
// process.kill(process.pid, 'SIGTERM')
})

17
irrigation.service Normal file
View File

@ -0,0 +1,17 @@
# $ loginctl enable-linger USERNAME
# copy this file to /home/USERNAME/.config/USERNAME/user
# then user `systemctl --user` commands (or sdu aliases if set up)
[Unit]
Description=Irrigation Controller Service
After=network-online.target
[Service]
StandardOutput=file:/opt/irrigation/std.log
StandardError=file:/opt/irrigation/err.log
Environment='UCI_ENV=pro UCI_LOG_PATH=/opt/irrigation/irrigation.json.log UCI_LOG_LEVEL=warn'
WorkingDirectory=/opt/irrigation
ExecStart=/usr/bin/node -r esm /opt/irrigation/index.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target

6
nodemon.json Normal file
View File

@ -0,0 +1,6 @@
{
"ignoreRoot": [".git"],
"watch": ["node_modules/@uci/","node_modules/@uci-utils/","./*"],
"ignore":["irrigation-settings.json"],
"ext":"js,json,yaml,yml"
}

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "irrigation",
"version": "1.0.0",
"description": "Irrigation System Relays",
"main": "index.js",
"scripts": {
"dev": "UCI_ENV=dev UCI_LOG_LEVEL=error nodemon -r esm index.js",
"devlog": "UCI_ENV=dev UCI_LOG_LEVEL=debug nodemon -r esm index.js",
"start": "node -r esm index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"@uci-utils/rx-class": "^0.1.5",
"@uci-utils/scheduler": "0.0.10",
"@uci/gpio": "^0.1.17",
"@uci/ha": "0.0.8",
"clone": "^2.1.2",
"deep-freeze": "0.0.1",
"deepmerge": "^4.2.2",
"delay": "^4.3.0",
"esm": "^3.2.25",
"load-yaml-file": "^0.2.0"
},
"devDependencies": {
"nodemon": "^2.0.4"
}
}

27
src/defaults.js Normal file
View File

@ -0,0 +1,27 @@
const ZONE = {
drip: false, // pauses pump for secs or uses the pump setting or default of 60
duration: 5,
// pump: true, // if zone is to use the pump, default is true
schedule:{
enabled: true,
simultaneous: false,
timing: {
hour: 6,
minute: 30,
delta: 12
}
}
}
const RUNNER = {
timerInterval:10, // seconds
// enabled: true
}
const PUMP = {
drip: 60, // pauses pump for 60 secs
prime: 5, // primes pump for drip 5 secs
use: true // use the pump by default (would be false for city water)
}
export { PUMP,ZONE, RUNNER }

67
src/ha-constants.js Normal file
View File

@ -0,0 +1,67 @@
const PUMP = {tmpl:'input_boolean.%r%',hass_function:'updateEntity'}
const RUNNER = {
countdown:'next_trigger',
nextDT:'next_trigger_dt',
nextName:'next_schedule_name',
running:'running_names',
queue:'queue_names'
}
const ZONE = {
enabled:{
tmpl: 'input_boolean.%r%_schedule_enabled',
hass_function:'updateEntity',
schedule : true,
path:'settings',
out_value_function:onoff,
in_value_function:tf
},
delta: {
tmpl:'input_select.%r%_schedule_delta',
schedule : true,
path:'settings.timing',
hass_function:'setSelect',
out_value_function: (value,deltas) => deltas[value.toString()],
in_value_function: (str,deltas) => Object.entries(deltas).find(delta => delta[1] === str)[0]
},
hour: {
tmpl: 'input_number.%r%_schedule_base_hour',
path:'settings.timing',
schedule : true
},
minute: {
tmpl:'input_number.%r%_schedule_base_minute',
path:'settings.timing',
schedule : true
},
nextDT: {
tmpl:'%r%_schedule_next_dt',
hass_function:'setVariable',
schedule : true,
},
countdown: {
tmpl: '%r%_schedule_countdown',
hass_function:'setVariable',
schedule: true
},
duration:{
tmpl: 'input_number.%r%_duration',
path:'settings'
},
state:{
tmpl: 'input_boolean.%r%_state',
hass_function:'updateEntity',
in_function: function (zone,value) {
// console.log(zone.id,'ha requested state change', value)
this.zoneOn(zone,value)
},
in_no_state_change: true
},
}
export { ZONE, PUMP, RUNNER}
function onoff(bol) { return bol ? 'on':'off'}
function tf(onoff) { return (onoff === 'on') ? true:false}

101
src/ha-mqtt.js Normal file
View File

@ -0,0 +1,101 @@
///***************** HOME ASSISTANT ************************
// for uci mqtt discovery in HA
const STATE_TOPIC = 'state'
const STATE_CMD = 'state'
const ENTITY = 'switch'
const DISCOVERY = 'homeassistant'
const NAMESPACE = 'uci'
const COMMAND_TOPIC = 'set' // will use payload as the command
function register(name,opts={}) {
const topic = `${opts.discovery || DISCOVERY}/` +
`${opts.entity || ENTITY}/` +
`${opts.namespace || NAMESPACE ? `${opts.namespace || NAMESPACE }` :''}`
console.log('subscription topic for HA',`${topic}/#`)
this.getSocket(name).subscribe(`${topic}/#`)
this.beforeProcessHook(async (packet) => {
// console.log('incoming ha mqtt packet to modify')
// console.dir(packet)
// let modified = Object.assign({},packet)
const topic = packet.cmd.split('/')
const cmd = topic.pop()
const id = topic.pop()
// console.log(cmd, id)
if (cmd=== (opts.state || STATE_TOPIC)) return null // ignore state commands (avoid infiite loop)
else if (cmd===(opts.command||COMMAND_TOPIC)) packet.cmd = packet.data.toLowerCase()
else packet.cmd = cmd
delete packet.data
Object.assign(packet,this.entities.find(ent => ent.id === id))
// console.log('translated to uci packet')
// console.dir(packet)
return packet
},
{name:name})
this.afterProcessHook(async (packet) => {
if (packet.error) {
let npacket = {}
npacket.cmd = 'error'
npacket.payload = JSON.stringify(packet)
return npacket
}
// console.log('after process', packet)
return packet
},
{name:name})
this.beforeSendHook(async (packet) => {
if (packet.cmd === (opts.stateCmd || STATE_CMD)) {
// console.log('preparing state topic/payload')
if (packet.state && packet.gpio) {
let mpacket = {
cmd: `${topic}/${packet.id}/${opts.stateCmd || STATE_CMD}`,
payload: ((packet.state || {})[packet.gpio.toString()] || '').toUpperCase()
}
// console.log('=============modified packet being pushed to broker================')
// console.dir(mpacket)
// console.log('================')
return mpacket
}
// else console.log('bad state packet state/gpio',packet.state,packet.gpio)
}
// console.log('packet ignored, nothing sent to to HA')
return null // only send/publish the required topic
},
{name:name})
}
export default register
function init(mqtt,entities,opts={}) {
console.log('initializing ha mqtt discovery')
entities.forEach(ent => {
const topic = `${opts.discovery || DISCOVERY}/` +
`${opts.entity || ENTITY}/` +
`${opts.namespace || NAMESPACE ? `${opts.namespace || NAMESPACE }/` :''}` +
`${ent.id}`
let payload = {
name:`${opts.namespace || NAMESPACE ? `${opts.namespace || NAMESPACE }_` :''}${ent.id}`,
command_topic:`${topic}/${opts.command || COMMAND_TOPIC}`,
state_topic: `${topic}/${opts.state || STATE_TOPIC}`,
json_attributes_topic:`${topic}/${opts.state || STATE_TOPIC}`,
json_attributes_template:'{{ value_json.alias | tojson }}',
}
payload = JSON.stringify(payload)
console.log('------ sending discovery payload to HA for ',ent.alias,':',ent.id,'-----')
console.log('topic=>',`${topic}/config`)
// console.log(payload)
mqtt.publish(`${topic}/info`,null) // clear existing
mqtt.publish(`${topic}/config`,payload,{retain:true})
})
}
export { register,init }

171
src/ha.js Normal file
View File

@ -0,0 +1,171 @@
import Hass from '@uci/ha'
import { PUMP, ZONE, RUNNER } from './ha-constants.js'
let initialized = false
async function init(opts) {
console.log('Initializing Home Assistant Integration')
opts = opts || this.config.hass
if (!opts.access_token) throw('no access token to HA supplied, not way to connect')
this.hass = new Hass(opts)
const hass = this.hass
hass.on('error',ev => console.log('!!!! HASS socket error !!!!!\n', ev))
hass.on('connection',async state => {
console.log('HA connection event >', state)
// if (hass.eventBusId) console.log('listener established for HA event bus')
if (state==='ready') {
console.log('HA connection ready, listener established for all of HA event bus')
for (let prop in RUNNER) {
let ent = `${opts.namespace ? opts.namespace+'_':''}${RUNNER[prop]}`
await hass.setVariable(ent, this.runner[prop])
}
await hass.updateEntity(
`input_boolean.${opts.namespace ? opts.namespace+'_':''}enabled`,
this.runner.enabled ? 'on':'off')
const pumpEnt = makeEntityName('pump',PUMP.tmpl,opts.namespace)
hass[PUMP.hass_function].call(hass,pumpEnt,this.pump.state) // reset state
if (!initialized) {
for (let prop in RUNNER) {
let ent = `${opts.namespace ? opts.namespace+'_':''}${RUNNER[prop]}`
this.runner.rxSubscribe(prop,'ha',value => {
// console.log('runner', ent, ev)
hass.setVariable(ent, value)
})
}
hass.on(
`input_boolean.${opts.namespace ? opts.namespace+'_':''}enabled`,
entity => {this.runner.enabled = entity.state==='on'?true:false
})
this.rxSubscribe('pump.state','ha',value => {
// console.log('pump state change sent to HA:',ev.value) //,pumpEnt,PUMP.hass_function)
hass[PUMP.hass_function].call(hass,pumpEnt,value)
})
hass.on(pumpEnt,
entity => {
// console.log('HASS: pump change request',entity.state)
this.pumpGPIO(entity.state)
})
console.log('done runner and pump HA setup')
// prepare the zones communication with HA
}
for (let id in this.zones) {
let _zone = this.zones[id]
// console.log('HA setup for', _zone.id)
await zone.call(this,_zone,'reset',opts.namespace)
// console.log('done resetting zone',_zone.id)
if (!initialized) {
await zone.call(this,_zone,'subscribe',opts.namespace)
await zone.call(this,_zone,'watch',opts.namespace)
}
}
console.log('done zones HA setup')
hass.ready = true // TODO but this in ha code
setTimeout(()=> { // wait for any incoming bogus stuff from ha
console.log('HA Initialization Complete')
initialized = true},1000)
} // end hass ready
else {
hass.eventBusID == null
hass.ready=false
// cleanup
}
}) // end connection listner
console.log('attempting connection to home assistant')
hass.connect()
} // end initHass
async function zone(zone,req,namespace) {
const hass = this.hass
if (typeof zone === 'string') zone = this.getZone(zone)
const sch = this.runner.getSchedule(zone.id)
// console.log('schedule subscriptions',sch.$get(['_rx_.props']))
if (req ==='reset') await this.hass.setSelect(makeEntityName(zone.id,ZONE['delta'].tmpl,namespace), Object.values(this.deltas))
Object.keys(ZONE).forEach(ent =>{
let path;let inst
if (ZONE[ent].schedule) {
inst = sch
path = [ZONE[ent].path,ent]
} else {
inst = this
path = [ZONE[ent].path,'zones',zone.id,ent]
}
// console.log(zone.id, ZONE[ent].schedule,ent,path)
let alterValue = (val) => {
return ZONE[ent].out_value_function ? ZONE[ent].out_value_function(val,this.deltas) : val
}
const entName = makeEntityName(zone.id,ZONE[ent].tmpl,namespace)
const func = this.hass[ZONE[ent].hass_function] || this.hass['setNumber']
// console.log(req,ent)
switch (req) {
case 'reset':
// console.log('to value:',alterValue(get(path)))
func.call(this.hass,entName,alterValue(inst.get(path)))
break
case 'subscribe':
// console.log(ent,entName,path,inst.id)
inst.rxSubscribe(path,'ha',async function (value) {
// console.log('subscribe event',ev)
value = alterValue(value)
// console.log(inst.id, entName, await hass.getEntities(entName))
let hassState = (await hass.getEntities(entName)).state
// console.log(hassState,value,ev.value)
if (typeof(value) === 'number') hassState = Number(hassState)
// console.log('hass value',hassState,'new value',value)
if (hassState !== value ) {
// console.log(ent,'prop',this.zones[zone.id].settings[ent])
// if (ent!=='countdown') console.log(zone.id,': new state sent to HA (old,new):',hassState,':',value ) // entName,value,func)
func.call(hass,entName,value)
}
}) // end subscribe
break
case 'unsubscribe':
break
case 'watch':
hass.on(entName,
function (zone,path,oent,entity) {
let value = oent.in_value_function ? oent.in_value_function(entity.state,this.deltas) : entity.state
// console.log('converted value', value,'access to this',!!this.set)
if (oent.in_function) oent.in_function.call(this,zone,value,path)
if (!oent.in_no_state_change) {
const curValue = inst.get(path)
if (typeof(curValue) === 'number') value = Number(value)
// console.log(curValue,value, initialized)
if (curValue !== value && initialized) {
// console.log(zone.id,ent,entName,'Updating with new value from HA (old,new)',curValue,value)
// console.log(`setting value from HA=> ${zone.id} : ${ent} : ${value}`)
inst.set(path,value)
}
}
}.bind(this,zone,path,ZONE[ent])
)
break
default:
}
}) // next zone entity
} // end zone
export { init, zone }
// create zone HA entity names
function makeEntityName(name,tmpl,namespace) {
const replaceWith = `${namespace ? namespace+'_':''}${name}`
return tmpl.replace('%r%',replaceWith)
}

290
src/irrigation.js Normal file
View File

@ -0,0 +1,290 @@
// 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)]
// )
// )