Skip to content

Commit

Permalink
Update IR sub device support (#261)
Browse files Browse the repository at this point in the history
* Add fan auto mode for IR AC device

* Get initial status for IR AC device.

* Sync IR AC device status from App.

* Add IR DIY device support.
  • Loading branch information
0x5e authored Mar 16, 2023
1 parent 25112f6 commit 97781d1
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 85 deletions.
199 changes: 124 additions & 75 deletions src/accessory/IRAirConditionerAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ const AC_MODE_AUTO = 2;
const AC_MODE_FAN = 3;
const AC_MODE_DEHUMIDIFIER = 4;

// const FAN_SPEED_AUTO = 0;
// const FAN_SPEED_LOW = 1;
const FAN_SPEED_AUTO = 0;
const FAN_SPEED_LOW = 1;
// const FAN_SPEED_MEDIUM = 2;
// const FAN_SPEED_HIGH = 3;
const FAN_SPEED_HIGH = 3;

export default class IRAirConditionerAccessory extends BaseAccessory {

Expand All @@ -30,39 +30,56 @@ export default class IRAirConditionerAccessory extends BaseAccessory {

// Required Characteristics
service.getCharacteristic(this.Characteristic.Active)
.onGet(() => {
return ([AC_MODE_COOL, AC_MODE_HEAT, AC_MODE_AUTO].includes(this.getMode()) && this.getPower() === POWER_ON) ? ACTIVE : INACTIVE;
})
.onSet(value => {
if (value === ACTIVE) {
// Turn off Dehumidifier & Fan
this.supportDehumidifier() && this.dehumidifierService().setCharacteristic(this.Characteristic.Active, INACTIVE);
this.supportFan() && this.fanService().setCharacteristic(this.Characteristic.Active, INACTIVE);
this.supportDehumidifier() && this.dehumidifierService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);
this.supportFan() && this.fanService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);
this.fanService().getCharacteristic(this.Characteristic.Active).value = INACTIVE;
}

if (value === ACTIVE && ![AC_MODE_COOL, AC_MODE_HEAT, AC_MODE_AUTO].includes(this.getMode())) {
this.setMode(AC_MODE_AUTO);
}
this.debounceSendACCommands();
this.setPower((value === ACTIVE) ? POWER_ON : POWER_OFF);
});

const { IDLE } = this.Characteristic.CurrentHeaterCoolerState;
service.setCharacteristic(this.Characteristic.CurrentHeaterCoolerState, IDLE);

this.configureTargetState();
this.configureCurrentTemperature();

// Optional Characteristics
this.configureRotationSpeed(service);

const key_range = this.device.remote_keys.key_range;
if (key_range.find(item => item.mode === AC_MODE_HEAT)) {
const range = this.getTempRange(AC_MODE_HEAT)!;
const [minValue, maxValue] = this.getTempRange(AC_MODE_HEAT)!;
service.getCharacteristic(this.Characteristic.HeatingThresholdTemperature)
.onSet(() => {
this.debounceSendACCommands();
.onGet(() => {
if (this.getMode() === AC_MODE_AUTO) {
return minValue;
}
return this.getTemp();
})
.setProps({ minValue: range[0], maxValue: range[1], minStep: 1 });
.onSet(value => {
if (this.getMode() === AC_MODE_AUTO) {
return;
}
this.setTemp(value);
})
.setProps({ minValue, maxValue, minStep: 1 });
}
if (key_range.find(item => item.mode === AC_MODE_COOL)) {
const range = this.getTempRange(AC_MODE_COOL)!;
const [minValue, maxValue] = this.getTempRange(AC_MODE_COOL)!;
service.getCharacteristic(this.Characteristic.CoolingThresholdTemperature)
.onSet(() => {
this.debounceSendACCommands();
})
.setProps({ minValue: range[0], maxValue: range[1], minStep: 1 });
.onGet(this.getTemp.bind(this))
.onSet(this.setTemp.bind(this))
.setProps({ minValue, maxValue, minStep: 1 });
}
}

Expand All @@ -76,13 +93,18 @@ export default class IRAirConditionerAccessory extends BaseAccessory {

// Required Characteristics
service.getCharacteristic(this.Characteristic.Active)
.onGet(() => {
return (this.getMode() === AC_MODE_DEHUMIDIFIER && this.getPower() === POWER_ON) ? ACTIVE : INACTIVE;
})
.onSet(value => {
if (value === ACTIVE) {
// Turn off AC & Fan
this.mainService().setCharacteristic(this.Characteristic.Active, INACTIVE);
this.supportFan() && this.fanService().setCharacteristic(this.Characteristic.Active, INACTIVE);
this.mainService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);
this.supportFan() && this.fanService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);
}
this.debounceSendACCommands();

this.setMode(AC_MODE_DEHUMIDIFIER);
this.setPower((value === ACTIVE) ? POWER_ON : POWER_OFF);
});

const { DEHUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState;
Expand All @@ -109,16 +131,22 @@ export default class IRAirConditionerAccessory extends BaseAccessory {

// Required Characteristics
service.getCharacteristic(this.Characteristic.Active)
.onGet(() => {
return (this.getMode() === AC_MODE_FAN && this.getPower() === POWER_ON) ? ACTIVE : INACTIVE;
})
.onSet(value => {
if (value === ACTIVE) {
// Turn off AC & Fan
this.mainService().setCharacteristic(this.Characteristic.Active, INACTIVE);
this.supportDehumidifier() && this.dehumidifierService().setCharacteristic(this.Characteristic.Active, INACTIVE);
// Turn off AC & Dehumidifier
this.mainService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);
this.supportDehumidifier() && this.dehumidifierService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);
}
this.debounceSendACCommands();

this.setMode(AC_MODE_FAN);
this.setPower((value === ACTIVE) ? POWER_ON : POWER_OFF);
});

// Optional Characteristics
this.configureTargetFanState(service);
this.configureRotationSpeed(service);
}

