UNPKG

homebridge-daikin-local-platform

Version:

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

644 lines (452 loc) 17.8 kB
import DaikinPlatformLogger from './logger'; import axios from 'axios'; import rateLimit from 'axios-rate-limit'; import { USER_AGENT, ENDPOINT, SECONDS_BETWEEN_REQUEST } from './const'; export const CLIMATE_MODE_FAN = '0000'; export const CLIMATE_MODE_HEATING = '0100'; export const CLIMATE_MODE_COOLING = '0200'; export const CLIMATE_MODE_AUTO = '0300'; export const CLIMATE_MODE_DEHUMIDIFY = '0500'; export const CLIMATE_MODE_HUMIDIFY = '0800'; export const CLIMATE_FAN_SPEED_AUTO = '0A00'; export const CLIMATE_FAN_SPEED_SLIENT = '0B00'; export const CLIMATE_FAN_SPEED_1 = '0300'; export const CLIMATE_FAN_SPEED_2 = '0400'; export const CLIMATE_FAN_SPEED_3 = '0500'; export const CLIMATE_FAN_SPEED_4 = '0600'; export const CLIMATE_FAN_SPEED_5 = '0700'; const CLIMATE_OPERATE_ON = '00'; const CLIMATE_OPERATE_OFF = '01'; const CLIMATE_OPERATE_SETTING = '02'; 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 = rateLimit(axios.create(), { maxRequests: 1, perMilliseconds: 500 }); export class DaikinDevice { protected _IP: string; protected _Response: object; protected _log: DaikinPlatformLogger; protected _lastUpdateTimestamp: number = 0; protected _callback: any = null; constructor( public readonly IP: string, private readonly log: DaikinPlatformLogger, ) { this._IP = IP; this._Response = {}; this._log = log; } public setCallback(callback:any) { this._callback = callback; } public async queryDevice(bForce:boolean = false): Promise<any> { if(!bForce && (Date.now() - this._lastUpdateTimestamp) < SECONDS_BETWEEN_REQUEST) { this.log.debug(`Daikin - queryDevice('${this._IP}'): Skipping query as last update was less than ${SECONDS_BETWEEN_REQUEST} micro seconds ago`); return this._Response; } const response = await http.request({ method: 'post', url: `http://${this._IP}${ENDPOINT}`, headers: { 'Accept': 'application/json; charset=UTF-8', 'Content-Type': 'application/json', 'User-Agent': USER_AGENT, }, data: COMMAND_QUERY_WITH_MD, }); this._lastUpdateTimestamp = Date.now(); if(response.status === 200) { //this.log.debug(`Daikin - queryDevice('${this._IP}'): Response: '${JSON.stringify(response.data)}'`); return response.data; } this.log.debug(`Daikin - queryDevice('${this._IP}'): Error: Invalid response status code: '${response.status}'`); return undefined; } public async fetchDeviceStatus(bForce:boolean = false): Promise<boolean> { const response = await this.queryDevice(bForce); this._Response = response; 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; } public async setShowSSID(bShow: boolean): Promise<boolean> { 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 }]}}]}; const response = await http.request({ method: 'post', url: `http://${this._IP}${ENDPOINT}`, headers: { 'Accept': 'application/json; charset=UTF-8', 'Content-Type': 'application/json', 'User-Agent': USER_AGENT, }, data: JSON.stringify(command), }); return response.status === 200; } public getMacAddress(): string { return this.extractValue(this._Response, '/dsiot/edge.adp_i', 'mac'); } public getSSID(): string { return this.extractValue(this._Response, '/dsiot/edge.adp_i', 'ssid'); } public getDeviceName(): string { return this.extractValue(this._Response, '/dsiot/edge.adp_d', 'name'); } public getDeviceReg(): string { return this.extractValue(this._Response, '/dsiot/edge.adp_i', 'reg'); } public getDeviceType(): string { return this.extractValue(this._Response, '/dsiot/edge.dev_i', 'type') + this.extractValue(this._Response, '/dsiot/edge.adp_i', 'enlv'); } public getFirmwareVersion(): string { return this.extractValue(this._Response, '/dsiot/edge.adp_i', 'ver'); } public getDeviceIP(): string { return this._IP; } public getPowerStatus(): boolean { return this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_A002/p_01') === '01'; } public getIndoorTemperature(): number { return parseInt(this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_A00B/p_01'), 16); } public getIndoorHumidity(): number { return parseInt(this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_A00B/p_02'), 16); } //0000:fan 0100:heating 0200:cooling 0300:auto 0500:dehumidify public getOperationMode(): string { return this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3001/p_01'); } public getOperationModeName(): string { const mode = this.getOperationMode(); if(mode === CLIMATE_MODE_FAN) { return 'Fan'; } else if(mode === CLIMATE_MODE_DEHUMIDIFY) { return 'Dehumidify'; } else if(mode === CLIMATE_MODE_AUTO) { return 'Auto'; } else if(mode === CLIMATE_MODE_HEATING) { return 'Heating'; } else if(mode === CLIMATE_MODE_COOLING) { return 'Cooling'; } return 'Unknown'; } public getTargetTemperatureWithMode(mode:string): number { if(mode === CLIMATE_MODE_HEATING) { return parseInt(this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3001/p_03'), 16) / 2.0; } else if(mode === CLIMATE_MODE_COOLING) { return parseInt(this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3001/p_02'), 16) / 2.0; } else if(mode === CLIMATE_MODE_AUTO) { return parseInt(this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3001/p_1D'), 16) / 2.0; } this.log.debug(`Daikin - getTargetTemperature(): Error: Invalid mode: '${mode}'`); return 0; } public getTargetTemperature(): number { const mode = this.getOperationMode(); return this.getTargetTemperatureWithMode(mode); } public getTargetTemperatureRange(): number[] { const mode = this.getOperationMode(); let pn = 'p_02'; if(mode === CLIMATE_MODE_HEATING) { pn = 'p_03'; } else if(mode === CLIMATE_MODE_COOLING) { pn = 'p_02'; } else if(mode === CLIMATE_MODE_AUTO) { pn = 'p_1D'; } else{ 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]; } public getCoolingThresholdTemperatureRange(): number[] { 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]; } public getHeatingThresholdTemperatureRange(): number[] { 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]; } public getFanSpeed(): string { const mode = this.getOperationMode(); let pn = 'p_09'; if(mode === CLIMATE_MODE_FAN) { pn = 'p_28'; } else if(mode === CLIMATE_MODE_DEHUMIDIFY) { pn = 'p_27'; } else if(mode === CLIMATE_MODE_AUTO) { pn = 'p_26'; } else if(mode === CLIMATE_MODE_HEATING) { pn = 'p_0A'; } else if(mode === CLIMATE_MODE_COOLING) { pn = 'p_09'; } return this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3001/' + pn); } public getFanSpeedName(): string { const speed = this.getFanSpeed(); if(speed === CLIMATE_FAN_SPEED_AUTO) { return 'Auto'; } else if(speed === CLIMATE_FAN_SPEED_SLIENT) { return 'Silent'; } else if(speed === CLIMATE_FAN_SPEED_1) { return '1'; } else if(speed === CLIMATE_FAN_SPEED_2) { return '2'; } else if(speed === CLIMATE_FAN_SPEED_3) { return '3'; } else if(speed === CLIMATE_FAN_SPEED_4) { return '4'; } else if(speed === CLIMATE_FAN_SPEED_5) { return '5'; } return 'Unknown'; } public getFanSpeedNumber(): number { const speed = this.getFanSpeed(); if(speed === CLIMATE_FAN_SPEED_AUTO){ return 0; } else if(speed === CLIMATE_FAN_SPEED_SLIENT){ return 1; } else if(speed === CLIMATE_FAN_SPEED_1){ return 2; } else if(speed === CLIMATE_FAN_SPEED_2){ return 3; } else if(speed === CLIMATE_FAN_SPEED_3){ return 4; } else if(speed === CLIMATE_FAN_SPEED_4){ return 5; } else if(speed === CLIMATE_FAN_SPEED_5){ return 6; } return 0; } public getMotionDetection(): boolean { return this.extractValue(this._Response, '/dsiot/edge/adr_0100.dgc_status', 'e_1002/e_3003/p_27') === '01'; } public async setMotionDetection(bEnable: boolean): Promise<boolean> { const command = [{"pn": "e_3003", "pch": [{"pn": "p_27", "pv": bEnable ? '01':'00'}]}]; return await this.sendCommand(command); } public async setPowerStatus(power: boolean): Promise<boolean> { const command = [{"pn": "e_3003", "pch": [{"pn": "p_2D", "pv": CLIMATE_OPERATE_SETTING }]}, {"pn": "e_A002","pch": [{"pn": "p_01", "pv": power ? '01':'00'}]}]; return await this.sendCommand(command); } public async setOperationMode(mode: string): Promise<boolean> { 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); } public async setTargetTemperature(temperature: number): Promise<boolean> { const mode = this.getOperationMode(); let pn = 'p_02'; const pv = (temperature * 2).toString(16); if(mode === CLIMATE_MODE_HEATING) { pn = 'p_03'; } else if(mode === CLIMATE_MODE_COOLING) { pn = 'p_02'; } else if(mode === CLIMATE_MODE_AUTO) { pn = 'p_1D'; } else{ return false; } let command = [{"pn": "e_3003", "pch": [{"pn": "p_2D", "pv": CLIMATE_OPERATE_SETTING}]}, {"pn": "e_3001","pch": [{"pn": pn, "pv": pv}]}]; if(mode === 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); } public async setFanSpeed(speed: string): Promise<boolean> { const mode = this.getOperationMode(); let pn = 'p_09'; if(mode === CLIMATE_MODE_FAN) { pn = 'p_28'; } else if(mode === CLIMATE_MODE_DEHUMIDIFY) { pn = 'p_27'; } else if(mode === CLIMATE_MODE_AUTO) { pn = 'p_26'; } else if(mode === CLIMATE_MODE_HEATING) { pn = 'p_0A'; } else if(mode === CLIMATE_MODE_COOLING) { pn = 'p_09'; } 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); } public extractValue(responsesData: object, fr: string, path: string): any | undefined { try { if(responsesData === undefined || responsesData.hasOwnProperty('responses') === false) { this.log.debug('Daikin - extractValue(): 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 (let i = 0; i < pathKeys.length; i++) { try{ const key = pathKeys[i]; for(const currentObjectElement of currentObject) { if ((currentObjectElement['pn'] === key && currentObjectElement.hasOwnProperty('pch'))) { currentObject = currentObjectElement['pch']; break; } else if (currentObjectElement['pn'] === key && currentObjectElement.hasOwnProperty('pv')) { if(i === pathKeys.length - 1) { return currentObjectElement['pv']; } } } } 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; } public extractObject(responsesData: object, fr: string, path: string): object | undefined { 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}]}]; public pushObject(jsonData: Object, pn: string, obj: object): object | undefined { 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; } protected async sendCommand(command: object): Promise<boolean> { const param = {"requests": [{"op": 3,"to": "/dsiot/edge/adr_0100.dgc_status","pc": {"pn": "dgc_status","pch": [{"pn": "e_1002","pch": command}]}}]}; const response = await http.request({ method: 'post', url: `http://${this._IP}${ENDPOINT}`, headers: { 'Accept': 'application/json; charset=UTF-8', 'Content-Type': 'application/json', 'User-Agent': USER_AGENT, }, data: 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; } } export class DaikinLocalAPI { private _devices: DaikinDevice[]; constructor( private readonly log: DaikinPlatformLogger, ) { this._devices = []; } public async fetchDevices(climateIPs:string[], bForce:boolean = false): Promise<DaikinDevice[]> { this.log.debug('Daikin: fetchDevices()'); this._devices = []; for(const ip of climateIPs) { const daikinDevice = new DaikinDevice(ip, this.log); if(await daikinDevice.fetchDeviceStatus(bForce)) { this._devices.push(daikinDevice); } } return this._devices; } }