UNPKG

homebridge-mhi-wfrac

Version:

A homebridge plugin for Mitshubishi WF-RAC aircos (using the Smart M-Air app)

456 lines 18 kB
import { Buffer } from 'buffer'; import axios from 'axios'; export class DeviceStatus { // eslint-disable-next-line max-len static outdoorTempList = [-50.0, -50.0, -50.0, -50.0, -50.0, -48.9, -46.0, -44.0, -42.0, -41.0, -39.0, -38.0, -37.0, -36.0, -35.0, -34.0, -33.0, -32.0, -31.0, -30.0, -29.0, -28.5, -28.0, -27.0, -26.0, -25.5, -25.0, -24.0, -23.5, -23.0, -22.5, -22.0, -21.5, -21.0, -20.5, -20.0, -19.5, -19.0, -18.5, -18.0, -17.5, -17.0, -16.5, -16.0, -15.5, -15.0, -14.6, -14.3, -14.0, -13.5, -13.0, -12.6, -12.3, -12.0, -11.5, -11.0, -10.6, -10.3, -10.0, -9.6, -9.3, -9.0, -8.6, -8.3, -8.0, -7.6, -7.3, -7.0, -6.6, -6.3, -6.0, -5.6, -5.3, -5.0, -4.6, -4.3, -4.0, -3.7, -3.5, -3.2, -3.0, -2.6, -2.3, -2.0, -1.7, -1.5, -1.2, -1.0, -0.6, -0.3, 0.0, 0.2, 0.5, 0.7, 1.0, 1.3, 1.6, 2.0, 2.2, 2.5, 2.7, 3.0, 3.2, 3.5, 3.7, 4.0, 4.2, 4.5, 4.7, 5.0, 5.2, 5.5, 5.7, 6.0, 6.2, 6.5, 6.7, 7.0, 7.2, 7.5, 7.7, 8.0, 8.2, 8.5, 8.7, 9.0, 9.2, 9.5, 9.7, 10.0, 10.2, 10.5, 10.7, 11.0, 11.2, 11.5, 11.7, 12.0, 12.2, 12.5, 12.7, 13.0, 13.2, 13.5, 13.7, 14.0, 14.2, 14.4, 14.6, 14.8, 15.0, 15.2, 15.5, 15.7, 16.0, 16.2, 16.5, 16.7, 17.0, 17.2, 17.5, 17.7, 18.0, 18.2, 18.5, 18.7, 19.0, 19.2, 19.4, 19.6, 19.8, 20.0, 20.2, 20.5, 20.7, 21.0, 21.2, 21.5, 21.7, 22.0, 22.2, 22.5, 22.7, 23.0, 23.2, 23.5, 23.7, 24.0, 24.2, 24.5, 24.7, 25.0, 25.2, 25.5, 25.7, 26.0, 26.2, 26.5, 26.7, 27.0, 27.2, 27.5, 27.7, 28.0, 28.2, 28.5, 28.7, 29.0, 29.2, 29.5, 29.7, 30.0, 30.2, 30.5, 30.7, 31.0, 31.3, 31.6, 32.0, 32.2, 32.5, 32.7, 33.0, 33.2, 33.5, 33.7, 34.0, 34.3, 34.6, 35.0, 35.2, 35.5, 35.7, 36.0, 36.3, 36.6, 37.0, 37.2, 37.5, 37.7, 38.0, 38.3, 38.6, 39.0, 39.3, 39.6, 40.0, 40.3, 40.6, 41.0, 41.3, 41.6, 42.0, 42.3, 42.6, 43.0]; // eslint-disable-next-line max-len static indoorTempList = [-30.0, -30.0, -30.0, -30.0, -30.0, -30.0, -30.0, -30.0, -30.0, -30.0, -30.0, -30.0, -30.0, -30.0, -30.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0, -24.0, -23.0, -22.5, -22.0, -21.0, -20.0, -19.5, -19.0, -18.0, -17.5, -17.0, -16.5, -16.0, -15.0, -14.5, -14.0, -13.5, -13.0, -12.5, -12.0, -11.5, -11.0, -10.5, -10.0, -9.5, -9.0, -8.6, -8.3, -8.0, -7.5, -7.0, -6.5, -6.0, -5.6, -5.3, -5.0, -4.5, -4.0, -3.6, -3.3, -3.0, -2.6, -2.3, -2.0, -1.6, -1.3, -1.0, -0.5, 0.0, 0.3, 0.6, 1.0, 1.3, 1.6, 2.0, 2.3, 2.6, 3.0, 3.2, 3.5, 3.7, 4.0, 4.3, 4.6, 5.0, 5.3, 5.6, 6.0, 6.3, 6.6, 7.0, 7.2, 7.5, 7.7, 8.0, 8.3, 8.6, 9.0, 9.2, 9.5, 9.7, 10.0, 10.3, 10.6, 11.0, 11.2, 11.5, 11.7, 12.0, 12.3, 12.6, 13.0, 13.2, 13.5, 13.7, 14.0, 14.2, 14.5, 14.7, 15.0, 15.3, 15.6, 16.0, 16.2, 16.5, 16.7, 17.0, 17.2, 17.5, 17.7, 18.0, 18.2, 18.5, 18.7, 19.0, 19.2, 19.5, 19.7, 20.0, 20.2, 20.5, 20.7, 21.0, 21.2, 21.5, 21.7, 22.0, 22.2, 22.5, 22.7, 23.0, 23.2, 23.5, 23.7, 24.0, 24.2, 24.5, 24.7, 25.0, 25.2, 25.5, 25.7, 26.0, 26.2, 26.5, 26.7, 27.0, 27.2, 27.5, 27.7, 28.0, 28.2, 28.5, 28.7, 29.0, 29.2, 29.5, 29.7, 30.0, 30.2, 30.5, 30.7, 31.0, 31.3, 31.6, 32.0, 32.2, 32.5, 32.7, 33.0, 33.2, 33.5, 33.7, 34.0, 34.2, 34.5, 34.7, 35.0, 35.3, 35.6, 36.0, 36.2, 36.5, 36.7, 37.0, 37.2, 37.5, 37.7, 38.0, 38.3, 38.6, 39.0, 39.2, 39.5, 39.7, 40.0, 40.3, 40.6, 41.0, 41.2, 41.5, 41.7, 42.0, 42.3, 42.6, 43.0, 43.2, 43.5, 43.7, 44.0, 44.3, 44.6, 45.0, 45.3, 45.6, 46.0, 46.2, 46.5, 46.7, 47.0, 47.3, 47.6, 48.0, 48.3, 48.6, 49.0, 49.3, 49.6, 50.0, 50.3, 50.6, 51.0, 51.3, 51.6, 52.0]; static OPERATION_MODES; static AIRFLOW; static HORIZONTAL_POSITIONS; static VERTICAL_POSITIONS; airFlow; coolHotJudge; electric; entrust; errorCode; indoorTemp; isAutoHeating; isSelfCleanOperation; isSelfCleanReset; isVacantProperty; modelNo; operation; operationMode; outdoorTemp; presetTemp; windDirectionLR; windDirectionUD; constructor() { this.airFlow = -1; this.coolHotJudge = false; this.electric = 0; this.entrust = false; this.errorCode = ''; this.indoorTemp = null; this.isAutoHeating = false; this.isSelfCleanOperation = false; this.isSelfCleanReset = false; this.isVacantProperty = 0; this.modelNo = 0; this.operation = false; this.operationMode = null; this.outdoorTemp = null; this.presetTemp = null; this.windDirectionLR = -1; this.windDirectionUD = -1; } static fromBase64(base64) { const deviceStatus = new DeviceStatus(); const statByte = Buffer.from(base64.replace('\n', ''), 'base64').toString('binary'); const statByteArray = []; for (let i = 0; i < statByte.length; i++) { const h = statByte.charCodeAt(i); if (h > 127) { statByteArray.push(-1 * (256 - h)); } else { statByteArray.push(h); } } const r3 = 18; const dataStart = statByteArray[r3] * 4 + 21; const dataLength = statByteArray.length - 2; const data = statByteArray.slice(dataStart, dataLength); const code = data[6] & 127; const zeroPad = (num, places) => String(num).padStart(places, '0'); const wosoFindMatch = (value, posVals, p = 0) => { const ret = -1; for (let i = 0; i < posVals.length; i++) { if (posVals[i] === value) { return i + p; } } return ret; }; deviceStatus.operation = (3 & data[2]) === 1; deviceStatus.presetTemp = data[4] / 2; deviceStatus.operationMode = wosoFindMatch(60 & data[2], [8, 16, 12, 4], 1); deviceStatus.airFlow = wosoFindMatch(15 & data[3], [7, 0, 1, 2, 6]); deviceStatus.windDirectionUD = (data[2] & 192) === 64 ? 0 : wosoFindMatch(240 & data[3], [0, 16, 32, 48], 1); deviceStatus.windDirectionLR = (data[12] & 3) === 1 ? 0 : wosoFindMatch(31 & data[11], [0, 1, 2, 3, 4, 5, 6], 1); deviceStatus.entrust = (12 & data[12]) === 4; deviceStatus.coolHotJudge = (data[8] & 8) <= 0; deviceStatus.modelNo = wosoFindMatch(data[0] & 127, [0, 1, 2]); deviceStatus.isVacantProperty = data[10] & 1; if (code === 0) { deviceStatus.errorCode = '00'; } else if ((data[6] & -128) <= 0) { deviceStatus.errorCode = `M${zeroPad(code, 2)}`; } else { deviceStatus.errorCode = `E${code}`; } let c = 0; const vals = []; for (let i = dataStart + 19; i < statByteArray.length - 2; i++) { vals[c] = statByteArray[i]; c++; } deviceStatus.electric = 0; for (let i = 0; i < vals.length; i += 4) { if ((vals[i] === -128) && (vals[i + 1] === 16)) { deviceStatus.outdoorTemp = this.outdoorTempList[vals[i + 2] & 0xFF]; } if ((vals[i] === -128) && (vals[i + 1] === 32)) { deviceStatus.indoorTemp = this.indoorTempList[vals[i + 2] & 0xFF]; } if ((vals[i] === -108) && (vals[i + 1] === 16)) { const bytes = new Uint8Array([vals[i + 2], vals[i + 3], 0, 0]); const uint = new Uint32Array(bytes.buffer)[0]; deviceStatus.electric = uint * 0.25; } } return deviceStatus; } static crc16ccitt(dataIn) { const data = []; for (let i = 0; i < dataIn.length; i++) { if (dataIn[i] > 127) { data[i] = (256 - dataIn[i]) * (-1); } else { data[i] = dataIn[i]; } } let i = 65535; for (let i1 = 0; i1 < data.length; i1++) { const b = data[i1]; for (let i2 = 0; i2 < 8; i2++) { let z = true; const z2 = ((b >> (7 - i2)) & 1) === 1; if (((i >> 15) & 1) !== 1) { z = false; } i <<= 1; if (z2 !== z) { i ^= 4129; } } } return (i & 65535); } toBase64() { let hb = this.commandToByte(); let hhb = hb.concat([1, 255, 255, 255, 255]); const crc = DeviceStatus.crc16ccitt(hhb); const arTo1 = hhb.concat([(crc & 255), ((crc >> 8) & 255)]); hb = DeviceStatus.receiveToBytes(this); hhb = hb.concat([1, 255, 255, 255, 255]); const crc2 = DeviceStatus.crc16ccitt(hhb); const arTo2 = hhb.concat([(crc2 & 255), ((crc2 >> 8) & 255)]); const newAR = arTo1.concat(arTo2); const byteArrayToBinaryStr = (byteArray) => { let ret = ''; for (let i = 0; i < byteArray.length; i++) { ret += String.fromCharCode(byteArray[i]); } return ret; }; const binary = byteArrayToBinaryStr(newAR); let ret = Buffer.from(binary, 'binary').toString('base64'); ret = ret.replace('\n', ''); return ret; } commandToByte() { const statByte = [0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; if (this.operation) { statByte[2] |= 3; } else { statByte[2] |= 2; } // Operating mode if (this.operationMode === 0) { statByte[2] |= 32; } else if (this.operationMode === 1) { statByte[2] |= 40; } else if (this.operationMode === 2) { statByte[2] |= 48; } else if (this.operationMode === 3) { statByte[2] |= 44; } else if (this.operationMode === 4) { statByte[2] |= 36; } // Air flow if (this.airFlow === 0) { statByte[3] |= 15; } else if (this.airFlow === 1) { statByte[3] |= 8; } else if (this.airFlow === 2) { statByte[3] |= 9; } else if (this.airFlow === 3) { statByte[3] |= 10; } else if (this.airFlow === 4) { statByte[3] |= 14; } // Vertical wind direction if (this.windDirectionUD === 0) { statByte[2] |= 192; statByte[3] |= 128; } else if (this.windDirectionUD === 1) { statByte[2] |= 128; statByte[3] |= 128; } else if (this.windDirectionUD === 2) { statByte[2] |= 128; statByte[3] |= 144; } else if (this.windDirectionUD === 3) { statByte[2] |= 128; statByte[3] |= 160; } else if (this.windDirectionUD === 4) { statByte[2] |= 128; statByte[3] |= 176; } // Horizontal wind direction if (this.windDirectionLR === 0) { statByte[12] |= 3; statByte[11] |= 16; } else if (this.windDirectionLR === 1) { statByte[12] |= 2; statByte[11] |= 16; } else if (this.windDirectionLR === 2) { statByte[12] |= 2; statByte[11] |= 17; } else if (this.windDirectionLR === 3) { statByte[12] |= 2; statByte[11] |= 18; } else if (this.windDirectionLR === 4) { statByte[12] |= 2; statByte[11] |= 19; } else if (this.windDirectionLR === 5) { statByte[12] |= 2; statByte[11] |= 20; } else if (this.windDirectionLR === 6) { statByte[12] |= 2; statByte[11] |= 21; } else if (this.windDirectionLR === 7) { statByte[12] |= 2; statByte[11] |= 22; } // Preset temperature let presetTemp = 25.0; if (this.operationMode !== 3 && this.presetTemp !== null) { presetTemp = this.presetTemp; } statByte[4] |= Math.floor(presetTemp / 0.5) + 128; statByte[12] |= this.entrust ? 12 : 8; if (this.modelNo === 1) { statByte[10] |= this.isVacantProperty ? 1 : 0; } if (this.modelNo !== 1 && this.modelNo !== 2) { return statByte; } statByte[10] |= this.isSelfCleanReset ? 4 : 0; statByte[10] |= this.isSelfCleanOperation ? 144 : 128; return statByte; } static receiveToBytes(status) { const statByte = [0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; if (status.operation) { statByte[2] |= 1; } // Operating mode if (status.operationMode === 1) { statByte[2] |= 8; } else if (status.operationMode === 2) { statByte[2] |= 16; } else if (status.operationMode === 3) { statByte[2] |= 12; } else if (status.operationMode === 4) { statByte[2] |= 4; } // Air flow if (status.airFlow === 0) { statByte[3] |= 7; } else if (status.airFlow === 2) { statByte[3] |= 1; } else if (status.airFlow === 3) { statByte[3] |= 2; } else if (status.airFlow === 4) { statByte[3] |= 6; } // Vertical wind direction if (status.windDirectionUD === 0) { statByte[2] |= 64; } else if (status.windDirectionUD === 2) { statByte[3] |= 16; } else if (status.windDirectionUD === 3) { statByte[3] |= 32; } else if (status.windDirectionUD === 4) { statByte[3] |= 48; } // Horizontal wind direction if (status.windDirectionLR === 0) { statByte[12] |= 1; } else if (status.windDirectionLR === 1) { statByte[11] |= 0; } else if (status.windDirectionLR === 2) { statByte[11] |= 1; } else if (status.windDirectionLR === 3) { statByte[11] |= 2; } else if (status.windDirectionLR === 4) { statByte[11] |= 3; } else if (status.windDirectionLR === 5) { statByte[11] |= 4; } else if (status.windDirectionLR === 6) { statByte[11] |= 5; } else if (status.windDirectionLR === 7) { statByte[11] |= 6; } // Preset temperature let presetTemp = 25.0; if (status.operationMode !== 3 && status.presetTemp !== null) { presetTemp = status.presetTemp; } statByte[4] |= Math.floor(presetTemp / 0.5); if (status.entrust) { statByte[12] |= 4; } if (!status.coolHotJudge) { statByte[8] |= 8; } if (status.modelNo === 1) { statByte[0] |= 1; } else if (status.modelNo === 2) { statByte[0] |= 2; } if (status.modelNo === 1) { statByte[10] |= status.isVacantProperty ? 1 : 0; } if (status.modelNo !== 1 && status.modelNo !== 2) { return statByte; } statByte[15] |= status.isSelfCleanOperation ? 1 : 0; return statByte; } } export class DeviceClient { ipAddress; port; operatorId; deviceId; log; status = new DeviceStatus(); constructor(ipAddress, port, operatorId, deviceId, log) { this.ipAddress = ipAddress; this.port = port; this.operatorId = operatorId; this.deviceId = deviceId; this.log = log; } async getDeviceStatus() { await this.call('getAirconStat') .then(data => this.status = DeviceStatus.fromBase64(data.contents.airconStat)); return this.status; } setAirflow(airFlow) { this.status.airFlow = airFlow; this.setDeviceStatus(this.status); return this.status; } setOperationMode(operationMode) { this.status.operationMode = operationMode; this.setDeviceStatus(this.status); return this.status; } setOperation(operation) { this.status.operation = operation; this.setDeviceStatus(this.status); return this.status; } setPresetTemp(presetTemp) { this.status.presetTemp = presetTemp; this.setDeviceStatus(this.status); return this.status; } async setDeviceStatus(status) { const contents = { airconId: this.deviceId, airconStat: status.toBase64(), }; await this.call('setAirconStat', contents) .then(data => this.status = DeviceStatus.fromBase64(data.contents.airconStat)); return this.status; } async call(command, contents = null) { let data; if (contents) { data = { apiVer: '1.0', command: command, deviceId: this.deviceId, operatorId: this.operatorId, timestamp: Math.floor(new Date().valueOf() / 1000), contents: contents, }; } else { data = { apiVer: '1.0', command: command, deviceId: this.deviceId, operatorId: this.operatorId, timestamp: Math.floor(new Date().valueOf() / 1000), }; } const body = JSON.stringify(data); // We must use axios, because fetch lowercases the headers, which the device does not like return await axios.post(`http://${this.ipAddress}:${this.port}/beaver/command`, body) .then(response => { if (response.status !== 200) { throw new Error(`HTTP error: ${response.status} ${response.statusText}`); } else { return response.data; } }); } } //# sourceMappingURL=device.js.map