homebridge-panasonic-ac-platform
Version:
Homebridge platform plugin providing HomeKit support for Panasonic Comfort Cloud devices.
781 lines (679 loc) • 35.5 kB
text/typescript
import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge';
import PanasonicPlatform from './platform';
import { ComfortCloudDeviceUpdatePayload, PanasonicAccessoryContext } from './types';
/**
* An instance of this class is created for each accessory the platform registers.
* Each accessory may expose multiple services of different service types.
*/
export default class IndoorUnitAccessory {
private service: Service;
sendDeviceUpdatePayload: any = {};
timerRefreshDeviceStatus;
timerSendDeviceUpdate;
timerSendDeviceUpdateRefresh;
timerSetFanSpeed;
devConfig;
deviceStatusFull;
deviceStatus;
exposeInsideTemp;
exposeOutdoorTemp;
exposePower;
exposeNanoe;
exposeInsideCleaning;
exposeEcoNavi;
exposeEcoFunction;
exposeAutoMode;
exposeCoolMode;
exposeHeatMode;
exposeDryMode;
exposeFanMode;
exposeNanoeStandAloneMode;
exposeQuietMode;
exposePowerfulMode;
exposeSwingUpDown;
exposeSwingLeftRight;
exposeFanSpeed;
constructor(
private readonly platform: PanasonicPlatform,
private readonly accessory: PlatformAccessory<PanasonicAccessoryContext>,
) {
// Individual config for each device (if exists).
if (this.platform.platformConfig.devices) {
this.devConfig = this.platform.platformConfig.devices.find((item) => item.name === accessory.context.device?.deviceName)
|| this.platform.platformConfig.devices.find((item) => item.name === accessory.context.device?.deviceGuid);
}
// Accessory Information
this.accessory.getService(this.platform.Service.AccessoryInformation)
?.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Panasonic')
.setCharacteristic(this.platform.Characteristic.Model, accessory.context.device?.deviceModuleNumber || 'Unknown')
.setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.context.device?.deviceGuid || 'Unknown');
// Heater Cooler
// https://developers.homebridge.io/#/service/HeaterCooler
this.service = this.accessory.getService(this.platform.Service.HeaterCooler)
|| this.accessory.addService(this.platform.Service.HeaterCooler);
// Characteristics configuration
// Each service must implement at-minimum the "required characteristics"
// See https://developers.homebridge.io/#/service/HeaterCooler
// Name (optional)
// This is what is displayed as the default name on the Home app
this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device?.deviceName || 'Unnamed');
// Active (required)
this.service.getCharacteristic(this.platform.Characteristic.Active).onSet(this.setActive.bind(this));
// Current Temperature (required)
this.service.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
.setProps({minValue: -100, maxValue: 100, minStep: 0.01});
// Target Heater-Cooler State (required)
this.service
.getCharacteristic(this.platform.Characteristic.TargetHeaterCoolerState)
.onSet(this.setTargetHeaterCoolerState.bind(this));
// Rotation Speed (optional)
this.service
.getCharacteristic(this.platform.Characteristic.RotationSpeed)
.setProps({minValue: 0, maxValue: 8, minStep: 1})
.onSet(this.setRotationSpeed.bind(this));
// Swing Mode (optional)
this.service
.getCharacteristic(this.platform.Characteristic.SwingMode)
.onSet(this.setSwingMode.bind(this));
// Cooling Threshold Temperature (optional)
this.service
.getCharacteristic(this.platform.Characteristic.CoolingThresholdTemperature)
.setProps({minValue: 16, maxValue: 30, minStep: 0.5})
.onSet(this.setThresholdTemperature.bind(this));
// Heating Threshold Temperature (optional)
this.service
.getCharacteristic(this.platform.Characteristic.HeatingThresholdTemperature)
.setProps({minValue: this.devConfig?.minHeatingTemperature || 16, maxValue: 30, minStep: 0.5})
.onSet(this.setThresholdTemperature.bind(this));
// Expose additional features - helper function
const manageService = (
exposeFlag: boolean | undefined,
serviceName: string,
serviceType: any, // Replace with proper Homebridge Service type if available
setter: ((value: any) => Promise<void>) | null = null,
) => {
const fullName = `${this.accessory.displayName} ${serviceName}`;
if (exposeFlag) {
const service = this.accessory.getService(fullName) || this.accessory.addService(serviceType, fullName, serviceName);
service.setCharacteristic(this.platform.Characteristic.ConfiguredName, fullName);
if (setter) {
service.getCharacteristic(this.platform.Characteristic.On).onSet(setter.bind(this));
}
this.platform.log.debug(`${this.accessory.displayName}: add ${serviceName}`);
return service;
} else {
const service = this.accessory.getService(fullName);
if (service) {
this.accessory.removeService(service);
this.platform.log.debug(`${this.accessory.displayName}: remove ${serviceName}`);
}
}
};
// Expose additional features
manageService(this.devConfig?.exposeInsideTemp, 'inside temp', this.platform.Service.TemperatureSensor);
manageService(this.devConfig?.exposeOutdoorTemp, 'out temp', this.platform.Service.TemperatureSensor);
manageService(this.devConfig?.exposePower, 'power', this.platform.Service.Switch, this.setPower);
manageService(this.devConfig?.exposeNanoe, 'nanoe', this.platform.Service.Switch, this.setNanoe);
manageService(this.devConfig?.exposeInsideCleaning, 'inside cleaning', this.platform.Service.Switch, this.setInsideCleaning);
manageService(this.devConfig?.exposeEcoNavi, 'eco navi', this.platform.Service.Switch, this.setEcoNavi);
manageService(this.devConfig?.exposeEcoFunction, 'eco function', this.platform.Service.Switch, this.setEcoFunction);
manageService(this.devConfig?.exposeAutoMode, 'auto mode', this.platform.Service.Switch, this.setAutoMode);
manageService(this.devConfig?.exposeCoolMode, 'cool mode', this.platform.Service.Switch, this.setCoolMode);
manageService(this.devConfig?.exposeHeatMode, 'heat mode', this.platform.Service.Switch, this.setHeatMode);
manageService(this.devConfig?.exposeDryMode, 'dry mode', this.platform.Service.Switch, this.setDryMode);
manageService(this.devConfig?.exposeFanMode, 'fan mode', this.platform.Service.Switch, this.setFanMode);
// Fan Speed (special case with RotationSpeed)
if (this.devConfig?.exposeFanSpeed) {
this.exposeFanSpeed = manageService(true, 'fan speed', this.platform.Service.Fan);
this.exposeFanSpeed.getCharacteristic(this.platform.Characteristic.RotationSpeed).onSet(this.setFanSpeed.bind(this));
} else {
manageService(false, 'fan speed', this.platform.Service.Fan);
}
// Update characteristic values asynchronously instead of using onGet handlers
this.refreshDeviceStatus();
}
// ===============================================================================================================================================
/**
* Retrieves the device status from Comfort Cloud and updates its characteristics.
*/
async refreshDeviceStatus() {
let logOutput = '';
this.platform.log.debug(`${this.accessory.displayName}: refresh status`);
try {
this.deviceStatusFull = await this.platform.comfortCloud.getDeviceStatus(
this.accessory.context.device.deviceGuid, this.accessory.displayName);
this.deviceStatus = this.deviceStatusFull.parameters;
// Active
if (this.deviceStatus.operate !== undefined) {
const active = this.deviceStatus.operate === 1
? this.platform.Characteristic.Active.ACTIVE
: this.platform.Characteristic.Active.INACTIVE;
this.service.updateCharacteristic(this.platform.Characteristic.Active, active);
logOutput += `${(active === 1) ? 'On' : 'Off'}`;
}
// Current Temperature
// If the temperature of the indoor unit is not available,
// default values will be used: 8°C for heating and 30°C for cooling and else.
// Temperature of 126 or higher from the API = null/failure
if (this.deviceStatus.insideTemperature < 126) {
this.service.updateCharacteristic(
this.platform.Characteristic.CurrentTemperature, this.deviceStatus.insideTemperature);
logOutput += `, Inside Temp. ${this.deviceStatus.insideTemperature}`;
} else {
logOutput += ', Inside Temp. not available';
this.platform.log.debug(`${this.accessory.displayName}: Inside temperature is not available - setting default temperature`);
this.service.updateCharacteristic(
this.platform.Characteristic.CurrentTemperature,
(this.deviceStatus.operationMode === 3) ? 8 : 30,
);
}
// Inside temperature for virtual sensor
if (this.exposeInsideTemp && this.deviceStatus.insideTemperature < 126) {
this.exposeInsideTemp.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.deviceStatus.insideTemperature);
}
// Outdoor temperature for logs
if (this.deviceStatus.outTemperature >= 126) {
logOutput += ', Outdoor Temp. not available';
} else {
logOutput += `, Outdoor Temp. ${this.deviceStatus.outTemperature}`;
}
// Outdoor temperature for virtual sensor
// Only check and set if the user wants to display the virtual sensor showing temp from outdoor unit.
if (this.exposeOutdoorTemp && this.deviceStatus.outTemperature < 126) {
this.exposeOutdoorTemp.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.deviceStatus.outTemperature);
}
// Current Heater-Cooler State and Target Heater-Cooler State
const currentTemp = this.service.getCharacteristic(this.platform.Characteristic.CurrentTemperature).value as number;
const setTemp = this.deviceStatus.temperatureSet;
const { operationMode } = this.deviceStatus;
const modes = {
0: { log: 'Auto Mode', target: 'AUTO', current: currentTemp < setTemp ? 'HEATING' : currentTemp > setTemp ? 'COOLING' : 'IDLE' },
3: { log: 'Heat Mode', target: 'HEAT', current: currentTemp < setTemp ? 'HEATING' : 'IDLE' },
2: { log: 'Cool Mode', target: 'COOL', current: currentTemp > setTemp ? 'COOLING' : 'IDLE' },
1: { log: 'Dry Mode', target: 'AUTO', current: 'IDLE' },
4: { log: 'Fan Mode', target: 'AUTO', current: 'IDLE' },
};
if (modes[operationMode]) {
const { log, target, current } = modes[operationMode];
logOutput += `, ${log}`;
this.service.updateCharacteristic(
this.platform.Characteristic.TargetHeaterCoolerState,
this.platform.Characteristic.TargetHeaterCoolerState[target],
);
this.service.updateCharacteristic(
this.platform.Characteristic.CurrentHeaterCoolerState,
this.platform.Characteristic.CurrentHeaterCoolerState[current],
);
} else {
this.platform.log.error(`Unknown operation mode: '${operationMode}'`);
}
// Rotation Speed
/**
* 1) The fanSpeed value in the Comfort Cloud payload doesn't always reflect
* the current operation mode. For example, when switching from
* fan speed 4 to Quiet Mode, the fanSpeed in the payload will remain 4.
* Based on tests, ecoMode seems to take precedence and we'll check it first.
*
* 2) HomeKit automatically moves the slider into the 0 position when
* the device is switched off. We don't have to handle this case manually.
*
* 3) See README for the mapping of Comfort Cloud payload to slider position.
*/
// Check status only when device is on
if (this.deviceStatus.operate === 1) {
let sliderValue = 8; // default AUTO
if (this.deviceStatus.ecoMode === 2) {
sliderValue = 1; // Quiet Mode
logOutput += ', Speed 1 (Quiet Mode)';
} else if (this.deviceStatus.ecoMode === 1) {
sliderValue = 7; // Powerful Mode
logOutput += ', Speed 5 (Powerful Mode)';
} else if (this.deviceStatus.ecoMode === 0) {
const fanSpeedMap = { 0: 8, 1: 2, 2: 3, 3: 4, 4: 5, 5: 6 };
sliderValue = fanSpeedMap[this.deviceStatus.fanSpeed] || 8;
logOutput += `, Speed ${this.deviceStatus.fanSpeed || 'Auto'}`;
}
this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed)
.updateValue(sliderValue);
}
// Swing Mode
if (this.deviceStatus.fanAutoMode !== 1) {
this.service.getCharacteristic(this.platform.Characteristic.SwingMode)
.updateValue(this.platform.Characteristic.SwingMode.SWING_ENABLED);
} else {
this.service.getCharacteristic(this.platform.Characteristic.SwingMode)
.updateValue(this.platform.Characteristic.SwingMode.SWING_DISABLED);
}
// Expose additional features
const updateChar = (expose, condition) => expose?.updateCharacteristic(this.platform.Characteristic.On, condition);
const isOn = this.deviceStatus.operate === 1;
updateChar(this.exposePower, isOn);
updateChar(this.exposeNanoe, this.deviceStatus.nanoe === 2);
updateChar(this.exposeInsideCleaning, this.deviceStatus.insideCleaning === 2);
updateChar(this.exposeEcoNavi, this.deviceStatus.ecoNavi === 2);
updateChar(this.exposeEcoFunction, this.deviceStatus.ecoFunctionData === 2);
updateChar(this.exposeAutoMode, isOn && this.deviceStatus.operationMode === 0);
updateChar(this.exposeCoolMode, isOn && this.deviceStatus.operationMode === 2);
updateChar(this.exposeHeatMode, isOn && this.deviceStatus.operationMode === 3);
updateChar(this.exposeDryMode, isOn && this.deviceStatus.operationMode === 1);
updateChar(this.exposeFanMode, isOn && this.deviceStatus.operationMode === 4 && this.deviceStatus.lastSettingMode === 1);
updateChar(this.exposeNanoeStandAloneMode, isOn && this.deviceStatus.operationMode === 4 && this.deviceStatus.lastSettingMode === 2);
updateChar(this.exposeSwingUpDown, [0, 2].includes(this.deviceStatus.fanAutoMode));
updateChar(this.exposeSwingLeftRight, [0, 3].includes(this.deviceStatus.fanAutoMode));
if (isOn) {
updateChar(this.exposeQuietMode, this.deviceStatus.ecoMode === 2);
updateChar(this.exposePowerfulMode, this.deviceStatus.ecoMode === 1);
}
// Expose fan speed
if (this.exposeFanSpeed) {
const isOn = this.deviceStatus.operate === 1;
const fanSpeed = this.deviceStatus.fanSpeed;
const speedMap = { 1: 10, 2: 30, 3: 50, 4: 70, 5: 90 };
const rotationSpeed = isOn ? (speedMap[fanSpeed] || 100) : 0;
this.exposeFanSpeed.updateCharacteristic(this.platform.Characteristic.On, isOn);
this.exposeFanSpeed.updateCharacteristic(this.platform.Characteristic.RotationSpeed, rotationSpeed);
}
// Cooling Threshold Temperature (optional)
// Heating Threshold Temperature (optional)
this.service.getCharacteristic(this.platform.Characteristic.HeatingThresholdTemperature)
.updateValue(setTemp);
this.service.getCharacteristic(this.platform.Characteristic.CoolingThresholdTemperature)
.updateValue(setTemp);
// log
if (this.platform.platformConfig.logsLevel >= 1) {
this.platform.log.info(`${this.accessory.displayName}: ${logOutput}.`);
}
} catch (error) {
this.platform.log.error('An error occurred while refreshing the device status. '
+ 'Turn on debug mode for more information.');
// Only log if a Promise rejection reason was provided.
// Some errors are already logged at source.
if (error) {
this.platform.log.debug(error);
}
// if you need to return an error to show the device as "Not Responding" in the Home app:
// throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
/**
* We should be able to pass an error object to the function to mark a service/accessory
* as 'Not Responding' in the Home App.
* (Only needs to be set on a single/primary characteristic of an accessory,
* and needs to be updated with a valid value when the accessory is available again.
* The error message text is for internal use only, and is not passed to the Home App.)
*
* Problem: The Typescript definitions suggest this is not permitted - commenting for now.
*/
/*
this.service.updateCharacteristic(
this.platform.Characteristic.Active,
new Error('Exception occurred in refreshDeviceStatus()'),
);
*/
}
// Schedule continuous device updates on the first run
// 10 minutes when device is on, 60 minutes device is off
clearTimeout(this.timerRefreshDeviceStatus);
this.timerRefreshDeviceStatus = null;
this.timerRefreshDeviceStatus = setTimeout(
this.refreshDeviceStatus.bind(this),
(this.service.getCharacteristic(this.platform.Characteristic.Active).value === 1) ? 10 * 60 * 1000 : 60 * 60 * 1000);
}
// ===============================================================================================================================================
/**
* Handle 'SET' requests from HomeKit
* These are sent when the user changes the state of an accessory,
* for example, turning on a Light bulb.
*/
async setActive(value: CharacteristicValue) {
this.platform.log.debug(`${this.accessory.displayName}: setActive()`);
const parameters: ComfortCloudDeviceUpdatePayload = {
operate: value === this.platform.Characteristic.Active.ACTIVE ? 1 : 0,
};
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](
`${this.accessory.displayName}: ${value === this.platform.Characteristic.Active.ACTIVE ? 'set On' : 'set Off'}`);
this.sendDeviceUpdate(
this.accessory.context.device.deviceGuid, parameters);
}
async setTargetHeaterCoolerState(value: CharacteristicValue) {
this.platform.log.debug(`${this.accessory.displayName}: setTargetHeaterCoolerState()`);
const parameters: ComfortCloudDeviceUpdatePayload = {
operate: 1,
};
switch (value) {
case this.platform.Characteristic.TargetHeaterCoolerState.AUTO:
if (this.platform.platformConfig.autoMode === 'fan') {
parameters.operationMode = 4;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Fan Mode`);
} else if (this.platform.platformConfig.autoMode === 'dry') {
parameters.operationMode = 1;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Dry mode`);
} else {
parameters.operationMode = 0;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Auto mode`);
}
break;
case this.platform.Characteristic.TargetHeaterCoolerState.COOL:
parameters.operationMode = 2;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Mode Cool`);
break;
case this.platform.Characteristic.TargetHeaterCoolerState.HEAT:
parameters.operationMode = 3;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Mode Heat`);
break;
default:
this.platform.log.error(`${this.accessory.displayName}: Unknown TargetHeaterCoolerState`, value);
return;
}
this.sendDeviceUpdate(this.accessory.context.device.deviceGuid, parameters);
}
async setRotationSpeed(value: CharacteristicValue) {
this.platform.log.debug(`${this.accessory.displayName}: setRotationSpeed()`);
const parameters: ComfortCloudDeviceUpdatePayload = {};
switch (value) {
// See README for the mapping of slider position to Comfort Cloud payload.
case 0:
// HomeKit independently switches off the accessory
// in this case, which triggers setActive().
// Nothing to handle here, but documenting for clarity.
break;
case 1:
parameters.ecoMode = 2;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Quiet Mode`);
break;
case 2: case 3: case 4: case 5: case 6:
parameters.ecoMode = 0;
parameters.fanSpeed = 1;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Fan speed ${value -1}`);
break;
case 7:
parameters.ecoMode = 1;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Powerful Mode`);
break;
case 8:
parameters.ecoMode = 0;
parameters.fanSpeed = 0;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Auto Mode`);
break;
default:
parameters.ecoMode = 0;
parameters.fanSpeed = 0;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Auto Mode`);
break;
}
this.sendDeviceUpdate(this.accessory.context.device.deviceGuid, parameters);
}
async setSwingMode(value: CharacteristicValue) {
this.platform.log.debug(`${this.accessory.displayName}: setSwingMode()`);
const parameters: ComfortCloudDeviceUpdatePayload = {};
if (value === this.platform.Characteristic.SwingMode.SWING_ENABLED) {
parameters.fanAutoMode = 0;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Swing mode Auto`);
} else if (value === this.platform.Characteristic.SwingMode.SWING_DISABLED) {
parameters.fanAutoMode = 1;
parameters.airSwingUD = (this.devConfig?.swingDefaultUD !== null) ? this.devConfig?.swingDefaultUD : 2;
parameters.airSwingLR = (this.devConfig?.swingDefaultLR !== null) ? this.devConfig?.swingDefaultLR : 2;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Swing mode Off`);
}
this.sendDeviceUpdate(this.accessory.context.device.deviceGuid, parameters);
}
// Expose additional features - helper function
async setMode(modeName: string, value: boolean) {
this.platform.log.debug(`${this.accessory.displayName}: ${modeName}()`);
// Configuration map for different modes
const MODE_CONFIGS = {
setPower: { key: 'operate', on: 1, off: 0 },
setNanoe: { key: 'nanoe', on: 2, off: 1 },
setInsideCleaning: { key: 'insideCleaning', on: 2, off: 1 },
setEcoNavi: { key: 'ecoNavi', on: 2, off: 1 },
setEcoFunction: { key: 'ecoFunctionData', on: 2, off: 1 },
setAutoMode: { key: 'operationMode', on: 0, off: 0, operate: true },
setCoolMode: { key: 'operationMode', on: 2, off: 0, operate: true },
setHeatMode: { key: 'operationMode', on: 3, off: 0, operate: true },
setDryMode: { key: 'operationMode', on: 1, off: 0, operate: true },
setFanMode: { key: 'operationMode', on: 4, off: 0, operate: true },
setNanoeStandAloneMode: { key: 'operationMode', on: 5, off: 0, operate: true },
setQuietMode: { key: 'ecoMode', on: 2, off: 0 },
setPowerfulMode: { key: 'ecoMode', on: 1, off: 0 },
};
const config = MODE_CONFIGS[modeName];
const parameters: ComfortCloudDeviceUpdatePayload = {};
if (config.operate) {
parameters.operate = value ? 1 : 0;
if (value) {
parameters[config.key] = config.on;
}
} else {
parameters[config.key] = value ? config.on : config.off;
}
const logLevel = this.platform.platformConfig.logsLevel >= 1 ? 'info' : 'debug';
const state = value ? 'On' : 'Off';
this.platform.log[logLevel](`${this.accessory.displayName}: ${modeName.replace('set', '')} ${state}`);
this.sendDeviceUpdate(this.accessory.context.device.deviceGuid, parameters);
}
// Expose additional features
async setPower(value: boolean) {
await this.setMode('setPower', value);
}
async setNanoe(value: boolean) {
await this.setMode('setNanoe', value);
}
async setInsideCleaning(value: boolean) {
await this.setMode('setInsideCleaning', value);
}
async setEcoNavi(value: boolean) {
await this.setMode('setEcoNavi', value);
}
async setEcoFunction(value: boolean) {
await this.setMode('setEcoFunction', value);
}
async setAutoMode(value: boolean) {
await this.setMode('setAutoMode', value);
}
async setCoolMode(value: boolean) {
await this.setMode('setCoolMode', value);
}
async setHeatMode(value: boolean) {
await this.setMode('setHeatMode', value);
}
async setDryMode(value: boolean) {
await this.setMode('setDryMode', value);
}
async setFanMode(value: boolean) {
await this.setMode('setFanMode', value);
}
async setNanoeStandAloneMode(value: boolean) {
await this.setMode('setNanoeStandAloneMode', value);
}
async setQuietMode(value: boolean) {
await this.setMode('setQuietMode', value);
}
async setPowerfulMode(value: boolean) {
await this.setMode('setPowerfulMode', value);
}
// set Swing Up Down
async setSwingUpDown(value) {
this.platform.log.debug(`${this.accessory.displayName}: setSwingUpDown()`);
const parameters: ComfortCloudDeviceUpdatePayload = {};
if (value) {
// if Swing Left Right is enabled than set Swing Auto (Up Down and Left Right)
if (this.deviceStatus.fanAutoMode === 3) {
parameters.fanAutoMode = 0;
} else {
parameters.fanAutoMode = 2;
}
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Swing Up Down On`);
} else {
if (this.deviceStatus.fanAutoMode === 0) {
parameters.fanAutoMode = 3;
} else {
parameters.fanAutoMode = 1;
}
parameters.airSwingUD = (this.devConfig?.swingDefaultUD !== null) ? this.devConfig?.swingDefaultUD : 2;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Swing Up Down Off`);
}
this.sendDeviceUpdate(this.accessory.context.device.deviceGuid, parameters);
}
// set Swing Left Right
async setSwingLeftRight(value) {
this.platform.log.debug(`${this.accessory.displayName}: setSwingLeftRight()`);
const parameters: ComfortCloudDeviceUpdatePayload = {};
if (value) {
// if Swing Up Down is enabled than set Swing Auto (Up Down and Left Right)
if (this.deviceStatus.fanAutoMode === 2) {
parameters.fanAutoMode = 0;
} else {
parameters.fanAutoMode = 3;
}
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Swing Left Right On`);
} else {
if (this.deviceStatus.fanAutoMode === 0) {
parameters.fanAutoMode = 2;
} else {
parameters.fanAutoMode = 1;
}
parameters.airSwingLR = (this.devConfig?.swingDefaultLR !== null) ? this.devConfig?.swingDefaultLR : 2;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: Swing Left Right Off`);
}
this.sendDeviceUpdate(this.accessory.context.device.deviceGuid, parameters);
}
// set Fan speed
async setFanSpeed(value) {
// set Fan speed
if (value >= 0 && value <= 100) {
this.platform.log.debug(`${this.accessory.displayName}: setFanSpeed(), value: ${value}`);
const parameters: ComfortCloudDeviceUpdatePayload = {};
if (value === 0) {
// Turn off
parameters.operate = 0;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: set off`);
} else if (value > 0 && value <= 20) {
parameters.ecoMode = 0;
parameters.fanSpeed = 1;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: set fan speed 1`);
} else if (value > 20 && value <= 40) {
parameters.ecoMode = 0;
parameters.fanSpeed = 2;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: set fan speed 2`);
} else if (value > 40 && value <= 60) {
parameters.ecoMode = 0;
parameters.fanSpeed = 3;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: set fan speed 3`);
} else if (value > 60 && value <= 80) {
parameters.ecoMode = 0;
parameters.fanSpeed = 4;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: set fan speed 4`);
} else if (value > 80 && value < 100) {
parameters.ecoMode = 0;
parameters.fanSpeed = 5;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: set fan speed 5`);
} else if (value === 100) {
// Auto mode
parameters.ecoMode = 0;
parameters.fanSpeed = 0;
this.platform.log[(this.platform.platformConfig.logsLevel >= 1) ? 'info' : 'debug'](`${this.accessory.displayName}: set fan speed auto`);
}
this.sendDeviceUpdate(this.accessory.context.device.deviceGuid, parameters);
}
}
// ===============================================================================================================================================
async setThresholdTemperature(value: CharacteristicValue) {
/**
* This function is used for Cooling AND Heating Threshold Temperature,
* which is fine in HEAT and COOL mode. But in AUTO mode, it results in a conflict
* because HomeKit allows setting a lower and an upper temperature but the remote control
* and Comfort Cloud app only set the target temperature.
*
* Option 1: Don't map the AUTO setting in HomeKit to AUTO on ComfortCloud (CC),
* but switch to COOL or HEAT on CC depending on the current room temperature.
* In that case, we could process the heating and cooling threshold accordingly.
* Caveat: HomeKit set to AUTO would show up as HEAT or COOL in the CC app, i.e.
* we would produce an inconsistent state across control interfaces.
*
* Option 2: Map AUTO in HomeKit to AUTO on CC and set the temperature which was set last
* as target temperature. The user would have to drag both sliders close to each other
* and treat it as one bar.
* Caveat: We cannot replace a range slider in HomeKit by a single value. Any user
* who doesn't read this note might be confused about this.
*
* Current choice is option 2 because the only implication for the user is wrongly set
* temperature in the worst case. Option 1 would offer full functionality, but decrease
* the compatibility with the Comfort Cloud app.
*/
this.platform.log.debug(
`Accessory: setThresholdTemperature() for device '${this.accessory.displayName}'`);
const parameters: ComfortCloudDeviceUpdatePayload = {
temperatureSet: value as number,
};
this.sendDeviceUpdate(this.accessory.context.device.deviceGuid, parameters);
}
async sendDeviceUpdate(guid: string, payload: ComfortCloudDeviceUpdatePayload = {}) {
try {
// HomeKit sends commands when a move starts, not when it ends, so there can be several commands during one move.
// Users often send several commands at once, e.g. in automation.
// Collect together all parameters sent in a specified time, so as not to send each parameters separately.
this.sendDeviceUpdatePayload = Object.assign(this.sendDeviceUpdatePayload, payload);
clearTimeout(this.timerSendDeviceUpdate);
this.timerSendDeviceUpdate = null;
// Only send non-empty payloads to prevent a '500 Internal Server Error'
if (Object.keys(this.sendDeviceUpdatePayload).length > 0) {
this.timerSendDeviceUpdate = setTimeout(() => {
// Workaround - API not storing fanSpeed and ecoMode.
// Apply only when device is turned off and it is turning on
// and there is no command to set fanSpeed or ecoMode.
if (this.deviceStatus.operate === 0
&& this.sendDeviceUpdatePayload.operate === 1
&& !Object.prototype.hasOwnProperty.call(this.sendDeviceUpdatePayload, 'fanSpeed')
&& !Object.prototype.hasOwnProperty.call(this.sendDeviceUpdatePayload, 'ecoMode')) {
const parameters: any = {};
switch (this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed).value) {
case 1:
parameters.ecoMode = 2;
break;
case 2:
parameters.fanSpeed = 1;
break;
case 3:
parameters.fanSpeed = 2;
break;
case 4:
parameters.fanSpeed = 3;
break;
case 5:
parameters.fanSpeed = 4;
break;
case 6:
parameters.fanSpeed = 5;
break;
case 7:
parameters.ecoMode = 1;
break;
default:
parameters.ecoMode = 0;
parameters.fanSpeed = 0;
break;
}
this.platform.log.debug(`${this.accessory.displayName}: Applying workaround fix for speed and eco mode, `
+ `adding parameters ${JSON.stringify(parameters)} to ${JSON.stringify(this.sendDeviceUpdatePayload)}.`);
this.sendDeviceUpdatePayload = Object.assign(this.sendDeviceUpdatePayload, parameters);
}
// Send update
this.platform.log.debug(`${this.accessory.displayName}: sendDeviceUpdatePayload: ${JSON.stringify(this.sendDeviceUpdatePayload)}`);
this.platform.comfortCloud.setDeviceStatus(guid, this.accessory.displayName, this.sendDeviceUpdatePayload);
// Reset payload
this.sendDeviceUpdatePayload = {};
// Refresh device status
clearTimeout(this.timerSendDeviceUpdateRefresh);
this.timerSendDeviceUpdateRefresh = null;
this.timerSendDeviceUpdateRefresh = setTimeout(this.refreshDeviceStatus.bind(this), 7500);
}, 2500);
}
} catch (error) {
this.platform.log.error('An error occurred while sending a device update. '
+ 'Turn on debug mode for more information.');
// Only log if a Promise rejection reason was provided.
// Some errors are already logged at source.
if (error) {
this.platform.log.debug(error);
}
}
}
}