UNPKG

homebridge-daikin-oneplus

Version:
586 lines 24.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DaikinApi = exports.DAIKIN_DEVICE_FOREGROUND_REFRESH_MS = exports.DAIKIN_DEVICE_BACKGROUND_REFRESH_MS = exports.DAIKIN_DEVICE_WRITE_DELAY_MS = void 0; /* eslint-disable @typescript-eslint/no-explicit-any */ const axios_1 = __importDefault(require("axios")); const process_1 = require("process"); // After sending an update to the Daikin API it will return old data for up to 15 seconds, so we // delay fetching data after an update by this amount. https://daikinone.com/openapi/documentation/ exports.DAIKIN_DEVICE_WRITE_DELAY_MS = 15 * 1000; // User is not interacting with a HomeKit controller - background updates for automations exports.DAIKIN_DEVICE_BACKGROUND_REFRESH_MS = 180 * 1000; // User is interacting with a HomeKit controller - latest data needed exports.DAIKIN_DEVICE_FOREGROUND_REFRESH_MS = 10 * 1000; class DaikinApi { constructor(user, password, log) { this._isInitialized = false; this._listeners = new Set(); this._lastUpdateTimeMs = -1; this._nextUpdateTimeMs = -1; this._noUpdateBeforeMs = 0; this._lastWriteStartTimeMs = -1; this._lastWriteFinishTimeMs = -1; this._lastReadStartTimeMs = -1; this._lastReadFinishTimeMs = -1; this.log = log; this.user = user; this.password = password; } addListener(l) { this._listeners.add(l); } removeListener(l) { this._listeners.delete(l); } notifyListeners() { for (const l of this._listeners) { l(); } } async Initialize() { await this.getToken(); if (this._token === undefined || this._token === null) { this.log.error('Unable to retrieve token.'); return; } await this.getDevices(); if (this._devices !== undefined) { this.log.debug('Found %s devices: ', this._devices.length); this._devices.forEach(element => { this.log.debug('Device: %s', element.name); }); } else { this.log.info('No devices found.'); return; } await this.getData(); this.log.debug('Loaded initial data.'); this._isInitialized = true; } isInitialized() { return this._isInitialized; } //TODO: if writing data or timer exists to write data, don't send get request //TODO: if data received while writing or waiting to write, toss //TODO: if above is done, then gets after, but within write delay time will get delayed. async getData() { this.log.debug('Getting data...'); this._lastReadStartTimeMs = this._monotonic_clock_ms(); this._lastReadFinishTimeMs = -1; this.log.debug('GS: %d ; %d ; WT: %d ; %d', this._lastReadStartTimeMs, this._lastReadFinishTimeMs, this._lastWriteStartTimeMs, this._lastWriteFinishTimeMs); this._devices && this._devices.forEach(async (device) => { const data = await this.getDeviceData(device.id); if (!data) { this.log.error('Unable to retrieve data for %s.', device.name); return; } this._updateCache(device.id, data); this.log.debug('Notifying all listeners'); this.notifyListeners(); }); this.log.debug('Updated data.'); this._lastReadFinishTimeMs = this._monotonic_clock_ms(); this.log.debug('GF: %d ; %d ; WT: %d ; %d', this._lastReadStartTimeMs, this._lastReadFinishTimeMs, this._lastWriteStartTimeMs, this._lastWriteFinishTimeMs); this._nextUpdateTimeMs = -1; this._scheduleUpdate(); } updateNow() { this._scheduleUpdate(undefined, true); } /** * Schedules the next update. The scheduler has 2 modes. * * ASAP: Pull an update as soon as updates are neither blocked by a prior `blockUntilMs` value * nor by the maximum refresh frequency `DAIKIN_DEVICE_FOREGROUND_REFRESH_MS`. * * Regular: Pull an update every `DAIKIN_DEVICE_BACKGROUND_REFRESH_MS`. * * @param blockUntilMs If given and > 0 then no updates are guaranteed to take place in the next `blockUntilMs` milliseconds. * @param asap perform update as soon as allowed by DAIKIN_DEVICE_FOREGROUND_REFRESH_MS */ _scheduleUpdate(blockUntilMs, asap = false) { if (asap) { this._scheduleAsap(blockUntilMs); } else { this._scheduleFuture(blockUntilMs); } } _scheduleAsap(blockUntilMs) { if (blockUntilMs) { this.log.error('Ignoring blockUntilMs when scheduling ASAP'); } const sinceLastUpdateMs = this._monotonic_clock_ms() - this._lastUpdateTimeMs; const minUntilNextUpdateMs = this._noUpdateBeforeMs - this._monotonic_clock_ms(); if (sinceLastUpdateMs > exports.DAIKIN_DEVICE_FOREGROUND_REFRESH_MS) { if (minUntilNextUpdateMs <= 0) { this.log.debug('Instant refresh now'); this._updateIn(0); } else { this.log.debug('Instant refresh when update is allowed in %d', minUntilNextUpdateMs); this._updateIn(minUntilNextUpdateMs); } } else { const sinceLastUpdateMs = this._monotonic_clock_ms() - this._lastUpdateTimeMs; const updateInMs = exports.DAIKIN_DEVICE_FOREGROUND_REFRESH_MS - sinceLastUpdateMs; this.log.debug('Next allowed poll in %d', updateInMs); this._updateIn(Math.max(minUntilNextUpdateMs, updateInMs)); } } _scheduleFuture(blockUntilMs) { let nextUpdateInMs; // set how long to wait to do an update (blockUntilMs) // and when the next update should happen (nextUpdateInMs) // as of 8/25/23, blockUntilMs will either be undefined or DAIKIN_DEVICE_WRITE_DELAY_MS only if (!blockUntilMs) { // just got data blockUntilMs = exports.DAIKIN_DEVICE_FOREGROUND_REFRESH_MS; nextUpdateInMs = exports.DAIKIN_DEVICE_BACKGROUND_REFRESH_MS; } else if (blockUntilMs < exports.DAIKIN_DEVICE_FOREGROUND_REFRESH_MS) { this.log.debug('BlockUntilMs too small %d is less than %d', blockUntilMs, exports.DAIKIN_DEVICE_FOREGROUND_REFRESH_MS); blockUntilMs = exports.DAIKIN_DEVICE_FOREGROUND_REFRESH_MS; nextUpdateInMs = exports.DAIKIN_DEVICE_FOREGROUND_REFRESH_MS; } else if (blockUntilMs === exports.DAIKIN_DEVICE_WRITE_DELAY_MS) { nextUpdateInMs = exports.DAIKIN_DEVICE_WRITE_DELAY_MS; } else { nextUpdateInMs = exports.DAIKIN_DEVICE_BACKGROUND_REFRESH_MS; } this._noUpdateBeforeMs = this._monotonic_clock_ms() + blockUntilMs; const scheduledRunInMs = this._nextUpdateTimeMs - this._monotonic_clock_ms(); if (this._nextUpdateTimeMs === -1 || blockUntilMs > scheduledRunInMs) { // if no run is scheduled at all OR if a run is scheduled for sooner than the desired minimum wait, push it into the future this._updateIn(blockUntilMs > nextUpdateInMs ? blockUntilMs : nextUpdateInMs); } else { // if the next update is already far enough in the future, nothing else to do this.log.debug('Not rescheduling next update because %d is after %d', scheduledRunInMs, blockUntilMs); } } _updateIn(nextUpdateMs) { if (this._updateTimeout) { clearTimeout(this._updateTimeout); } this._updateTimeout = setTimeout(async () => { this._lastUpdateTimeMs = this._monotonic_clock_ms(); await this.getData(); }, nextUpdateMs); this._nextUpdateTimeMs = this._monotonic_clock_ms() + nextUpdateMs; this.log.debug('Scheduled update in %d.', nextUpdateMs); } _monotonic_clock_ms() { return Number(process_1.hrtime.bigint() / BigInt(1000000)); } async getToken() { this.log.debug('Getting token...'); return axios_1.default.post('https://api.daikinskyport.com/users/auth/login', { email: this.user, password: this.password, }, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, }).then((response) => this.setToken(response)) .catch((error) => this.logError('Error getting token:', error)); } async setToken(response) { this._token = response.data; this._tokenExpiration = new Date(); const expSeconds = this._tokenExpiration.getSeconds() + this._token.accessTokenExpiresIn - (exports.DAIKIN_DEVICE_BACKGROUND_REFRESH_MS / 1000) * 2; //Set expiration a little early. this._tokenExpiration.setSeconds(expSeconds); } getDevices() { return this.getRequest('https://api.daikinskyport.com/devices') .then((response) => this._devices = response); } getDeviceData(device) { return this.getRequest(`https://api.daikinskyport.com/deviceData/${device}`); } refreshToken() { if (typeof this._token === 'undefined' || typeof this._token.refreshToken === 'undefined' || !this._token.refreshToken) { this.log.debug('Cannot refresh token. Getting new token.'); return this.getToken(); } axios_1.default.post('https://api.daikinskyport.com/users/auth/token', { email: this.user, refreshToken: this._token.refreshToken, }, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, }).then((response) => this.setToken(response)) .catch((error) => this.logError('Error refreshing token:', error)); } getRequest(uri) { if (new Date() >= this._tokenExpiration) { this.refreshToken(); } if (!this._token) { this.log.error('No token for request: %s', uri); return Promise.resolve(); } return axios_1.default.get(uri, { headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${this._token.accessToken}`, }, }).then((response) => response.data) .catch((error) => { this.logError(`Error with getRequest: ${uri}`, error); return Promise.resolve(); }); } getDeviceList() { return this._devices; } getDeviceName(deviceName, deviceNameCustom) { switch (deviceName) { case 0: return deviceNameCustom; case 1: return 'main room'; case 2: return 'upstairs'; case 3: return 'downstairs'; case 4: return 'hallway'; case 5: return 'bedroom'; case 6: return 'kitchen'; default: return 'other'; } } deviceHasData(deviceId) { const device = this._cachedDeviceById(deviceId); if (typeof device === 'undefined' || typeof device.data === 'undefined') { return false; } return true; } getCurrentStatus(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.equipmentStatus; } getCurrentTemp(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.tempIndoor; } getOutdoorTemp(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.tempOutdoor; } getTargetState(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.mode; } getOneCleanFanActive(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.oneCleanFanActive; } getCirculateAirFanActive(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.fanCirculate === 0 ? false : true; } getCirculateAirFanSpeed(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.fanCirculateSpeed; } getTargetTemp(deviceId) { const device = this._cachedDeviceById(deviceId); switch (device.data.mode) { case 1 /* TargetHeatingCoolingState.HEAT */: case 4 /* TargetHeatingCoolingState.AUXILIARY_HEAT */: case 3 /* TargetHeatingCoolingState.AUTO */: return device.data.hspActive; case 2 /* TargetHeatingCoolingState.COOL */: default: return device.data.cspActive; } } heatingThresholdTemperature(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.hspActive; } coolingThresholdTemperature(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.cspActive; } getCurrentHumidity(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.humIndoor; } getOutdoorHumidity(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.humOutdoor; } getTargetHumidity(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.humSP; } getAirQualityLevel(deviceId, forIndoor) { const device = this._cachedDeviceById(deviceId); return forIndoor ? device.data.aqIndoorLevel : device.data.aqOutdoorLevel; } getOzone(deviceId, forIndoor) { const device = this._cachedDeviceById(deviceId); return forIndoor ? 0 : device.data.aqOutdoorOzone; } getAirQualityValue(deviceId, forIndoor) { const device = this._cachedDeviceById(deviceId); return forIndoor ? device.data.aqIndoorValue : device.data.aqOutdoorValue; } getPM2_5Density(deviceId, forIndoor) { const device = this._cachedDeviceById(deviceId); return forIndoor ? device.data.aqIndoorParticlesValue : device.data.aqOutdoorParticles; } getVocDensity(deviceId, forIndoor) { const device = this._cachedDeviceById(deviceId); return forIndoor ? device.data.aqIndoorVOCValue : 0; } getDisplayUnits(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.units; } getScheduleState(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.schedOverride === 0 && device.data.schedEnabled && !device.data.geofencingAway; } getAwayState(deviceId) { const device = this._cachedDeviceById(deviceId); return device.data.geofencingAway; } async setTargetTemps(deviceId, targetTemp, heatThreshold, coolThreshold) { var _a; const deviceData = (_a = this._cachedDeviceById(deviceId)) === null || _a === void 0 ? void 0 : _a.data; if (!deviceData) { this.log.info('Device data could not be retrieved. Unable to set target temp. (%s)', deviceId); return false; } let requestedData; // Only send the request if the request provides a value pertinent to the current state/mode. switch (deviceData.mode) { case 1 /* TargetHeatingCoolingState.HEAT */: case 4 /* TargetHeatingCoolingState.AUXILIARY_HEAT */: if (!targetTemp) { return true; } requestedData = { hspHome: targetTemp, }; break; case 2 /* TargetHeatingCoolingState.COOL */: if (!targetTemp) { return true; } requestedData = { cspHome: targetTemp, }; break; case 0 /* TargetHeatingCoolingState.OFF */: // Do nothing when off return true; case 3 /* TargetHeatingCoolingState.AUTO */: //Disregard setting Target Temp when in auto/off if (targetTemp) { return true; } if (coolThreshold) { // Setting cool threshold for auto this.pendingCoolThreshold = coolThreshold; } else { // Setting heat threshold for auto this.pendingHeatThreshold = heatThreshold; } if (!this.pendingHeatThreshold || !this.pendingCoolThreshold) { return true; } requestedData = { hspHome: this.pendingHeatThreshold, cspHome: this.pendingCoolThreshold, }; // Reset pending thresholds this.pendingCoolThreshold = undefined; this.pendingHeatThreshold = undefined; break; default: this.log.info('Device is in an unknown state: %s. Unable to set target temp. (%s)', deviceData.mode, deviceId); return false; } if (deviceData.schedEnabled) { requestedData.schedOverride = 1; } return this.putRequest(deviceId, requestedData, 'setTargetTemps', 'Error updating target temp:'); } async setTargetState(deviceId, requestedState) { const requestedData = { mode: requestedState, }; return this.putRequest(deviceId, requestedData, 'setTargetState', 'Error updating target state:'); } async setOneCleanFanActive(deviceId, requestedState) { const requestedData = { oneCleanFanActive: requestedState, }; return this.putRequest(deviceId, requestedData, 'setOneCleanFanActive', 'Error updating OneClean fan:'); } async setCirculateAirFanActive(deviceId, requestedState) { const requestedData = { fanCirculate: requestedState ? 1 : 0, }; return this.putRequest(deviceId, requestedData, 'setCirculateAirFanActive', 'Error updating Circulate Air fan:'); } async setCirculateAirFanSpeed(deviceId, requestedSpeed) { let requestedData; if (requestedSpeed === -1) { requestedData = { fanCirculate: 0, fanCirculateSpeed: 1, }; } else { requestedData = { fanCirculateSpeed: requestedSpeed, }; } return this.putRequest(deviceId, requestedData, 'setCirculateAirFanSpeed', 'Error updating Circulate Air fan and speed:'); } async setDisplayUnits(deviceId, requestedUnits) { const requestedData = { units: requestedUnits, }; return this.putRequest(deviceId, requestedData, 'setDisplayUnits', 'Error updating display units:'); } async setTargetHumidity(deviceId, requestedHumidity) { const requestedData = { humSP: requestedHumidity, }; return this.putRequest(deviceId, requestedData, 'setTargetHumidity', 'Error updating target humidity:'); } async setScheduleState(deviceId, requestedState) { let requestedData; // when enabling the schedule state, a schedule must exist. if (requestedState) { requestedData = { geofencingAway: false, schedOverride: 0, schedEnabled: true, }; } else { requestedData = { schedEnabled: false, }; } this.log.debug('Schedule for %s: %s: %s', deviceId, requestedState, requestedData); return this.putRequest(deviceId, requestedData, 'setScheduleState', 'Error updating schedule state:'); } async setAwayState(deviceId, requestedState, enableSchedule) { let requestedData; if (requestedState) { // when enabling the away state, the schedule (if it exists) is automatically paused. requestedData = { geofencingAway: true, }; } else { if (enableSchedule) { requestedData = { geofencingAway: false, schedEnabled: true, }; } else { requestedData = { geofencingAway: false, }; } } return this.putRequest(deviceId, requestedData, 'setAwayState', 'Error updating away state:'); } //TODO: track data to be written per device //TODO: buffer write requests per device for up to a second (create timer? per device that when elapsed writes anything requested for it) //TODO: reset timer on every device's request. once there's a full second without a request, then send? //TODO: always update cache data with requested so that local stays current with what will be state once written. putRequest(deviceId, requestData, caller, errorHeader) { this.log.debug('Writing data: %s-> device: %s; requestData: %s', caller, deviceId, JSON.stringify(requestData)); this._lastWriteStartTimeMs = this._monotonic_clock_ms(); this._lastWriteFinishTimeMs = -1; this.log.debug('WS: %d ; %d ; GT: %d ; %d', this._lastWriteStartTimeMs, this._lastWriteFinishTimeMs, this._lastReadStartTimeMs, this._lastReadFinishTimeMs); return axios_1.default.put(`https://api.daikinskyport.com/deviceData/${deviceId}`, requestData, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': `Bearer ${this._token.accessToken}`, }, }) .then(res => { this.log.debug('%s-> device: %s; response: %s', caller, deviceId, JSON.stringify(res.data)); this._lastWriteFinishTimeMs = this._monotonic_clock_ms(); this.log.debug('WF: %d ; %d ; GT: %d ; %d', this._lastWriteStartTimeMs, this._lastWriteFinishTimeMs, this._lastReadStartTimeMs, this._lastReadFinishTimeMs); this._updateCache(deviceId, requestData); this._scheduleUpdate(exports.DAIKIN_DEVICE_WRITE_DELAY_MS); return true; }) .catch((error) => { this.logError(errorHeader, error); this._lastWriteFinishTimeMs = this._monotonic_clock_ms(); return false; }); } _updateCache(deviceId, partialUpdate) { const cachedDevice = this._cachedDeviceById(deviceId); if (cachedDevice) { const updatedData = { ...cachedDevice.data, ...partialUpdate, }; cachedDevice.data = updatedData; //this.log.debug('Updated cache for %s - %s', deviceId, JSON.stringify(partialUpdate)); this.log.debug('Updated cache for %s', deviceId); } else { this.log.error('Cache update for device that doesn\'t exist:', deviceId); } } _cachedDeviceById(deviceId) { if (!this._devices) { return undefined; } return this._devices.find(e => e.id === deviceId); } logError(message, error) { this.log.error(message); if (error.response) { // When response status code is out of 2xx range this.log.error('Error with response:'); this.log.error(error.response.data); this.log.error(error.response.status); this.log.error(error.response.headers); } else if (error.request) { // When no response was received after request was made this.log.error('Error with request:'); this.log.error(error.request); } else { // Error this.log.error('General error:'); this.log.error(error.message); } return false; } } exports.DaikinApi = DaikinApi; //# sourceMappingURL=daikinapi.js.map