Expand All @@ -137,6 +165,46 @@ export default class IRAirConditionerAccessory extends BaseAccessory {
|| this.accessory.addService(this.Service.Fanv2, this.accessory.displayName + ' Fan');
}

getPower() {
const status = this.getStatus('power')!;
return (status.value === true || parseInt(status.value.toString()) === 1) ? POWER_ON : POWER_OFF;
}

setPower(value) {
this.getStatus('power')!.value = value;
this.debounceSendACCommands();
}

getMode() {
const status = this.getStatus('mode')!;
return parseInt(status.value.toString());
}

setMode(value) {
this.getStatus('mode')!.value = value;
this.debounceSendACCommands();
}

getWind() {
const status = this.getStatus('wind')!;
return parseInt(status.value.toString());
}

setWind(value) {
this.getStatus('wind')!.value = value;
this.debounceSendACCommands();
}

getTemp() {
const status = this.getStatus('temp')!;
return parseInt(status.value.toString());
}

setTemp(value) {
this.getStatus('temp')!.value = value;
this.debounceSendACCommands();
}

getKeyRangeItem(mode: number) {
return this.device.remote_keys.key_range.find(item => item.mode === mode);
}
Expand Down Expand Up @@ -181,72 +249,53 @@ export default class IRAirConditionerAccessory extends BaseAccessory {
}

