diff --git a/.homeycompose/flow/triggers/receive_status_boolean.json b/.homeycompose/flow/triggers/receive_status_boolean.json index 718b8d8a..7d0007e4 100644 --- a/.homeycompose/flow/triggers/receive_status_boolean.json +++ b/.homeycompose/flow/triggers/receive_status_boolean.json @@ -6,7 +6,7 @@ "en": "Status [[code]] gets a new Boolean value" }, "hint": { - "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs." + "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs. Data points available outside of this specification may not actually be reported by the device." }, "args": [ { diff --git a/.homeycompose/flow/triggers/receive_status_json.json b/.homeycompose/flow/triggers/receive_status_json.json index a770fd1f..d8f7be25 100644 --- a/.homeycompose/flow/triggers/receive_status_json.json +++ b/.homeycompose/flow/triggers/receive_status_json.json @@ -6,7 +6,7 @@ "en": "Status [[code]] gets a new JSON value" }, "hint": { - "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs." + "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs. Data points available outside of this specification may not actually be reported by the device." }, "args": [ { diff --git a/.homeycompose/flow/triggers/receive_status_number.json b/.homeycompose/flow/triggers/receive_status_number.json index 12bc56af..01714a69 100644 --- a/.homeycompose/flow/triggers/receive_status_number.json +++ b/.homeycompose/flow/triggers/receive_status_number.json @@ -6,7 +6,7 @@ "en": "Status [[code]] gets a new Number value" }, "hint": { - "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs." + "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs. Data points available outside of this specification may not actually be reported by the device." }, "args": [ { diff --git a/.homeycompose/flow/triggers/receive_status_string.json b/.homeycompose/flow/triggers/receive_status_string.json index de1f2996..b6865d44 100644 --- a/.homeycompose/flow/triggers/receive_status_string.json +++ b/.homeycompose/flow/triggers/receive_status_string.json @@ -6,7 +6,7 @@ "en": "Status [[code]] gets a new String value" }, "hint": { - "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs." + "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs. Data points available outside of this specification may not actually be reported by the device." }, "args": [ { diff --git a/app.json b/app.json index d6b86fe9..7ed73b5b 100644 --- a/app.json +++ b/app.json @@ -392,7 +392,7 @@ "en": "Status [[code]] gets a new Boolean value" }, "hint": { - "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs." + "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs. Data points available outside of this specification may not actually be reported by the device." }, "args": [ { @@ -433,7 +433,7 @@ "en": "Status [[code]] gets a new JSON value" }, "hint": { - "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs." + "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs. Data points available outside of this specification may not actually be reported by the device." }, "args": [ { @@ -474,7 +474,7 @@ "en": "Status [[code]] gets a new Number value" }, "hint": { - "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs." + "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs. Data points available outside of this specification may not actually be reported by the device." }, "args": [ { @@ -515,7 +515,7 @@ "en": "Status [[code]] gets a new String value" }, "hint": { - "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs." + "en": "This is an advanced Flow card, that can be used to receive any status from a Tuya device. You can find the status codes in the Tuya API documentation at https://homey.link/tuya-docs. Data points available outside of this specification may not actually be reported by the device." }, "args": [ { diff --git a/app.ts b/app.ts index 13b0fbe4..af0dc0c6 100644 --- a/app.ts +++ b/app.ts @@ -5,7 +5,7 @@ import * as TuyaOAuth2Util from './lib/TuyaOAuth2Util'; import TuyaOAuth2Device from './lib/TuyaOAuth2Device'; import sourceMapSupport from 'source-map-support'; -import { type TuyaScene } from './types/TuyaApiTypes'; +import type { TuyaDeviceDataPoint, TuyaScene, TuyaStatusResponse } from './types/TuyaApiTypes'; import { type ArgumentAutocompleteResults } from 'homey/lib/FlowCard'; sourceMapSupport.install(); @@ -14,10 +14,17 @@ const CACHE_KEY = 'scenes'; const CACHE_TTL = 30; type DeviceArgs = { device: TuyaOAuth2Device }; -type StatusCodeArgs = { code: { id: string } }; +type StatusCodeArgs = { code: AutoCompleteArg }; type StatusCodeState = { code: string }; type HomeyTuyaScene = Pick; +type AutoCompleteArg = { + name: string; + id: string; + title: string; + dataPoint: boolean; +}; + module.exports = class TuyaOAuth2App extends OAuth2App { static OAUTH2_CLIENT = TuyaOAuth2Client; static OAUTH2_DEBUG = process.env.DEBUG === '1'; @@ -34,29 +41,56 @@ module.exports = class TuyaOAuth2App extends OAuth2App { value, }: { device: TuyaOAuth2Device; - code: string | { id: string }; + code: AutoCompleteArg; value: unknown; }): Promise => { - if (typeof code === 'object') code = code.id; - await device.sendCommand({ code, value }); + if (code.dataPoint) { + await device.setDataPoint(code.id, value); + } else { + await device.sendCommand({ code: code.id, value }); + } }; - const generalControlAutocompleteListener = async ( + const autocompleteListener = async ( query: string, args: DeviceArgs, filter: ({ value }: { value: unknown }) => boolean, ): Promise => { + function convert( + values: TuyaStatusResponse | Array, + dataPoints: boolean, + ): ArgumentAutocompleteResults { + return values + .filter(filter) + .filter(({ code }: { code: string }) => { + return code.toLowerCase().includes(query.toLowerCase()); + }) + .map(value => ({ + name: value.code, + id: value.code, + title: value.code, + dataPoint: dataPoints, + })); + } + const status = await args.device.getStatus(); - return status - .filter(filter) - .filter(({ code }: { code: string }) => { - return code.toLowerCase().includes(query.toLowerCase()); - }) - .map(({ code }: { code: string }) => ({ - name: code, - id: code, - title: code, - })); + const statusOptions = convert(status, false); + + const dataPoints = await args.device.queryDataPoints(); + const dataPointOptions = convert(dataPoints.properties, true); + + // Remove duplicates, preferring status options + const combinedMap: Record = {}; + + for (const dataPointOption of dataPointOptions) { + combinedMap[dataPointOption.name] = dataPointOption; + } + + for (const statusOption of statusOptions) { + combinedMap[statusOption.name] = statusOption; + } + + return Object.values(combinedMap); }; // Register Tuya Web API Flow Cards @@ -65,7 +99,7 @@ module.exports = class TuyaOAuth2App extends OAuth2App { .getActionCard('send_command_string') .registerRunListener(sendCommandRunListener) .registerArgumentAutocompleteListener('code', async (query: string, args: DeviceArgs) => - generalControlAutocompleteListener( + autocompleteListener( query, args, ({ value }) => typeof value === 'string' && !TuyaOAuth2Util.hasJsonStructure(value), @@ -76,14 +110,14 @@ module.exports = class TuyaOAuth2App extends OAuth2App { .getActionCard('send_command_number') .registerRunListener(sendCommandRunListener) .registerArgumentAutocompleteListener('code', async (query: string, args: DeviceArgs) => - generalControlAutocompleteListener(query, args, ({ value }) => typeof value === 'number'), + autocompleteListener(query, args, ({ value }) => typeof value === 'number'), ); this.homey.flow .getActionCard('send_command_boolean') .registerRunListener(sendCommandRunListener) .registerArgumentAutocompleteListener('code', async (query: string, args: DeviceArgs) => - generalControlAutocompleteListener(query, args, ({ value }) => typeof value === 'boolean'), + autocompleteListener(query, args, ({ value }) => typeof value === 'boolean'), ); this.homey.flow @@ -99,7 +133,7 @@ module.exports = class TuyaOAuth2App extends OAuth2App { }, ) .registerArgumentAutocompleteListener('code', async (query: string, args: DeviceArgs) => - generalControlAutocompleteListener( + autocompleteListener( query, args, ({ value }) => typeof value === 'object' || TuyaOAuth2Util.hasJsonStructure(value), @@ -111,14 +145,14 @@ module.exports = class TuyaOAuth2App extends OAuth2App { .getDeviceTriggerCard('receive_status_boolean') .registerRunListener((args: StatusCodeArgs, state: StatusCodeState) => args.code.id === state.code) .registerArgumentAutocompleteListener('code', async (query: string, args: DeviceArgs) => - generalControlAutocompleteListener(query, args, ({ value }) => typeof value === 'boolean'), + autocompleteListener(query, args, ({ value }) => typeof value === 'boolean'), ); this.homey.flow .getDeviceTriggerCard('receive_status_json') .registerRunListener((args: StatusCodeArgs, state: StatusCodeState) => args.code.id === state.code) .registerArgumentAutocompleteListener('code', async (query: string, args: DeviceArgs) => - generalControlAutocompleteListener( + autocompleteListener( query, args, ({ value }) => typeof value === 'object' || TuyaOAuth2Util.hasJsonStructure(value), @@ -129,14 +163,14 @@ module.exports = class TuyaOAuth2App extends OAuth2App { .getDeviceTriggerCard('receive_status_number') .registerRunListener((args: StatusCodeArgs, state: StatusCodeState) => args.code.id === state.code) .registerArgumentAutocompleteListener('code', async (query: string, args: DeviceArgs) => - generalControlAutocompleteListener(query, args, ({ value }) => typeof value === 'number'), + autocompleteListener(query, args, ({ value }) => typeof value === 'number'), ); this.homey.flow .getDeviceTriggerCard('receive_status_string') .registerRunListener((args: StatusCodeArgs, state: StatusCodeState) => args.code.id === state.code) .registerArgumentAutocompleteListener('code', async (query: string, args: DeviceArgs) => - generalControlAutocompleteListener( + autocompleteListener( query, args, ({ value }) => typeof value === 'string' && !TuyaOAuth2Util.hasJsonStructure(value), diff --git a/lib/TuyaOAuth2Client.ts b/lib/TuyaOAuth2Client.ts index cb7d0405..496ec881 100644 --- a/lib/TuyaOAuth2Client.ts +++ b/lib/TuyaOAuth2Client.ts @@ -226,9 +226,20 @@ export default class TuyaOAuth2Client extends OAuth2Client { } async queryDataPoints(deviceId: string): Promise { + // https://developer.tuya.com/en/docs/cloud/116cc8bf6f?id=Kcp2kwfrpe719 return this._get(`/v2.0/cloud/thing/${deviceId}/shadow/properties`); } + async setDataPoint(deviceId: string, dataPointId: string, value: unknown): Promise { + // https://developer.tuya.com/en/docs/cloud/c057ad5cfd?id=Kcp2kxdzftp91 + const payload = { + properties: JSON.stringify({ + [dataPointId]: value, + }), + }; + return this._post(`/v2.0/cloud/thing/${deviceId}/shadow/properties/issue`, payload); + } + async getWebRTCConfiguration({ deviceId }: { deviceId: string }): Promise { // https://developer.tuya.com/en/docs/cloud/96c3154b0d?id=Kam7q5rz91dml return this._get(`/v1.0/devices/${deviceId}/webrtc-configs`); diff --git a/lib/TuyaOAuth2Device.ts b/lib/TuyaOAuth2Device.ts index 28079f70..f29653d9 100644 --- a/lib/TuyaOAuth2Device.ts +++ b/lib/TuyaOAuth2Device.ts @@ -1,5 +1,5 @@ import { OAuth2Device } from 'homey-oauth2app'; -import { TuyaCommand, TuyaStatusResponse, TuyaWebRTC } from '../types/TuyaApiTypes'; +import { TuyaCommand, TuyaDeviceDataPointResponse, TuyaStatusResponse, TuyaWebRTC } from '../types/TuyaApiTypes'; import { TuyaStatus, TuyaStatusUpdate } from '../types/TuyaTypes'; import TuyaOAuth2Client from './TuyaOAuth2Client'; @@ -212,6 +212,16 @@ export default class TuyaOAuth2Device extends OAuth2Device { }); } + async queryDataPoints(): Promise { + const { deviceId } = this.data; + return this.oAuth2Client.queryDataPoints(deviceId); + } + + setDataPoint(dataPointId: string, value: unknown): Promise { + const { deviceId } = this.data; + return this.oAuth2Client.setDataPoint(deviceId, dataPointId, value); + } + async getWebRTC(): Promise { const { deviceId } = this.data; return this.oAuth2Client.getWebRTCConfiguration({ deviceId });