homebridge-daikin-local-platform
Version:
Homebridge platform plugin providing HomeKit support for Daikin air conditioners with local control
426 lines • 19.8 kB
JavaScript
;
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