iobroker.melcloud
Version:
462 lines (404 loc) • 18.1 kB
JavaScript
"use strict";
const FormData = require("form-data");
const MelCloudAtaDevice = require("./melcloudAtaDevice");
const MelCloudAtwDevice = require("./melcloudAtwDevice");
const MelCloudErvDevice = require("./melcloudErvDevice");
const commonDefines = require("./commonDefines");
const HttpStatus = require("http-status-codes");
const Axios = require("axios").default;
const Https = require("https");
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(adapter) {
this.adapter = adapter; // store adapter instance
this.language = adapter.config.melCloudLanguage;
this.username = adapter.config.melCloudEmail;
this.password = adapter.config.melCloudPassword;
this.contextKey = "";
this.useFahrenheit = false;
this.isConnected = false;
this.retryCounter = 0;
this.customHttpsAgent = adapter.config.ignoreSslErrors ? new Https.Agent({ rejectUnauthorized: false }) : null;
}
async GetContextKey(callback, callback2) {
this.adapter.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.35.1.0");
formData.append("CaptchaResponse", "");
formData.append("Email", this.username);
formData.append("Language", this.language);
formData.append("Password", this.password);
formData.append("Persist", "true");
Axios({
url: loginUrl,
method: "POST",
data: formData,
httpsAgent: this.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(response => {
if (!response) {
this.adapter.log.error(`There was a problem receiving the response from: ${loginUrl}`);
this.isConnected = false;
this.adapter.setAdapterConnectionState(false);
} else {
const statusCode = response.status;
const statusText = response.statusText;
this.adapter.log.debug(
`Received response from: ${loginUrl} (status code: ${statusCode} - ${statusText})`,
);
if (statusCode != HttpStatus.StatusCodes.OK) {
this.isConnected = false;
this.adapter.setAdapterConnectionState(false);
this.adapter.log.error(
`Invalid HTTP status code (${statusCode} - ${statusText}). Login failed!`,
);
return;
}
const responseData = response.data;
this.adapter.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;
}
this.adapter.log.error(errText);
this.isConnected = false;
this.adapter.setAdapterConnectionState(false);
} else {
this.useFahrenheit = responseData.LoginData.UseFahrenheit;
this.contextKey = responseData.LoginData.ContextKey;
contextKeyInvalidationTimeout = setTimeout(() => {
this.adapter.log.debug(`Context key invalidated. Getting a new one for the next request.`);
this.GetContextKey(); // only get new context key without refreshing device data
}, contextKeyInvalidationTimer);
this.isConnected = true;
this.adapter.setAdapterConnectionState(true);
this.adapter.log.debug(`Login successful. ContextKey: ${this.contextKey}`);
callback && this.GetDevices(callback, callback2);
}
}
})
.catch(error => {
this.adapter.log.error(`There was a problem sending login to: ${loginUrl}`);
this.adapter.log.error(error);
if (error.response && error.response.status && error.response.status == 429) {
this.adapter.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.",
);
}
this.isConnected = false;
this.adapter.setAdapterConnectionState(false);
});
}
async GetDevices(callback, callback2) {
this.adapter.log.debug("Fetching devices...");
const getDevicesUrl = "https://app.melcloud.com/Mitsubishi.Wifi.Client/User/ListDevices";
Axios.get(getDevicesUrl, {
httpsAgent: this.customHttpsAgent,
headers: {
Host: "app.melcloud.com",
"X-MitsContextKey": this.contextKey,
},
})
.then(response => {
if (!response) {
this.adapter.log.error(`There was a problem receiving the response from: ${getDevicesUrl}`);
this.isConnected = false;
this.adapter.setAdapterConnectionState(false);
} else {
const statusCode = response.status;
const statusText = response.statusText;
this.adapter.log.debug(
`Received response from: ${getDevicesUrl} (status code: ${statusCode} - ${statusText})`,
);
if (statusCode != HttpStatus.StatusCodes.OK) {
this.isConnected = false;
this.adapter.setAdapterConnectionState(false);
this.adapter.log.error(
`Invalid HTTP status code (${statusCode} - ${statusText}). Getting devices failed!`,
);
return;
}
this.isConnected = true;
this.adapter.setAdapterConnectionState(true);
const responseData = response.data;
if (responseData == null) {
return;
}
this.adapter.log.debug(`Response from cloud: ${JSON.stringify(responseData)}`);
const foundDevices = [];
this.adapter.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;
this.CreateDevices(building, devices, foundDevices);
for (let f = 0; f < building.Structure.Floors.length; f++) {
const devices = building.Structure.Floors[f].Devices;
this.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;
this.CreateDevices(building, devices, foundDevices);
}
}
for (let a = 0; a < building.Structure.Areas.length; a++) {
const devices = building.Structure.Areas[a].Devices;
this.CreateDevices(building, devices, foundDevices);
}
}
callback && callback(foundDevices, callback2);
}
})
.catch(error => {
this.adapter.log.error(`There was a problem getting devices from: ${getDevicesUrl}`);
this.adapter.log.error(error);
if (error.response && error.response.status && error.response.status == 429) {
this.adapter.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.",
);
}
this.isConnected = false;
this.adapter.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:
this.CreateAtaDevice(building, deviceJson, foundDevices);
break;
case commonDefines.DeviceTypes.AirToWater:
this.CreateAtwDevice(building, deviceJson, foundDevices);
break;
case commonDefines.DeviceTypes.EnergyRecoveryVentilation:
this.CreateErvDevice(building, deviceJson, foundDevices);
break;
default:
this.adapter.log.error(
`Received unknown device type '${deviceType}'. Please report this to the developer!`,
);
break;
}
}
}
CreateAtaDevice(building, deviceJson, foundDevices) {
const newDevice = new MelCloudAtaDevice.MelCloudDevice(this.adapter, this);
// "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;
this.adapter.log.debug(`Got ATA device from cloud: ${deviceJson.DeviceID} (${deviceJson.DeviceName})`);
foundDevices.push(newDevice);
this.adapter.deviceObjects.push(newDevice);
}
CreateAtwDevice(building, deviceJson, foundDevices) {
const newDevice = new MelCloudAtwDevice.MelCloudDevice(this.adapter, this);
// "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;
}
this.adapter.log.debug(`Got ATW device from cloud: ${deviceJson.DeviceID} (${deviceJson.DeviceName})`);
foundDevices.push(newDevice);
this.adapter.deviceObjects.push(newDevice);
}
CreateErvDevice(building, deviceJson, foundDevices) {
const newDevice = new MelCloudErvDevice.MelCloudDevice(this.adapter, this);
// "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.minTempCoolDry = deviceJson.Device.MinTempCoolDry;
newDevice.maxTempCoolDry = deviceJson.Device.MaxTempCoolDry;
newDevice.minTempHeat = deviceJson.Device.MinTempHeat;
newDevice.maxTempHeat = deviceJson.Device.MaxTempHeat;
newDevice.minTempAuto = deviceJson.Device.MinTempAutomatic;
newDevice.maxTempAuto = deviceJson.Device.MaxTempAutomatic;
newDevice.roomTemp = deviceJson.Device.RoomTemperature;
newDevice.outdoorTemp = deviceJson.Device.OutdoorTemperature;
newDevice.actualSupplyFanSpeed = deviceJson.Device.ActualSupplyFanSpeed;
newDevice.actualExhaustFanSpeed = deviceJson.Device.ActualExhaustFanSpeed;
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.fanSpeed = deviceJson.Device.SetFanSpeed;
this.adapter.log.debug(`Got ERV device from cloud: ${deviceJson.DeviceID} (${deviceJson.DeviceName})`);
foundDevices.push(newDevice);
this.adapter.deviceObjects.push(newDevice);
}
async CreateAndSaveDevices(newDevices, callback) {
this.adapter.log.debug("Saving device data...");
const currentDeviceIDs = this.adapter.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];
this.adapter.deleteMelDevice(deviceID);
}
for (let d = 0; d < newDevices.length; d++) {
const device = newDevices[d];
await device.CreateAndSave();
await device.UpdateDeviceData("ALL");
}
callback && callback();
}
startPolling() {
let jobInterval = this.adapter.config.pollingInterval * 60000 + Math.floor(Math.random() * 5 * 1000);
const updateData = async () => {
jobInterval = this.adapter.config.pollingInterval * 60000 + Math.floor(Math.random() * 5 * 1000);
this.GetDevices(this.CreateAndSaveDevices.bind(this));
if (this.isConnected) {
this.retryCounter = 0;
pollingJob = setTimeout(updateData, jobInterval);
} else {
if (this.retryCounter < maxRetries) {
this.retryCounter++;
this.adapter.log.warn(
`Connection to MELCloud lost - reconnecting (try ${this.retryCounter} of ${maxRetries})...`,
);
pollingJob = setTimeout(updateData, jobInterval);
} else {
this.retryCounter = 0;
this.adapter.log.error(
"Connection to MELCloud lost, polling temporarily disabled! Trying again in one hour.",
);
pollingJob = setTimeout(updateData, retryInterval);
}
}
};
pollingJob = setTimeout(updateData, jobInterval);
}
stopPolling() {
clearTimeout(pollingJob);
this.adapter.log.debug("Cleared polling timer.");
}
stopContextKeyInvalidation() {
clearTimeout(contextKeyInvalidationTimeout);
this.adapter.log.debug("Cleared context key invalidation timer.");
}
}
exports.MelCloudPlatform = MelcloudPlatform;