Skip to content

Commit

Permalink
feat(iotevents): support timer actions (#19949)
Browse files Browse the repository at this point in the history
This PR is a part of roadmap in #17711. And if this PR is merged, I will close this issue and create some good first issues to implement rest actions and expressions.

This PR supports the timer actions (`SetTimerAction`, `ResetTimerAction` and `ClearTimerAction`) and `timeout()` expression.

These allow to embed a timer to the state machine of the detector model.
Below figure illustrate the state machine [Device Heartbeat](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-examples-dhb.html). This detector model is used to alert if the message is interrupted for a certain period of time. The integ-test included in this PR is example of creating Device Heartbeat detector model.

```mermaid
stateDiagram-v2
  [*] --> Online: set a timer on input message
  Online --> Online: reset the timer\non input messages
  Online --> Offline: timeout the timer
  Offline --> Online: input messages
```

----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)?
	* [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
yamatatsu authored Aug 3, 2022
1 parent ecff7bd commit af301dd
Show file tree
Hide file tree
Showing 20 changed files with 924 additions and 6 deletions.
79 changes: 73 additions & 6 deletions packages/@aws-cdk/aws-iotevents-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,78 @@ AWS IoT Events can trigger actions when it detects a specified event or transiti

Currently supported are:

- Use timer
- Set variable to detector instanse
- Invoke a Lambda function

## Use timer

The code snippet below creates an Action that creates the timer with duration in seconds.

```ts
import * as iotevents from '@aws-cdk/aws-iotevents';
import * as actions from '@aws-cdk/aws-iotevents-actions';

declare const input: iotevents.IInput;

const state = new iotevents.State({
stateName: 'MyState',
onEnter: [{
eventName: 'test-event',
condition: iotevents.Expression.currentInput(input),
actions: [
new actions.SetTimerAction('MyTimer', {
duration: cdk.Duration.seconds(60),
}),
],
}],
});
```

Setting duration by [IoT Events Expression](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-expressions.html):

```ts
new actions.SetTimerAction('MyTimer', {
durationExpression: iotevents.Expression.inputAttribute(input, 'payload.durationSeconds'),
})
```

And the timer can be reset and cleared. Below is an example of general
[Device HeartBeat](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-examples-dhb.html)
Detector Model:

```ts
const online = new iotevents.State({
stateName: 'Online',
onEnter: [{
eventName: 'enter-event',
condition: iotevents.Expression.currentInput(input),
actions: [
new actions.SetTimerAction('MyTimer', {
duration: cdk.Duration.seconds(60),
}),
],
}],
onInput: [{
eventName: 'input-event',
condition: iotevents.Expression.currentInput(input),
actions: [
new actions.ResetTimerAction('MyTimer'),
],
}],
onExit: [{
eventName: 'exit-event',
actions: [
new actions.ClearTimerAction('MyTimer'),
],
}],
});
const offline = new iotevents.State({ stateName: 'Offline' });

online.transitionTo(offline, { when: iotevents.Expression.timeout('MyTimer') });
offline.transitionTo(online, { when: iotevents.Expression.currentInput(input) });
```

## Set variable to detector instanse

The code snippet below creates an Action that set variable to detector instanse
Expand All @@ -44,12 +113,10 @@ const state = new iotevents.State({
eventName: 'test-event',
condition: iotevents.Expression.currentInput(input),
actions: [
actions: [
new actions.SetVariableAction(
'MyVariable',
iotevents.Expression.inputAttribute(input, 'payload.temperature'),
),
],
new actions.SetVariableAction(
'MyVariable',
iotevents.Expression.inputAttribute(input, 'payload.temperature'),
),
],
}],
});
Expand Down
22 changes: 22 additions & 0 deletions packages/@aws-cdk/aws-iotevents-actions/lib/clear-timer-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as iotevents from '@aws-cdk/aws-iotevents';
import { Construct } from 'constructs';

