Skip to content

Commit

Permalink
Add IR/RF device support. (#191)
Browse files Browse the repository at this point in the history
* tidy the code

* Reuse `Name`, `ConfiguredName` Characteristic.

* Add some IR Remote Controls support.

* Add generic infrared remote support (ac not included yet).

* Add IRAirConditioner accessory.

* Update docs.
  • Loading branch information
0x5e authored Feb 14, 2023
1 parent 48322a2 commit 8d117eb
Show file tree
Hide file tree
Showing 15 changed files with 461 additions and 49 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- Add Temperature Control Socket support (`wkcz`).
- Add Environmental Detector support (`hjjcy`).
- Add Water Valve Controller support (`sfkzq`).
- Add IR/RF Remote Control support (`infrared_tv`, `infrared_stb`, `infrared_box`, `infrared_ac`, `infrared_fan`, `infrared_light`, `infrared_amplifier`, `infrared_projector`, `infrared_waterheater`, `infrared_airpurifier`).


### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Fork version of the official Tuya Homebridge plugin, with a focus on fixing bugs
- Lower development costs for new accessory categories.
- Supports Tuya Scenes (Tap-to-Run).
- Includes the ability to override device configurations, which enables support for "non-standard" DPs.
- Supports over 40+ device categories, including most lights, switches, sensors, cameras, etc.
- Supports over 50+ device categories, including most lights, switches, sensors, cameras, IR/RF remote, etc.


## Supported Tuya Devices
Expand Down
19 changes: 19 additions & 0 deletions SUPPORTED_DEVICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,25 @@ Most category code is pinyin abbreviation of Chinese name.
| Tracker | 定位器 | tracker | | |


## IR/RF Remote Control

| Name | Name (zh) | Code | Homebridge Service | Supported |
| ---- | ---- | ---- | ---- | ---- |
| Universal Remote Control | 万能遥控器 | wnykq | ||
| TV | 电视 | infrared_tv | Switch ||
| STB | 机顶盒 | infrared_stb | Switch ||
| TV Box | 电视盒子 | infrared_box | Switch ||
| Air Conditioner | 空调 | infrared_ac | Heater Cooler<br> Humidifier Dehumidifier<br> Fanv2 ||
| Fan | 电风扇 | infrared_fan | Switch ||
| Light || infrared_light | Switch ||
| Amplifier | 音响 | infrared_amplifier | Switch ||
| Projector | 投影仪 | infrared_projector | Switch ||
| DVD | DVD | qt | Switch ||
| Camera | 相机 | qt | Switch ||
| Water Heater | 热水器 | infrared_waterheater | Switch ||
| Air Purifier | 净化器 | infrared_airpurifier | Switch ||


## Others

For the undocumented product category, you can get code and name from `/v1.0/iot-03/device-categories`, no more detail informations.
14 changes: 14 additions & 0 deletions src/accessory/AccessoryFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import CameraAccessory from './CameraAccessory';
import SceneAccessory from './SceneAccessory';
import AirConditionerAccessory from './AirConditionerAccessory';
import IRControlHubAccessory from './IRControlHubAccessory';
import IRGenericAccessory from './IRGenericAccessory';
import IRAirConditionerAccessory from './IRAirConditionerAccessory';


export default class AccessoryFactory {
Expand Down Expand Up @@ -180,6 +182,18 @@ export default class AccessoryFactory {
break;
}

// IR Remote Control
if (device.remote_keys) {
switch (device.remote_keys.category_id) {
case 5: // AC
handler = new IRAirConditionerAccessory(platform, accessory);
break;
default:
handler = new IRGenericAccessory(platform, accessory);
break;
}
}

if (handler && !handler.checkRequirements()) {
handler = undefined;
}
Expand Down
8 changes: 2 additions & 6 deletions src/accessory/DimmerAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Service } from 'homebridge';
import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice';
import { remap, limit } from '../util/util';
import BaseAccessory from './BaseAccessory';
import { configureName } from './characteristic/Name';
import { configureOn } from './characteristic/On';

const SCHEMA_CODE = {
Expand Down Expand Up @@ -31,12 +32,7 @@ export default class DimmerAccessory extends BaseAccessory {
const service = this.accessory.getService(_schema.code)
|| this.accessory.addService(this.Service.Lightbulb, name, _schema.code);

service.setCharacteristic(this.Characteristic.Name, name);
if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) {
service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning
service.setCharacteristic(this.Characteristic.ConfiguredName, name);
}

configureName(this, service, name);
configureOn(this, service, this.getSchema('switch' + suffix, 'switch_led' + suffix));
this.configureBrightness(service, suffix);
}
Expand Down
252 changes: 252 additions & 0 deletions src/accessory/IRAirConditionerAccessory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import debounce from 'debounce';
import BaseAccessory from './BaseAccessory';

const POWER_OFF = 0;
const POWER_ON = 1;

const AC_MODE_COOL = 0;
const AC_MODE_HEAT = 1;
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_MEDIUM = 2;
// const FAN_SPEED_HIGH = 3;

export default class IRAirConditionerAccessory extends BaseAccessory {

configureServices() {
this.configureAirConditioner();
this.configureDehumidifier();
this.configureFan();
}

configureAirConditioner() {

const service = this.mainService();
const { INACTIVE, ACTIVE } = this.Characteristic.Active;

// Required Characteristics
service.getCharacteristic(this.Characteristic.Active)
.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.debounceSendACCommands();
});

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

this.configureTargetState();

// 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)!;
service.getCharacteristic(this.Characteristic.HeatingThresholdTemperature)
.onSet(() => {
this.debounceSendACCommands();
})
.setProps({ minValue: range[0], maxValue: range[1], minStep: 1 });
}
if (key_range.find(item => item.mode === AC_MODE_COOL)) {
const range = this.getTempRange(AC_MODE_COOL)!;
service.getCharacteristic(this.Characteristic.CoolingThresholdTemperature)
.onSet(() => {
this.debounceSendACCommands();
})
.setProps({ minValue: range[0], maxValue: range[1], minStep: 1 });
}
}

