homebridge-melcloud2
Version:
Homebridge plugin for Mitsubishi Melcloud
521 lines (454 loc) • 16.8 kB
JavaScript
// Fibaro Melcloud Platform plugin for HomeBridge
//
// Remember to add platform to config.json. Example:
// "platforms": [
// {
// "platform": "Melcloud",
// "name": "Melcloud",
// "username": "PUT USERNAME OF YOUR MELCLOUD ACCOUNT HERE",
// "password": "PUT PASSWORD OF YOUR MELCLOUD ACCOUNT HERE"
// }
// ],
//
// When you attempt to add a device, it will ask for a "PIN code".
// The default code for all HomeBridge accessories is 031-45-154.
let Service;
let Characteristic;
const request = require('request');
class MelcloudBridgedAccessory {
constructor(services) {
this.services = services;
}
getServices() {
const services = [];
const informationService = this.platform.getInformationService(this);
services.push(informationService);
for (let s = 0; s < this.services.length; s++) {
const service = this.services[s];
for (let i = 0; i < service.characteristics.length; i++) {
let characteristic = service.controlService.getCharacteristic(service.characteristics[i]);
if (!characteristic) {
characteristic = service.controlService.addCharacteristic(service.characteristics[i]);
}
this.platform.bindCharacteristicEvents(characteristic, service, this);
}
services.push(service.controlService);
}
return services;
}
}
class MelcloudPlatform {
constructor(log, config) {
this.log = log;
this.language = config.language;
this.username = config.username;
this.password = config.password;
this.ContextKey = null;
this.UseFahrenheit = null;
this.CurrentHeatingCoolingStateUUID = (new Characteristic.CurrentHeatingCoolingState()).UUID;
this.TargetHeatingCoolingStateUUID = (new Characteristic.TargetHeatingCoolingState()).UUID;
this.CurrentTemperatureUUID = (new Characteristic.CurrentTemperature()).UUID;
this.TargetTemperatureUUID = (new Characteristic.TargetTemperature()).UUID;
this.TemperatureDisplayUnitsUUID = (new Characteristic.TemperatureDisplayUnits()).UUID;
this.RotationSpeedUUID = (new Characteristic.RotationSpeed()).UUID;
this.CurrentHorizontalTiltAngleUUID = (new Characteristic.CurrentHorizontalTiltAngle()).UUID;
this.TargetHorizontalTiltAngleUUID = (new Characteristic.TargetHorizontalTiltAngle()).UUID;
this.CurrentVerticalTiltAngleUUID = (new Characteristic.CurrentVerticalTiltAngle()).UUID;
this.TargetVerticalTiltAngleUUID = (new Characteristic.TargetVerticalTiltAngle()).UUID;
this.currentAirInfoExecution = 0;
this.airInfoExecutionPending = [];
this.model = config.model;
this.manufacturer = config.manufacturer;
this.serialNumber = config.serialNumber;
}
accessories(callback) {
this.log('Fetching Melcloud devices...');
// login
const url = 'https://app.melcloud.com/Mitsubishi.Wifi.Client/Login/ClientLogin';
const form = {
AppVersion: '1.9.3.0',
CaptchaChallenge: '',
CaptchaResponse: '',
Email: this.username,
Language: this.language,
Password: this.password,
Persist: 'true',
};
const method = 'post';
request({ url, form, method }, (err, response) => {
if (err) {
this.log(`There was a problem sending login to: ${url}`);
this.log(err);
return callback([]);
}
try {
const r = JSON.parse(response.body);
this.ContextKey = r.LoginData.ContextKey;
this.UseFahrenheit = r.LoginData.UseFahrenheit;
this.log(`ContextKey: ${this.ContextKey}`);
return this.getDevices(callback);
} catch (e) {
this.log(`There was a problem parsing login request: ${url}`);
this.log(e);
return callback(e);
}
});
}
getDevices(callback) {
const url = 'https://app.melcloud.com/Mitsubishi.Wifi.Client/User/ListDevices';
const method = 'get';
request({
url,
method,
headers: {
'X-MitsContextKey': this.ContextKey,
},
}, (err, response) => {
if (err) {
this.log(`There was a problem getting devices from: ${url}`);
this.log(err);
return;
}
try {
const r = JSON.parse(response.body);
const foundAccessories = [];
for (let b = 0; b < r.length; b++) {
const building = r[b];
let devices = building.Structure.Devices;
this.createAccessories(building, devices, foundAccessories);
for (let f = 0; f < building.Structure.Floors.length; f++) {
devices = building.Structure.Floors[f].Devices;
this.createAccessories(building, devices, foundAccessories);
for (let a = 0; a < building.Structure.Floors[f].Areas.length; a++) {
devices = building.Structure.Floors[f].Areas[a].Devices;
this.createAccessories(building, devices, foundAccessories);
}
}
for (let a = 0; a < building.Structure.Areas.length; a++) {
devices = building.Structure.Areas[a].Devices;
this.createAccessories(building, devices, foundAccessories);
}
}
callback(foundAccessories);
} catch (e) {
this.log(`There was a problem parsing devices from: ${url}`);
this.log(e);
}
});
}
createAccessories(building, devices, foundAccessories) {
for (let d = 0; d < devices.length; d++) {
const device = devices[d];
const accessory = new MelcloudBridgedAccessory([
{
controlService: new Service.Thermostat(device.DeviceName),
characteristics: [
Characteristic.CurrentHeatingCoolingState,
Characteristic.TargetHeatingCoolingState,
Characteristic.CurrentTemperature,
Characteristic.TargetTemperature,
Characteristic.TemperatureDisplayUnits,
Characteristic.RotationSpeed,
Characteristic.CurrentHorizontalTiltAngle,
Characteristic.TargetHorizontalTiltAngle,
Characteristic.CurrentVerticalTiltAngle,
Characteristic.TargetVerticalTiltAngle,
],
},
]);
accessory.platform = this;
accessory.remoteAccessory = device;
accessory.id = device.DeviceID;
accessory.name = device.DeviceName;
accessory.model = this.model || '';
accessory.manufacturer = this.manufacturer || 'Mitsubishi';
accessory.serialNumber = this.serialNumber || device.SerialNumber;
accessory.airInfo = null;
accessory.buildingId = building.ID;
this.log(`Found device: ${device.DeviceName}`);
foundAccessories.push(accessory);
}
}
proxyAirInfo(callback, characteristic, service, homebridgeAccessory, value, operation) {
if (homebridgeAccessory.airInfo != null) {
this.log(`Data already available for: ${homebridgeAccessory.name} - ${characteristic.displayName}`);
operation(callback, characteristic, service, homebridgeAccessory, value);
if (this.airInfoExecutionPending.length) {
const args = this.airInfoExecutionPending.shift();
this.log(`Dequeuing remote request for. ${args[3].name} - ${args[1].displayName}`);
this.proxyAirInfo(...args);
}
return;
}
this.log(`Getting data for: ${homebridgeAccessory.name} - ${characteristic.displayName}`);
if (this.currentAirInfoExecution < 1) {
homebridgeAccessory.airInfoRequestSent = true;
this.currentAirInfoExecution++;
const url = `https://app.melcloud.com/Mitsubishi.Wifi.Client/Device/Get?id=${homebridgeAccessory.id}&buildingID=${homebridgeAccessory.buildingId}`;
const method = 'get';
const that = this;
request({
url,
method,
headers: {
'X-MitsContextKey': homebridgeAccessory.platform.ContextKey,
},
}, (err, response) => {
if (err || response.body.search('<!DOCTYPE html>') !== -1) {
that.log(`There was a problem getting info from: ${url}`);
that.log(`for device: ${homebridgeAccessory.name}`);
that.log(`Error: ${err}`);
homebridgeAccessory.airInfo = null;
callback();
} else {
try {
homebridgeAccessory.airInfo = JSON.parse(response.body);
operation(callback, characteristic, service, homebridgeAccessory, value);
// cache airInfo data for 1 minutes
setTimeout(() => {
homebridgeAccessory.airInfo = null;
}, 60 * 1000);
} catch (e) {
that.log(`Error: ${e}`);
}
}
that.currentAirInfoExecution--;
if (that.airInfoExecutionPending.length) {
const args = that.airInfoExecutionPending.shift();
that.log(`Dequeuing remote request for: ${args[3].name} - ${args[1].displayName}`);
that.proxyAirInfo(...args);
}
});
} else {
this.log(`Queing remote request data for: ${homebridgeAccessory.name} - ${characteristic.displayName}`);
this.airInfoExecutionPending.push(arguments);
}
}
getAccessoryValue(callback, characteristic, service, homebridgeAccessory) {
const r = homebridgeAccessory.airInfo;
if (characteristic.UUID === homebridgeAccessory.platform.CurrentHeatingCoolingStateUUID) {
if (r.Power === false) {
callback(undefined, Characteristic.CurrentHeatingCoolingState.OFF);
} else {
switch (r.OperationMode) {
case 1:
callback(undefined, Characteristic.CurrentHeatingCoolingState.HEAT);
return;
case 3:
callback(undefined, Characteristic.CurrentHeatingCoolingState.COOL);
return;
default:
// Melcloud can return also 2 (deumidity), 7 (Ventilation), 8 (auto)
// We try to return 5 which is undefined in homekit
callback(undefined, 5);
}
}
} else if (characteristic.UUID === homebridgeAccessory.platform.TargetHeatingCoolingStateUUID) {
if (r.Power === false) {
callback(undefined, Characteristic.TargetHeatingCoolingState.OFF);
} else {
switch (r.OperationMode) {
case 1:
callback(undefined, Characteristic.TargetHeatingCoolingState.HEAT);
return;
case 3:
callback(undefined, Characteristic.TargetHeatingCoolingState.COOL);
return;
case 8:
callback(undefined, Characteristic.TargetHeatingCoolingState.AUTO);
return;
default:
// Melcloud can return also 2 (deumidity), 7 (Ventilation)
// We try to return 5 which is undefined in homekit
callback(undefined, 5);
}
}
} else if (characteristic.UUID === homebridgeAccessory.platform.CurrentTemperatureUUID) {
callback(undefined, r.RoomTemperature);
} else if (characteristic.UUID === homebridgeAccessory.platform.TargetTemperatureUUID) {
callback(undefined, r.SetTemperature);
} else if (characteristic.UUID === homebridgeAccessory.platform.TemperatureDisplayUnitsUUID) {
if (homebridgeAccessory.platform.UseFahrenheit) {
callback(undefined, Characteristic.TemperatureDisplayUnits.FAHRENHEIT);
} else {
callback(undefined, Characteristic.TemperatureDisplayUnits.CELSIUS);
}
} else if (characteristic.UUID === homebridgeAccessory.platform.RotationSpeedUUID) {
const { SetFanSpeed } = r;
const { NumberOfFanSpeeds } = r;
const fanSpeed = (SetFanSpeed / NumberOfFanSpeeds) * 100.0;
callback(undefined, fanSpeed);
} else if (characteristic.UUID === homebridgeAccessory.platform.CurrentHorizontalTiltAngleUUID
|| characteristic.UUID === homebridgeAccessory.platform.TargetHorizontalTiltAngleUUID) {
const { VaneHorizontal } = r;
const HorizontalTilt = -90.0 + 45.0 * (VaneHorizontal - 1);
callback(undefined, HorizontalTilt);
} else if (characteristic.UUID === homebridgeAccessory.platform.CurrentVerticalTiltAngleUUID
|| characteristic.UUID === homebridgeAccessory.platform.TargetVerticalTiltAngleUUID) {
const { VaneVertical } = r;
const VerticallTilt = 90.0 - 45.0 * (5 - VaneVertical);
callback(undefined, VerticallTilt);
} else {
callback(undefined, 0);
}
}
setAccessoryValue(callback, characteristic, service, homebridgeAccessory, value) {
const r = homebridgeAccessory.airInfo;
if (characteristic.UUID === homebridgeAccessory.platform.TargetHeatingCoolingStateUUID) {
switch (value) {
case Characteristic.TargetHeatingCoolingState.OFF:
r.Power = false;
r.EffectiveFlags = 1;
break;
case Characteristic.TargetHeatingCoolingState.HEAT:
r.Power = true;
r.OperationMode = 1;
r.EffectiveFlags = 1 + 2;
break;
case Characteristic.TargetHeatingCoolingState.COOL:
r.Power = true;
r.OperationMode = 3;
r.EffectiveFlags = 1 + 2;
break;
case Characteristic.TargetHeatingCoolingState.AUTO:
r.Power = true;
r.OperationMode = 8;
r.EffectiveFlags = 1 + 2;
break;
default:
callback();
return;
}
} else if (characteristic.UUID === homebridgeAccessory.platform.TargetTemperatureUUID) {
r.SetTemperature = value;
r.EffectiveFlags = 4;
} else if (characteristic.UUID === homebridgeAccessory.platform.TemperatureDisplayUnitsUUID) {
let UseFahrenheit = false;
if (value === Characteristic.TemperatureDisplayUnits.FAHRENHEIT) UseFahrenheit = true;
homebridgeAccessory.platform.updateApplicationOptions(UseFahrenheit);
homebridgeAccessory.platform.UseFahrenheit = UseFahrenheit;
callback();
return;
} else if (characteristic.UUID === homebridgeAccessory.platform.RotationSpeedUUID) {
r.SetFanSpeed = ((value / 100.0) * r.NumberOfFanSpeeds).toFixed(0);
r.EffectiveFlags = 8;
} else if (characteristic.UUID === homebridgeAccessory.platform.TargetHorizontalTiltAngleUUID) {
r.VaneHorizontal = ((value + 90.0) / 45.0 + 1.0).toFixed(0);
r.EffectiveFlags = 256;
} else if (characteristic.UUID === homebridgeAccessory.platform.TargetVerticalTiltAngleUUID) {
r.VaneVertical = ((value + 90.0) / 45.0 + 1.0).toFixed(0);
r.EffectiveFlags = 16;
} else {
callback();
return;
}
const url = 'https://app.melcloud.com/Mitsubishi.Wifi.Client/Device/SetAta';
const method = 'post';
const body = JSON.stringify(homebridgeAccessory.airInfo);
const that = this;
request({
url,
method,
body,
headers: {
'X-MitsContextKey': homebridgeAccessory.platform.ContextKey,
'content-type': 'application/json',
},
}, (err) => {
if (err) {
that.log(`There was a problem setting info to: ${url}`);
that.log(err);
}
callback();
});
}
updateApplicationOptions(UseFahrenheit) {
const url = 'https://app.melcloud.com/Mitsubishi.Wifi.Client/User/UpdateApplicationOptions';
const method = 'post';
const body = `{UseFahrenheit:${UseFahrenheit},EmailOnCommsError:false,EmailOnUnitError:false,EmailCommsErrors:1,EmailUnitErrors:1,RestorePages:false,MarketingCommunication:false,AlternateEmailAddress:"",Fred:4}`;
const that = this;
request({
url,
method,
body,
headers: {
'X-MitsContextKey': this.ContextKey,
'content-type': 'application/json',
},
}, (err) => {
if (err) {
that.log(`There was a problem setting Application Option to: ${url}`);
that.log(err);
}
});
}
getInformationService(homebridgeAccessory) {
const informationService = new Service.AccessoryInformation();
informationService
.setCharacteristic(Characteristic.Name, homebridgeAccessory.name)
.setCharacteristic(Characteristic.Manufacturer, homebridgeAccessory.manufacturer)
.setCharacteristic(Characteristic.Model, homebridgeAccessory.model)
.setCharacteristic(Characteristic.SerialNumber, homebridgeAccessory.serialNumber);
return informationService;
}
bindCharacteristicEvents(characteristic, service, homebridgeAccessory) {
let readOnly = true;
for (let i = 0; i < characteristic.props.perms.length; i++) if (characteristic.props.perms[i] === 'pw') readOnly = false;
if (!readOnly) {
characteristic
.on('set', (value, callback, context) => {
if (context !== 'fromMelcloud') {
homebridgeAccessory.platform.proxyAirInfo(
callback,
characteristic,
service,
homebridgeAccessory,
value,
homebridgeAccessory.platform.setAccessoryValue,
);
}
});
}
characteristic.on('get', (callback) => {
homebridgeAccessory.platform.proxyAirInfo(
callback,
characteristic,
service,
homebridgeAccessory,
null,
homebridgeAccessory.platform.getAccessoryValue,
);
});
}
getServices(homebridgeAccessory) {
const services = [];
const informationService = homebridgeAccessory.platform.getInformationService(
homebridgeAccessory,
);
services.push(informationService);
for (let s = 0; s < homebridgeAccessory.services.length; s++) {
const service = homebridgeAccessory.services[s];
for (let i = 0; i < service.characteristics.length; i++) {
let characteristic = service.controlService.getCharacteristic(
service.characteristics[i],
);
if (!characteristic) {
characteristic = service
.controlService.addCharacteristic(service.characteristics[i]);
}
homebridgeAccessory.platform.bindCharacteristicEvents(
characteristic,
service,
homebridgeAccessory,
);
}
services.push(service.controlService);
}
return services;
}
}
module.exports = (homebridge) => {
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
homebridge.registerPlatform('homebridge-melcloud2', 'melcloud2', MelcloudPlatform);
};