UNPKG

homebridge-daikin-local-platform

Version:

Homebridge platform plugin providing HomeKit support for Daikin air conditioners with local control

426 lines 19.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DaikinLocalAPI = exports.DaikinDevice = exports.FAN_SPEED_TABLE = exports.CLIMATE_FAN_SPEED_5 = exports.CLIMATE_FAN_SPEED_4 = exports.CLIMATE_FAN_SPEED_3 = exports.CLIMATE_FAN_SPEED_2 = exports.CLIMATE_FAN_SPEED_1 = exports.CLIMATE_FAN_SPEED_SLIENT = exports.CLIMATE_FAN_SPEED_AUTO = exports.CLIMATE_MODE_HUMIDIFY = exports.CLIMATE_MODE_DEHUMIDIFY = exports.CLIMATE_MODE_AUTO = exports.CLIMATE_MODE_COOLING = exports.CLIMATE_MODE_HEATING = exports.CLIMATE_MODE_FAN = void 0; const axios_1 = __importDefault(require("axios")); const axios_rate_limit_1 = __importDefault(require("axios-rate-limit")); const const_1 = require("./const"); exports.CLIMATE_MODE_FAN = '0000'; exports.CLIMATE_MODE_HEATING = '0100'; exports.CLIMATE_MODE_COOLING = '0200'; exports.CLIMATE_MODE_AUTO = '0300'; exports.CLIMATE_MODE_DEHUMIDIFY = '0500'; exports.CLIMATE_MODE_HUMIDIFY = '0800'; exports.CLIMATE_FAN_SPEED_AUTO = '0A00'; exports.CLIMATE_FAN_SPEED_SLIENT = '0B00'; exports.CLIMATE_FAN_SPEED_1 = '0300'; exports.CLIMATE_FAN_SPEED_2 = '0400'; exports.CLIMATE_FAN_SPEED_3 = '0500'; exports.CLIMATE_FAN_SPEED_4 = '0600'; exports.CLIMATE_FAN_SPEED_5 = '0700'; const CLIMATE_OPERATE_ON = '00'; const CLIMATE_OPERATE_OFF = '01'; const CLIMATE_OPERATE_SETTING = '02'; // Target temperature lives under a mode-dependent `pn` key inside e_1002/e_3001. const TARGET_TEMP_PN_BY_MODE = { [exports.CLIMATE_MODE_HEATING]: 'p_03', [exports.CLIMATE_MODE_COOLING]: 'p_02', [exports.CLIMATE_MODE_AUTO]: 'p_1D', }; // Fan speed lives under a mode-dependent `pn` key inside e_1002/e_3001. // Modes not listed here fall back to the cooling key (p_09). const FAN_SPEED_PN_BY_MODE = { [exports.CLIMATE_MODE_FAN]: 'p_28', [exports.CLIMATE_MODE_DEHUMIDIFY]: 'p_27', [exports.CLIMATE_MODE_AUTO]: 'p_26', [exports.CLIMATE_MODE_HEATING]: 'p_0A', [exports.CLIMATE_MODE_COOLING]: 'p_09', }; const FAN_SPEED_PN_DEFAULT = 'p_09'; // Bidirectional mapping between Daikin fan-speed codes, the HomeKit rotation-speed // step (0 = auto ... 6 = level 5) and the human-readable name. exports.FAN_SPEED_TABLE = [ { code: exports.CLIMATE_FAN_SPEED_AUTO, number: 0, name: 'Auto' }, { code: exports.CLIMATE_FAN_SPEED_SLIENT, number: 1, name: 'Silent' }, { code: exports.CLIMATE_FAN_SPEED_1, number: 2, name: '1' }, { code: exports.CLIMATE_FAN_SPEED_2, number: 3, name: '2' }, { code: exports.CLIMATE_FAN_SPEED_3, number: 4, name: '3' }, { code: exports.CLIMATE_FAN_SPEED_4, number: 5, name: '4' }, { code: exports.CLIMATE_FAN_SPEED_5, number: 6, name: '5' }, ]; const COMMAND_QUERY = '{"requests":[{"op":2,"to":"/dsiot/edge.adp_i?filter=pv"},{"op":2,"to":"/dsiot/edge.adp_d?filter=pv"},{"op":2,"to":"/dsiot/edge.adp_f?filter=pv"},{"op":2,"to":"/dsiot/edge.dev_i?filter=pv"},{"op":2,"to":"/dsiot/edge/adr_0100.dgc_status?filter=pv"}]}'; const COMMAND_QUERY_WITH_MD = '{"requests":[{"op":2,"to":"/dsiot/edge.adp_i?filter=pv"},{"op":2,"to":"/dsiot/edge.adp_d?filter=pv"},{"op":2,"to":"/dsiot/edge.adp_f?filter=pv"},{"op":2,"to":"/dsiot/edge.dev_i?filter=pv"},{"op":2,"to":"/dsiot/edge/adr_0100.dgc_status"},{"op":2,"to":"/dsiot/edge/adr_0200.dgc_status"}]}'; const http = (0, axios_rate_limit_1.default)(axios_1.default.create(), { maxRequests: 1, perMilliseconds: 500 }); class DaikinDevice { constructor(IP, log) { this.IP = IP; this.log = log; this._lastUpdateTimestamp = 0; this._callback = null; // Shared promise for an in-flight query so concurrent reads don't each hit the unit. this._inflightQuery = null; this._IP = IP; this._Response = {}; this._log = log; } setCallback(callback) { this._callback = callback; } async post(data) { return http.request({ method: 'post', url: `http://${this._IP}${const_1.ENDPOINT}`, headers: { 'Accept': 'application/json; charset=UTF-8', 'Content-Type': 'application/json', 'User-Agent': const_1.USER_AGENT, }, data, timeout: const_1.REQUEST_TIMEOUT_MS, }); } async queryDevice(bForce = false) { if (!bForce) { if ((Date.now() - this._lastUpdateTimestamp) < const_1.MIN_REQUEST_INTERVAL_MS) { this.log.debug(`Daikin - queryDevice('${this._IP}'): Skipping query as last update was less than ${const_1.MIN_REQUEST_INTERVAL_MS} ms ago`); return this._Response; } // Coalesce concurrent (non-forced) reads onto a single request. if (this._inflightQuery) { return this._inflightQuery; } } const request = this._doQuery(); if (!bForce) { this._inflightQuery = request; } try { return await request; } finally { if (this._inflightQuery === request) { this._inflightQuery = null; } } } async _doQuery() { try { const response = await this.post(COMMAND_QUERY_WITH_MD); this._lastUpdateTimestamp = Date.now(); if (response.status === 200) { //this.log.debug(`Daikin - queryDevice('${this._IP}'): Response: '${JSON.stringify(response.data)}'`); this._Response = response.data; return response.data; } this.log.debug(`Daikin - queryDevice('${this._IP}'): Error: Invalid response status code: '${response.status}'`); } catch (e) { this.log.debug(`Daikin - queryDevice('${this._IP}'): Error: '${e}'`); } return undefined; } async fetchDeviceStatus(bForce = false) { const response = await this.queryDevice(bForce); if (response === undefined) { this.log.error(`Daikin - fetchDeviceStatus(${bForce}): Error: No response from device`); return false; } if (!this.getMacAddress()) { this.log.error(`Daikin - fetchDeviceStatus(${bForce}): Error: ${this._IP} no MAC address found`); this.log.debug(`Daikin - fetchDeviceStatus(${bForce}): Response: '${this._Response}'`); return false; } this.log.debug(`Daikin - fetchDeviceStatus(${bForce}): Name: ${this.getDeviceName()} MAC:${this.getMacAddress()} Power:${this.getPowerStatus()} Temp:${this.getIndoorTemperature()} Humidity:${this.getIndoorHumidity()} Target Temp:${this.getTargetTemperature()}' Mode:${this.getOperationModeName()} FanSpeed:${this.getFanSpeedName()} `); if (this._callback) { this._callback(this); } return true; } async setShowSSID(bShow) { this.log.debug(`Daikin - setShowSSID(${bShow}): Name: ${this.getDeviceName()}`); const command = { "requests": [{ "op": 3, "to": "/dsiot/edge.adp_d", "pc": { "pn": "adp_d", "pch": [{ "pn": "disp_ssid", "pv": bShow ? 0 : 1 }] } }] }; try { const response = await this.post(JSON.stringify(command)); return response.status === 200; } catch (e) { this.log.debug(`Daikin - setShowSSID('${this._IP}'): Error: '${e}'`); } return false; } getMacAddress() { return this.extractValue(this._Response, '/dsiot/edge.adp_i', 'mac'); } getSSID() { return this.extractValue(this._Response, '/dsiot/edge.adp_i', 'ssid'); } getDeviceName() { return this.extractValue(this._Response, '/dsiot/edge.adp_d', 'name'); } getDeviceReg() { return this.extractValue(this._Response, '/dsiot/edge.adp_i', 'reg'); } getDeviceType() { return this.extractValue(this._Response, '/dsiot/edge.dev_i', 'type') + this.extractValue(this._Response, '/dsiot/edge.adp_i', 'enlv'); } getFirmwareVersion() { return this.extractValue(this._Response, '/dsiot/edge.adp_i', 'ver'); } getDeviceIP() { return this._IP; } getPowerStatus() { return this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_A002/p_01') === '01'; } getIndoorTemperature() { return parseInt(this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_A00B/p_01'), 16); } getIndoorHumidity() { return parseInt(this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_A00B/p_02'), 16); } // Outdoor temperature lives on the outdoor unit at adr_0200, encoded as // little-endian signed int16 of (temperature * 2). Returns NaN if absent. getOutdoorTemperature() { const raw = this.extractValue(this._Response, '/dsiot/edge/adr_0200.dgc_status', 'e_1003/e_A00D/p_01'); if (typeof raw !== 'string' || raw.length !== 4) { return NaN; } const lo = parseInt(raw.substring(0, 2), 16); const hi = parseInt(raw.substring(2, 4), 16); let val = (hi << 8) | lo; if (val & 0x8000) { val -= 0x10000; } return val / 2; } //0000:fan 0100:heating 0200:cooling 0300:auto 0500:dehumidify getOperationMode() { return this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3001/p_01'); } getOperationModeName() { const mode = this.getOperationMode(); if (mode === exports.CLIMATE_MODE_FAN) { return 'Fan'; } else if (mode === exports.CLIMATE_MODE_DEHUMIDIFY) { return 'Dehumidify'; } else if (mode === exports.CLIMATE_MODE_AUTO) { return 'Auto'; } else if (mode === exports.CLIMATE_MODE_HEATING) { return 'Heating'; } else if (mode === exports.CLIMATE_MODE_COOLING) { return 'Cooling'; } return 'Unknown'; } getTargetTemperatureWithMode(mode) { const pn = TARGET_TEMP_PN_BY_MODE[mode]; if (!pn) { this.log.debug(`Daikin - getTargetTemperature(): Error: Invalid mode: '${mode}'`); return 0; } return parseInt(this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3001/' + pn), 16) / 2.0; } getTargetTemperature() { const mode = this.getOperationMode(); return this.getTargetTemperatureWithMode(mode); } getTargetTemperatureRange() { const mode = this.getOperationMode(); const pn = TARGET_TEMP_PN_BY_MODE[mode]; if (!pn) { this.log.debug(`Daikin - getTargetTemperatureRange(): Error: Invalid mode: '${mode}'`); return [0, 0]; } const md = this.extractObject(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3001/' + pn); const min = md ? parseInt(md['md']['mi'], 16) / 2.0 : 0; const max = md ? parseInt(md['md']['mx'], 16) / 2.0 : 0; return [min, max]; } getCoolingThresholdTemperatureRange() { const md = this.extractObject(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3001/p_02'); const min = md ? parseInt(md['md']['mi'], 16) / 2.0 : 0; const max = md ? parseInt(md['md']['mx'], 16) / 2.0 : 0; return [min, max]; } getHeatingThresholdTemperatureRange() { const md = this.extractObject(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3001/p_03'); const min = md ? parseInt(md['md']['mi'], 16) / 2.0 : 0; const max = md ? parseInt(md['md']['mx'], 16) / 2.0 : 0; return [min, max]; } getFanSpeed() { var _a; const mode = this.getOperationMode(); const pn = (_a = FAN_SPEED_PN_BY_MODE[mode]) !== null && _a !== void 0 ? _a : FAN_SPEED_PN_DEFAULT; return this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3001/' + pn); } getFanSpeedName() { var _a, _b; const speed = this.getFanSpeed(); return (_b = (_a = exports.FAN_SPEED_TABLE.find(entry => entry.code === speed)) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : 'Unknown'; } getFanSpeedNumber() { var _a, _b; const speed = this.getFanSpeed(); return (_b = (_a = exports.FAN_SPEED_TABLE.find(entry => entry.code === speed)) === null || _a === void 0 ? void 0 : _a.number) !== null && _b !== void 0 ? _b : 0; } getMotionDetection() { return this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3003/p_27') === '01'; } async setMotionDetection(bEnable) { const command = [{ "pn": "e_3003", "pch": [{ "pn": "p_27", "pv": bEnable ? '01' : '00' }] }]; return await this.sendCommand(command); } async setPowerStatus(power) { // p_2D is the operation-type flag. Sending SETTING ('02') makes the unit treat the // stop as a mere config change, so it skips the post-stop 内部クリーン (mould-proof) dry // cycle. Sending the genuine OFF ('01') makes it a normal user stop, which triggers the // dry cycle when the unit's auto-internal-clean setting is enabled — matching the remote. const operate = power ? CLIMATE_OPERATE_ON : CLIMATE_OPERATE_OFF; const command = [{ "pn": "e_3003", "pch": [{ "pn": "p_2D", "pv": operate }] }, { "pn": "e_A002", "pch": [{ "pn": "p_01", "pv": power ? '01' : '00' }] }]; return await this.sendCommand(command); } async setOperationMode(mode) { const command = [{ "pn": "e_3003", "pch": [{ "pn": "p_2D", "pv": CLIMATE_OPERATE_SETTING }] }, { "pn": "e_3001", "pch": [{ "pn": "p_01", "pv": mode }] }]; return await this.sendCommand(command); } async setTargetTemperature(temperature) { const mode = this.getOperationMode(); const pn = TARGET_TEMP_PN_BY_MODE[mode]; if (!pn) { return false; } const pv = (temperature * 2).toString(16); const command = [{ "pn": "e_3003", "pch": [{ "pn": "p_2D", "pv": CLIMATE_OPERATE_SETTING }] }, { "pn": "e_3001", "pch": [{ "pn": pn, "pv": pv }] }]; if (mode === exports.CLIMATE_MODE_COOLING) { this.pushObject(command, 'e_3001', { "pn": "p_0B", "pv": '0A' }); this.pushObject(command, 'e_3001', { "pn": "p_0C", "pv": '01' }); } return await this.sendCommand(command); } async setFanSpeed(speed) { var _a; const mode = this.getOperationMode(); const pn = (_a = FAN_SPEED_PN_BY_MODE[mode]) !== null && _a !== void 0 ? _a : FAN_SPEED_PN_DEFAULT; const command = [{ "pn": "e_3003", "pch": [{ "pn": "p_2D", "pv": CLIMATE_OPERATE_SETTING }] }, { "pn": "e_3001", "pch": [{ "pn": pn, "pv": speed }] }]; return await this.sendCommand(command); } extractValue(responsesData, fr, path) { const element = this.extractObject(responsesData, fr, path); return element ? element['pv'] : undefined; } extractObject(responsesData, fr, path) { try { if (responsesData === undefined || responsesData.hasOwnProperty('responses') === false) { this.log.debug('Daikin - extractObject(): Error: No responses object found'); return undefined; } let currentObject = responsesData['responses']; for (const response of currentObject) { if (response['fr'] === fr) { currentObject = response['pc']['pch']; } } const pathKeys = path.split('/'); for (const key of pathKeys) { try { for (const currentObjectElement of currentObject) { if (currentObjectElement['pn'] === key && currentObjectElement.hasOwnProperty('pch')) { currentObject = currentObjectElement['pch']; break; } else if (currentObjectElement['pn'] === key && currentObjectElement.hasOwnProperty('pv')) { return currentObjectElement; } } } catch (e) { this.log.debug('Daikin - extractValue(): Error:' + e); } } } catch (e) { this.log.debug('Daikin - extractValue(): Error:' + e); } this.log.debug('Daikin - extractValue(): Error: No value found for path:' + path); return undefined; } // const command = [{"pn": "e_3003", "pch": [{"pn": "p_2D", "pv": CLIMATE_OPERATE_SETTING}]}, {"pn": "e_3001","pch": [{"pn": pn, "pv": speed}]}]; pushObject(jsonData, pn, obj) { try { for (const i in jsonData) { const currentObject = jsonData[i]; if (currentObject['pn'] === pn && currentObject.hasOwnProperty('pch')) { currentObject['pch'].push(obj); return jsonData; } } } catch (e) { this.log.debug('Daikin - pushObject(): Error:' + e); } return undefined; } async sendCommand(command) { const param = { "requests": [{ "op": 3, "to": "/dsiot/edge/adr_0100.dgc_status", "pc": { "pn": "dgc_status", "pch": [{ "pn": "e_1002", "pch": command }] } }] }; try { const response = await this.post(JSON.stringify(param)); if (response.status === 200) { this.log.debug(`Daikin - sendCommand('${this._IP}'): '${JSON.stringify(param)} ' : Response: '${JSON.stringify(response.data)}'`); } else { this.log.debug(`Daikin - sendCommand('${this._IP}'): '${JSON.stringify(param)} ' : Error: Invalid response status code: '${response.status}'`); } await this.fetchDeviceStatus(true); return response.status === 200; } catch (e) { this.log.debug(`Daikin - sendCommand('${this._IP}'): Error: '${e}'`); } return false; } } exports.DaikinDevice = DaikinDevice; const FETCH_ATTEMPTS = 3; const FETCH_RETRY_DELAY_MS = 2000; class DaikinLocalAPI { constructor(log) { this.log = log; this._devices = []; } async fetchDevices(climateIPs, bForce = false) { this.log.debug('Daikin: fetchDevices()'); this._devices = []; for (const ip of climateIPs) { const daikinDevice = new DaikinDevice(ip, this.log); if (await this.fetchWithRetry(daikinDevice, bForce)) { this._devices.push(daikinDevice); } else { this.log.error(`Daikin: device at ${ip} did not respond after ${FETCH_ATTEMPTS} attempts - skipping.`); } } return this._devices; } // A device that is briefly unreachable at startup (e.g. still booting) would // otherwise be dropped until Homebridge restarts; retry a few times first. async fetchWithRetry(device, bForce) { for (let attempt = 1; attempt <= FETCH_ATTEMPTS; attempt++) { if (await device.fetchDeviceStatus(bForce || attempt > 1)) { return true; } if (attempt < FETCH_ATTEMPTS) { await new Promise(resolve => setTimeout(resolve, FETCH_RETRY_DELAY_MS)); } } return false; } } exports.DaikinLocalAPI = DaikinLocalAPI; //# sourceMappingURL=daikin-local.js.map