Skip to content

Commit 8d117eb

Browse files
authored
Add IR/RF device support. (#191)
* 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.
1 parent 48322a2 commit 8d117eb

15 files changed

Lines changed: 461 additions & 49 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- Add Temperature Control Socket support (`wkcz`).
1717
- Add Environmental Detector support (`hjjcy`).
1818
- Add Water Valve Controller support (`sfkzq`).
19+
- 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`).
1920

2021

2122
### Fixed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Fork version of the official Tuya Homebridge plugin, with a focus on fixing bugs
2020
- Lower development costs for new accessory categories.
2121
- Supports Tuya Scenes (Tap-to-Run).
2222
- Includes the ability to override device configurations, which enables support for "non-standard" DPs.
23-
- Supports over 40+ device categories, including most lights, switches, sensors, cameras, etc.
23+
- Supports over 50+ device categories, including most lights, switches, sensors, cameras, IR/RF remote, etc.
2424

2525

2626
## Supported Tuya Devices

SUPPORTED_DEVICES.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,25 @@ Most category code is pinyin abbreviation of Chinese name.
173173
| Tracker | 定位器 | tracker | | |
174174

175175

176+
## IR/RF Remote Control
177+
178+
| Name | Name (zh) | Code | Homebridge Service | Supported |
179+
| ---- | ---- | ---- | ---- | ---- |
180+
| Universal Remote Control | 万能遥控器 | wnykq | ||
181+
| TV | 电视 | infrared_tv | Switch ||
182+
| STB | 机顶盒 | infrared_stb | Switch ||
183+
| TV Box | 电视盒子 | infrared_box | Switch ||
184+
| Air Conditioner | 空调 | infrared_ac | Heater Cooler<br> Humidifier Dehumidifier<br> Fanv2 ||
185+
| Fan | 电风扇 | infrared_fan | Switch ||
186+
| Light || infrared_light | Switch ||
187+
| Amplifier | 音响 | infrared_amplifier | Switch ||
188+
| Projector | 投影仪 | infrared_projector | Switch ||
189+
| DVD | DVD | qt | Switch ||
190+
| Camera | 相机 | qt | Switch ||
191+
| Water Heater | 热水器 | infrared_waterheater | Switch ||
192+
| Air Purifier | 净化器 | infrared_airpurifier | Switch ||
193+
194+
176195
## Others
177196

178197
For the undocumented product category, you can get code and name from `/v1.0/iot-03/device-categories`, no more detail informations.

src/accessory/AccessoryFactory.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import CameraAccessory from './CameraAccessory';
3434
import SceneAccessory from './SceneAccessory';
3535
import AirConditionerAccessory from './AirConditionerAccessory';
3636
import IRControlHubAccessory from './IRControlHubAccessory';
37+
import IRGenericAccessory from './IRGenericAccessory';
38+
import IRAirConditionerAccessory from './IRAirConditionerAccessory';
3739

3840

3941
export default class AccessoryFactory {
@@ -180,6 +182,18 @@ export default class AccessoryFactory {
180182
break;
181183
}
182184

185+
// IR Remote Control
186+
if (device.remote_keys) {
187+
switch (device.remote_keys.category_id) {
188+
case 5: // AC
189+
handler = new IRAirConditionerAccessory(platform, accessory);
190+
break;
191+
default:
192+
handler = new IRGenericAccessory(platform, accessory);
193+
break;
194+
}
195+
}
196+
183197
if (handler && !handler.checkRequirements()) {
184198
handler = undefined;
185199
}

src/accessory/DimmerAccessory.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Service } from 'homebridge';
22
import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice';
33
import { remap, limit } from '../util/util';
44
import BaseAccessory from './BaseAccessory';
5+
import { configureName } from './characteristic/Name';
56
import { configureOn } from './characteristic/On';
67

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

34-
service.setCharacteristic(this.Characteristic.Name, name);
35-
if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) {
36-
service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning
37-
service.setCharacteristic(this.Characteristic.ConfiguredName, name);
38-
}
39-
35+
configureName(this, service, name);
4036
configureOn(this, service, this.getSchema('switch' + suffix, 'switch_led' + suffix));
4137
this.configureBrightness(service, suffix);
4238
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import debounce from 'debounce';
2+
import BaseAccessory from './BaseAccessory';
3+
4+
const POWER_OFF = 0;
5+
const POWER_ON = 1;
6+
7+
const AC_MODE_COOL = 0;
8+
const AC_MODE_HEAT = 1;
9+
const AC_MODE_AUTO = 2;
10+
const AC_MODE_FAN = 3;
11+
const AC_MODE_DEHUMIDIFIER = 4;
12+
13+
// const FAN_SPEED_AUTO = 0;
14+
// const FAN_SPEED_LOW = 1;
15+
// const FAN_SPEED_MEDIUM = 2;
16+
// const FAN_SPEED_HIGH = 3;
17+
18+
export default class IRAirConditionerAccessory extends BaseAccessory {
19+
20+
configureServices() {
21+
this.configureAirConditioner();
22+
this.configureDehumidifier();
23+
this.configureFan();
24+
}
25+
26+
configureAirConditioner() {
27+
28+
const service = this.mainService();
29+
const { INACTIVE, ACTIVE } = this.Characteristic.Active;
30+
31+
// Required Characteristics
32+
service.getCharacteristic(this.Characteristic.Active)
33+
.onSet(value => {
34+
if (value === ACTIVE) {
35+
// Turn off Dehumidifier & Fan
36+
this.supportDehumidifier() && this.dehumidifierService().setCharacteristic(this.Characteristic.Active, INACTIVE);
37+
this.supportFan() && this.fanService().setCharacteristic(this.Characteristic.Active, INACTIVE);
38+
}
39+
this.debounceSendACCommands();
40+
});
41+
42+
const { IDLE } = this.Characteristic.CurrentHeaterCoolerState;
43+
service.setCharacteristic(this.Characteristic.CurrentHeaterCoolerState, IDLE);
44+
45+
this.configureTargetState();
46+
47+
// Optional Characteristics
48+
this.configureRotationSpeed(service);
49+
50+
const key_range = this.device.remote_keys.key_range;
51+
if (key_range.find(item => item.mode === AC_MODE_HEAT)) {
52+
const range = this.getTempRange(AC_MODE_HEAT)!;
53+
service.getCharacteristic(this.Characteristic.HeatingThresholdTemperature)
54+
.onSet(() => {
55+
this.debounceSendACCommands();
56+
})
57+
.setProps({ minValue: range[0], maxValue: range[1], minStep: 1 });
58+
}
59+
if (key_range.find(item => item.mode === AC_MODE_COOL)) {
60+
const range = this.getTempRange(AC_MODE_COOL)!;
61+
service.getCharacteristic(this.Characteristic.CoolingThresholdTemperature)
62+
.onSet(() => {
63+
this.debounceSendACCommands();
64+
})
65+
.setProps({ minValue: range[0], maxValue: range[1], minStep: 1 });
66+
}
67+
}
68+
69+
configureDehumidifier() {
70+
if (!this.supportDehumidifier()) {
71+
return;
72+
}
73+
74+
const service = this.dehumidifierService();
75+
const { INACTIVE, ACTIVE } = this.Characteristic.Active;
76+
77+
// Required Characteristics
78+
service.getCharacteristic(this.Characteristic.Active)
79+
.onSet(value => {
80+
if (value === ACTIVE) {
81+
// Turn off AC & Fan
82+
this.mainService().setCharacteristic(this.Characteristic.Active, INACTIVE);
83+
this.supportFan() && this.fanService().setCharacteristic(this.Characteristic.Active, INACTIVE);
84+
}
85+
this.debounceSendACCommands();
86+
});
87+
88+
const { DEHUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState;
89+
service.setCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState, DEHUMIDIFYING);
90+
91+
const { DEHUMIDIFIER } = this.Characteristic.TargetHumidifierDehumidifierState;
92+
service.getCharacteristic(this.Characteristic.TargetHumidifierDehumidifierState)
93+
.updateValue(DEHUMIDIFIER)
94+
.setProps({ validValues: [DEHUMIDIFIER] });
95+
96+
service.setCharacteristic(this.Characteristic.CurrentRelativeHumidity, 0);
97+
98+
// Optional Characteristics
99+
this.configureRotationSpeed(service);
100+
}
101+
102+
configureFan() {
103+
if (!this.supportFan()) {
104+
return;
105+
}
106+
107+
const service = this.fanService();
108+
const { INACTIVE, ACTIVE } = this.Characteristic.Active;
109+
110+
// Required Characteristics
111+
service.getCharacteristic(this.Characteristic.Active)
112+
.onSet(value => {
113+
if (value === ACTIVE) {
114+
// Turn off AC & Fan
115+
this.mainService().setCharacteristic(this.Characteristic.Active, INACTIVE);
116+
this.supportDehumidifier() && this.dehumidifierService().setCharacteristic(this.Characteristic.Active, INACTIVE);
117+
}
118+
this.debounceSendACCommands();
119+
});
120+
121+
// Optional Characteristics
122+
this.configureRotationSpeed(service);
123+
}
124+
125+
mainService() {
126+
return this.accessory.getService(this.Service.HeaterCooler)
127+
|| this.accessory.addService(this.Service.HeaterCooler);
128+
}
129+
130+
dehumidifierService() {
131+
return this.accessory.getService(this.Service.HumidifierDehumidifier)
132+
|| this.accessory.addService(this.Service.HumidifierDehumidifier, this.accessory.displayName + ' Dehumidifier');
133+
}
134+
135+
fanService() {
136+
return this.accessory.getService(this.Service.Fanv2)
137+
|| this.accessory.addService(this.Service.Fanv2, this.accessory.displayName + ' Fan');
138+
}
139+
140+
getKeyRangeItem(mode: number) {
141+
return this.device.remote_keys.key_range.find(item => item.mode === mode);
142+
}
143+
144+
supportDehumidifier() {
145+
return this.getKeyRangeItem(AC_MODE_DEHUMIDIFIER) !== undefined;
146+
}
147+
148+
supportFan() {
149+
return this.getKeyRangeItem(AC_MODE_FAN) !== undefined;
150+
}
151+
152+
getTempRange(mode: number) {
153+
const keyRangeItem = this.getKeyRangeItem(mode);
154+
if (!keyRangeItem || !keyRangeItem.temp_list || keyRangeItem.temp_list.length === 0) {
155+
return undefined;
156+
}
157+
158+
const min = keyRangeItem.temp_list[0].temp;
159+
const max = keyRangeItem.temp_list[keyRangeItem.temp_list.length - 1].temp;
160+
return [min, max];
161+
}
162+
163+
configureTargetState() {
164+
const { AUTO, HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState;
165+
166+
const validValues: number[] = [];
167+
const key_range = this.device.remote_keys.key_range;
168+
if (key_range.find(item => item.mode === AC_MODE_AUTO)) {
169+
validValues.push(AUTO);
170+
}
171+
if (key_range.find(item => item.mode === AC_MODE_HEAT)) {
172+
validValues.push(HEAT);
173+
}
174+
if (key_range.find(item => item.mode === AC_MODE_COOL)) {
175+
validValues.push(COOL);
176+
}
177+
178+
if (validValues.length === 0) {
179+
this.log.warn('Invalid mode range for TargetHeaterCoolerState:', key_range);
180+
return;
181+
}
182+
183+
this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState)
184+
.onSet(() => {
185+
this.debounceSendACCommands();
186+
})
187+
.setProps({ validValues });
188+
}
189+
190+
configureRotationSpeed(service) {
191+
service.getCharacteristic(this.Characteristic.RotationSpeed)
192+
.onSet(() => {
193+
this.debounceSendACCommands();
194+
})
195+
.setProps({ minValue: 0, maxValue: 3, minStep: 1, unit: 'speed' });
196+
}
197+
198+
debounceSendACCommands = debounce(this.sendACCommands, 100);
199+
200+
async sendACCommands() {
201+
202+
let power = POWER_ON;
203+
let mode = -1;
204+
let temp = -1;
205+
let wind = -1;
206+
207+
// Determine AC mode
208+
const { ACTIVE } = this.Characteristic.Active;
209+
if (this.mainService().getCharacteristic(this.Characteristic.Active).value === ACTIVE) {
210+
const { HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState;
211+
const value = this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState)
212+
.value as number;
213+
if (value === HEAT) {
214+
mode = AC_MODE_HEAT;
215+
} else if (value === COOL) {
216+
mode = AC_MODE_COOL;
217+
} else {
218+
mode = AC_MODE_AUTO;
219+
}
220+
} else if (this.supportDehumidifier() && this.dehumidifierService().getCharacteristic(this.Characteristic.Active).value === ACTIVE) {
221+
mode = AC_MODE_DEHUMIDIFIER;
222+
} else if (this.supportFan() && this.fanService().getCharacteristic(this.Characteristic.Active).value === ACTIVE) {
223+
mode = AC_MODE_FAN;
224+
} else {
225+
// No mode
226+
power = POWER_OFF;
227+
}
228+
229+
if (mode === AC_MODE_AUTO) {
230+
temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number;
231+
wind = this.mainService().getCharacteristic(this.Characteristic.RotationSpeed).value as number;
232+
} else if (mode === AC_MODE_HEAT) {
233+
temp = this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature).value as number;
234+
wind = this.mainService().getCharacteristic(this.Characteristic.RotationSpeed).value as number;
235+
} else if (mode === AC_MODE_COOL) {
236+
temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number;
237+
wind = this.mainService().getCharacteristic(this.Characteristic.RotationSpeed).value as number;
238+
} else if (mode === AC_MODE_DEHUMIDIFIER) {
239+
temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number;
240+
wind = this.dehumidifierService().getCharacteristic(this.Characteristic.RotationSpeed).value as number;
241+
} else if (mode === AC_MODE_FAN) {
242+
temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number;
243+
wind = this.fanService().getCharacteristic(this.Characteristic.RotationSpeed).value as number;
244+
}
245+
246+
(power === POWER_ON) && this.mainService().setCharacteristic(this.Characteristic.CurrentTemperature, temp);
247+
248+
const { parent_id, id } = this.device;
249+
await this.deviceManager.sendInfraredACCommands(parent_id, id, power, mode, temp, wind);
250+
251+
}
252+
}

src/accessory/IRControlHubAccessory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const SCHEMA_CODE = {
77
CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'],
88
};
99

10-
export default class TemperatureHumidityIRSensorAccessory extends BaseAccessory {
10+
export default class IRControlHubAccessory extends BaseAccessory {
1111

1212
requiredSchema() {
1313
return [];

0 commit comments

Comments
 (0)