/*
* This file is part of the Companion project
* Copyright (c) 2018 Bitfocus AS
* Authors: William Viker <william@bitfocus.io>, Håkon Nessjøen <haakon@bitfocus.io>
*
* This program is free software.
* You should have received a copy of the MIT licence as well as the Bitfocus
* Individual Contributor License Agreement for companion along with
* this program.
*
* You can be released from the requirements of the license by purchasing
* a commercial license. Buying such a license is mandatory as soon as you
* develop commercial activities involving the Companion software without
* disclosing the source code of your own applications.
*
*/
const debug = require('debug')('lib/Schedule/Controller');
const CoreBase = require('../Core/Base');
const fs = require('fs');
class ScheduleController extends CoreBase {
/**
* @param {Registry} registry
*/
constructor(registry) {
super(registry, 'scheduler');
this.btnReleaseTime = 20;
this.config = [];
this.plugins = [];
this.io = null;
this.loadPlugins();
this.initConfig();
this.system.on('io_connect', client => {
client.on('schedule_get', this.getSchedule.bind(this, client));
client.on('schedule_save_item', this.saveSchedule.bind(this, client));
client.on('schedule_update_item', this.updateSchedule.bind(this, client));
client.on('schedule_plugins', this.sendPlugins.bind(this, client));
});
}
/**
* Call an action
* This method is called from within a plugin and sends the id, the scheduler determines what should then happen
* @param {number} id
*/
action(id) {
const event = this.config.find(x => x.id === id);
if (!event) {
this.log('error', 'Could not find configuration for action.')
return;
}
const [bank, button] = this.getBankButton(event.button);
this.log('info', `Push button ${bank}.${button} via <code>${event.title}</code>`);
this.system.emit('bank_pressed', bank, button, true, 'scheduler');
setTimeout(() => {
this.system.emit('bank_pressed', bank, button, false, 'scheduler');
this.log('info', `Release button ${bank}.${button} via <code>${event.title}</code>`);
}, this.btnReleaseTime);
// Update the last run
event.lastRun = new Date();
this.saveToDb();
if (this.io) {
this.io().emit('schedule_refresh', this.config);
}
}
/**
* Cleans the configuration to store in the database
* @param {Object} config
* @return {Object}
*/
cleanConfig(config) {
const cleanConfig = Object.assign({
title: '',
type: null,
config: {},
button: '1.1',
lastRun: null,
disabled: false
}, config);
if (Array.isArray(config.config) && config.config.length === 1) {
cleanConfig.config = config.config[0];
}
if (!('id' in cleanConfig) || cleanConfig.id === null) {
cleanConfig.id = this.getNextId();
}
cleanConfig.button = this.getBankButton(cleanConfig.button).join('.');
return cleanConfig;
}
/**
* Loads plugin based parameters
* These parameters are dynamic and aren't stored to the database, but are
* needed for the front end. For example, the configuration description.
* @param {Object} event
* @return {Object} Event object with plugin passed params
*/
eventLoadType(event) {
const plugin = this.getPlugin(event.type);
if (!plugin) {
this.log('error', `Could not load plugin type ${event.type}.`);
event.configDesc = 'Unknown schedule type.';
}
else {
event.configDesc = plugin.configDesc(event.config);
}
return event;
}
/**
* Register or unregister an event from being watched
* @param {Object} config
* @param {boolean} add Add or remove the event from the watch schedule
*/
eventWatch(config, add = true) {
let plugin = this.getPlugin(config.type);
if (!plugin) {
return;
}
if (!add) {
this.log('info', 'Removing scheduled event.');
plugin.remove(config.id);
}
else if (config.disabled === false) {
this.log('info', 'Adding scheduled event.');
plugin.add(config.id, config);
}
}
/**
* Get event index from an event ID
* @param {number} id
* @return {number}
*/
findEventIndex(id) {
return this.config.findIndex(x => x.id === id);
}
/**
* Get the bank and button number from a string
* @param {string} button
* @return {number[]}
* @access protected
*/
getBankButton(button) {
const bank = parseInt(button);
const buttonNumber = parseInt(button.toString().replace(/(.*)\./, ''));
return [bank, buttonNumber];
}
/**
* Get the next unique event ID
* @return {number}
* @access protected
*/
getNextId() {
if (this.config.length === 0) {
return 1;
}
const curMaxId = Math.max.apply(Math, this.config.map(i => i.id));
if (curMaxId <= 0 || isNaN(curMaxId)) {
debug('current max id is invalid; this may be a bug or a corruption that may require a reset of the scheduler config');
this.log('warn', 'Configuration appears to be corrupt.');
return 1;
}
else {
return curMaxId + 1;
}
}
/**
* Gets the plugin from a request type
* @param {string} type
* @return {?Object}
*/
getPlugin(type) {
let plugin = this.plugins.find(p => p.type === type);
if (!plugin) {
this.log('error', 'Plugin not loaded.');
return null;
}
return plugin;
}
/**
* Sends the event list to the callback
* @param {SocketIO} socket
* @param {scheduleGetCb} cb
*/
getSchedule(socket, cb) {
/**
* @callback scheduleGetCb
* @param {Object[]}
*/
cb(this.config.map(i => this.eventLoadType(i)));
}
/**
* Initialize the configuration and start any schedules
*/
initConfig() {
this.config = this.db().getKey('scheduler', []);
this.startSchedule();
}
/**
* Loads plugins from the schedule plugin directory
*/
loadPlugins() {
const path = this.registry.getAppRoot() + '/lib/Schedule/Plugin';
const pluginsFolder = fs.readdirSync(path);
pluginsFolder.forEach(p => {
if (p === 'base.js' || p.match(/\.js$/) === null) {
return;
}
try {
const plugin = require(path + '/' + p);
this.plugins.push(new plugin(this));
}
catch (e) {
debug(e);
this.log('error', `Error loading plugin ${p}`);
}
});
}
/**
* Replaced an event configuration
* @param {SocketIO} socket
* @param {Object} newData
* @param {scheduleSaveCb} cb
*/
saveSchedule(socket, newData, cb) {
const cleanData = this.cleanConfig(newData);
let idx = this.findEventIndex(cleanData.id);
if (idx === -1) {
this.config.push(cleanData);
}
else {
// Keep the last run and disabled status from the old config
cleanData.lastRun = this.config[idx].lastRun;
cleanData.disabled = this.config[idx].disabled;
this.eventWatch(this.config[idx], false);
this.config[idx] = cleanData;
}
this.saveToDb()
this.eventWatch(cleanData);
/**
* @callback scheduleSaveCb
* @param {Object} event Updated event
*/
cb(this.eventLoadType(cleanData));
socket.broadcast.emit('schedule_refresh', this.config);
}
/**
* Saves to database
*/
saveToDb() {
this.db().setKey('scheduler', this.config);
//this.db().setDirty();
}
/**
*
* @param {SocketIO} socket
* @param {pluginCallback} cb
*/
sendPlugins(socket, cb) {
/**
* @callback pluginCallback
* @param {Object[]}
*/
cb(this.plugins.map(p => p.frontEnd()));
}
/**
* Starts all event schedules
*/
startSchedule() {
this.config.forEach(i => this.eventWatch(i));
}
/**
* Updates a schedule
* Minor updates and deletions
* @param {SocketIO} socket
* @param {number} id Event ID
* @param {Object} newData If deleted property is set, event is deleted
* @param {scheduleUpdateCb} cb
*/
updateSchedule(socket, id, newData, cb) {
const idx = this.findEventIndex(id);
if (idx === -1) {
this.log('error', 'Scheduled event could not be found.');
return;
}
// Stop watching old event
this.eventWatch(this.config[idx], false);
/**
* @callback scheduleUpdateCb
* @param {?Object} event Will return null if event was deleted
*/
if ('deleted' in newData) {
this.config.splice(idx, 1);
cb(null);
}
else {
this.config[idx] = {...this.config[idx], ...newData};
cb(this.eventLoadType(this.config[idx]));
this.eventWatch(this.config[idx]);
}
this.saveToDb();
socket.broadcast.emit('schedule_refresh', this.config);
}
}
exports = module.exports = ScheduleController;