/**
* The action to delete an existing timer.
*/
export class ClearTimerAction implements iotevents.IAction {
/**
* @param timerName the name of the timer
*/
constructor(private readonly timerName: string) {}

bind(_scope: Construct, _options: iotevents.ActionBindOptions): iotevents.ActionConfig {
return {
configuration: {
clearTimer: {
timerName: this.timerName,
},
},
};
}
}
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-iotevents-actions/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export * from './clear-timer-action';
export * from './set-variable-action';
export * from './lambda-invoke-action';
export * from './reset-timer-action';
export * from './set-timer-action';
export * from './timer-duration';
22 changes: 22 additions & 0 deletions packages/@aws-cdk/aws-iotevents-actions/lib/reset-timer-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as iotevents from '@aws-cdk/aws-iotevents';
import { Construct } from 'constructs';

/**
* The action to reset an existing timer.
*/
export class ResetTimerAction implements iotevents.IAction {
/**
* @param timerName the name of the timer
*/
constructor(private readonly timerName: string) {}

bind(_scope: Construct, _options: iotevents.ActionBindOptions): iotevents.ActionConfig {
return {
configuration: {
resetTimer: {
timerName: this.timerName,
},
},
};
}
}
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-iotevents-actions/lib/set-timer-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as iotevents from '@aws-cdk/aws-iotevents';
import { Construct } from 'constructs';
import { TimerDuration } from './timer-duration';

/**
* The action to create a timer with duration in seconds.
*/
export class SetTimerAction implements iotevents.IAction {
/**
* @param timerName the name of the timer
* @param timerDuration the duration of the timer
*/
constructor(
private readonly timerName: string,
private readonly timerDuration: TimerDuration,
) {
}

bind(_scope: Construct, _options: iotevents.ActionBindOptions): iotevents.ActionConfig {
return {
configuration: {
setTimer: {
timerName: this.timerName,
durationExpression: this.timerDuration._bind(),
},
},
};
}
}
54 changes: 54 additions & 0 deletions packages/@aws-cdk/aws-iotevents-actions/lib/timer-duration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as iotevents from '@aws-cdk/aws-iotevents';
import { Duration } from '@aws-cdk/core';

/**
* The duration of the timer.
*/
export abstract class TimerDuration {
/**
* Create a timer-duration from Duration.
*
* The range of the duration is 60-31622400 seconds.
* The evaluated result of the duration expression is rounded down to the nearest whole number.
* For example, if you set the timer to 60.99 seconds, the evaluated result of the duration expression is 60 seconds.
*/
public static fromDuration(duration: Duration): TimerDuration {
const seconds = duration.toSeconds();
if (seconds < 60) {
throw new Error(`duration cannot be less than 60 seconds, got: ${duration.toString()}`);
}
if (seconds > 31622400) {
throw new Error(`duration cannot be greater than 31622400 seconds, got: ${duration.toString()}`);
}
return new TimerDurationImpl(seconds.toString());
}

/**
* Create a timer-duration from Expression.
*
* You can use a string expression that includes numbers, variables ($variable.<variable-name>),
* and input values ($input.<input-name>.<path-to-datum>) as the duration.
*
* The range of the duration is 60-31622400 seconds.
* The evaluated result of the duration expression is rounded down to the nearest whole number.
* For example, if you set the timer to 60.99 seconds, the evaluated result of the duration expression is 60 seconds.
*/
public static fromExpression(expression: iotevents.Expression): TimerDuration {
return new TimerDurationImpl(expression.evaluate());
}

/**
* @internal
*/
public abstract _bind(): string;
}

