homebridge-mhi-wfrac
Version:
A homebridge plugin for Mitshubishi WF-RAC aircos (using the Smart M-Air app)
373 lines • 21.4 kB
JavaScript
import { DeviceClient, DeviceStatus } from './device.js';
export class WFRACAccessory {
platform;
accessory;
static REFRESH_INTERVAL = 10000;
deviceName;
deviceMac;
deviceId;
ipAddress;
port = 51443;
operatorId;
device;
thermostatService;
fanService;
dehumidifierService = null;
refreshTimeout = null;
selfManaged;
constructor(platform, accessory, ip, operatorId, selfManaged) {
this.platform = platform;
this.accessory = accessory;
this.deviceName = accessory.context.device.name;
this.deviceMac = accessory.context.device.mac;
this.deviceId = accessory.context.device.deviceId || accessory.context.device.mac;
this.ipAddress = ip;
this.operatorId = operatorId;
this.selfManaged = selfManaged;
accessory.context.device.ip = ip;
const airconId = accessory.context.device.airconId || this.deviceMac;
this.device = new DeviceClient(this.ipAddress, this.port, this.operatorId, this.deviceId, airconId, this.platform.log, this.platform.config.ignoreConnectionErrors);
// set accessory information
this.accessory.getService(this.platform.Service.AccessoryInformation)
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Mitsubishi Heavy Industries.')
.setCharacteristic(this.platform.Characteristic.Model, 'WF-RAC Smart M-Air Series')
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.deviceMac);
this.thermostatService = this.accessory.getService(this.platform.Service.Thermostat)
|| this.accessory.addService(this.platform.Service.Thermostat);
this.fanService = this.accessory.getService(this.platform.Service.Fanv2)
|| this.accessory.addService(this.platform.Service.Fanv2);
// Remove leftover services from older versions (away mode, 3D auto swing, self-clean, etc.).
// The accessory may have multiple services of the same type with different subtypes,
// so we iterate the full service list rather than relying on getService().
const obsoleteServiceUuids = new Set([
this.platform.Service.Switch.UUID,
this.platform.Service.FilterMaintenance.UUID,
]);
for (const service of [...this.accessory.services]) {
if (obsoleteServiceUuids.has(service.UUID)) {
this.accessory.removeService(service);
}
}
// Conditionally create/remove dehumidifier service
const hideDehumidifier = this.accessory.context.device.hideDehumidifier || false;
if (hideDehumidifier) {
// Remove dehumidifier service if it exists
const existingDehumidifierService = this.accessory.getService(this.platform.Service.HumidifierDehumidifier);
if (existingDehumidifierService) {
this.accessory.removeService(existingDehumidifierService);
}
this.dehumidifierService = null;
}
else {
// Create or get dehumidifier service
this.dehumidifierService = this.accessory.getService(this.platform.Service.HumidifierDehumidifier)
|| this.accessory.addService(this.platform.Service.HumidifierDehumidifier);
}
this.thermostatService.getCharacteristic(this.platform.Characteristic.TemperatureDisplayUnits)
.onGet(() => this.platform.Characteristic.TemperatureDisplayUnits.CELSIUS);
this.thermostatService.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
.setProps({ minValue: DeviceStatus.indoorTempList.at(0), maxValue: DeviceStatus.indoorTempList.at(-1), minStep: 0.1 });
this.thermostatService.getCharacteristic(this.platform.Characteristic.TargetTemperature)
.setProps({ minValue: 18, maxValue: 30, minStep: 0.5 });
this.fanService.getCharacteristic(this.platform.Characteristic.RotationSpeed).setProps({ minValue: 0, maxValue: 100, minStep: 25 });
if (this.dehumidifierService) {
this.dehumidifierService.getCharacteristic(this.platform.Characteristic.TargetHumidifierDehumidifierState)
.setProps({ validValues: [this.platform.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER] });
this.dehumidifierService.getCharacteristic(this.platform.Characteristic.CurrentHumidifierDehumidifierState)
.setProps({ validValues: [
this.platform.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE,
this.platform.Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING,
] });
}
this.thermostatService.getCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState)
.onSet(this.setTargetHeatingCoolingState.bind(this));
this.thermostatService.getCharacteristic(this.platform.Characteristic.TargetTemperature)
.onSet(this.setTargetTemperature.bind(this));
this.fanService.getCharacteristic(this.platform.Characteristic.Active)
.onSet(this.setFanActive.bind(this));
this.fanService.getCharacteristic(this.platform.Characteristic.TargetFanState)
.onSet(this.setTargetFanState.bind(this));
this.fanService.getCharacteristic(this.platform.Characteristic.RotationSpeed)
.onSet(this.setRotationSpeed.bind(this));
if (this.dehumidifierService) {
this.dehumidifierService.getCharacteristic(this.platform.Characteristic.Active)
.onSet(this.setHumidifierActive.bind(this));
}
// We do not implement the target humidifier state, since we only accept DEHUMIDIFIER as a valid value.
this.refreshStatus();
}
async ensureRegistered() {
if (!this.selfManaged || this.accessory.context.registered) {
return;
}
try {
// Mirror the Home Assistant flow: fetch the device-reported airconId first.
// Some firmware (notably the HTTPS variant) returns an airconId that differs
// from the MAC, and updateAccountInfo must be called with the device-reported
// value or registration silently fails.
if (!this.accessory.context.device.airconId) {
const info = await this.device.getDeviceInfo();
this.accessory.context.device.airconId = info.airconId;
this.platform.log.info(`${this.deviceName}: airconId=${info.airconId}`);
}
const tz = process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
const result = await this.device.updateAccountInfo(tz);
if (result === 2) {
this.platform.log.error(`${this.deviceName}: device has reached the 4-account limit. ` +
'Remove an unused remote in the Smart M-Air app, then restart Homebridge.');
return;
}
// Match HA behaviour: treat any non-2 response as success. Some firmware
// versions return the result code as a string, omit it on success, or
// include extra fields. Being permissive here avoids spurious "registered=false"
// states that block all subsequent commands.
if (result !== 0 && result !== undefined) {
this.platform.log.warn(`${this.deviceName}: updateAccountInfo returned result=${result} — proceeding optimistically`);
}
this.accessory.context.registered = true;
this.platform.log.info(`${this.deviceName}: registered (operatorId=${this.operatorId})`);
}
catch (error) {
if (this.platform.config.ignoreConnectionErrors && this.device.isConnectionError(error)) {
this.platform.log.debug(`${this.deviceName}: registration deferred (${error.message})`);
return;
}
this.platform.log.warn(`${this.deviceName}: registration failed: ${error.message}`);
}
}
handleSetError(action, error) {
const err = error;
if (this.platform.config.ignoreConnectionErrors && this.device.isConnectionError(err)) {
this.platform.log.debug(`Ignoring connection error while ${action} for ${this.deviceName}: ${err.message}`);
return;
}
this.platform.log.error(`Error ${action} for ${this.deviceName}: ${err}`);
throw err;
}
async runSetCommand(action, fn) {
if (this.refreshTimeout) {
clearTimeout(this.refreshTimeout);
this.refreshTimeout = null;
}
try {
await this.ensureRegistered();
await fn();
this.updateStatus();
}
catch (error) {
this.handleSetError(action, error);
}
finally {
this.refreshTimeout = setTimeout(() => this.refreshStatus(), WFRACAccessory.REFRESH_INTERVAL);
}
}
refreshStatus() {
if (this.refreshTimeout) {
clearTimeout(this.refreshTimeout);
}
// Skip status refresh if a command is in progress to avoid race conditions
if (!this.device.isCommandInProgress) {
this.ensureRegistered()
.then(() => this.device.getDeviceStatus())
.then(() => this.updateStatus())
.catch((error) => {
if (!this.platform.config.ignoreConnectionErrors || !this.device.isConnectionError(error)) {
this.platform.log.error(`Error getting status for ${this.deviceName}: ${error}`);
}
});
}
this.refreshTimeout = setTimeout(() => this.refreshStatus(), WFRACAccessory.REFRESH_INTERVAL);
}
updateStatus() {
if (this.device.status.indoorTemp !== null) {
this.thermostatService.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.device.status.indoorTemp);
}
if (this.device.status.presetTemp !== null && this.device.status.operationMode !== 3) {
const clamped = Math.min(30, Math.max(18, this.device.status.presetTemp));
this.thermostatService.updateCharacteristic(this.platform.Characteristic.TargetTemperature, clamped);
}
let currentHeatingCoolingState = this.platform.Characteristic.CurrentHeatingCoolingState.OFF;
let targetHeatingCoolingState = this.platform.Characteristic.TargetHeatingCoolingState.OFF;
let currentFanActive = this.platform.Characteristic.Active.INACTIVE;
let currentFanState = this.platform.Characteristic.CurrentFanState.INACTIVE;
let fanSpeed = 0;
let targetFanState = this.platform.Characteristic.TargetFanState.AUTO;
let currentDehumidifierActive = this.platform.Characteristic.Active.INACTIVE;
let currentHumidifierDehumidifierState = this.platform.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE;
let targetHumidifierDehumidifierState = this.platform.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER;
if (this.device.status.operation) {
currentFanActive = this.platform.Characteristic.Active.ACTIVE;
fanSpeed = this.device.status.airFlow * 25;
currentFanState = (this.device.status.airFlow === 0) ?
this.platform.Characteristic.CurrentFanState.IDLE : this.platform.Characteristic.CurrentFanState.BLOWING_AIR;
targetFanState = (this.device.status.airFlow === 0) ?
this.platform.Characteristic.TargetFanState.AUTO : this.platform.Characteristic.TargetFanState.MANUAL;
if (this.device.status.operationMode === 0 || this.device.status.operationMode === -1) {
targetHeatingCoolingState = this.platform.Characteristic.TargetHeatingCoolingState.AUTO;
if (this.device.status.coolHotJudge) {
currentHeatingCoolingState = this.platform.Characteristic.CurrentHeatingCoolingState.HEAT;
}
else {
currentHeatingCoolingState = this.platform.Characteristic.CurrentHeatingCoolingState.COOL;
}
}
else if (this.device.status.operationMode === 1) {
targetHeatingCoolingState = this.platform.Characteristic.TargetHeatingCoolingState.COOL;
currentHeatingCoolingState = this.platform.Characteristic.CurrentHeatingCoolingState.COOL;
}
else if (this.device.status.operationMode === 2) {
targetHeatingCoolingState = this.platform.Characteristic.TargetHeatingCoolingState.HEAT;
currentHeatingCoolingState = this.platform.Characteristic.CurrentHeatingCoolingState.HEAT;
}
else if (this.device.status.operationMode === 3) {
currentHeatingCoolingState = this.platform.Characteristic.CurrentHeatingCoolingState.OFF;
targetHeatingCoolingState = this.platform.Characteristic.TargetHeatingCoolingState.OFF;
}
else if (this.device.status.operationMode === 4) {
currentDehumidifierActive = this.platform.Characteristic.Active.ACTIVE;
currentHumidifierDehumidifierState = this.platform.Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING;
targetHumidifierDehumidifierState = this.platform.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER;
if (this.device.status.presetTemp < this.device.status.indoorTemp) {
currentHeatingCoolingState = this.platform.Characteristic.CurrentHeatingCoolingState.COOL;
}
else {
currentHeatingCoolingState = this.platform.Characteristic.CurrentHeatingCoolingState.OFF;
}
targetHeatingCoolingState = this.platform.Characteristic.TargetHeatingCoolingState.AUTO;
currentFanActive = this.platform.Characteristic.Active.INACTIVE;
currentFanState = this.platform.Characteristic.CurrentFanState.BLOWING_AIR;
fanSpeed = 0;
targetFanState = this.platform.Characteristic.TargetFanState.AUTO;
}
}
this.thermostatService.updateCharacteristic(this.platform.Characteristic.CurrentHeatingCoolingState, currentHeatingCoolingState);
this.thermostatService.updateCharacteristic(this.platform.Characteristic.TargetHeatingCoolingState, targetHeatingCoolingState);
this.fanService.updateCharacteristic(this.platform.Characteristic.Active, currentFanActive);
this.fanService.updateCharacteristic(this.platform.Characteristic.CurrentFanState, currentFanState);
this.fanService.updateCharacteristic(this.platform.Characteristic.RotationSpeed, fanSpeed);
this.fanService.updateCharacteristic(this.platform.Characteristic.TargetFanState, targetFanState);
if (this.dehumidifierService) {
this.dehumidifierService.updateCharacteristic(this.platform.Characteristic.Active, currentDehumidifierActive);
this.dehumidifierService.updateCharacteristic(this.platform.Characteristic.CurrentHumidifierDehumidifierState, currentHumidifierDehumidifierState);
this.dehumidifierService.updateCharacteristic(this.platform.Characteristic.TargetHumidifierDehumidifierState, targetHumidifierDehumidifierState);
}
}
async setTargetHeatingCoolingState(value) {
return this.runSetCommand('setting heating/cooling state', async () => {
switch (value) {
case this.platform.Characteristic.TargetHeatingCoolingState.OFF:
this.platform.log.info(`Turning ${this.deviceName} off`);
await this.device.setOperation(false);
break;
case this.platform.Characteristic.TargetHeatingCoolingState.HEAT:
this.platform.log.info(`Setting ${this.deviceName} to heating mode`);
await this.device.setOperationMode(2);
if (!this.device.status.operation) {
this.platform.log.info(`Turning ${this.deviceName} on`);
await this.device.setOperation(true);
}
break;
case this.platform.Characteristic.TargetHeatingCoolingState.COOL:
this.platform.log.info(`Setting ${this.deviceName} to cooling mode`);
await this.device.setOperationMode(1);
if (!this.device.status.operation) {
this.platform.log.info(`Turning ${this.deviceName} on`);
await this.device.setOperation(true);
}
break;
case this.platform.Characteristic.TargetHeatingCoolingState.AUTO:
this.platform.log.info(`Setting ${this.deviceName} to auto cooling/heating mode`);
await this.device.setOperationMode(0);
if (!this.device.status.operation) {
this.platform.log.info(`Turning ${this.deviceName} on`);
await this.device.setOperation(true);
}
this.platform.log.info(`Setting ${this.deviceName} fan speed to auto`);
await this.device.setAirflow(0);
break;
}
});
}
async setTargetTemperature(value) {
return this.runSetCommand('setting temperature', async () => {
const temperature = value;
this.platform.log.info(`Setting ${this.deviceName} temperature to ${temperature}°C`);
await this.device.setPresetTemp(temperature);
});
}
async setFanActive(value) {
return this.runSetCommand('setting fan active', async () => {
switch (value) {
case this.platform.Characteristic.Active.INACTIVE:
if (this.device.status.operationMode === 3) {
this.platform.log.info(`Turning ${this.deviceName} off after setting fan inactive`);
await this.device.setOperation(false);
}
else {
this.platform.log.info(`Setting ${this.deviceName} fan speed to AUTO`);
await this.device.setAirflow(0);
}
break;
case this.platform.Characteristic.Active.ACTIVE:
if (!this.device.status.operation) {
this.platform.log.info(`Turning ${this.deviceName} on and setting operation mode to fan mode`);
await this.device.setOperationMode(3);
await this.device.setOperation(true);
}
else {
this.platform.log.info(`Setting ${this.deviceName} fan speed to AUTO`);
await this.device.setAirflow(0);
}
break;
}
});
}
async setTargetFanState(value) {
return this.runSetCommand('setting target fan state', async () => {
switch (value) {
case this.platform.Characteristic.TargetFanState.AUTO:
this.platform.log.info(`Setting ${this.deviceName} fan speed to AUTO`);
await this.device.setAirflow(0);
break;
case this.platform.Characteristic.TargetFanState.MANUAL:
break;
}
});
}
async setRotationSpeed(value) {
return this.runSetCommand('setting rotation speed', async () => {
if (value === 0) {
this.platform.log.info(`Setting fan speed for ${this.deviceName} to auto`);
await this.device.setAirflow(0);
}
else {
this.platform.log.info(`Setting fan speed for ${this.deviceName} to ${value}`);
await this.device.setAirflow(Math.round(value / 25));
}
if (!this.device.status.operation) {
this.platform.log.info(`Turning ${this.deviceName} on and setting operation mode to fan mode`);
await this.device.setOperationMode(3);
await this.device.setOperation(true);
}
});
}
async setHumidifierActive(value) {
return this.runSetCommand('setting dehumidifier active', async () => {
switch (value) {
case this.platform.Characteristic.Active.INACTIVE:
this.platform.log.info(`Setting ${this.deviceName} dehumidifier inactive`);
await this.device.setOperationMode(0);
break;
case this.platform.Characteristic.Active.ACTIVE:
this.platform.log.info(`Setting ${this.deviceName} dehumidifier active`);
await this.device.setOperationMode(4);
if (!this.device.status.operation) {
this.platform.log.info(`Turning ${this.deviceName} on`);
await this.device.setOperation(true);
}
break;
}
});
}
}
//# sourceMappingURL=platformAccessory.js.map