configureDehumidifier() {
if (!this.supportDehumidifier()) {
return;
}

const service = this.dehumidifierService();
const { INACTIVE, ACTIVE } = this.Characteristic.Active;

// Required Characteristics
service.getCharacteristic(this.Characteristic.Active)
.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.debounceSendACCommands();
});

const { DEHUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState;
service.setCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState, DEHUMIDIFYING);

const { DEHUMIDIFIER } = this.Characteristic.TargetHumidifierDehumidifierState;
service.getCharacteristic(this.Characteristic.TargetHumidifierDehumidifierState)
.updateValue(DEHUMIDIFIER)
.setProps({ validValues: [DEHUMIDIFIER] });

service.setCharacteristic(this.Characteristic.CurrentRelativeHumidity, 0);

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

configureFan() {
if (!this.supportFan()) {
return;
}

const service = this.fanService();
const { INACTIVE, ACTIVE } = this.Characteristic.Active;

// Required Characteristics
service.getCharacteristic(this.Characteristic.Active)
.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);
}
this.debounceSendACCommands();
});

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

mainService() {
return this.accessory.getService(this.Service.HeaterCooler)
|| this.accessory.addService(this.Service.HeaterCooler);
}

dehumidifierService() {
return this.accessory.getService(this.Service.HumidifierDehumidifier)
|| this.accessory.addService(this.Service.HumidifierDehumidifier, this.accessory.displayName + ' Dehumidifier');
}

fanService() {
return this.accessory.getService(this.Service.Fanv2)
|| this.accessory.addService(this.Service.Fanv2, this.accessory.displayName + ' Fan');
}

getKeyRangeItem(mode: number) {
return this.device.remote_keys.key_range.find(item => item.mode === mode);
}

supportDehumidifier() {
return this.getKeyRangeItem(AC_MODE_DEHUMIDIFIER) !== undefined;
}

supportFan() {
return this.getKeyRangeItem(AC_MODE_FAN) !== undefined;
}

getTempRange(mode: number) {
const keyRangeItem = this.getKeyRangeItem(mode);
if (!keyRangeItem || !keyRangeItem.temp_list || keyRangeItem.temp_list.length === 0) {
return undefined;
}

const min = keyRangeItem.temp_list[0].temp;
const max = keyRangeItem.temp_list[keyRangeItem.temp_list.length - 1].temp;
return [min, max];
}

configureTargetState() {
const { AUTO, HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState;

const validValues: number[] = [];
const key_range = this.device.remote_keys.key_range;
if (key_range.find(item => item.mode === AC_MODE_AUTO)) {
validValues.push(AUTO);
}
if (key_range.find(item => item.mode === AC_MODE_HEAT)) {
validValues.push(HEAT);
}
if (key_range.find(item => item.mode === AC_MODE_COOL)) {
validValues.push(COOL);
}

if (validValues.length === 0) {
this.log.warn('Invalid mode range for TargetHeaterCoolerState:', key_range);
return;
}

this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState)
.onSet(() => {
this.debounceSendACCommands();
})
.setProps({ validValues });
}

configureRotationSpeed(service) {
service.getCharacteristic(this.Characteristic.RotationSpeed)
.onSet(() => {
this.debounceSendACCommands();
})
.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);

}
}
2 changes: 1 addition & 1 deletion src/accessory/IRControlHubAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const SCHEMA_CODE = {
CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'],
};

export default class TemperatureHumidityIRSensorAccessory extends BaseAccessory {
export default class IRControlHubAccessory extends BaseAccessory {

requiredSchema() {
return [];
Expand Down
Loading

0 comments on commit 8d117eb

Please sign in to comment.