UNPKG

iobroker.melcloud

Version:
388 lines (333 loc) 16 kB
"use strict"; const FormData = require("form-data"); const MelCloudAtaDevice = require("./melcloudAtaDevice"); const MelCloudAtwDevice = require("./melcloudAtwDevice"); const commonDefines = require("./commonDefines"); const HttpStatus = require("http-status-codes"); const Axios = require("axios").default; const Https = require("https"); let gthat = null; // pointer to "this" from main.js/MelCloud instance let gthis = null; // pointer to "this" from MelcloudPlatform let pollingJob = null; // runs at user-defined interval to update data from MELCloud let contextKeyInvalidationTimeout = null; // fixed timeout to update context key from MELCloud const contextKeyInvalidationTimer = 12 * 60 * 60 * 1000; // time (in ms) after which a new context key should be obtained const maxRetries = 3; // number of connection retries when connection was lost const retryInterval = 60 * 60 * 1000; // time (in ms) after which a new reconnection try should be made class MelcloudPlatform { constructor(that) { gthat = that; gthis = this; this.language = that.config.melCloudLanguage; this.username = that.config.melCloudEmail; this.password = that.config.melCloudPassword; this.contextKey = ""; this.useFahrenheit = false; this.isConnected = false; this.retryCounter = 0; this.customHttpsAgent = that.config.ignoreSslErrors ? new Https.Agent({ rejectUnauthorized: false }) : null; } async GetContextKey(callback, callback2) { gthat.log.debug("Fetching context key..."); // Login const loginUrl = "https://app.melcloud.com/Mitsubishi.Wifi.Client/Login/ClientLogin"; const formData = new FormData(); formData.append("AppVersion", "1.32.1.0"); formData.append("CaptchaResponse", ""); formData.append("Email", gthis.username); formData.append("Language", gthis.language); formData.append("Password", gthis.password); formData.append("Persist", "true"); Axios({ url: loginUrl, method: "POST", data: formData, httpsAgent: gthis.customHttpsAgent, headers: { "Authority": "app.melcloud.com", "Accept": "application/json, text/javascript, */*; q=0.01", "Accept-Language": "de-DE,de;q=0.9,en-DE;q=0.8,en;q=0.7,en-US;q=0.6,la;q=0.5", "Origin": "https://app.melcloud.com/", "Referer": "https://app.melcloud.com/", "Sec-Fetch-Mode": "cors", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", "X-Requested-With": "XMLHttpRequest" }, }) .then(function handleLoginResponse(response) { if (!response) { gthat.log.error(`There was a problem receiving the response from: ${loginUrl}`); gthis.isConnected = false; gthat.setAdapterConnectionState(false); } else { const statusCode = response.status; const statusText = response.statusText; gthat.log.debug(`Received response from: ${loginUrl} (status code: ${statusCode} - ${statusText})`); if (statusCode != HttpStatus.StatusCodes.OK) { gthis.isConnected = false; gthat.setAdapterConnectionState(false); gthat.log.error(`Invalid HTTP status code (${statusCode} - ${statusText}). Login failed!`); return; } const responseData = response.data; gthat.log.debug(`Response from cloud: ${JSON.stringify(responseData)}`); if (responseData.LoginData == null) { let errText = `Login failed (error code: ${responseData.ErrorId})! Error message: `; switch (responseData.ErrorId) { case 1: errText += "Incorrect username and/or password. Verify your credentials in the adapter settings."; break; case 6: errText += "Too many failed login attempts, account is temporarily locked (max. 60min). Try again later."; break; default: errText += responseData.ErrorMessage; break; } gthat.log.error(errText); gthis.isConnected = false; gthat.setAdapterConnectionState(false); } else { gthis.useFahrenheit = responseData.LoginData.UseFahrenheit; gthis.contextKey = responseData.LoginData.ContextKey; contextKeyInvalidationTimeout = setTimeout(async function invalidateContextKey() { gthat.log.debug(`Context key invalidated. Getting a new one for the next request.`); gthis.GetContextKey(); // only get new context key without refreshing device data }, contextKeyInvalidationTimer); gthis.isConnected = true; gthat.setAdapterConnectionState(true); gthat.log.debug(`Login successful. ContextKey: ${gthis.contextKey}`); callback && gthis.GetDevices(callback, callback2); } } }).catch(error => { gthat.log.error(`There was a problem sending login to: ${loginUrl}`); gthat.log.error(error); if (error.response && error.response.status && error.response.status == 429) { gthat.log.error("You have probably been rate limited by the MELCloud servers because of too much requests. Stop the adapter for a few hours, increase the polling interval in the settings and try again later."); } gthis.isConnected = false; gthat.setAdapterConnectionState(false); }); } async GetDevices(callback, callback2) { gthat.log.debug("Fetching devices..."); const getDevicesUrl = "https://app.melcloud.com/Mitsubishi.Wifi.Client/User/ListDevices"; Axios.get(getDevicesUrl, { httpsAgent: gthis.customHttpsAgent, headers: { "Host": "app.melcloud.com", "X-MitsContextKey": gthis.contextKey } }).then(function handleDeviceResponse(response) { if (!response) { gthat.log.error(`There was a problem receiving the response from: ${getDevicesUrl}`); gthis.isConnected = false; gthat.setAdapterConnectionState(false); } else { const statusCode = response.status; const statusText = response.statusText; gthat.log.debug(`Received response from: ${getDevicesUrl} (status code: ${statusCode} - ${statusText})`); if (statusCode != HttpStatus.StatusCodes.OK) { gthis.isConnected = false; gthat.setAdapterConnectionState(false); gthat.log.error(`Invalid HTTP status code (${statusCode} - ${statusText}). Getting devices failed!`); return; } gthis.isConnected = true; gthat.setAdapterConnectionState(true); const responseData = response.data; if (responseData == null) return; gthat.log.debug(`Response from cloud: ${JSON.stringify(responseData)}`); const foundDevices = []; gthat.deviceObjects.length = 0; // clear main array first before adding new devices for (let b = 0; b < responseData.length; b++) { const building = responseData[b]; const devices = building.Structure.Devices; gthis.CreateDevices(building, devices, foundDevices); for (let f = 0; f < building.Structure.Floors.length; f++) { const devices = building.Structure.Floors[f].Devices; gthis.CreateDevices(building, devices, foundDevices); for (let a = 0; a < building.Structure.Floors[f].Areas.length; a++) { const devices = building.Structure.Floors[f].Areas[a].Devices; gthis.CreateDevices(building, devices, foundDevices); } } for (let a = 0; a < building.Structure.Areas.length; a++) { const devices = building.Structure.Areas[a].Devices; gthis.CreateDevices(building, devices, foundDevices); } } callback && callback(foundDevices, callback2); } }).catch(error => { gthat.log.error(`There was a problem getting devices from: ${getDevicesUrl}`); gthat.log.error(error); if (error.response && error.response.status && error.response.status == 429) { gthat.log.error("You have probably been rate limited by the MELCloud servers because of too much requests. Stop the adapter for a few hours, increase the polling interval in the settings and try again later."); } gthis.isConnected = false; gthat.setAdapterConnectionState(false); }); } CreateDevices(building, devices, foundDevices) { for (let d = 0; d < devices.length; d++) { const deviceJson = devices[d]; const deviceType = deviceJson.Device.DeviceType; switch (deviceType) { case commonDefines.DeviceTypes.AirToAir: gthis.CreateAtaDevice(building, deviceJson, foundDevices); break; case commonDefines.DeviceTypes.AirToWater: gthis.CreateAtwDevice(building, deviceJson, foundDevices); break; default: gthat.log.error(`Received unknown device type '${deviceType}'. Please report this to the developer!`); break; } } } CreateAtaDevice(building, deviceJson, foundDevices) { const newDevice = new MelCloudAtaDevice.MelCloudDevice(gthat); newDevice.platform = gthis; // "info" newDevice.id = deviceJson.DeviceID; newDevice.name = deviceJson.DeviceName; newDevice.serialNumber = deviceJson.SerialNumber; newDevice.macAddress = deviceJson.MacAddress; newDevice.buildingId = building.ID; newDevice.floorId = deviceJson.FloorID; newDevice.canCool = deviceJson.Device.CanCool; newDevice.canDry = deviceJson.Device.CanDry; newDevice.canHeat = deviceJson.Device.CanHeat; newDevice.minTempCoolDry = deviceJson.Device.MinTempCoolDry; newDevice.maxTempCoolDry = deviceJson.Device.MaxTempCoolDry; if (newDevice.canHeat) newDevice.minTempHeat = deviceJson.Device.MinTempHeat; if (newDevice.canHeat) newDevice.maxTempHeat = deviceJson.Device.MaxTempHeat; newDevice.minTempAuto = deviceJson.Device.MinTempAutomatic; newDevice.maxTempAuto = deviceJson.Device.MaxTempAutomatic; newDevice.roomTemp = deviceJson.Device.RoomTemperature; newDevice.actualFanSpeed = deviceJson.Device.ActualFanSpeed; newDevice.numberOfFanSpeeds = deviceJson.Device.NumberOfFanSpeeds; newDevice.lastCommunication = deviceJson.Device.LastTimeStamp; newDevice.deviceOnline = !deviceJson.Device.Offline; newDevice.deviceHasError = deviceJson.Device.HasError; newDevice.errorMessages = deviceJson.Device.ErrorMessages; newDevice.errorCode = deviceJson.Device.ErrorCode; // "control" newDevice.power = deviceJson.Device.Power; newDevice.operationMode = deviceJson.Device.OperationMode; newDevice.targetTemp = deviceJson.Device.SetTemperature; newDevice.fanSpeed = deviceJson.Device.FanSpeed; newDevice.vaneVerticalDirection = deviceJson.Device.VaneVerticalDirection; newDevice.vaneHorizontalDirection = deviceJson.Device.VaneHorizontalDirection; gthat.log.debug(`Got ATA device from cloud: ${deviceJson.DeviceID} (${deviceJson.DeviceName})`); foundDevices.push(newDevice); gthat.deviceObjects.push(newDevice); } CreateAtwDevice(building, deviceJson, foundDevices) { const newDevice = new MelCloudAtwDevice.MelCloudDevice(gthat); newDevice.platform = gthis; // "info" newDevice.id = deviceJson.DeviceID; newDevice.name = deviceJson.DeviceName; newDevice.serialNumber = deviceJson.SerialNumber; newDevice.macAddress = deviceJson.MacAddress; newDevice.buildingId = building.ID; newDevice.floorId = deviceJson.FloorID; newDevice.canCool = deviceJson.Device.CanCool; newDevice.canHeat = deviceJson.Device.CanHeat; newDevice.hasZone2 = deviceJson.Device.HasZone2; newDevice.roomTemperatureZone1 = deviceJson.Device.RoomTemperatureZone1; if (newDevice.hasZone2) newDevice.roomTemperatureZone2 = deviceJson.Device.RoomTemperatureZone2; newDevice.mixingTankWaterTemperature = deviceJson.Device.MixingTankWaterTemperature; newDevice.condensingTemperature = deviceJson.Device.CondensingTemperature; newDevice.outdoorTemperature = deviceJson.Device.OutdoorTemperature; newDevice.flowTemperature = deviceJson.Device.FlowTemperature; newDevice.flowTemperatureZone1 = deviceJson.Device.FlowTemperatureZone1; if (newDevice.hasZone2) newDevice.flowTemperatureZone2 = deviceJson.Device.FlowTemperatureZone2; newDevice.flowTemperatureBoiler = deviceJson.Device.FlowTemperatureBoiler; newDevice.returnTemperature = deviceJson.Device.ReturnTemperature; newDevice.returnTemperatureZone1 = deviceJson.Device.ReturnTemperatureZone1; if (newDevice.hasZone2) newDevice.ReturnTemperatureZone2 = deviceJson.Device.ReturnTemperatureZone2; newDevice.returnTemperatureBoiler = deviceJson.Device.ReturnTemperatureBoiler; newDevice.tankWaterTemperature = deviceJson.Device.TankWaterTemperature; newDevice.heatPumpFrequency = deviceJson.Device.HeatPumpFrequency; newDevice.operationState = deviceJson.Device.OperationMode; newDevice.lastCommunication = deviceJson.Device.LastTimeStamp; newDevice.deviceOnline = !deviceJson.Device.Offline; newDevice.deviceHasError = deviceJson.Device.HasError; newDevice.errorMessages = deviceJson.Device.ErrorMessages; newDevice.errorCode = deviceJson.Device.ErrorCode; // "control" newDevice.power = deviceJson.Device.Power; newDevice.forcedHotWaterMode = deviceJson.Device.ForcedHotWaterMode; newDevice.operationModeZone1 = deviceJson.Device.OperationModeZone1; if (newDevice.hasZone2) newDevice.operationModeZone2 = deviceJson.Device.OperationModeZone2; newDevice.setTankWaterTemperature = deviceJson.Device.SetTankWaterTemperature; newDevice.setTemperatureZone1 = deviceJson.Device.SetTemperatureZone1; if (newDevice.hasZone2) newDevice.setTemperatureZone2 = deviceJson.Device.SetTemperatureZone2; newDevice.setHeatFlowTemperatureZone1 = deviceJson.Device.SetHeatFlowTemperatureZone1; if (newDevice.hasZone2) newDevice.setHeatFlowTemperatureZone2 = deviceJson.Device.SetHeatFlowTemperatureZone2; newDevice.setCoolFlowTemperatureZone1 = deviceJson.Device.SetCoolFlowTemperatureZone1; if (newDevice.hasZone2) newDevice.setCoolFlowTemperatureZone2 = deviceJson.Device.SetCoolFlowTemperatureZone2; gthat.log.debug(`Got ATW device from cloud: ${deviceJson.DeviceID} (${deviceJson.DeviceName})`); foundDevices.push(newDevice); gthat.deviceObjects.push(newDevice); } async CreateAndSaveDevices(newDevices, callback) { gthat.log.debug("Saving device data..."); const currentDeviceIDs = gthat.currentKnownDeviceIDs; const newDeviceIDs = []; for (let i = 0; i < newDevices.length; i++) { const newDevice = newDevices[i]; newDeviceIDs.push(newDevice.id); } const deviceIDsToDelete = currentDeviceIDs.filter(x => !newDeviceIDs.includes(x)); for (let d = 0; d < deviceIDsToDelete.length; d++) { const deviceID = deviceIDsToDelete[d]; gthat.deleteMelDevice(deviceID); } for (let d = 0; d < newDevices.length; d++) { const device = newDevices[d]; await device.CreateAndSave(); // create device states initially if not exisiting await device.UpdateDeviceData("ALL"); // update all device data with values from cloud } callback && callback(); } // Update data regularly according to pollingInterval (if enabled) startPolling() { let jobInterval = gthat.config.pollingInterval * 60000 + Math.floor(Math.random() * 5 * 1000); // polling interval in milliseconds plus some random amount between 0 and 5000ms pollingJob = setTimeout(async function updateData() { jobInterval = gthat.config.pollingInterval * 60000 + Math.floor(Math.random() * 5 * 1000); // slightly change interval every run gthis.GetDevices(gthis.CreateAndSaveDevices); if (gthis.isConnected) { gthis.retryCounter = 0; pollingJob = setTimeout(updateData, jobInterval); } else { if (gthis.retryCounter < maxRetries) { gthis.retryCounter++; gthat.log.warn(`Connection to MELCloud lost - reconnecting (try ${gthis.retryCounter} of ${maxRetries})...`); pollingJob = setTimeout(updateData, jobInterval); } else { gthis.retryCounter = 0; gthat.log.error("Connection to MELCloud lost, polling temporarily disabled! Trying again in one hour."); pollingJob = setTimeout(updateData, retryInterval); } } }, jobInterval); } stopPolling() { clearTimeout(pollingJob); gthat.log.debug("Cleared polling timer."); } stopContextKeyInvalidation() { clearTimeout(contextKeyInvalidationTimeout); gthat.log.debug("Cleared context key invalidation timer."); } } exports.MelCloudPlatform = MelcloudPlatform;