From 5edc8309db767c77c8bcb1ab6e071a16d4354a95 Mon Sep 17 00:00:00 2001 From: Zefiro Date: Wed, 29 Nov 2023 01:04:19 +0100 Subject: [PATCH] Updates for Medusa --- medusa.js | 3 +- things.js | 16 ++++--- timer.js | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ zwave.js | 2 +- 4 files changed, 137 insertions(+), 9 deletions(-) create mode 100755 timer.js diff --git a/medusa.js b/medusa.js index a1281a7..faece78 100755 --- a/medusa.js +++ b/medusa.js @@ -234,8 +234,9 @@ const web = require('./web')(god, 'web') const network = require('./network')(god, 'net') const scenario = require('./scenario')(god, 'scenario') //const screenkeys = require('./screenkeys')(god, 'keys') -const extender = require('./extender')(god, 'extender') +const extender = god.extender = require('./extender')(god, 'extender') god.zwave = require('./zwave.js')(god) +god.timerController = require('./timer.js')(god, 'timer', 'medusa-timer') god.thingController = require('./things')(god, 'things') diff --git a/things.js b/things.js index 9e5de44..80e5203 100755 --- a/things.js +++ b/things.js @@ -456,12 +456,14 @@ class Button extends Thing { getValue() { return '' } onAction(action) { - let mqttString = this.def.mqtt - let index = mqttString.indexOf(' ') - let topic = mqttString.substr(0, index) - let message = mqttString.substr(index + 1) - this.logger.debug('Action for %s (%o): send "%s" "%s"', this.def.id, action, topic, message) - god.mqtt.publish(topic, message) + let mqttList = isArray(this.def.mqtt) ? this.def.mqtt : [ this.def.mqtt ] + for(let mqttString of mqttList) { + let index = mqttString.indexOf(' ') + let topic = mqttString.substr(0, index) + let message = mqttString.substr(index + 1) + this.logger.debug('Action for Button %s (%o): send "%s" "%s"', this.def.id, action, topic, message) + god.mqtt.publish(topic, message) + } } /** Buttons can't be poked */ @@ -825,7 +827,7 @@ module.exports = function(god2, loggerName = 'things') { /** Gets called from clients (websocket), expects the thing id and action with thing-specific commands */ onAction: function(id, action) { let thing = god.things[id] - this.logger.debug('action for %s (%s): %o', id, thing.def.name, action) + this.logger.debug('onAction for %s (%s): %o', id, thing.def.name, action) if (thing) thing.onAction(action) }, diff --git a/timer.js b/timer.js new file mode 100755 index 0000000..2796b6f --- /dev/null +++ b/timer.js @@ -0,0 +1,125 @@ +/* Starts actions at specific absolute or relative times + */ + +const winston = require('winston') + +// https://stackoverflow.com/a/16608045/131146 +var isObject = function(a) { + return (!!a) && (a.constructor === Object); +} +var isArray = function(a) { + return (!!a) && (a.constructor === Array); +} + +var god, logger + +class Timer { + id = undefined + isActive = true + firesAt = undefined + timeoutId = undefined + + constructor(config) { + this.logger = logger + this.id = config.id ?? 'Timer #' + this.autoIncrementId++ + this.config = config + if (config.relTime) { + // TODO see https://stackoverflow.com/a/1214753/131146 + // regex simple statements of '5m' or '3s' or 5min 3ssec' + let match = config.relTime.match(/\s*\+?((\d+)\s*m(in)?)?\s*((\d+)\s*s(ec)?)?/) + if (match) { + let min = match[2] ?? 0 + let sec = match[5] ?? 0 + this.firesAt = new Date(Date.now() + ((min * 60 + sec) * 1000)) + this.logger.debug('Set timeout for %s to %d min %d sec', this.id, min, sec) + } else { + this.logger.error('reltime unrecognized: %s ', config.relTime) + } + } + if (!this.firesAt) { + this.logger.error('No firing time defined for timer ', id) + return + } + let fireIn = this.firesAt - Date.now() + this.timeoutId = setTimeout(this.fire.bind(this), fireIn) + } + + unregister(reason) { + if (this.isActive || this.timeoutId) { + this.isActive = false + clearTimeout(this.timeoutId) + this.timeoutId = undefined + this.logger.debug('Unregistered "%s"', this.id) + } else { + this.logger.debug('Unregister called on "%s", but not active', this.id) + } + } + + async fire() { + this.isActive = false + this.timeoutId = undefined + this.logger.info('Timer "%s" fired, performing action', this.id) + let actions = isArray(this.config.action) ? this.config.action : [ this.config ] + for(let action of actions) { + if (action.action == 'mqtt') { + let index = action.mqtt.indexOf(' ') + let topic = action.mqtt.substr(0, index) + let message = action.mqtt.substr(index + 1) + this.logger.debug('Timer "%s": sending mqtt: "%s" "%s"', this.id, topic, message) + god.mqtt.publish(topic, message) + } else if (action.action == 'extender') { + let result = await god.extender.send(action.cmnd); + this.logger.debug('Timer "%s": sending extender: "%s" -> "%s"', this.id, action.cmnd, result) + } else { + this.logger.error('Timer "%s": action unrecognized: %s', this.id, action.action) + } + } + } +} + +class RepeatingTimer extends Timer { + // TODO WIP +} + +class TimerController { + timers = {} + + constructor(mqttTopic) { + this.logger = logger + this.mqttTopic = mqttTopic + god.mqtt.addTrigger('cmnd/' + this.mqttTopic + '/#', 'timer', this.onMqttMessage.bind(this)) + } + + async onMqttMessage(trigger, topic, message, packet) { + let data = message.toString() + this.logger.debug('Received mqtt %s: %s', topic, data) + try { + data = JSON.parse(data) + } catch(e) { + this.logger.error('MQTT: Failed to parse JSON: ' + data) + return + } + if (topic == 'cmnd/' + this.mqttTopic + '/set') { + if (this.timers[data.id]) this.timers[id].unregister() + this.timers[id] = new Timer(data) + } + } + +} + +module.exports = function(god2, loggerName = 'timer', _mqttTopic = undefined) { + var self = { + + mqttTopic: _mqttTopic ?? loggerName, + timerController: undefined, + + init: function() { + god = god2 + this.logger = logger = winston.loggers.get(loggerName) + timerController = new TimerController(_mqttTopic) + }, +} + + self.init() + return self +} diff --git a/zwave.js b/zwave.js index 61382e4..fa6cfd6 100755 --- a/zwave.js +++ b/zwave.js @@ -1,4 +1,4 @@ -/* Connects to ZWave2MQTT on topic zwave/# +/* Connects to ZWave2MQTT on topic 'zwave/#' */ const winston = require('winston')