diff --git a/.homeycompose/capabilities/mute_button.json b/.homeycompose/capabilities/mute_button.json new file mode 100644 index 00000000..0ef38d91 --- /dev/null +++ b/.homeycompose/capabilities/mute_button.json @@ -0,0 +1,10 @@ +{ + "type": "boolean", + "title": { + "en": "Mute" + }, + "getable": false, + "setable": true, + "uiComponent": "button", + "icon": "assets/capabilities/button_mute.svg" +} diff --git a/.homeycompose/capabilities/off_button.json b/.homeycompose/capabilities/off_button.json new file mode 100644 index 00000000..9093e0dd --- /dev/null +++ b/.homeycompose/capabilities/off_button.json @@ -0,0 +1,10 @@ +{ + "type": "boolean", + "title": { + "en": "Off" + }, + "getable": false, + "setable": true, + "uiComponent": "button", + "icon": "assets/capabilities/button_off.svg" +} diff --git a/.homeycompose/capabilities/on_button.json b/.homeycompose/capabilities/on_button.json new file mode 100644 index 00000000..22a70942 --- /dev/null +++ b/.homeycompose/capabilities/on_button.json @@ -0,0 +1,10 @@ +{ + "type": "boolean", + "title": { + "en": "On" + }, + "getable": false, + "setable": true, + "uiComponent": "button", + "icon": "assets/capabilities/button_on.svg" +} diff --git a/.homeycompose/capabilities/power_button.json b/.homeycompose/capabilities/power_button.json new file mode 100644 index 00000000..b7a91089 --- /dev/null +++ b/.homeycompose/capabilities/power_button.json @@ -0,0 +1,10 @@ +{ + "type": "boolean", + "title": { + "en": "Power" + }, + "getable": false, + "setable": true, + "uiComponent": "button", + "icon": "assets/capabilities/button_power.svg" +} diff --git a/app.json b/app.json index ac966691..df28ddf9 100644 --- a/app.json +++ b/app.json @@ -2455,6 +2455,81 @@ } ] }, + { + "id": "infrared_remote_press", + "title": { + "en": "Press button" + }, + "titleFormatted": { + "en": "Press [[button]]" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=infrared_remote" + }, + { + "name": "button", + "type": "autocomplete", + "title": { + "en": "Button" + } + } + ] + }, + { + "id": "infrared_remote_mute_button", + "title": { + "en": "Press mute button" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=infrared_remote&capabilities=mute_button" + } + ] + }, + { + "id": "infrared_remote_power_button", + "title": { + "en": "Press power button" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=infrared_remote&capabilities=power_button" + } + ] + }, + { + "id": "infrared_remote_on_button", + "title": { + "en": "Press on button" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=infrared_remote&capabilities=on_button" + } + ] + }, + { + "id": "infrared_remote_off_button", + "title": { + "en": "Press off button" + }, + "args": [ + { + "type": "device", + "name": "device", + "filter": "driver_id=infrared_remote&capabilities=off_button" + } + ] + }, { "id": "light_switch_pir", "title": { @@ -4557,6 +4632,73 @@ } ] }, + { + "capabilities": [ + "on_button", + "off_button", + "mute_button" + ], + "connectivity": [ + "cloud" + ], + "platforms": [ + "local", + "cloud" + ], + "images": { + "small": "/drivers/infrared_remote/assets/images/small.png", + "large": "/drivers/infrared_remote/assets/images/large.png", + "xlarge": "/drivers/infrared_remote/assets/images/xlarge.png" + }, + "pair": [ + { + "id": "welcome", + "navigation": { + "next": "login_oauth2" + } + }, + { + "id": "login_oauth2", + "template": "login_oauth2" + }, + { + "id": "list_devices", + "template": "list_devices", + "navigation": { + "next": "add_devices" + } + }, + { + "id": "add_devices", + "template": "add_devices" + } + ], + "repair": [ + { + "id": "login_oauth2", + "template": "login_oauth2" + } + ], + "class": "remote", + "name": { + "en": "Infrared Remote", + "nl": "Infrarood Afstandsbediening" + }, + "id": "infrared_remote", + "settings": [ + { + "id": "deviceSpecification", + "type": "label", + "label": { + "en": "Device Specification" + }, + "hint": { + "en": "The Tuya specification of this device" + }, + "value": "" + } + ] + }, { "capabilities": [ "measure_water", @@ -6955,6 +7097,16 @@ } ] }, + "mute_button": { + "type": "boolean", + "title": { + "en": "Mute" + }, + "getable": false, + "setable": true, + "uiComponent": "button", + "icon": "assets/capabilities/button_mute.svg" + }, "night_mode": { "title": { "en": "Night Mode" @@ -6965,6 +7117,26 @@ "setable": true, "icon": "assets/capabilities/sleep_mode.svg" }, + "off_button": { + "type": "boolean", + "title": { + "en": "Off" + }, + "getable": false, + "setable": true, + "uiComponent": "button", + "icon": "assets/capabilities/button_off.svg" + }, + "on_button": { + "type": "boolean", + "title": { + "en": "On" + }, + "getable": false, + "setable": true, + "uiComponent": "button", + "icon": "assets/capabilities/button_on.svg" + }, "open_window_sensor": { "type": "boolean", "title": { @@ -6982,6 +7154,16 @@ }, "icon": "assets/capabilities/open_window.svg" }, + "power_button": { + "type": "boolean", + "title": { + "en": "Power" + }, + "getable": false, + "setable": true, + "uiComponent": "button", + "icon": "assets/capabilities/button_power.svg" + }, "ptz_control_horizontal": { "type": "enum", "title": { diff --git a/assets/capabilities/button_mute.svg b/assets/capabilities/button_mute.svg new file mode 100644 index 00000000..2d244ae3 --- /dev/null +++ b/assets/capabilities/button_mute.svg @@ -0,0 +1 @@ + diff --git a/assets/capabilities/button_off.svg b/assets/capabilities/button_off.svg new file mode 100644 index 00000000..2ef7bc38 --- /dev/null +++ b/assets/capabilities/button_off.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/capabilities/button_on.svg b/assets/capabilities/button_on.svg new file mode 100644 index 00000000..04a46c6f --- /dev/null +++ b/assets/capabilities/button_on.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/capabilities/button_power.svg b/assets/capabilities/button_power.svg new file mode 100644 index 00000000..5c3459a4 --- /dev/null +++ b/assets/capabilities/button_power.svg @@ -0,0 +1 @@ + diff --git a/drivers/dimmer/driver.ts b/drivers/dimmer/driver.ts index 139fa67e..449792e2 100644 --- a/drivers/dimmer/driver.ts +++ b/drivers/dimmer/driver.ts @@ -7,7 +7,7 @@ import { TuyaDeviceSpecificationResponse, } from '../../types/TuyaApiTypes'; import type { StandardDeviceFlowArgs, StandardFlowArgs } from '../../types/TuyaTypes'; -import TuyaOAuth2DeviceDimmer from './device'; +import type TuyaOAuth2DeviceDimmer from './device'; import { SIMPLE_DIMMER_CAPABILITIES } from './TuyaDimmerConstants'; import TRANSLATIONS from './translations.json'; diff --git a/drivers/infrared_remote/assets/device_classes/air-purifier.svg b/drivers/infrared_remote/assets/device_classes/air-purifier.svg new file mode 100644 index 00000000..5c41938f --- /dev/null +++ b/drivers/infrared_remote/assets/device_classes/air-purifier.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/infrared_remote/assets/device_classes/camera.svg b/drivers/infrared_remote/assets/device_classes/camera.svg new file mode 100644 index 00000000..c4ac2fe4 --- /dev/null +++ b/drivers/infrared_remote/assets/device_classes/camera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/infrared_remote/assets/device_classes/climate.svg b/drivers/infrared_remote/assets/device_classes/climate.svg new file mode 100644 index 00000000..2e40e63b --- /dev/null +++ b/drivers/infrared_remote/assets/device_classes/climate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/infrared_remote/assets/device_classes/dvd-bluray.svg b/drivers/infrared_remote/assets/device_classes/dvd-bluray.svg new file mode 100644 index 00000000..d2f97cb6 --- /dev/null +++ b/drivers/infrared_remote/assets/device_classes/dvd-bluray.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/infrared_remote/assets/device_classes/fan.svg b/drivers/infrared_remote/assets/device_classes/fan.svg new file mode 100644 index 00000000..00860345 --- /dev/null +++ b/drivers/infrared_remote/assets/device_classes/fan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/infrared_remote/assets/device_classes/kettle.svg b/drivers/infrared_remote/assets/device_classes/kettle.svg new file mode 100644 index 00000000..14fc7838 --- /dev/null +++ b/drivers/infrared_remote/assets/device_classes/kettle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/infrared_remote/assets/device_classes/light-bulb2.svg b/drivers/infrared_remote/assets/device_classes/light-bulb2.svg new file mode 100644 index 00000000..43986a28 --- /dev/null +++ b/drivers/infrared_remote/assets/device_classes/light-bulb2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/infrared_remote/assets/device_classes/projector.svg b/drivers/infrared_remote/assets/device_classes/projector.svg new file mode 100644 index 00000000..a8419c20 --- /dev/null +++ b/drivers/infrared_remote/assets/device_classes/projector.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/infrared_remote/assets/device_classes/set-top-box.svg b/drivers/infrared_remote/assets/device_classes/set-top-box.svg new file mode 100644 index 00000000..339e9b61 --- /dev/null +++ b/drivers/infrared_remote/assets/device_classes/set-top-box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/infrared_remote/assets/device_classes/speaker.svg b/drivers/infrared_remote/assets/device_classes/speaker.svg new file mode 100644 index 00000000..a2019a6f --- /dev/null +++ b/drivers/infrared_remote/assets/device_classes/speaker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/infrared_remote/assets/device_classes/tv.svg b/drivers/infrared_remote/assets/device_classes/tv.svg new file mode 100644 index 00000000..4d629834 --- /dev/null +++ b/drivers/infrared_remote/assets/device_classes/tv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/infrared_remote/assets/icon.svg b/drivers/infrared_remote/assets/icon.svg new file mode 100644 index 00000000..1be8294f --- /dev/null +++ b/drivers/infrared_remote/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drivers/infrared_remote/assets/images/large.png b/drivers/infrared_remote/assets/images/large.png new file mode 100644 index 00000000..87f2c3bc Binary files /dev/null and b/drivers/infrared_remote/assets/images/large.png differ diff --git a/drivers/infrared_remote/assets/images/small.png b/drivers/infrared_remote/assets/images/small.png new file mode 100644 index 00000000..09aacaef Binary files /dev/null and b/drivers/infrared_remote/assets/images/small.png differ diff --git a/drivers/infrared_remote/assets/images/xlarge.png b/drivers/infrared_remote/assets/images/xlarge.png new file mode 100644 index 00000000..30e6c89d Binary files /dev/null and b/drivers/infrared_remote/assets/images/xlarge.png differ diff --git a/drivers/infrared_remote/device.ts b/drivers/infrared_remote/device.ts new file mode 100644 index 00000000..022be4fc --- /dev/null +++ b/drivers/infrared_remote/device.ts @@ -0,0 +1,38 @@ +import TuyaOAuth2Device from '../../lib/TuyaOAuth2Device'; + +export default class TuyaOAuth2DeviceIrController extends TuyaOAuth2Device { + async onOAuth2Init(): Promise { + await super.onOAuth2Init(); + + if (this.hasCapability('on_button')) { + this.registerCapabilityListener('on_button', () => this.sendKeyCommand(undefined, 'PowerOn').then()); + } + + if (this.hasCapability('off_button')) { + this.registerCapabilityListener('off_button', () => this.sendKeyCommand(undefined, 'PowerOff').then()); + } + + if (this.hasCapability('power_button')) { + this.registerCapabilityListener('power_button', () => this.sendKeyCommand(undefined, 'Power').then()); + } + + if (this.hasCapability('mute_button')) { + this.registerCapabilityListener('mute_button', () => + this.sendKeyCommand(undefined, this.getStoreValue('tuya_mute_key')).then(), + ); + } + } + + async sendKeyCommand(keyId?: number, keyString?: string): Promise { + const { deviceId, controllerId } = this.getData(); + const categoryId = this.getStoreValue('tuya_remote_category') as number; + return this.oAuth2Client.sendKeyCommand(controllerId, deviceId, categoryId, keyId, keyString); + } + + async sendAircoCommand(code: string, value: number): Promise { + const { deviceId, controllerId } = this.getData(); + return this.oAuth2Client.sendAircoCommand(controllerId, deviceId, code, value); + } +} + +module.exports = TuyaOAuth2DeviceIrController; diff --git a/drivers/infrared_remote/driver.compose.json b/drivers/infrared_remote/driver.compose.json new file mode 100644 index 00000000..512ff07b --- /dev/null +++ b/drivers/infrared_remote/driver.compose.json @@ -0,0 +1,9 @@ +{ + "$extends": "tuya", + "class": "remote", + "name": { + "en": "Infrared Remote", + "nl": "Infrarood Afstandsbediening" + }, + "capabilities": ["on_button", "off_button", "mute_button"] +} diff --git a/drivers/infrared_remote/driver.flow.compose.json b/drivers/infrared_remote/driver.flow.compose.json new file mode 100644 index 00000000..8369b7be --- /dev/null +++ b/drivers/infrared_remote/driver.flow.compose.json @@ -0,0 +1,50 @@ +{ + "actions": [ + { + "id": "infrared_remote_press", + "title": { + "en": "Press button" + }, + "titleFormatted": { + "en": "Press [[button]]" + }, + "args": [ + { + "name": "button", + "type": "autocomplete", + "title": { "en": "Button" } + } + ] + }, + { + "id": "infrared_remote_mute_button", + "title": { + "en": "Press mute button" + }, + "$filter": "capabilities=mute_button" + }, + { + "id": "infrared_remote_power_button", + "title": { + "en": "Press power button" + }, + "$filter": "capabilities=power_button" + }, + { + "id": "infrared_remote_on_button", + "title": { + "en": "Press on button" + }, + "$filter": "capabilities=on_button" + }, + { + "id": "infrared_remote_off_button", + "title": { + "en": "Press off button" + }, + "$filter": "capabilities=off_button" + } + ], + "conditions": [], + "triggers": [] +} diff --git a/drivers/infrared_remote/driver.settings.compose.json b/drivers/infrared_remote/driver.settings.compose.json new file mode 100644 index 00000000..dceebc0d --- /dev/null +++ b/drivers/infrared_remote/driver.settings.compose.json @@ -0,0 +1,5 @@ +[ + { + "$extends": "deviceSpecification" + } +] diff --git a/drivers/infrared_remote/driver.ts b/drivers/infrared_remote/driver.ts new file mode 100644 index 00000000..95cf1429 --- /dev/null +++ b/drivers/infrared_remote/driver.ts @@ -0,0 +1,337 @@ +import { OAuth2DeviceResult, OAuth2Driver } from 'homey-oauth2app'; +import TuyaOAuth2Client from '../../lib/TuyaOAuth2Client'; +import { TuyaDeviceResponse, TuyaIrRemoteKeysResponse, TuyaIrRemoteResponse } from '../../types/TuyaApiTypes'; +import * as TuyaOAuth2Util from '../../lib/TuyaOAuth2Util'; +import { StandardDeviceFlowArgs } from '../../types/TuyaTypes'; +import type TuyaOAuth2DeviceIrController from './device'; +import { ArgumentAutocompleteResults } from 'homey/lib/FlowCard'; + +type PairingRemote = TuyaIrRemoteResponse & { + controllerId: string; +}; + +type ButtonArgs = { + button: + | { + name: string; + id: number; + airco: false; + } + | { + name: string; + code: string; + value: number; + airco: true; + }; +}; + +function processKeyName(string: string): string { + const cleanName = string.toLowerCase().replace('_', ' '); + return capitalize(cleanName); +} + +function capitalize(string: string): string { + const words = string.split(' ').map(word => { + return word.charAt(0).toUpperCase() + word.slice(1); + }); + return words.join(' '); +} + +function generateKeys(remoteKeys: TuyaIrRemoteKeysResponse): ArgumentAutocompleteResults { + const results: ArgumentAutocompleteResults = []; + + const airco = remoteKeys.category_id === 5; + + for (const key of remoteKeys.key_list) { + // Skip the standard keys in air conditioner remotes, as they are placeholders + if (!(airco && key.standard_key)) { + results.push({ + name: processKeyName(key.key_name), + id: key.key_id, + airco: false, + }); + } + } + + if (airco) { + results.push( + { + name: 'Power On', + code: 'power', + value: 1, + airco: true, + }, + { + name: 'Power Off', + code: 'power', + value: 0, + airco: true, + }, + ); + + const modes: Record = {}; + const temperatures: Set = new Set(); + const fanSpeeds: Record = {}; + + for (const range of remoteKeys.key_range) { + modes[range.mode] = range.mode_name; + + for (const temperature of range.temp_list) { + temperatures.add(temperature.temp); + + for (const fanSpeed of temperature.fan_list) { + fanSpeeds[fanSpeed.fan] = fanSpeed.fan_name; + } + } + } + + for (const mode in modes) { + results.push({ + name: processKeyName(modes[mode]), + code: 'mode', + value: mode, + airco: true, + }); + } + + for (const fanSpeed in fanSpeeds) { + results.push({ + name: processKeyName(fanSpeeds[fanSpeed]), + code: 'wind', + value: fanSpeed, + airco: true, + }); + } + + const sortedTemperatures: number[] = Array.from(temperatures).sort(); + + for (const temperature of sortedTemperatures) { + results.push({ + name: `Temperature ${temperature}`, + code: 'temp', + value: temperature, + airco: true, + }); + } + } + + return results; +} + +module.exports = class TuyaOAuth2DriverIrController extends OAuth2Driver { + async onInit(): Promise { + await super.onInit(); + + const buttonAutocompleteListener = (query: string, args: StandardDeviceFlowArgs): ArgumentAutocompleteResults => { + const results = (args.device.getStoreValue('tuya_remote_keys') as ArgumentAutocompleteResults).filter(result => { + const searchString = result.name.toLowerCase(); + const queryWords = query.toLowerCase().split(' '); + // Return false if any query word is not included + for (const queryWord of queryWords) { + if (!searchString.includes(queryWord)) { + return false; + } + } + return true; + }); + + return results; + }; + + this.homey.flow + .getActionCard('infrared_remote_press') + .registerArgumentAutocompleteListener('button', (query: string, args: StandardDeviceFlowArgs) => + buttonAutocompleteListener(query, args), + ) + .registerRunListener(async (args: StandardDeviceFlowArgs & ButtonArgs) => { + const button = args.button; + if (button.airco) { + await (args.device as TuyaOAuth2DeviceIrController).sendAircoCommand(button.code, button.value); + } else { + await (args.device as TuyaOAuth2DeviceIrController).sendKeyCommand(button.id); + } + }); + + for (const button of ['mute', 'power', 'on', 'off']) { + this.homey.flow + .getActionCard(`infrared_remote_${button}_button`) + .registerRunListener(async (args: StandardDeviceFlowArgs) => { + await (args.device as TuyaOAuth2DeviceIrController).triggerCapabilityListener(`${button}_button`, true); + }); + } + } + + async onPairListDevices({ oAuth2Client }: { oAuth2Client: TuyaOAuth2Client }): Promise { + const devices = await oAuth2Client.getDevices(); + + const deviceIndex: Record = {}; // ID to device + for (const device of devices) { + deviceIndex[device.id] = device; + } + + const controllerDevices = devices.filter(device => device.category === 'wnykq'); + + const filteredDevices: TuyaDeviceResponse[] = []; + const pairingRemotes: Record = {}; // ID to remote + + for (const controller of controllerDevices) { + const controllerRemotes = await oAuth2Client.getRemotes(controller.id); + + for (const controllerRemote of controllerRemotes) { + const remoteDevice = deviceIndex[controllerRemote.remote_id]; + if (!oAuth2Client.isRegistered(remoteDevice.product_id, remoteDevice.id)) { + filteredDevices.push(remoteDevice); + pairingRemotes[remoteDevice.id] = { ...controllerRemote, controllerId: controller.id }; + } + } + } + + const listDevices: OAuth2DeviceResult[] = []; + + this.log('Listing devices to pair:'); + + for (const device of filteredDevices) { + const remote = pairingRemotes[device.id]; + this.log('Device:', JSON.stringify(TuyaOAuth2Util.redactFields(device))); + + const deviceSpecs = + (await oAuth2Client + .getSpecification(device.id) + .catch(e => this.log('Device specification retrieval failed', e))) ?? undefined; + const dataPoints = + (await oAuth2Client.queryDataPoints(device.id).catch(e => this.log('Device properties retrieval failed', e))) ?? + undefined; + const remoteKeys = + (await oAuth2Client + .getRemoteKeys(remote.controllerId, remote.remote_id) + .catch(e => this.log('Remote keys retrieval failed', e))) ?? undefined; + + // GitHub #178: Some device do not have the status property at all. + // Make sure to populate it with an empty array instead. + if (!Array.isArray(device.status)) { + device.status = []; + } + + const combinedSpecification = { + device: TuyaOAuth2Util.redactFields(device), + specifications: deviceSpecs ?? '', + data_points: dataPoints?.properties ?? '', + }; + + const joinedOnOff: undefined | boolean = remoteKeys?.duplicate_power; + let muteKey: undefined | string = undefined; + + if (remoteKeys) { + for (const key of remoteKeys.key_list) { + if (key.key.toLowerCase() === 'mute') { + muteKey = key.key; + break; + } + } + } + + const capabilities: string[] = []; + + if (joinedOnOff !== undefined) { + if (joinedOnOff) { + capabilities.push('power_button'); + } else { + capabilities.push('on_button', 'off_button'); + } + } + + if (muteKey !== undefined) capabilities.push('mute_button'); + + const tuyaRemoteKeys = remoteKeys !== undefined ? generateKeys(remoteKeys) : []; + + const deviceProperties: OAuth2DeviceResult = { + capabilities: capabilities, + store: { + tuya_capabilities: [], + tuya_category: device.category, + tuya_remote_keys: tuyaRemoteKeys, + tuya_remote_category: remoteKeys?.category_id, + tuya_joined_onoff: joinedOnOff, + tuya_mute_key: muteKey, + }, + capabilitiesOptions: {}, + settings: { + deviceSpecification: JSON.stringify(combinedSpecification, undefined, 2), + }, + name: device.name, + data: { + deviceId: device.id, + productId: device.product_id, + controllerId: remote.controllerId, + }, + }; + + switch (remoteKeys?.category_id) { + case 1: + // Set-top box + // eslint-disable-next-line no-fallthrough + case 3: + // Television box + deviceProperties.class = 'settopbox'; + deviceProperties.icon = 'device_classes/set-top-box.svg'; + break; + case 2: + // Television + deviceProperties.class = 'tv'; + deviceProperties.icon = 'device_classes/tv.svg'; + break; + case 4: + // DVD + deviceProperties.class = 'mediaplayer'; + deviceProperties.icon = 'device_classes/dvd-bluray.svg'; + break; + case 5: + // Air conditioner + deviceProperties.class = 'airconditioning'; + deviceProperties.icon = 'device_classes/climate.svg'; + break; + case 6: + // Projector + deviceProperties.class = 'tv'; + deviceProperties.icon = 'device_classes/projector.svg'; + break; + case 7: + // Speaker + deviceProperties.class = 'speaker'; + deviceProperties.icon = 'device_classes/speaker.svg'; + break; + case 8: + // Fan + deviceProperties.class = 'fan'; + deviceProperties.icon = 'device_classes/fan.svg'; + break; + case 9: + // Camera + deviceProperties.class = 'camera'; + deviceProperties.icon = 'device_classes/camera.svg'; + break; + case 10: + // Light + deviceProperties.class = 'light'; + deviceProperties.icon = 'device_classes/light-bulb2.svg'; + break; + case 11: + // Purifier + deviceProperties.class = 'airpurifier'; + deviceProperties.icon = 'device_classes/air-purifier.svg'; + break; + case 12: + // Water heater + deviceProperties.class = 'kettle'; + deviceProperties.icon = 'device_classes/kettle.svg'; + break; + } + + this.log('Props:', JSON.stringify(deviceProperties)); + + listDevices.push(deviceProperties); + } + + return listDevices; + } +}; diff --git a/drivers/infrared_remote/pair/welcome.assets/chevron-right.png b/drivers/infrared_remote/pair/welcome.assets/chevron-right.png new file mode 100644 index 00000000..d7331b23 Binary files /dev/null and b/drivers/infrared_remote/pair/welcome.assets/chevron-right.png differ diff --git a/drivers/infrared_remote/pair/welcome.assets/logos.png b/drivers/infrared_remote/pair/welcome.assets/logos.png new file mode 100644 index 00000000..faefa496 Binary files /dev/null and b/drivers/infrared_remote/pair/welcome.assets/logos.png differ diff --git a/drivers/infrared_remote/pair/welcome.html b/drivers/infrared_remote/pair/welcome.html new file mode 100644 index 00000000..14a7e06e --- /dev/null +++ b/drivers/infrared_remote/pair/welcome.html @@ -0,0 +1,60 @@ + + +

+ +

+ +

+ +

+ + + + +

+ +

+ +

diff --git a/lib/TuyaOAuth2Client.ts b/lib/TuyaOAuth2Client.ts index d755a3fe..3b0e37bf 100644 --- a/lib/TuyaOAuth2Client.ts +++ b/lib/TuyaOAuth2Client.ts @@ -10,6 +10,8 @@ import { TuyaDeviceResponse, TuyaDeviceSpecificationResponse, TuyaHome, + TuyaIrRemoteKeysResponse, + TuyaIrRemoteResponse, TuyaScenesResponse, TuyaStatusResponse, TuyaToken, @@ -293,6 +295,43 @@ export default class TuyaOAuth2Client extends OAuth2Client { }); } + /* + * Infrared + */ + async getRemotes(infraredControllerId: string): Promise { + return this._get(`/v2.0/infrareds/${infraredControllerId}/remotes`); + } + + async getRemoteKeys(infraredControllerId: string, infraredRemoteId: string): Promise { + return this._get(`/v2.0/infrareds/${infraredControllerId}/remotes/${infraredRemoteId}/keys`); + } + + async sendKeyCommand( + infraredControllerId: string, + infraredRemoteId: string, + categoryId: number, + keyId?: number, + keyString?: string, + ): Promise { + return this._post(`/v2.0/infrareds/${infraredControllerId}/remotes/${infraredRemoteId}/raw/command`, { + category_id: categoryId, + key_id: keyId, + key: keyString, + }); + } + + async sendAircoCommand( + infraredControllerId: string, + infraredRemoteId: string, + code: string, + value: number, + ): Promise { + return this._post(`/v2.0/infrareds/${infraredControllerId}/air-conditioners/${infraredRemoteId}/command`, { + code: code, + value: value, + }); + } + /* * Webhooks */ diff --git a/types/TuyaApiTypes.ts b/types/TuyaApiTypes.ts index 63ef282f..5a425de4 100644 --- a/types/TuyaApiTypes.ts +++ b/types/TuyaApiTypes.ts @@ -128,3 +128,66 @@ export type TuyaWebRTC = { hardware_capability: number[]; }; }; + +// Ir Remotes +export type TuyaIrRemoteResponse = { + area_id: number; // 0 if undefined + area_name?: string; + brand_id: number; + brand_name: string; + category_id: number; + operator_id: number; // 0 if undefined + operator_name?: string; + remote_id: string; + remote_index: number; + remote_name: string; +}; + +export type TuyaIrRemoteKeysResponse = { + brand_id: number; + category_id: number; + remote_index: number; + single_air: boolean; // Boolean Indicates whether it is a unitary air conditioner. + duplicate_power: boolean; // Boolean Indicates whether the power-on button is the same as the power-off button. + key_list: TuyaIrRemoteKey[]; // List The list of keys. + key_range: TuyaIrRemoteRange[]; // List The range of keys. +}; + +export type TuyaIrRemoteKey = { + key: string; // String The key. + key_id: number; // Integer The key ID. + key_name: string; // String The name of a specified key. + standard_key: boolean; // Boolean Indicates whether it is a standard key. +}; + +export type TuyaIrRemoteRange = { + mode: TuyaIrRemoteRangeMode; + mode_name: string; + temp_list: TuyaIrRemoteRangeTemp[]; +}; + +export const enum TuyaIrRemoteRangeMode { + cooling = 0, + heating = 1, + automatic = 2, + air_supply = 3, + dehumidification = 4, +} + +export type TuyaIrRemoteRangeTemp = { + temp: number; // Integer The temperature. + temp_name: string; // String The temperature name. + fan_list: TuyaIrRemoteRangeTempFan[]; // Set The value range of the wind speed. +}; + +export type TuyaIrRemoteRangeTempFan = { + fan: TuyaIrRemoteRangeTempFanSpeed; // Speed + fan_name: string; // Speed name +}; + +export const enum TuyaIrRemoteRangeTempFanSpeed { + automatic = 0, + low = 1, + medium = 2, + high = 3, +} diff --git a/types/homey-oauth2app/index.d.ts b/types/homey-oauth2app/index.d.ts index 1c4bd997..d1bc4cd4 100644 --- a/types/homey-oauth2app/index.d.ts +++ b/types/homey-oauth2app/index.d.ts @@ -97,6 +97,7 @@ declare module 'homey-oauth2app' { [key: string]: any; }; }; + class?: string; } export class OAuth2Token {