Skip to content

Commit

Permalink
added ZWave things
Browse files Browse the repository at this point in the history
  • Loading branch information
Zefiro committed Apr 12, 2023
1 parent d59f60a commit b24d7f9
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 78 deletions.
2 changes: 1 addition & 1 deletion POS.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const fs = require('fs')
const fsa = fs.promises


module.exports = function(god, loggerName = 'POS') {
module.exports = function(god, loggerName = 'POS') {
var self = {

controller: {},
Expand Down
9 changes: 4 additions & 5 deletions grag.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ const { exec } = require("child_process")
const socketIo = require('socket.io')
const dns = require('dns')
const moment = require('moment')
const jsonc = require('./jsonc')()
const yaml = require('js-yaml')
const util = require('util')
const exec2 = util.promisify(require('child_process').exec);
Expand All @@ -34,9 +33,9 @@ const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch

console.log('Press <ctrl>+C to exit.')

let sConfigFile = 'prod.json'
let sConfigFile = 'prod.yaml'
console.log("Loading config " + sConfigFile)
let config = yaml.load(fs.readFileSync(path.resolve(__dirname, 'config', 'prod.yaml'), 'utf8'))
let config = yaml.load(fs.readFileSync(path.resolve(__dirname, 'config', sConfigFile), 'utf8'))

var isTerminated = false
async function terminate(errlevel) {
Expand Down Expand Up @@ -194,11 +193,11 @@ god.mqtt = mqtt

// initialization race condition, hope for the best... (later code parts could already access mpd1/2 before the async func finishes)
var mpd1
(async () => { mpd1 = await require('./mpd')(god, 'localhost', 'mpd1') })()
(async () => { mpd1 = await require('./mpd')(god, 'localhost', 'mpd1', 'grag-mpd1') })()


var mpd2
(async () => { mpd2 = await require('./mpd')(god, 'grag-hoardpi', 'mpd2') })()
(async () => { mpd2 = await require('./mpd')(god, 'grag-hoardpi', 'mpd2', 'grag-mpd2') })()


const web = require('./web')(god, 'web')
Expand Down
50 changes: 33 additions & 17 deletions medusa.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const { exec } = require("child_process")
const socketIo = require('socket.io')
const dns = require('dns')
const moment = require('moment')
const jsonc = require('./jsonc')()
const yaml = require('js-yaml')
const util = require('util')
const exec2 = util.promisify(require('child_process').exec);

Expand All @@ -48,10 +48,10 @@ const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch

console.log('Press <ctrl>+C to exit.')

let sConfigFile = 'prod.json'
let sConfigFile = 'prod.yaml'
console.log("Loading config " + sConfigFile)
let configBuffer = fs.readFileSync(path.resolve(__dirname, 'config', sConfigFile), 'utf-8')
let config = jsonc.parse(configBuffer)
let config = yaml.load(fs.readFileSync(path.resolve(__dirname, 'config', sConfigFile), 'utf8'))


var isTerminated = false
async function terminate(errlevel) {
Expand Down Expand Up @@ -203,12 +203,19 @@ function addNamedLogger(name, level = 'debug', label = name) {
const logger = winston.loggers.get('main')
logger.info(config.name + ' waking up and ready for service')

const mqtt = require('./mqtt')(config.mqtt, god)
god.mqtt = mqtt

// initialization race condition, hope for the best... (later code parts could already access mpd1/2 before the async func finishes)
// TODO add loggers
const wodoinco = require('./wodoinco')('/dev/ttyWoDoInCo')
const extender = require('./extender')('/dev/ttyExtender')

if (config.mqtt) {
const mqtt = require('./mqtt')(config.mqtt, god)
god.mqtt = mqtt
}

// initialization race condition, hope for the best... (later code parts could already access mpd before the async func finishes)
var mpd
(async () => { mpd = await require('./mpd')(god, 'localhost', 'mpd') })()
(async () => { mpd = await require('./mpd')(god, 'localhost', 'mpd', 'medusa-mpd') })()

const web = require('./web')(god, 'web')
//const gpio = require('./gpio')(god, 'gpio')
Expand All @@ -220,10 +227,8 @@ 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 things = require('./things')(god, 'things')
// TODO add loggers, update constructor signature
const wodoinco = require('./wodoinco')('/dev/ttyWoDoInCo')
const extender = require('./extender')('/dev/ttyExtender')
god.zwave = require('./zwave.js')(god)
god.thingController = require('./things')(god, 'things')



Expand Down Expand Up @@ -496,13 +501,15 @@ async function wodoinco2(item, value) {
console.log("Switching Light off")
} else {
console.log("Unknown command for Light: " + value)
return
}
}
let result = await wodoinco.send(txt);
console.log("Wodoinco2: result='" + result + "'")
}

io.of('/browser').on('connection', async (socket) => {

io.of('/browser').on('connection', async (socket) => {
god.ioSocketList[socket.id] = {
socket: socket,
subscriptions: {}
Expand Down Expand Up @@ -604,10 +611,19 @@ god.ioOnConnected.push(socket => socket.on('things', function(data) {
logger.debug('Pushing full thing-config to client on request')
socket.emit('things', Object.values(god.things).map(thing => thing.fullJson))
}
if (data == 'retrieveThingGroups') {
logger.debug('Pushing all groups to client on request')
socket.emit('thingGroups', god.thingController.getGroupDefinitions())
}
if (data == 'retrieveScenarios') {
logger.debug('Pushing all scenarios to client on request')
socket.emit('scenarios', god.thingController.getScenario())
socket.emit('thingScenario', god.thingController.getCurrentScenario())
}
if (data.id && data.action) {
things.onAction(data.id, data.action)
god.thingController.onAction(data.id, data.action)
}
}))
}))
god.onThingChanged.push(thing => god.whiteboard.getCallbacks('things').forEach(cb => cb(thing.json)))

const ignore = () => {}
Expand Down Expand Up @@ -646,8 +662,8 @@ web.addListener("cave", "Pum", async (req, res) => { openhab('pum', 'TOG
wodoinco.addListener("A Tast A", async (txt) => { console.log("WoDoInCo: Light toggled: " + txt) })
wodoinco.addListener("A Tast B", async (txt) => { extender2('Speaker', 'on'); console.log((await mpd.fadePlay(2)) + " (" + (await mpMpdVol90()) + ")" ) })
wodoinco.addListener("A Tast C", async (txt) => { extender2('Speaker', 'timed-off'); console.log(await mpd.fadePause(45)) })
wodoinco.addListener("A Tast Do", async (txt) => { console.log(await changeVolume(+2)) })
wodoinco.addListener("A Tast Du", async (txt) => { console.log(await changeVolume(-2)) })
wodoinco.addListener("A Tast Do", async (txt) => { console.log(await mpd.changeVolume(+2)) })
wodoinco.addListener("A Tast Du", async (txt) => { console.log(await mpd.changeVolume(-2)) })

wodoinco.addListener("A PC Light to 0", ignore )
wodoinco.addListener("A PC Light to 1", ignore )
Expand Down
4 changes: 2 additions & 2 deletions mpd.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ var Q = require('q')
const util = require('util')
const moment = require('moment')

module.exports = async function(god, mpdHost = 'localhost', id = 'mpd') {
module.exports = async function(god, mpdHost = 'localhost', id = 'mpd', _mqttTopic = undefined) {
var self = {

mappingFilename: 'mpd-youtube-cache.json',
Expand All @@ -25,7 +25,7 @@ const moment = require('moment')
mpdstatus: {},
faderTimerId: undefined,
logger: {},
mqttTopic: 'grag-' + id,
mqttTopic: _mqttTopic ?? id,
watchdog: {
counter: 0,
maxReconnectTries: 1, // warning: this is a sync-recursive call
Expand Down
3 changes: 2 additions & 1 deletion public/grag3.html
Original file line number Diff line number Diff line change
Expand Up @@ -201,13 +201,14 @@
console.log('AUTO', thing.id, thing.def.render.autohide, currentScenario?.hide?.indexOf(thing.id), thing.status)
if (thing.def.render.autohide) return true
if (currentScenario?.hide?.indexOf(thing.id) > -1) return true
if (thing.status == 'dead' && thing.def.render.hiddenIfDead) return true
return false
}

// returns true if a thing is hideable, alive (except hiddenIfDead), and the value is as the scenario expects it
function calcIfAutoHidden(thing) {
if (thing.status == 'dead' && thing.def.render.hiddenIfDead) return true
if (!calcIfHideable(thing)) return false
if (thing.status == 'dead') return thing.def.render.hiddenIfDead
return calcScenarioExpectation(thing).asExpected
}

Expand Down
1 change: 1 addition & 0 deletions scenario.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const winston = require('winston')
init: async function() {
this.logger = winston.loggers.get(loggerName)
god.mqtt.addTrigger('cmnd/' + this.mqttTopic, 'cmnd-scenario', this.onMqttCmnd.bind(this))
if (!god.config.scenarios) god.config.scenarios = { "": {} }
Object.keys(god.config.scenarios).forEach(key => this.initTriggers(key, god.config.scenarios[key]))
},

Expand Down
102 changes: 98 additions & 4 deletions things.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ class Thing {
return this.def.id
}

/** This function is called when a thing-specific action should be triggered, e.g. "switch light on". For most things this sends the appropriate MQTT commands */
onAction(data) {
this.logger.warn('Abstract base class for ' + this.id + ': action not supported')
}

/** internally used to change this.status, propagates the new value to listeners (can be skipped if done manually anyway) */
setstatus(newStatus, propagateChange = true) {
if (this.status != newStatus) {
if (this.status == ThingStatus.dead) this.logger.info(this.def.id + ' is alive again')
Expand Down Expand Up @@ -120,6 +122,7 @@ class Thing {
}
}

/** Called from checkAlive() when a thing is considered stale/dead. Should try to provoke the thing to answer something. */
poke(now) {
this.logger.warn('Abstract base class for ' + this.id + ': poking not supported')
this.lastpoked = now
Expand All @@ -131,8 +134,8 @@ class MusicPlayer extends Thing {
constructor(id, def) {
super(id, def)
this.lastState = {}
this.onMqttStateUpdate = this.onMqttStateUpdate.bind(this)
god.mqtt.addTrigger('tele/' + def.device + '/STATE', def.id, this.onMqttStateUpdate)
this.onMpdMqttStateUpdate = this.onMpdMqttStateUpdate.bind(this)
god.mqtt.addTrigger('tele/' + def.device + '/STATE', def.id, this.onMpdMqttStateUpdate)

}

Expand All @@ -148,7 +151,7 @@ class MusicPlayer extends Thing {
}

// Callback for MQTT messages for the MPD subsystem
async onMqttStateUpdate(trigger, topic, message, packet) {
async onMpdMqttStateUpdate(trigger, topic, message, packet) {
let newState = message.toString()
try {
let json = JSON.parse(newState)
Expand Down Expand Up @@ -412,6 +415,7 @@ class Onkyo extends Thing {

}

/** Represents a simple, stateless button on the UI which triggers a specific MQTT message */
class Button extends Thing {
constructor(id, def) {
super(id, def)
Expand All @@ -435,8 +439,96 @@ class Button extends Thing {
god.mqtt.publish(topic, message)
}

/** Buttons can't be poked */
poke(now) {
this.lastpoked = now
}
}

class ZWave extends Thing {
constructor(id, def) {
super(id, def)
this.status = ThingStatus.ignored
god.zwave.addChangeListener(this.onZWaveUpdate.bind(this))
}

get json() {
return {
id: this.def.id,
lastUpdated: this.lastUpdated,
status: this.status.name,
value: god.zwave.getNodeValue(this.def.nodeId, '37/' + (this.def.nodeSubId ?? 0) + '/currentValue/value') ? 'ON' : 'OFF'
}
}

/** called from zwave.js when an MQTT update is received */
onZWaveUpdate(nodeId, nodeData, relativeTopic, value) {
if (nodeId != this.def.nodeId) return
let propagateChange = false
if (relativeTopic == '37/' + (this.def.nodeSubId ?? 0) + '/currentValue') propagateChange = true
if (relativeTopic == 'status/status') {
let newStatus = ThingStatus.dead
if (nodeData?.status?.status == 'Alive') newStatus = ThingStatus.alive
if (this.status != newStatus) {
this.setstatus(newStatus, false)
propagateChange = true
}
}
this.logger.warn('ZWave update on node %s: %s = %s (propagate=%s)', nodeId, relativeTopic, value, propagateChange)
this.lastUpdated = new Date() // update timestamp even if the value is unchanged
if (propagateChange) {
god.onThingChanged.forEach(cb => cb(this))
}
}

onAction(action) {
let topic = 'zwave/' + this.def.nodeId + '/37/' + (this.def.nodeSubId ?? 0) + '/targetValue/set'
let message = action == 'ON' ? "true" : "false"
this.logger.info('Action for %s (%o): send "%s" "%s"', this.def.id, action, topic, message)
god.mqtt.publish(topic, message)
}

// TODO alive check not working: neither stale nor lastUpdate updating nor check for 'Dead'
checkAlive(now) {
switch (this.status) {
case ThingStatus.ignored:
// no updating, no poking
break;
case ThingStatus.alive:
if (now - this.lastUpdated > Thing.consideredStaleMs) {
this.setstatus(ThingStatus.stale)
this.logger.info('Status for ' + this.def.id + ' has gone stale, poking it')
this.poke(now)
} else {
let nodeData = god.zwave.getNode(this.def.nodeId)
if (nodeData?.status?.status != 'Alive') this.setstatus(ThingStatus.dead)
}
break;
case ThingStatus.uninitialized:
case ThingStatus.stale:
if (now - this.lastUpdated > Thing.consideredDeadMs) {
this.setstatus(ThingStatus.dead)
this.logger.info(this.def.id + ' appears to be dead :(')
this.poke(now)
}
if (now - this.lastpoked > Thing.pokeIntervalMs) {
this.poke(now)
}
break;
case ThingStatus.dead:
if (now - this.lastpoked > Thing.pokeIntervalMs) {
this.poke(now)
}
break;
default:
this.logger.error('ThingStatus for ' + this.id + ' is invalid: ' + this.status)
this.setstatus(ThingStatus.ignored)
break;
}
}

// does nothing - don't know how to poke ZWave things, or zwave-js
poke(now) {
// this.lastpoked = now
}
}

Expand Down Expand Up @@ -563,6 +655,8 @@ module.exports = function(god2, loggerName = 'things') {
god.things[def.id] = new Button(def.id, def)
} else if (def.api == 'onkyo') {
god.things[def.id] = new Onkyo(def.id, def)
} else if (def.api == 'zwave') {
god.things[def.id] = new ZWave(def.id, def)
} else {
this.logger.error('Thing %s has undefined api "%s"', def.id, def.api)
}
Expand Down
Loading

0 comments on commit b24d7f9

Please sign in to comment.