diff --git a/lib/src/app/data.dart b/lib/src/app/data.dart index 90348f3abe..4161cb8989 100644 --- a/lib/src/app/data.dart +++ b/lib/src/app/data.dart @@ -18,6 +18,66 @@ import '../utils.dart'; /// {@category Viam SDK} typedef DatabaseConnection = GetDatabaseConnectionResponse; +/// Represents a tabular data point and its associated metadata. +class TabularDataPoint { + /// The robot part ID + final String partId; + + /// The resource name + final String resourceName; + + /// The resource subtype + /// Example: `rdk:component:sensor` + final String resourceSubtype; + + /// The method used for data capture + /// Example: `Readings` + final String methodName; + + /// The time at which the data point was captured + final DateTime timeCaptured; + + /// The organization ID + final String organizationId; + + /// The location ID + final String locationId; + + /// The robot name + final String robotName; + + /// The robot ID + final String robotId; + + /// The robot part name + final String partName; + + /// Additional parameters associated with the data capture method + final dynamic methodParameters; + + /// A list of tags associated with the data point + final List tags; + + /// The captured data + final Map payload; + + TabularDataPoint({ + required this.partId, + required this.resourceName, + required this.resourceSubtype, + required this.methodName, + required this.timeCaptured, + required this.organizationId, + required this.locationId, + required this.robotName, + required this.robotId, + required this.partName, + required this.methodParameters, + required this.tags, + required this.payload, + }); +} + /// gRPC client used for retrieving, uploading, and modifying stored data from app.viam.com. /// /// All calls must be authenticated. @@ -103,6 +163,54 @@ class DataClient { return response.rawData.map((e) => BsonCodec.deserialize(BsonBinary.from(e))).toList(); } + /// Obtain unified tabular data and metadata from the specified data source. + /// + /// Returns a stream of data points. + /// + /// For more information, see [Data Client API](https://docs.viam.com/appendix/apis/data-client/). + Stream exportTabularData( + String partId, + String resourceName, + String resourceSubtype, + String methodName, + DateTime? startTime, + DateTime? endTime, + ) async* { + final interval = CaptureInterval(); + if (startTime != null) { + interval.start = Timestamp()..seconds = Int64((startTime.millisecondsSinceEpoch / 1000).floor()); + } + if (endTime != null) { + interval.end = Timestamp()..seconds = Int64((endTime.millisecondsSinceEpoch / 1000).floor()); + } + + final request = ExportTabularDataRequest() + ..partId = partId + ..resourceName = resourceName + ..resourceSubtype = resourceSubtype + ..methodName = methodName + ..interval = interval; + final responses = _dataClient.exportTabularData(request); + + await for (var response in responses) { + yield TabularDataPoint( + partId: response.partId, + resourceName: response.resourceName, + resourceSubtype: response.resourceSubtype, + methodName: response.methodName, + timeCaptured: response.timeCaptured.toDateTime(), + organizationId: response.organizationId, + locationId: response.locationId, + robotName: response.robotName, + robotId: response.robotId, + partName: response.partName, + methodParameters: response.methodParameters.toMap(), + tags: response.tags, + payload: response.payload.toMap(), + ); + } + } + /// Delete tabular data older than a provided number of days from an organization. /// /// Returns the number of pieces of data that were deleted. diff --git a/test/unit_test/app/data_client_test.dart b/test/unit_test/app/data_client_test.dart index 01f12f1199..bd61bb6e2e 100644 --- a/test/unit_test/app/data_client_test.dart +++ b/test/unit_test/app/data_client_test.dart @@ -180,6 +180,100 @@ void main() { expect(response, equals(data)); }); + test('exportTabularData', () async { + final timeCaptured1 = DateTime.utc(2023, 1, 1); + final timeCaptured2 = DateTime.utc(2023, 1, 2); + final Map methodParams1 = {}; + final Map methodParams2 = {'key': 'method2'}; + final Map payload1 = {'key': 'value1'}; + final Map payload2 = {'key': 'value2'}; + + final List data = [ + TabularDataPoint( + partId: 'partId1', + resourceName: 'resourceName1', + resourceSubtype: 'resourceSubtype1', + methodName: 'Readings', + timeCaptured: timeCaptured1, + organizationId: 'orgId1', + locationId: 'locationId1', + robotName: 'robot1', + robotId: 'robotId1', + partName: 'part1', + methodParameters: methodParams1, + tags: [], + payload: payload1, + ), + TabularDataPoint( + partId: 'partId1', + resourceName: 'resourceName1', + resourceSubtype: 'resourceSubtype1', + methodName: 'Readings', + timeCaptured: timeCaptured2, + organizationId: 'orgId1', + locationId: 'locationId1', + robotName: 'robot1', + robotId: 'robotId1', + partName: 'part1', + methodParameters: methodParams2, + tags: [], + payload: payload2, + ), + ]; + + when(serviceClient.exportTabularData(any)).thenAnswer((_) => MockResponseStream.list([ + ExportTabularDataResponse( + partId: 'partId1', + resourceName: 'resourceName1', + resourceSubtype: 'resourceSubtype1', + methodName: 'Readings', + timeCaptured: Timestamp.fromDateTime(timeCaptured1), + organizationId: 'orgId1', + locationId: 'locationId1', + robotName: 'robot1', + robotId: 'robotId1', + partName: 'part1', + methodParameters: methodParams1.toStruct(), + tags: [], + payload: payload1.toStruct(), + ), + ExportTabularDataResponse( + partId: 'partId1', + resourceName: 'resourceName1', + resourceSubtype: 'resourceSubtype1', + methodName: 'Readings', + timeCaptured: Timestamp.fromDateTime(timeCaptured2), + organizationId: 'orgId1', + locationId: 'locationId1', + robotName: 'robot1', + robotId: 'robotId1', + partName: 'part1', + methodParameters: methodParams2.toStruct(), + tags: [], + payload: payload2.toStruct(), + ), + ])); + + final response = dataClient.exportTabularData('partId1', 'resourceName1', 'resourceSubtype1', 'methodName', null, null); + var index = 0; + await for (final point in response) { + expect(point.partId, equals(data[index].partId)); + expect(point.resourceName, equals(data[index].resourceName)); + expect(point.resourceSubtype, equals(data[index].resourceSubtype)); + expect(point.methodName, equals(data[index].methodName)); + expect(point.timeCaptured, equals(data[index].timeCaptured)); + expect(point.organizationId, equals(data[index].organizationId)); + expect(point.locationId, equals(data[index].locationId)); + expect(point.robotName, equals(data[index].robotName)); + expect(point.robotId, equals(data[index].robotId)); + expect(point.partName, equals(data[index].partName)); + expect(point.methodParameters, equals(data[index].methodParameters)); + expect(point.tags, equals(data[index].tags)); + expect(point.payload, equals(data[index].payload)); + index++; + } + }); + test('deleteTabularData', () async { when(serviceClient.deleteTabularData(any)) .thenAnswer((_) => MockResponseFuture.value(DeleteTabularDataResponse()..deletedCount = Int64(12)));