class TimerDurationImpl extends TimerDuration {
constructor(private readonly durationExpression: string) {
super();
}

public _bind() {
return this.durationExpression;
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iotevents-actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@aws-cdk/assertions": "0.0.0",
"@aws-cdk/cdk-build-tools": "0.0.0",
"@aws-cdk/integ-runner": "0.0.0",
"@aws-cdk/integ-tests": "0.0.0",
"@aws-cdk/pkglint": "0.0.0",
"@types/jest": "^27.5.2",
"jest": "^27.5.1"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Template } from '@aws-cdk/assertions';
import * as iotevents from '@aws-cdk/aws-iotevents';
import * as cdk from '@aws-cdk/core';
import * as actions from '../../lib';

let stack: cdk.Stack;
let input: iotevents.IInput;
beforeEach(() => {
stack = new cdk.Stack();
input = iotevents.Input.fromInputName(stack, 'MyInput', 'test-input');
});

test('Default property', () => {
// WHEN
new iotevents.DetectorModel(stack, 'MyDetectorModel', {
initialState: new iotevents.State({
stateName: 'test-state',
onEnter: [{
eventName: 'test-eventName',
condition: iotevents.Expression.currentInput(input),
actions: [
new actions.ClearTimerAction('MyTimer'),
],
}],
}),
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', {
DetectorModelDefinition: {
States: [{
OnEnter: {
Events: [{
Actions: [{
ClearTimer: {
TimerName: 'MyTimer',
},
}],
}],
},
}],
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Stack verification steps:
* * put a message
* * aws iotevents-data batch-put-message --region=us-east-1 --messages=messageId=(date | md5),inputName=test_input,payload=(echo '{"payload":{"deviceId":"000"}}' | base64)
*/
import * as iotevents from '@aws-cdk/aws-iotevents';
import * as cdk from '@aws-cdk/core';
import { IntegTest } from '@aws-cdk/integ-tests';
import * as actions from '../../lib';

/**
* This example will creates the detector model for Device HeartBeat Monitoring.
*
* @see https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-examples-dhb.html
*/
class TestStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);

const input = new iotevents.Input(this, 'MyInput', {
inputName: 'test_input',
attributeJsonPaths: ['payload.deviceId'],
});

const online = new iotevents.State({
stateName: 'Online',
onEnter: [{
eventName: 'enter-event',
condition: iotevents.Expression.currentInput(input),
actions: [
new actions.SetTimerAction('MyTimer', actions.TimerDuration.fromDuration(cdk.Duration.seconds(60))),
],
}],
onInput: [{
eventName: 'input-event',
condition: iotevents.Expression.currentInput(input),
actions: [
new actions.ResetTimerAction('MyTimer'),
],
}],
onExit: [{
eventName: 'exit-event',
actions: [
new actions.ClearTimerAction('MyTimer'),
],
}],
});
const offline = new iotevents.State({ stateName: 'Offline' });

online.transitionTo(offline, { when: iotevents.Expression.timeout('MyTimer') });
offline.transitionTo(online, { when: iotevents.Expression.currentInput(input) });

new iotevents.DetectorModel(this, 'MyDetectorModel', {
detectorKey: 'payload.deviceId',
initialState: online,
});
}
}

// GIVEN
const app = new cdk.App();
const stack = new TestStack(app, 'iotevents-timer-actions-test-stack');
new IntegTest(app, 'TimerActions', { testCases: [stack] });
app.synth();
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Template } from '@aws-cdk/assertions';
import * as iotevents from '@aws-cdk/aws-iotevents';
import * as cdk from '@aws-cdk/core';
import * as actions from '../../lib';

let stack: cdk.Stack;
let input: iotevents.IInput;
beforeEach(() => {
stack = new cdk.Stack();
input = iotevents.Input.fromInputName(stack, 'MyInput', 'test-input');
});

test('Default property', () => {
// WHEN
new iotevents.DetectorModel(stack, 'MyDetectorModel', {
initialState: new iotevents.State({
stateName: 'test-state',
onEnter: [{
eventName: 'test-eventName',
condition: iotevents.Expression.currentInput(input),
actions: [
new actions.ResetTimerAction('MyTimer'),
],
}],
}),
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', {
DetectorModelDefinition: {
States: [{
OnEnter: {
Events: [{
Actions: [{
ResetTimer: {
TimerName: 'MyTimer',
},
}],
}],
},
}],
},
});
});
Loading

0 comments on commit af301dd

Please sign in to comment.