this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState)
.onSet(() => {
this.debounceSendACCommands();
.onGet(() => ({
[AC_MODE_COOL.toString()]: COOL,
[AC_MODE_HEAT.toString()]: HEAT,
[AC_MODE_AUTO.toString()]: AUTO,
}[this.getMode().toString()] || AUTO))
.onSet(value => {
this.setMode({
[COOL.toString()]: AC_MODE_COOL,
[HEAT.toString()]: AC_MODE_HEAT,
[AUTO.toString()]: AC_MODE_AUTO,
}[value.toString()]);
})
.setProps({ validValues });
}

configureCurrentTemperature() {
this.mainService().getCharacteristic(this.Characteristic.CurrentTemperature)
.onGet(this.getTemp.bind(this));
}

configureTargetFanState(service) {
const { MANUAL, AUTO } = this.Characteristic.TargetFanState;
service.getCharacteristic(this.Characteristic.TargetFanState)
.onGet(() => (this.getWind() === FAN_SPEED_AUTO) ? AUTO : MANUAL)
.onSet(value => {
this.setWind((value === AUTO) ? FAN_SPEED_AUTO : FAN_SPEED_LOW);
});
}

configureRotationSpeed(service) {
service.getCharacteristic(this.Characteristic.RotationSpeed)
.onSet(() => {
this.debounceSendACCommands();
.onGet(() => (this.getWind() === FAN_SPEED_AUTO) ? FAN_SPEED_HIGH : this.getWind())
.onSet(value => {
// if (this.getWind() === FAN_SPEED_AUTO) {
// return;
// }
if (value !== 0) {
this.setWind(value);
}
})
.setProps({ minValue: 0, maxValue: 3, minStep: 1, unit: 'speed' });
}

debounceSendACCommands = debounce(this.sendACCommands, 100);

async sendACCommands() {

let power = POWER_ON;
let mode = -1;
let temp = -1;
let wind = -1;

// Determine AC mode
const { ACTIVE } = this.Characteristic.Active;
if (this.mainService().getCharacteristic(this.Characteristic.Active).value === ACTIVE) {
const { HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState;
const value = this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState)
.value as number;
if (value === HEAT) {
mode = AC_MODE_HEAT;
} else if (value === COOL) {
mode = AC_MODE_COOL;
} else {
mode = AC_MODE_AUTO;
}
} else if (this.supportDehumidifier() && this.dehumidifierService().getCharacteristic(this.Characteristic.Active).value === ACTIVE) {
mode = AC_MODE_DEHUMIDIFIER;
} else if (this.supportFan() && this.fanService().getCharacteristic(this.Characteristic.Active).value === ACTIVE) {
mode = AC_MODE_FAN;
} else {
// No mode
power = POWER_OFF;
}

if (mode === AC_MODE_AUTO) {
temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number;
wind = this.mainService().getCharacteristic(this.Characteristic.RotationSpeed).value as number;
} else if (mode === AC_MODE_HEAT) {
temp = this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature).value as number;
wind = this.mainService().getCharacteristic(this.Characteristic.RotationSpeed).value as number;
} else if (mode === AC_MODE_COOL) {
temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number;
wind = this.mainService().getCharacteristic(this.Characteristic.RotationSpeed).value as number;
} else if (mode === AC_MODE_DEHUMIDIFIER) {
temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number;
wind = this.dehumidifierService().getCharacteristic(this.Characteristic.RotationSpeed).value as number;
} else if (mode === AC_MODE_FAN) {
temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number;
wind = this.fanService().getCharacteristic(this.Characteristic.RotationSpeed).value as number;
}

(power === POWER_ON) && this.mainService().setCharacteristic(this.Characteristic.CurrentTemperature, temp);

const { parent_id, id } = this.device;
await this.deviceManager.sendInfraredACCommands(parent_id, id, power, mode, temp, wind);

await this.deviceManager.sendInfraredACCommands(parent_id, id, this.getPower(), this.getMode(), this.getTemp(), this.getWind());
}
}
7 changes: 5 additions & 2 deletions src/accessory/IRGenericAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ export default class IRGenericAccessory extends BaseAccessory {
async sendInfraredCommands(key: TuyaIRRemoteKeyListItem) {
const { parent_id, id } = this.device;
const { category_id, remote_index } = this.device.remote_keys;
const res = await this.deviceManager.sendInfraredCommands(parent_id, id, category_id, remote_index, key.key, key.key_id);
return res;
if (key.learning_code) {
await this.deviceManager.sendInfraredDIYCommands(parent_id, id, key.learning_code);
} else {
await this.deviceManager.sendInfraredCommands(parent_id, id, category_id, remote_index, key.key, key.key_id);
}
}

}
1 change: 1 addition & 0 deletions src/device/TuyaDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type TuyaIRRemoteKeyListItem = {
key_id: number;
key_name: string;
standard_key: boolean;
learning_code?: string; // IR DIY device learning code.
};

export type TuyaIRRemoteTempListItem = {
Expand Down
54 changes: 46 additions & 8 deletions src/device/TuyaDeviceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ export default class TuyaDeviceManager extends EventEmitter {
return res;
}

async getInfraredACStatus(infraredID: string, remoteID: string) {
const res = await this.api.get(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/ac/status`);
return res;
}

async getInfraredDIYKeys(infraredID: string, remoteID: string) {
const res = await this.api.get(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/learning-codes`);
return res;
}

async updateInfraredRemotes(allDevices: TuyaDevice[]) {

const irDevices = allDevices.filter(device => device.isIRControlHub());
Expand All @@ -138,19 +148,42 @@ export default class TuyaDeviceManager extends EventEmitter {
continue;
}

for (const remoteInfo of res.result) {
const device = allDevices.find(device => device.id === remoteInfo.remote_id);
if (!device) {
for (const { category_id, remote_id } of res.result) {
const subDevice = allDevices.find(device => device.id === remote_id);
if (!subDevice) {
continue;
}
device.parent_id = irDevice.id;
device.schema = [];
const res = await this.getInfraredKeys(irDevice.id, device.id);
subDevice.parent_id = irDevice.id;
subDevice.schema = [];
const res = await this.getInfraredKeys(irDevice.id, subDevice.id);
if (!res.success) {
this.log.warn('Get infrared remote keys failed. deviceId = %d, code = %s, msg = %s', device.id, res.code, res.msg);
this.log.warn('Get infrared remote keys failed. deviceId = %d, code = %s, msg = %s', subDevice.id, res.code, res.msg);
continue;
}
device.remote_keys = res.result;
subDevice.remote_keys = res.result;

if (subDevice.category === 'infrared_ac') { // AC Device
const res = await this.getInfraredACStatus(irDevice.id, subDevice.id);
if (!res.success) {
this.log.warn('Get infrared ac status failed. deviceId = %d, code = %s, msg = %s', subDevice.id, res.code, res.msg);
continue;
}
subDevice.status = Object.entries(res.result).map(([key, value]) => ({code: key, value} as TuyaDeviceStatus));
} else if (category_id === 999) { // DIY Device
const res = await this.getInfraredDIYKeys(irDevice.id, subDevice.id);
if (!res.success) {
this.log.warn('Get infrared diy keys failed. deviceId = %d, code = %s, msg = %s', subDevice.id, res.code, res.msg);
continue;
}
for (const key of subDevice.remote_keys.key_list) {
const item = (res.result as []).find(item => item['id'] === key.key_id && item['key'] === key.key);
if (!item) {
continue;
}
this.log.debug('learning_code:', item['code']);
key.learning_code = item['code'];
}
}
}
}
}
Expand All @@ -171,6 +204,11 @@ export default class TuyaDeviceManager extends EventEmitter {
return res;
}

async sendInfraredDIYCommands(infraredID: string, remoteID: string, code: string) {
const res = await this.api.post(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/learning-codes`, { code });
return res;
}


async getLockTemporaryKey(deviceID: string) {
// const res = await this.api.post(`/v1.0/smart-lock/devices/${deviceID}/door-lock/password-ticket`);
Expand Down

0 comments on commit 97781d1

Please sign in to comment.