homebridge-melcloud-control
Version:
Homebridge plugin to control Mitsubishi Air Conditioner, Heat Pump and Energy Recovery Ventilation.
287 lines (257 loc) • 14.3 kB
JavaScript
import EventEmitter from 'events';
import Functions from './functions.js';
import { ApiUrls, HeatPump } from './constants.js';
class MelCloudAtw extends EventEmitter {
constructor(account, device, defaultTempsFile, melCloudClass) {
super();
this.accountTypeMelCloud = account.type === 'melcloud';
this.logWarn = account.log?.warn;
this.logError = account.log?.error;
this.logDebug = account.log?.debug;
this.restFulEnabled = account.restFul?.enable;
this.mqttEnabled = account.mqtt?.enable;
this.deviceId = device.id;
this.defaultTempsFile = defaultTempsFile;
this.functions = new Functions(this.logWarn, this.logError, this.logDebug)
.on('warn', warn => this.emit('warn', warn))
.on('error', error => this.emit('error', error))
.on('debug', debug => this.emit('debug', debug));
//set default values
this.deviceData = {};
this.client = melCloudClass.client;
//handle melcloud events
melCloudClass.on('client', (client) => {
this.client = client;
}).on(this.deviceId, async (type, message) => {
switch (type) {
case 'ws':
try {
const deviceData = structuredClone(this.deviceData);
const messageType = message.messageType;
const messageData = message.Data;
const settings = this.functions.parseArrayNameValue(messageData.settings);
switch (messageType) {
case 'unitStateChanged':
//update values
for (const [key, value] of Object.entries(settings)) {
if (!this.functions.isValidValue(value)) continue;
//update holiday mode
if (key === 'HolidayMode') {
deviceData.HolidayMode.Enabled = value;
continue;
}
//update device settings
if (key in deviceData.Device) {
deviceData.Device[key] = value;
}
}
break;
case 'atwUnitFrostProtectionTriggered':
deviceData.FrostProtection.Active = messageData.active;
//update device settings
for (const [key, value] of Object.entries(settings)) {
if (!this.functions.isValidValue(value) || key === 'SetTemperature') continue;
if (key in deviceData.Device) {
deviceData.Device[key] = value;
}
}
break;
case 'unitHolidayModeTriggered':
deviceData.Device.Power = settings.Power;
deviceData.HolidayMode.Enabled = settings.HolidayMode;
deviceData.HolidayMode.Active = messageData.active;
break;
case 'unitWifiSignalChanged':
deviceData.Rssi = messageData.rssi;
break;
case 'unitCommunicationRestored':
deviceData.Device.IsConnected = true;
break;
default:
if (this.logDebug) this.emit('debug', `Unit ${this.deviceId}, received unknown message type: ${messageType}`);
return;
}
//update state
if (this.logDebug) this.emit('debug', `Web socket update unit ${this.deviceId} settings: ${JSON.stringify(deviceData.Device, null, 2)}`);
await this.updateState('ws', deviceData);
} catch (error) {
if (this.logError) this.emit('error', `Web socket unit ${this.deviceId} process message error: ${error}`);
}
break;
case 'request':
try {
//update device data
const deviceData = structuredClone(this.deviceData);
Object.assign(deviceData, message);
//update state
if (this.logDebug) this.emit('debug', `Request update unit ${this.deviceId} settings: ${JSON.stringify(deviceData.Device, null, 2)}`);
await this.updateState('request', deviceData);
} catch (error) {
if (this.logError) this.emit('error', `Request unit ${this.deviceId} process message error: ${error}`);
}
break;
default:
if (this.logDebug) this.emit('debug', `Unit ${this.deviceId}, received unknown event type: ${type}`);
return;
}
});
}
async updateState(type, deviceData) {
try {
if (!this.accountTypeMelCloud) {
deviceData.Device.OperationMode = HeatPump.OperationModeMapStringToEnum[deviceData.Device.OperationMode] ?? deviceData.Device.OperationMode;
deviceData.Device.OperationModeZone1 = HeatPump.OperationModeZoneMapStringToEnum[deviceData.Device.OperationModeZone1] ?? deviceData.Device.OperationModeZone1;
deviceData.Device.OperationModeZone2 = HeatPump.OperationModeZoneMapStringToEnum[deviceData.Device.OperationModeZone2] ?? deviceData.Device.OperationModeZone2;
deviceData.Device.HasHotWaterTank = deviceData.Device.HasHotWater ?? false;
}
if (this.logDebug) this.emit('debug', `Device Data: ${JSON.stringify(deviceData, null, 2)}`);
//device
const serialNumber = deviceData.SerialNumber || '4.0.0';
const firmwareAppVersion = deviceData.Device?.FirmwareAppVersion || '4.0.0';
const hasHotWaterTank = deviceData.Device?.HasHotWaterTank || false;
const hasZone2 = ![false, 0, 'None', null, undefined].includes(deviceData?.Device?.HasZone2);
const ftcModel = deviceData.FtcModel;
deviceData.Device.HasZone2 = hasZone2;
//units
const units = Array.isArray(deviceData.Device?.Units) ? deviceData.Device?.Units : [];
const { indoor, outdoor } = units.reduce((acc, unit) => {
const target = unit.IsIndoor ? 'indoor' : 'outdoor';
acc[target] = {
id: unit.ID,
device: unit.Device,
serialNumber: unit.SerialNumber ?? 'Undefined',
modelNumber: unit.ModelNumber ?? 0,
model: unit.Model ?? false,
type: unit.UnitType ?? 0
};
return acc;
}, { indoor: {}, outdoor: {} });
//filter info
const { Device: _ignored, ...info } = deviceData;
//check state changes
const previousState = JSON.stringify(this.deviceData);
const currentState = JSON.stringify(deviceData);
if (previousState === currentState) return;
this.deviceData = deviceData;
//restFul
if (this.restFulEnabled) {
this.emit('restFul', 'info', info);
this.emit('restFul', 'state', deviceData.Device);
}
//mqtt
if (this.mqttEnabled) {
this.emit('mqtt', 'Info', info);
this.emit('mqtt', 'State', deviceData.Device);
}
//emit info
this.emit('deviceInfo', indoor.model, outdoor.model, serialNumber, firmwareAppVersion, hasHotWaterTank, hasZone2, ftcModel);
//emit state
this.emit('deviceState', deviceData);
return true;
} catch (error) {
throw new Error(`Check state error: ${error.message}`);
};
}
async send(accountType, displayType, deviceData, payload = {}, flag = null) {
try {
let method = null
let path = '';
let update = false;
switch (accountType) {
case "melcloud":
switch (flag) {
case 'account':
payload = { data: payload.LoginData };
path = ApiUrls.Post.UpdateApplicationOptions;
break;
default:
flag = !flag ? HeatPump.EffectiveFlags.Power : HeatPump.EffectiveFlags.Power + flag;
payload = this.functions.toPascalCaseKeys({
...payload,
power: payload.power !== false,
deviceID: deviceData.Device.DeviceID,
effectiveFlags: flag,
hasPendingCommand: true,
});
path = ApiUrls.Post.Atw;
update = true;
break;
}
if (this.logDebug) this.emit('debug', `Send data: ${JSON.stringify(payload, null, 2)}`);
await this.client(path, { method: 'POST', data: payload });
//update state
if (update) {
deviceData.Device = { ...deviceData.Device, ...payload };
setTimeout(() => {
this.updateState('request', deviceData);
}, 500);
}
return true;
case "melcloudhome":
switch (flag) {
case 'frostprotection':
payload = {
enabled: payload.enabled,
min: payload.min,
max: payload.max,
units: { ATW: [deviceData.DeviceID] }
};
method = 'POST';
path = ApiUrls.Home.Post.ProtectionFrost;
deviceData.FrostProtection.Enabled = payload.enabled;
deviceData.FrostProtection.Min = payload.min;
deviceData.FrostProtection.Max = payload.max;
break;
case 'holidaymode':
payload = {
enabled: payload.enabled,
startDate: deviceData.HolidayMode.StartDate,
endDate: deviceData.HolidayMode.EndDate,
units: { ATW: [deviceData.DeviceID] }
};
method = 'POST';
path = ApiUrls.Home.Post.HolidayMode;
deviceData.HolidayMode.Enabled = payload.enabled;
break;
case 'schedule':
payload = { enabled: payload.enabled };
method = 'PUT';
path = ApiUrls.Home.Put.ScheduleEnabled.replace('deviceid', deviceData.DeviceID);
deviceData.ScheduleEnabled = payload.enabled;
break;
case 'scene':
method = 'PUT';
path = `${ApiUrls.Home.Put.SceneEnableDisable.replace('sceneid', payload.id)}/${payload.enabled ? 'enable' : 'disable'}`;
const scene = deviceData.Scenes.find(s => s.Id === payload.id);
if (scene) scene.Enabled = payload.enabled;
payload = {};
break;
default:
if (payload.operationMode != null) payload.operationMode = HeatPump.OperationModeMapEnumToString[payload.operationMode];
if (payload.operationModeZone1 != null) payload.operationModeZone1 = HeatPump.OperationModeZoneMapEnumToString[payload.operationModeZone1];
if (payload.operationModeZone2 != null) payload.operationModeZone2 = HeatPump.OperationModeZoneMapEnumToString[payload.operationModeZone2];
//cleanup undefined
Object.keys(payload).forEach(key => {
if (payload[key] === undefined) {
delete payload[key];
}
});
method = 'PUT';
path = ApiUrls.Home.Put.Atw.replace('deviceid', deviceData.DeviceID);
deviceData.Device = { ...deviceData.Device, ...payload };
break
}
if (this.logDebug) this.emit('debug', `Send data: ${JSON.stringify(payload, null, 2)}`);
await this.client(path, { method: method, data: payload });
return true;
default:
if (this.logWarn) this.emit('warn', `Received unknown account type: ${accountType}`);
return;
}
} catch (error) {
if (error.response?.status === 500) return;
throw new Error(`Send data error: ${error.message}`);
}
}
}
export default MelCloudAtw;