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 schedulermaster
parent
477c7d8611
commit
5576ddea54
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
/node_modules/
|
||||||
|
/.yalc/
|
||||||
|
/irrigation.yml
|
|
@ -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.
|
|
@ -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 "
|
|
@ -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 "
|
|
@ -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 "
|
|
@ -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 "
|
|
@ -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 "
|
|
@ -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
|
|
@ -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')
|
||||||
|
})
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"ignoreRoot": [".git"],
|
||||||
|
"watch": ["node_modules/@uci/","node_modules/@uci-utils/","./*"],
|
||||||
|
"ignore":["irrigation-settings.json"],
|
||||||
|
"ext":"js,json,yaml,yml"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }
|
|
@ -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}
|
|
@ -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 }
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)]
|
||||||
|
// )
|
||||||
|
// )
|
Loading…
Reference in New Issue