.github

Implementation

Tools

In order to achieve higher software quality and reliability we decided to opt for the following optional tools:

Devices management

Protocol

It is required that every device while requesting an ip from the LAN DHCP server will also specify a name to bind to that ip. This allows the server to store this name and use it to reach devices even if they change their ip.

Devices discovery

Devices announce themselves on a configurable known port by sending broadcast UDP datagrams including basic data (id, name, lan hostname and port) that can be used by the server to register them to the system. The server DeviceDiscovererUDPAdapter.ts will keep device announces in memory for a finite amount of time before forgetting them.

DeviceCommunicationProtocol

The communication protocols expects devices to expose the following http routes on the port that he announced during discovery:

Real time property updates

The DevicesService allows to subscribe for receiving device property updates. One of these subscribers is SocketIOPropertyUpdatesSubscriberAdapter.ts which is a SocketIO server that will will relay the updates to every client that connects to it.

Repositories

All repositories of this bounded context are implemented by extending a generic implementation (look at BaseRepositoryMongoAdapter.ts for more infos)

Http API

Each service has its own http controller which defines routes and http logic (you can look at http adapters).

DTOs are used when the interface to be exposed has to be different from the internal interface of entities. (This is done only due to time constraints, in an ideal scenario it would be better to define DTOs for each entity)

Notifications Management

The NotificationProtocol exposes a SocketIO endpoint to which the client can then connect in order to receive the notifications sent by the server (DeviceOfflineNotifications or other kind of notifications, like the one sent from the script instructions).

To receive a notification a user must first emits the event “login” sending its email, then the server will send an event “notification” to that user when needed (based on DeviceOfflineNotificationSubscriptions, script instructions or script error), sending a message.

Scripts Management

Script

According to the modelling of the Script aggregate, it is an interface that allows users to execute instructions.

Task and Automation extends the Script interface in order to have the behaviour described in the modelling part.

When executed, a Script creates an ExecutionEnvironment for that execution (because a script can be started while it is already started, it will create another, fresh one, ExecutionEnvironment every time it is started).

If a Script returns an error after being executed, a notification will be sent to the admin.

Instructions

There are multiple instructions, some which just do some action like waiting, starting other tasks, creating costants, sending a notification or executing device actions, others control the flow of the script, like if or if-else.

Condition

A Condition is a data structure which contains two ConstantInstructions, a negate field and a ConditionOperator.

It has also an evaluate(ExecutionEnvironment) method, which returns a boolean based on the ConditionOperator, the two ConstantInstructions and the negate field, which, if it is true, switch the boolean returned by the evaluate method (if negate = true and negate should return true, than evaluate returns false)

To be created, the two ConstantInstructions and the ConditionOperator must have the same type.

ConditionOperator

There are multiple ConditionOperators:

The evaluate method of the ConditionOperator gets two arguments, the left and right constants, in order to evaluate if the operator returns true or false.

ExecutionEnvironment

At runtime there is the need to have a data structure that saves all the constants with their respective values, and that is the ExecutionEnvironment (indeed, it is here that the createDevicePropertyConstantInstruction will save its value at runtime).

It is also the data structure which contain the token of the user executing the task, if there is one.

NodeRef

In order to check the syntax of a script, NodeRefs are used, which are a data structure that just contains its superNode, which is another NodeRef.

There can be 3 types of NodeRefs:

The instruction contained in the ThenNodeRef and in the ElseNodeRef is used when creating the script in order to know for each instruction in which If/IfElseInstruction put it.

ConstantRef

Other than nodeRefs, it is needed the use of another data structure, called ConstantRef. It contains a ConstantInstruction and the nodeRef in which the constant has been defined. This is usefull for the following reasons:

In both cases a syntax error is returned.

ScriptBuilder

According to the modelling of the ScriptBuilder, it is an abstract class with methods that create a script checking its syntax.

Build

There are two methods to create a script, both used only by the ScriptsService: build() and buildWithId(ScriptID). The build() method will create a script with a random ScriptID, while the buildWithId(ScriptID) will create a script with a given ScriptID.

Both methods return a InvalidScriptError if the syntax of the script is wrong, if the name of the script is empty or if the period trigger of an automation have a zero or negative period.

AddInstructions

Every add[Instruction] method get a nodeRef and return the new scriptBuilder, which is the same of the one used but with the new instruction and the new InvalidScriptErrors, if there are some.

E.g.

// Creation of task builder
const builderAndRef = new TaskBuilder("taskName");
const builder = builderAndRef[0];
const rootNodeRef = builderAndRef[1];

// Adding wait instruction
const newBuilder = builder.addWait(rootNodeRef, 10);
// *newBuilder* has a wait instruction with 10 seconds on the root of the script,
// while *builder* is still empty

There are also some add[Instruction] that returns other data structure besides the new builder:

E.g.

// Creation of task builder
const builderAndRef = new TaskBuilder("taskName");
const builder = builderAndRef[0];
const rootNodeRef = builderAndRef[1];

// Adding an integer constant with value 10
const newBuilderAndCRef1 = builder.addCreateConstant(
  rootNodeRef,
  "C1",
  Type.IntType,
  10
);
const c1 = newBuilderAndCRef1[1];
const newBuilder1 = newBuilderAndCRef1[0];

// Adding an integer constant with value 15
const newBuilderAndCRef2 = newBuilder1.addCreateConstant(
  rootNodeRef,
  "C2",
  Type.IntType,
  15
);
const c2 = newBuilderAndCRef2[1];
const newBuilder2 = newBuilderAndCRef2[0];

// Adding an if-else instruction checking if the two constants are equal
const newBuilderAndRef1 = newBuilder2.addIfElse(
  rootNodeRef,
  c1,
  c2,
  false,
  ConditionOperatorType.NumberEOperator
);
const thenRef = newBuilderAndRef1[1];
const elseRef = newBuilderAndRef1[2];
const newbuilder3 = newBuilderAndRef1[0];

// Add a send notification inside the then of the previously created if-else
const thenBuilder = newBuilder3.addSendNotification(
  thenRef,
  Email("pippo@email.com"),
  "Not sent"
);

// Add a send notification inside the else of the previously created if-else
const elseBuilder = thenBuilder.addSendNotification(
  elseRef,
  Email("pluto@email.com"),
  "Sent"
);

// When executing a script created with the *elseBuilder* TaskBuilder, it will be sent a notification to pluto@email.com with the message: "Sent".

ScriptsService

The ScriptsService is the service that manage all the Tasks and Automations.

There are methods to retrieve one or more Tasks/Automations, methods that accept a ScriptBuilder to create or modify a Task/Automation, methods that remove a Task/Automation, a method to start a Task and a method to change the state of an automation between enabled or disabled.

At the start of the server, the ScriptsService will start all the automations created that are enabled, starting to listen for DeviceEvents (for the DeviceEvent triggered Automations) or waiting (for the period triggered Automations).

When removing or editing an automation, the old one will be stopped if there are no errors while doing it.

Other doc

Distribuited doc