UNPKG

ecowitt-gateway

Version:

Retrieve data and get and set settings for Ecowitt Gateways

467 lines (390 loc) 16.9 kB
const dgram = require('dgram'); const net = require('net'); const GWUtils = require('./GWUtils'); const { COMMANDS } = require('./Commands'); class EWGateway { constructor(ipAddr, port = 45000, useReadRainData = null, debug = false) { this.ipAddr = ipAddr; this.port = port; this.debug = debug; this.utils = new GWUtils(); this.useReadRainData = false; const gateway = this; const buildPacket = (command, data) => { var size = (data !== null ? data.length : 0) + 3; var body = [command, size].concat(data !== null ? data : []); return new Uint8Array([255, 255].concat(body, [GWUtils.calcChecksum(body)])); }; const checkResponse = (resp, cmd, callback) => { if (resp == null) { callback(resp, 'No Response'); } else if (resp.length < 3) { callback(resp, 'Invalid Response'); } else if (resp[2] != cmd) { callback(resp, 'Invalid Command Code Response'); } else if (resp[resp.length - 1] != GWUtils.calcChecksum(resp.slice(2, resp.length - 1))) { callback(resp, 'Invalid Checksum'); } else { callback(resp, null); } }; this.runCommand = (command, data = null) => { return new Promise((res, rej) => { const client = new net.Socket(); client.setTimeout(2000, () => { client.destroy(); rej('Connection Timeout') }); client.connect(port, ipAddr, () => { if (gateway.debug) { console.debug(`Connected. Executing CMD 0x${command.toString(16)}`); } client.write(buildPacket(command, data)); }); client.on('data', (buffer) => { if (gateway.debug) { console.debug(`Received Data: ${buffer != null ? buffer.length : 0} bytes`); } client.destroy(); // kill client after server's response as to not mix up commands checkResponse(buffer, command, (data, err) => { err ? rej(err) : res(data); }); }); client.on('close', () => { if (gateway.debug) console.debug('Connection Closed'); }); client.on('error', rej); client.on('timeout', () => rej('Connection Timeout')); }); }; if (useReadRainData == null) { this.getFirmwareVersion() .then(firmware => { if (firmware && !firmware.toUpperCase().includes('GW2000')) { this.useReadRainData = true; } }) .catch(e => console.error(`Unable to get Ecowitt Gateway firmware version. Error: ${JSON.stringify(e)}`)); } else{ this.useReadRainData = useReadRainData; } } getSensors(filter = null) { return new Promise((res, rej) => { this.runCommand(COMMANDS.CMD_READ_SENSOR_ID_NEW) .then(buffer => { if (buffer.length > 200) { const sensors = this.utils.parseSensorData(buffer, filter); if (filter === null) { this.sensors = sensors; } res(sensors); } else { rej('Invalid Data Length'); } }) .catch(rej); }); } getLiveData(includeRain = true, filterActiveSensors = false) { return new Promise((res, rej) => { this.runCommand(COMMANDS.CMD_LIVEDATA) .then(buffer => { var data = this.utils.parseLiveData(buffer); const appendRain = (data) => { (this.useReadRainData ? this.getRainData() : this.getRain()) .then(rainData => { Object.assign(data, rainData); }) .catch() .finally(_ => { res(data); }); }; const filterSensors = (data, sensors) => { if (data.lowbatt) { Object.keys(data.lowbatt).forEach(key => { var ukey = key.toUpperCase(); if (typeof data.lowbatt[key] === 'object') { Object.keys(data.lowbatt[key]).forEach(chn => { const uchn = chn.toUpperCase(); const usen = key.toUpperCase(); if (sensors.filter(s => s.type === `${usen}_${uchn}` && s.status === 'active').length < 1) { delete data.lowbatt[key][chn]; } }); if (Object.keys(data.lowbatt[key]).length < 1) { delete data.lowbatt[key]; } } else { if (sensors.filter(s => s.type === ukey && s.status === 'active').length < 1) { delete data.lowbatt[key]; } } }); } if (includeRain) { appendRain(data); } else { res(data); } }; if (filterActiveSensors) { if (this.sensors === undefined) { this.getSensors() .then(sensors => { filterSensors(data, sensors); }); } else { filterSensors(data, this.sensors); } } else if (includeRain) { appendRain(data); } else { res(data); } }) .catch(rej); }); } getRainData() { return new Promise((res, rej) => { this.runCommand(COMMANDS.CMD_READ_RAINDATA) .then(buffer => { try { res(this.utils.parseRainData(buffer)); } catch (error) { rej(error); } }) .catch(rej); }); } setRainData(data) { return new Promise((res, rej) => { this.getRainData() .then(rd => { Object.assign(rd, data); if (typeof rd.rain_rate !== 'number' || rd.rain_rate < 0) { rej('Rain Rate must be a number >= 0.'); return; } if (typeof rd.rain_day !== 'number' || rd.rain_day < 0) { rej('Rain Day must be a number >= 0.'); return; } if (typeof rd.rain_week !== 'number' || rd.rain_week < 0) { rej('Rain Week must be a number >= 0.'); return; } if (typeof rd.rain_month !== 'number' || rd.rain_month < 0) { rej('Rain Month must be a number >= 0.'); return; } if (typeof rd.rain_year !== 'number' || rd.rain_year < 0) { rej('Rain Year must be a number >= 0.'); return; } const packetRain = this.utils.packRainData(rd); this.runCommand(COMMANDS.CMD_WRITE_RAINDATA, packetRain) .then(cr => { res({ status: 'Rain Updated', data: rd }); }) .catch(rej); }) .catch(rej); }); } //New Version with Piezo Data getRain() { return new Promise((res, rej) => { this.runCommand(COMMANDS.CMD_READ_RAIN) .then(buffer => { try { res(this.utils.parseRain(buffer)); } catch (error) { rej(error) } }) .catch(rej); }); } getSoilMoistureCalibration() { return new Promise((res, rej) => { this.runCommand(COMMANDS.CMD_GET_SOILHUMIAD) .then(buffer => { res(this.utils.parseSoilData(buffer)); }) .catch(rej); }); } setSoilMoistureCalibration(data) { return new Promise((res, rej) => { this.getSoilMoistureCalibration() .then(sd => { Object.assign(sd, data); if (typeof sd.channel !== 'number' || sd.channel < 1 || sd.channel > 8) { rej('Channel must be a number from 1 to 8.'); return; } if (typeof sd.calibration_enabled !== 'number' || sd.calibration_enabled < 0 || sd.calibration_enabled > 1) { rej('Customize Calibration Option must be either 1 or enabled or 0 for disabled.'); return; } if (typeof sd.min_ad !== 'number' || sd.min_ad < 0) { rej('min AD must be a number >= 0.'); return; } if (typeof sd.max_ad !== 'number' || sd.max_ad < 0) { rej('min AD must be a number >= 0.'); return; } const packetSoil = this.utils.packSoilData(sd); this.runCommand(COMMANDS.CMD_SET_SOILHUMIAD, packetSoil) .then(ss => { const ssResult = this.utils.parseResult(ss); if (ssResult.result) { res({ status: 'Soil Updated', data: sd }); } else { rej({ status: 'Soil Calibration Not Updated', data: sd }); } }); }) .catch(rej); }); } getPM25Offset() { return new Promise((res, rej) => { this.runCommand(COMMANDS.CMD_GET_PM25_OFFSET) .then(buffer => { res(this.utils.parsePM25Data(buffer)); }) .catch(rej); }); } getCO2Offset() { return new Promise((res, rej) => { this.runCommand(COMMANDS.CMD_GET_CO2_OFFSET) .then(buffer => { res(this.utils.parseCO2OffsetData(buffer)); }) .catch(rej); }); } getFirmwareVersion() { return new Promise((res, rej) => { this.runCommand(COMMANDS.CMD_READ_FIRMWARE) .then(buffer => { res(buffer.slice(5, buffer.length - 1).toString('ascii')); }) .catch(rej); }); } getSystemParams() { return new Promise((res, rej) => { this.runCommand(COMMANDS.CMD_READ_SSSS) .then(buffer => { //todo parse res(buffer.toString('hex')); }) .catch(rej); }); } getMacAddr() { return new Promise((res, rej) => { this.runCommand(COMMANDS.CMD_READ_SATION_MAC) .then(buffer => { res(buffer.toString('hex', 4, buffer.length - 1).toUpperCase()); }) .catch(rej); }); } getCustomServerInfo() { return new Promise((res, rej) => { this.runCommand(COMMANDS.CMD_READ_CUSTOMIZED) .then(buffer => { var info = this.utils.parseCustomServerInfo(buffer); this.runCommand(COMMANDS.CMD_READ_USR_PATH) .then(buffer => { Object.assign(info, this.utils.parseUserPathInfo(buffer)); res(info); }); }) .catch(rej); }); } setCustomServerInfo(data) { return new Promise((res, rej) => { this.getCustomServerInfo() .then(csi => { Object.assign(csi, data); if (csi.interval < 16) { rej('Upload Interval must be >= 16 and < 3600 seconds.'); return; } if (csi.protocol !== 'wunderground' && csi.protocol !== 'ecowitt') { rej('Protocol must be \'wunderground\' or \'ecowitt\'.'); return; } const packetCI = this.utils.packCustomServerInfo(csi); const packetUP = this.utils.packUserPathInfo(csi); this.runCommand(COMMANDS.CMD_WRITE_CUSTOMIZED, packetCI) .then(cr => { const crResult = this.utils.parseResult(cr); if (crResult.result) { this.runCommand(COMMANDS.CMD_WRITE_USR_PATH, packetUP) .then(wspRes => { const wspResult = this.utils.parseResult(wspRes); if (wspResult.result) { res({ status: 'Server Updated', data: csi }); } else { rej({ status: 'Server Not Updated', data: csi }) } }); } else { rej({ status: 'Server Not Updated', data: csi }) } }) .catch(rej); }) .catch(rej); }); } static discover(timeout = 5000) { return new Promise((res, rej) => { const server = dgram.createSocket('udp4'); var ips = []; server.on('message', (msg, rinfo) => { if (!ips.includes(rinfo.address)) { ips.push(rinfo.address); } }); server.bind(59387); setTimeout(() => { server.close(() => { res(ips); }); }, timeout); }); } } module.exports = EWGateway;