Skip to content

Commit

Permalink
Merge pull request #179 from Drenso/add-datapoints-to-flows
Browse files Browse the repository at this point in the history
Add data points to send_command flows
  • Loading branch information
bobvandevijver authored Aug 22, 2024
2 parents dc22b39 + 9a50ff9 commit 6c97e8d
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .homeycompose/flow/triggers/receive_status_boolean.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
2 changes: 1 addition & 1 deletion .homeycompose/flow/triggers/receive_status_json.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
2 changes: 1 addition & 1 deletion .homeycompose/flow/triggers/receive_status_number.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
2 changes: 1 addition & 1 deletion .homeycompose/flow/triggers/receive_status_string.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down
8 changes: 4 additions & 4 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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": [
{
Expand Down
82 changes: 58 additions & 24 deletions app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<TuyaScene, 'id' | 'name'>;

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';
Expand All @@ -34,29 +41,56 @@ module.exports = class TuyaOAuth2App extends OAuth2App {
value,
}: {
device: TuyaOAuth2Device;
code: string | { id: string };
code: AutoCompleteArg;
value: unknown;
}): Promise<void> => {
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<ArgumentAutocompleteResults> => {
function convert(
values: TuyaStatusResponse | Array<TuyaDeviceDataPoint>,
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<string, ArgumentAutocompleteResults[number]> = {};

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
Expand All @@ -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),
Expand All @@ -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
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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),
Expand Down
11 changes: 11 additions & 0 deletions lib/TuyaOAuth2Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,20 @@ export default class TuyaOAuth2Client extends OAuth2Client<TuyaOAuth2Token> {
}

async queryDataPoints(deviceId: string): Promise<TuyaDeviceDataPointResponse> {
// 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<void> {
// 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<TuyaWebRTC> {
// https://developer.tuya.com/en/docs/cloud/96c3154b0d?id=Kam7q5rz91dml
return this._get(`/v1.0/devices/${deviceId}/webrtc-configs`);
Expand Down
12 changes: 11 additions & 1 deletion lib/TuyaOAuth2Device.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -212,6 +212,16 @@ export default class TuyaOAuth2Device extends OAuth2Device<TuyaOAuth2Client> {
});
}

async queryDataPoints(): Promise<TuyaDeviceDataPointResponse> {
const { deviceId } = this.data;
return this.oAuth2Client.queryDataPoints(deviceId);
}

setDataPoint(dataPointId: string, value: unknown): Promise<void> {
const { deviceId } = this.data;
return this.oAuth2Client.setDataPoint(deviceId, dataPointId, value);
}

async getWebRTC(): Promise<TuyaWebRTC> {
const { deviceId } = this.data;
return this.oAuth2Client.getWebRTCConfiguration({ deviceId });
Expand Down

0 comments on commit 6c97e8d

Please sign in to comment.