From 7f13804b2e35908251f59afd24998b047b13225b Mon Sep 17 00:00:00 2001 From: SuHun Han Date: Sat, 7 Apr 2018 13:34:05 +0900 Subject: [PATCH] Add auto discover feature (#1) --- config.js | 321 +++++++++++++++++++++++++++--------------------------- index.js | 89 +++++++++------ 2 files changed, 215 insertions(+), 195 deletions(-) diff --git a/config.js b/config.js index e6ff1a4..8e85448 100644 --- a/config.js +++ b/config.js @@ -1,160 +1,161 @@ -// TODO: separate all classes - -const sleep = require('sleep-promise'); -const { DateTime } = require('luxon'); - -const Condition = require('./condition'); -const Device = require('./device'); -const config = require(`${process.cwd()}/config.json`); - -class Mode { - - constructor(name, config) { - this.name = name; - this.features = config; - this.interval = config.interval || 1000; - this.devices = []; - this._dependents = undefined; - } - - get dependents() { - if (this._dependents !== undefined) { - return this._dependents; - } - - let depends = Object.keys(this.features) - .reduce((conds, key) => { - const current = this.features[key]; - if (current.conditions === undefined) { - return conds; - } - - const keys = current.conditions.map(Object.keys); - conds = conds.concat(...keys, key); - return conds; - }, []) - .filter(feature => Device.FEATURES.includes(feature)); - depends = Array.from(new Set(depends)); - - this._dependents = depends; - return depends; - } - - addDevices(...devices) { - const { dependents } = this; - devices.forEach(device => { - device.setParentMode(this); - device.setPollingInterval(this.interval); - device.subscribe(...dependents); - }); - this.devices = this.devices.concat(devices); - } - - get actions() { - const actions = Object.keys(this.features) - .reduce((acts, feature) => { - if (this.features[feature].conditions === undefined) { - return acts; - } - - const { action, conditions } = this.features[feature]; - const conds = conditions - .map(cond => ({ action: cond.action, testers: Condition.fromConfig(cond), feature })); - acts.push({ - feature, - action, - conditions: conds, - }); - - return acts; - }, []); - - return actions; - } - - get defaults() { - return Object.keys(this.features).reduce((defaults, current) => { - if (this.dependents.includes(current)) { - defaults[current] = [this.features[current].default] || null; - } - return defaults; - }, {}); - } - - async loop() { - const { actions, defaults, dependents } = this; - const form = dependents - .filter(item => Device.CHANGABLES.includes(item)) - .reduce((previous, current) => ({ ...previous, [current]: null }), {}); - - for (;;) { - // better ideas? - // load all device information from depdents - const time = DateTime.local(); - const devices = await Promise.all(this.devices.map(async (device) => ({ - device, - data: { - ...device.stats, - time, - }, - }))); - - // try to match - for (const info of devices) { - const { device, data } = info; - const changes = { ...form }; - - for (const action of actions) { - const { conditions, feature } = action; - - for (const condition of conditions) { - const tests = condition.testers.map(tester => tester.test(data)); - const yet = tests.some(result => result === null); - if (yet === true) { - delete changes[feature]; - continue; - } - - const passed = tests.every(result => result === true); - if (passed === true) { - changes[feature] = condition; - } - } - } - - // apply changes - const promises = []; - for (const [feature, condition] of Object.entries(changes)) { - const next = condition !== null ? condition.action : null; - - if (next === null && defaults[feature] !== null && data[feature] !== defaults[feature]) { - promises.push(device.update(feature, ...defaults[feature])); - } else { - promises.push(device.update(feature, ...next)); - } - } - - await Promise.all(promises); - } - - await sleep(this.interval); - } - } - -} - - -class Config { - - constructor(config) { - this.devices = config.devices; - this.modes = Object.keys(config.modes).reduce((modes, current) => { - const mode = new Mode(current, config.modes[current]); - modes.push(mode); - return modes; - }, []); - } - -} - -module.exports = Object.freeze(new Config(config)); +// TODO: separate all classes + +const sleep = require('sleep-promise'); +const { DateTime } = require('luxon'); + +const Condition = require('./condition'); +const Device = require('./device'); +const config = require(`${process.cwd()}/config.json`); + +class Mode { + + constructor(name, config) { + this.name = name; + this.features = config; + this.interval = config.interval || 1000; + this.devices = []; + this._dependents = undefined; + } + + get dependents() { + if (this._dependents !== undefined) { + return this._dependents; + } + + let depends = Object.keys(this.features) + .reduce((conds, key) => { + const current = this.features[key]; + if (current.conditions === undefined) { + return conds; + } + + const keys = current.conditions.map(Object.keys); + conds = conds.concat(...keys, key); + return conds; + }, []) + .filter(feature => Device.FEATURES.includes(feature)); + depends = Array.from(new Set(depends)); + + this._dependents = depends; + return depends; + } + + addDevices(...devices) { + const { dependents } = this; + devices.forEach(device => { + device.setParentMode(this); + device.setPollingInterval(this.interval); + device.subscribe(...dependents); + }); + this.devices = this.devices.concat(devices); + } + + get actions() { + const actions = Object.keys(this.features) + .reduce((acts, feature) => { + if (this.features[feature].conditions === undefined) { + return acts; + } + + const { action, conditions } = this.features[feature]; + const conds = conditions + .map(cond => ({ action: cond.action, testers: Condition.fromConfig(cond), feature })); + acts.push({ + feature, + action, + conditions: conds, + }); + + return acts; + }, []); + + return actions; + } + + get defaults() { + return Object.keys(this.features).reduce((defaults, current) => { + if (this.dependents.includes(current)) { + defaults[current] = [this.features[current].default] || null; + } + return defaults; + }, {}); + } + + async loop() { + const { actions, defaults, dependents } = this; + const form = dependents + .filter(item => Device.CHANGABLES.includes(item)) + .reduce((previous, current) => ({ ...previous, [current]: null }), {}); + + for (;;) { + // better ideas? + // load all device information from depdents + const time = DateTime.local(); + const devices = await Promise.all(this.devices.map(async (device) => ({ + device, + data: { + ...device.stats, + time, + }, + }))); + + // try to match + for (const info of devices) { + const { device, data } = info; + const changes = { ...form }; + + for (const action of actions) { + const { conditions, feature } = action; + + for (const condition of conditions) { + const tests = condition.testers.map(tester => tester.test(data)); + const yet = tests.some(result => result === null); + if (yet === true) { + delete changes[feature]; + continue; + } + + const passed = tests.every(result => result === true); + if (passed === true) { + changes[feature] = condition; + } + } + } + + // apply changes + const promises = []; + for (const [feature, condition] of Object.entries(changes)) { + const next = condition !== null ? condition.action : null; + + if (next === null && defaults[feature] !== null && data[feature] !== defaults[feature]) { + promises.push(device.update(feature, ...defaults[feature])); + } else { + promises.push(device.update(feature, ...next)); + } + } + + await Promise.all(promises); + } + + await sleep(this.interval); + } + } + +} + + +class Config { + + constructor(config) { + this.devices = config.devices; + this.discover = config.discover; + this.modes = Object.keys(config.modes).reduce((modes, current) => { + const mode = new Mode(current, config.modes[current]); + modes.push(mode); + return modes; + }, []); + } + +} + +module.exports = Object.freeze(new Config(config)); diff --git a/index.js b/index.js index e9482da..ba21448 100644 --- a/index.js +++ b/index.js @@ -1,35 +1,54 @@ -const Device = require('./device'); -const config = require('./config'); - -const purifiers = []; - -const main = async () => { - const { devices, modes } = config; - - for (const [ name, args ] of Object.entries(devices)) { - purifiers.push(new Device(name, args.ip, args.mode)); - } - await Promise.all(purifiers.map(purifier => purifier.connect())); - - // mode can have multiple devices - modes.forEach(mode => { - const devices = purifiers.filter(p => p.modeName === mode.name); - mode.addDevices(...devices); - }); - - await Promise.all(modes.map(mode => mode.loop())); -}; - -const loop = async () => { - for (;;) { - try { - await main(); - } catch (e) { - console.error('unexpected error:', e); - } - console.log('restarting..'); - } -}; - -loop() - .catch(e => console.error(e)); +const miio = require('miio'); + +const Device = require('./device'); +const config = require('./config'); + +const purifiers = []; + +const main = async () => { + const { discover, devices, modes } = config; + // collect only IP addresses (to be used in auto discovering) + const deviceIPs = Object.values(devices).map(device => device.ip); + + // TODO: separate codes + if (discover.enabled === true && discover.mode !== '') { + const browser = miio.browse(); + browser.on('available', reg => { + const { address, token, model, hostname } = reg; + if (address && token && model.includes('airpurifier') && !deviceIPs.includes(address)) { + const device = new Device(hostname, address, discover.mode); + device.connect() + .then(() => modes.find(mode => mode.name === discover.mode).addDevices(device)) + .then(() => console.info(String(new Date), `Device added: ${address} (mode: ${discover.mode})`)); + } + }); + } + + // TODO: use proxy to observe object changes + for (const [ name, args ] of Object.entries(devices)) { + purifiers.push(new Device(name, args.ip, args.mode)); + } + await Promise.all(purifiers.map(purifier => purifier.connect())); + + // mode can have multiple devices + modes.forEach(mode => { + const devices = purifiers.filter(p => p.modeName === mode.name); + mode.addDevices(...devices); + }); + + await Promise.all(modes.map(mode => mode.loop())); +}; + +const loop = async () => { + for (;;) { + try { + await main(); + } catch (e) { + console.error('unexpected error:', e); + } + console.log('restarting..'); + } +}; + +loop() + .catch(e => console.error(e));