To implement the AWS Lambda function scroll down to the Code source section and paste the following code under index.mjs
. The .mjs extension in AWS Lambda indicates that the file is an ECMAScript (ES) module.
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb';
const client = new DynamoDBClient({ region: "eu-central-1" });
const dynamoDb = DynamoDBDocumentClient.from(client);
const TABLE_NAME = 'SensorReadings';
const ATTRIBUTE_NAME = 'temperature'; // The name of the attribute (column) you want to average
const TIMESTAMP_ATTRIBUTE_NAME = '#ts'; // Placeholder for the attribute name for the timestamp
export const handler = async (event) => {
const now = new Date();
const N = 10; // Number of minutes
const timeThreshold = new Date(now.getTime() - N * 60 * 1000);
// Format the time threshold to ISO 8601 string
const formattedTimeThreshold = timeThreshold.toISOString();
// Look for records not older than the timeThreshold
const params = {
TableName: TABLE_NAME,
FilterExpression: `${TIMESTAMP_ATTRIBUTE_NAME} >= :timeThreshold`,
ExpressionAttributeNames: {
'#ts': 'timestamp', // Map the placeholder to the actual attribute name
},
ExpressionAttributeValues: {
':timeThreshold': formattedTimeThreshold,
},
};
try {
const data = await dynamoDb.send(new ScanCommand(params));
const items = data.Items;
if (!items || items.length === 0) {
return {
statusCode: 200,
body: JSON.stringify({ average: 0 }),
};
}
const numericalValues = items.map(item => parseFloat(item[ATTRIBUTE_NAME])).filter(value => !isNaN(value));
if (numericalValues.length === 0) {
return {
statusCode: 200,
body: JSON.stringify({ average: 0 }),
};
}
const sum = numericalValues.reduce((acc, value) => acc + value, 0);
const average = sum / numericalValues.length;
return {
statusCode: 200,
body: JSON.stringify({ average }),
};
} catch (error) {
console.error('Error reading from DynamoDB', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Could not read from DynamoDB' }),
};
}
};
In the code above, you first import the AWS SDK for JavaScript v3. Specifically, the following code imports the necessary classes from the AWS SDK for working with DynamoDB:
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb';
An instance of DynamoDBClient is then created, specifying the AWS region (eu-central-1 in this case):
const client = new DynamoDBClient({ region: "eu-central-1" }); // Update to your region
const dynamoDb = DynamoDBDocumentClient.from(client);
The client initializes a new DynamoDB client for the specified region, while dynamoDb
wraps the DynamoDB client in a document client for easier interaction with DynamoDB items.
Next, you have three constants as shown below:
const TABLE_NAME = 'SensorReadings';
const ATTRIBUTE_NAME = 'temperature'; // The name of the attribute (column) you want to average
const TIMESTAMP_ATTRIBUTE_NAME = '#ts'; // Placeholder for the attribute name for the timestamp
Next there is a declaration of the handler function, which is the entry point for the AWS Lambda function:
export const handler = async (event) => {
const now = new Date();
const N = 10; // Number of minutes
const timeThreshold = new Date(now.getTime() - N * 60 * 1000);
// Format the time threshold to ISO 8601 string
const formattedTimeThreshold = timeThreshold.toISOString();
// Subsequent declarations
}
The handler receives an event object which in this case is ignored. Then, the handler declares four constants (variables whose values cannot be reassigned):
When dealing with timestamps and comparing dates across different systems, such as IoT devices and servers, it is crucial to handle the time zones correctly to ensure accurate comparisons and calculations. IoT devices may operate in various time zones which can differ from the time zone of the server running the AWS Lambda function. To manage this, you will use the ISO 8601 format for timestamps.
The ISO 8601 format is a standardized way to represent dates and times. It includes the date, time, and time zone information, ensuring consistency across different systems. The format looks like this: YYYY-MM-DDTHH:mm:ss.sssZ, where Z indicates the UTC (Coordinated Universal Time) time zone.
By formatting the time threshold to an ISO 8601 string, you ensure that both the IoT device’s timestamps and the server’s time threshold are in a consistent format. Comparing timestamps in a standardized format ensures that the time-based filtering logic works correctly.
Now you will configure the DynamoDB scan parameters:
const params = {
TableName: TABLE_NAME,
FilterExpression: `${TIMESTAMP_ATTRIBUTE_NAME} >= :timeThreshold`,
ExpressionAttributeNames: {
'#ts': 'timestamp', // Map the placeholder to the actual attribute name
},
ExpressionAttributeValues: {
':timeThreshold': formattedTimeThreshold,
},
};
Here, FilterExpression
is used to filter the items to only those with a timestamp greater than or equal to the time threshold. Then, ExpressionAttributeNames
maps the placeholder to the actual attribute name. Finally, ExpressionAttributeValues
defines the value for the placeholder.
The AWS Lambda function then executes the scan command against the table with the specified parameters, as shown below:
try {
const data = await dynamoDb.send(new ScanCommand(params));
const items = data.Items;
// Items processing
} catch (error) {
console.error('Error reading from DynamoDB', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Could not read from DynamoDB' }),
};
}
After executing the scan command you will get the list of items in the form of a JSON collection. You process them as follows:
if (!items || items.length === 0) {
return {
statusCode: 200,
body: JSON.stringify({ average: 0 }),
};
}
const numericalValues = items.map(item => parseFloat(item[ATTRIBUTE_NAME])).filter(value => !isNaN(value));
if (numericalValues.length === 0) {
return {
statusCode: 200,
body: JSON.stringify({ average: 0 }),
};
}
const sum = numericalValues.reduce((acc, value) => acc + value, 0);
const average = sum / numericalValues.length;
return {
statusCode: 200,
body: JSON.stringify({ average }),
};
If no items are found, it will return 0 as the average value. Otherwise, it will convert the attribute values to numbers and this filters out any non-numeric values. Next, it will calculate the sum and average of the numerical values. Finally, this average value is returned to the caller.