Skip to content

Code-based Rules

The main target of SMARTUNIFIER is to build up the connectivity between systems. Sometimes integrations become more complex and it might require to build up Rules via the code editor using the Scala programming language. SMARTUNIFIER extends the Scala programming language with additional operators and methods to simplify the realization of data transfer between Information Models.

Similar to Mappings via drag and drop, there is no knowledge of the underlying communication protocol (e.g., MQTT, OPCUA, etc.) needed. Protocols are hidden behind the corresponding Information Models. The parameter values of an Information Model are stored in the objects of type VariableDefinition[T] or PropertyDefinition[T]. These contain additional information and methods rather than just the parameter values. They also provide methods to listen for changes and conversion between variable types.

Basics

Code editor

To write custom code for the rules you need to switch to the edit view by pressing the "Edit Code" button. Keep in mind, then once in the code view, you cannot switch back to the graphical editor.

Mapping Code - Rule Code Edit

Compiling

Compile the code for the selected rule by clicking the "Compile" button and check for compilation errors before saving the rule.

Mapping Code - Rule Compiling

Rule Construct

A rule always starts with a Trigger. After the trigger, call mapTo and define the function body by adding curly braces. Depending on the trigger, declare the object which will contain the data of the trigger. It is recommend to use the name that corresponds to the type of the trigger (e.g., event, variable)

In the "actions" the variable assignment is done but also calculations and more complex operations can be realized by writing scala code. More examples can be found here Target-to-Source Mapping

1
2
3
4
5
6
Trigger mapTo { TriggerDataObject =>
  Try {
    // Actions
    TargetModel.TargetVariable := SourceModel.SourceVariable
  }
}

Node elements

Variables and Properties are SMARTUNIFIER specific object of the types VariableDefinition[T]/PropertyDefinition[T] These object have custom functions and operator how they can be accessed and modified.

A value can be assigned to Variables or Properties by using the := operator.

For example

TargetModel.TargetVariable := SourceModel.SourceVariable

TargetModel.StringVariable := "Hello"

TargetModel.IntProperty := 1234

The actual value is store in a Scala Option[T] (e.g. Option[String]).

The Option object can be accessed by calling get

val myVariable: Option[String] = SourceModel.StringVariable.get

Logging

Action logging

Action logging can be added in the rule implementation by calling CommunicationLogger.log. The Communication logger will provide log entries in a standardized format.

1
2
3
4
5
6
7
8
EquipmentModel.Alarm mapTo {variable =>
   MesModel.EquipmentAlarm.send(event => {
     Try {
       event.EquipmentId := EnterpriseModel.EquipmentName
       CommunicationLogger.log(variable, event)
     }
   })
}

Custom logging

Custom logging can be added by using the logger object. The logger object privides functions for info, warn, error debug and trace log entries. Use the scala string concatenation for build log string. E.g., s"Variable has changed. New value=${variable.get}

1
2
3
4
5
6
7
8
9
EquipmentModel.Alarm mapTo {variable =>
   MesModel.EquipmentAlarm.send(event => {
     Try {
       event.EquipmentId := EnterpriseModel.EquipmentName
       logger.info(s"Alarm triggered. Current state is ${variable.get}")
       CommunicationLogger.log(variable, event)
     }
   })
}

Trigger Types

Tree Member

The following Information Model elements can be used as a trigger: Variables, Events, Commands. The snippet below shows how the trigger is defined:

<Information Model>.<Element from the Information Model> mapTo { <Element type> =>
1
2
3
4
5
6
EquipmentDataModel.ItemNr mapTo { variable =>
   Try {
     EquipmentDataModel.DemoData.Temperature := RestServerModel.DemoData.Temperature
     EquipmentDataModel.DemoData.Pressure := RestServerModel.DemoData.Pressure
   }
}

Schedulers

With schedulers, you can execute Rules at specified times or intervals. You can choose from the following scheduler types:

Typically, the scheduler is started automatically and executes the rule once the instance is started, and used channels are in the "Connected" state.

However, you can manually trigger the execution and termination of a rule by using the start/stop function of the scheduler:

# Start Rule
_trigger.Schedulers("<name of rule>").start()
# Stop Rule
_trigger.Schedulers("<name of rule>").stop()

Fixed Rate Scheduler

Rules can be scheduled to run continuously at a fixed rate. Instead of defining an element of the Information Model as a trigger, the fixedRateScheduler method can be used. The snippet below shows how the fixed rate scheduler is defined:

_trigger.fixedRateScheduler(<Cron Expression>)
1
2
3
_trigger.fixedRateScheduler("0/1 * * * * ? *") mapTo(() => {
  model1.StringVariable := model2.StringVariable
})
Example Expressions
Expression Description
0/1 * * * * ? Every second
0/20 * * * * ? Every 20 seconds
15 0/2 * * * ? Every other minute, starting at 15 seconds past the minute.
0 0/2 8-17 * * ? Every other minute, between 8am and 5pm (17 o'clock).
0 0/3 17-23 * * ? Every three minutes but only between 5pm and 11pm
0 0 10am 1,15 * ? 10am on the 1st and 15th days of the month
0,30 * * ? * MON-FRI Every 30 seconds on Weekdays (Monday through Friday)
0,30 * * ? * SAT,SUN Every 30 seconds on Weekends (Saturday and Sunday)

Fixed Delay Scheduler

Rules can be scheduled to run at a fixed rate with an initial delay. The snippet below shows how the fixed delay scheduler is defined:

_trigger.fixedDelayScheduler(<Initial Delay>, <Period>, <Unit>) mapTo(() =>
1
2
3
4
_trigger.fixedDelayScheduler(10, 60, SECONDS) mapTo(() => Try{
  EquipmentDataModel.DemoData.Temperature := RestServerModel.DemoData.Temperature
  EquipmentDataModel.DemoData.Pressure := RestServerModel.DemoData.Pressure
})

Timeout Scheduler

Rules can be scheduled to run after a specific timeout. The snippet below demonstrates how the timeout scheduler is defined:

_trigger.timeoutScheduler(<Delay>, <Unit>) mapTo(() =>
1
2
3
4
_trigger.timeoutScheduler(60, SECONDS) mapTo(() => Try{
  EquipmentDataModel.DemoData.Temperature := RestServerModel.DemoData.Temperature
  EquipmentDataModel.DemoData.Pressure := RestServerModel.DemoData.Pressure
})

Instance Events

Rules can be triggered by state changes of the [Intance][instance]. The following list shows the possible transistions and states.

Event State
StartInstanceEvent InstanceStartingState
ConnectingInstanceEvent InstanceConnectingState
ConnectedInstanceEvent InstanceConnectedState
DisconnectedInstanceEvent InstanceDisconnectedState
StopInstanceEvent InstanceStoppingState
StoppedInstanceEvent InstanceStoppedState

onTransitionTo

Triggered when changing to a specific state It receives the state the instance came from and the triggering event

1
2
3
_instance.onTransitionTo(InstanceConnectedState) { (from, event) =>

}

onTransition

Triggered on one specific transition. It receives the triggering event

1
2
3
_instance.onTransition(InstanceConnectedState, InstanceDisconnectedState) { event =>

}

onAnyTransition

Triggerd on every state transistion. It received the from state, to state and the event.

1
2
3
_instance.onAnyTransition((from, to, event) =>

}

isIn

Check if the instance is in a specific state.

1
2
3
if (_instance.isIn(InstanceConnectedState)) {

}

Channel Events

Rules can be triggered by the state change of a channel. The following list shows the possible transistions and states.

Event State
StartChannelEvent StartingState
ChannelConnectingEvent ConnectingState
ChannelConnectFailureEvent ConnectFailureState
ChannelConnectedEvent ConnectedState
ChannelDisconnectedEvent DisconnectedState
StopChannelEvent StoppingState
StoppedChannelEvent StoppedState
StartChannelFailureEvent StartFailureState
StopChannelFailureEvent StopFailureState
RunChannelFailureEvent RunningFailureState

onTransitionTo

Triggered on one specific transition. It receives the triggering event.

1
2
3
model.internal.onTransitionTo(ConnectedState) { (from, event) =>

}

onTransition

Triggered on one specific transition. It receives the triggering event.

1
2
3
model.internal.onTransition(InstanceConnectedState, InstanceDisconnectedState) { event =>

}

onAnyTransition

Triggerd on every state transistion. It received the from state, to state and the event.

1
2
3
model.internal.onAnyTransition((from, to, event) =>

}

Target-to-Source Mapping

Node Types Sharing the Same Custom Data Type

When the target and source Node Types in the Information Model are both of the same Custom Data Type, the [mapping] can be simplified:

// Mapping of two Events with the same type
event1 := event2

The two Node Types have to be of the same kind, e.g., both are Events or both are Variables.

Node Types with Different Custom Data Types

Variables to Events

This mapping is utilized when static data needs to be transformed into an Event. This is often the case when data originates from a variable-based data server (such as OPC UA server, Modbus, Iso-On-TCP) and is required to be mapped to an event or message-based target system (like MQTT, Kafka, Databases, etc.).

The example below illustrates the mapping of Variables from the EnterpriseModel and the EquipmentModel to an Event within the MesModel:

  • Trigger: EquipmentModel.Alarm (line 1)
  • TriggerInstance of EquipmentModel.Alarm: variable (line 1)
  • Invoke the send method on the EquipmentAlarm Event (line 2) and define the TriggerInstance as event (line 2)
  • Variable assignment is performed using the assignment operator :=. Both target and source are specified by entering the path of the variables in the Information Model, for example, event.EquipmentId and EnterpriseModel.EquipmentName (line 4)
// Rule - StartOrder - Variable/Event
EquipmentModel.Alarm mapTo {variable =>
   MesModel.EquipmentAlarm.send(event => {
     Try {
       event.EquipmentId := EnterpriseModel.EquipmentName
       event.OrderNr := EquipmentModel.CurrentOrder.OrderNr
       event.MaterialID := EquipmentModel.CurrentMaterialID
       event.AlarmInfo := EquipmentModel.AlarmInfo
       CommunicationLogger.log(variable, event)
     }    
   })
}

Event to Variables

This mapping is utilized when dealing with event-driven data that needs to be mapped to Variables. This scenario often occurs when data originates from an event or message-based system (e.g., MQTT, Kafka, Databases, etc.) and needs to be mapped to a variable-based data server (such as OPC UA server, Modbus, Iso-On-TCP).

The example below outlines the mapping of values from the TransferNewOrder Event in the MesModel into Variables within the EquipmentModel:

  • The Trigger is specified by entering the path of the Event MesModel.TransferNewOrder (line 1). Since an Event is utilized as the Trigger, the TriggerInstance is appropriately named event (line 1).
  • In the function body, the Complex Variable NewOrder and the Simple Data Type Variable NewMESOrderFlag are provided with data from the MesModel's TransferNewOrder Event.
  • Targets are specified by entering the path of the Variables, such as EquipmentModel.NewOrder.OrderNr (line 3).
  • To assign values to OrderNr, MaterialNr, and Quantity of the Complex Variable NewOrder, enter the TriggerInstance event followed by the variable name from the TransferNewOrder Event, e.g., event.OrderNr (line 3).
  • In this case, it is also possible to assign the Variable NewMesOrderFlag a Boolean value like true (line 6)
1
2
3
4
5
6
7
8
9
// Rule - TransferNewOrder - Event/Variable
MesModel.TransferNewOrder mapTo { event =>
   Try {
     EquipmentModel.NewOrder.OrderNr := event.OrderNr
     EquipmentModel.NewOrder.MaterialNr := event.MaterialNr
     EquipmentModel.NewOrder.Quantity := event.Quantity
     EquipmentModel.NewMESOrderFlag := true
   }  
}

Event to Commands

This mapping is employed when dealing with event-driven data that needs to be mapped to a Command. This scenario may arise when incoming event or message-driven data should be enriched with data from another system (such as a database or a REST server) before being further mapped to another event-driven message.

The following scenario describes a rule that maps incoming data from a file to MQTT. When the FileEvent is triggered, the rule first executes the DatabaseCommand to retrieve data from a database (the result of the reply can be accessed directly afterward):

  • Trigger is specified by entering the path of the Event file.FileEvent (line 1). Since an Event serves as the Trigger, the TriggerInstance should be named event (line 1)
  • Within the function body, execute a Command. The execution of a Command is specified by entering the path of the Command and calling the execute function at the end of the path (line 2). The TriggerInstance is named command (line 4).
  • Lines 4-6 illustrate the first part of the Command execution, where values from the source model are assigned to the Command Parameters.
  • Every Command includes a Reply, which necessitates defining the reply section (line 8).
  • After retrieving data from the database, send out the data over MQTT. In the reply function body, specify the path of the MqttEvent. Since this is the second Event, the TriggerInstance can be named event1 (line 10).
  • Within the reply function body, assign values from the FileEvent (lines 11-13) as well as from the Reply (lines 14-15) to the MqttEvent.
// Rule - File2MqttWithDB - Event/Commands
file.FileEvent mapTo {event =>
  database.DatabaseCommand.execute(command => {
    Try {
      command.orderNr := event.orderNr
      command.materialNr := event.materialNr
      CommunicationLogger.log(event, command)
    }
  }, reply => {
    mqtt.MqttEvent.send(event1 => {
      Try {
        event1.Quality := event.quality
        event1.OrderNr := event.orderNr
        event1.MaterialNr := event.materialNr
        event1.Customer := reply.customer
        event1.Product := reply.product
        CommunicationLogger.log(reply, event1)
      }
    })
  })
}

Properties to Variables

When a Property serves as the source and a Variable as the target, the mapping is straightforward: the Property is assigned to the Variable using the assignment operator :=. This approach may be utilized when dealing with an XML structure that includes XML-Attributes, which are modeled as Properties in the Information Model, while the target system expects the data to be presented as Variables.

// Rule - Property/Variables
propertyNodeType := variableNodeType

Mapping Including Lists

If there are Lists structures within an Information Model that need to be mapped to another Information Model, it is necessary to iterate through the list items using a foreach loop.

The following scenario describes a Rule that maps incoming data from a file to MQTT. The MQTT Model contains a List called DataList.

  • Initialize a variable named listItem to reference a newItem in the DataList (line 6)
  • Then, assign the value from the file event to this variable listItem (line 8)
// Rule - FileToMQTT - Lists
csv.FileEvent mapTo { event =>

  event.items.foreach { item =>
    mqtt.MqttEvent.send(event1 => {
      Try {
        val listItem = event1.DataList.newItem

        listItem.Timestamp := item.Timestamp
        listItem.Pressure := item.Alarmlevel

        CommunicationLogger.log(event, event1)
      }
    })
  }
}

Note

Lists can only be mapped in the code view.

Code constructs

SMARTUNIFIER includes custom code constructs that can be used within mappings, allowing for operations such as type conversions directly at the variable which are of type TVariableDefinition[_] or TPropertyDefinition[_].

Converters

SMARTUNIFIER provides in implicit variable type conversion if possible. For most type not convertion needs to done manually, as it is done automatically in the mapping.

If the variables to be mapped to each other are not of the same data type, use the provided type converters. Converters can be used on Information Model nodes such as Variables and Properties.

Method Description Example
toBoolean(definition: TVariableDefinition[T])
toBoolean(definition: TPropertyDefinition[T])
Converts a variable to a Boolean
Either the literal true or the literal false
toBoolean(m1.IntVariable)
toBoolean(m1.IntProperty)
toByte(definition: TVariableDefinition[T])
toByte(definition: TPropertyDefinition[T])
Conversion of a variable to a Byte
8-bit signed value. Range from -128 to 127
toByte(m1.IntVariable)
toByte(m1.IntProperty)
toShort(definition: TVariableDefinition[T])
toShort(definition: TPropertyDefinition[T])
Conversion of a variable to a Short
16-bit signed value. Range -32768 to 32767
toShort(m1.IntVariable)
toShort(m1.IntProperty)
toInt(definition: TVariableDefinition[T])
toInt(definition: TPropertyDefinition[T])
Conversion of a variable to an Integer
32-bit signed value. Range -2147483648 to 2147483647
toInt(m1.StringVariable)
toInt(m1.IntProperty)
toLong(definition: TVariableDefinition[T])
toLong(definition: TPropertyDefinition[T])
Conversion of a variable to a Long
64-bit signed value. Range -9223372036854775808 to 9223372036854775807
toLong(m1.IntVariable)
toLong(m1.IntProperty)
toFloat(definition: TVariableDefinition[T])
toFloat(definition: TPropertyDefinition[T])
Conversion of a variable to a Float
32-bit IEEE 754 single-precision float
toFloat(m1.IntVariable)
toFloat(m1.IntProperty)
toDouble(definition: TVariableDefinition[T])
toDouble(definition: TPropertyDefinition[T])
Conversion of a variable to a Double
64-bit IEEE 754 double-precision float
toDouble(m1.IntVariable)
toDouble(m1.IntProperty)
toStr(definition: TVariableDefinition[T])
toStr(definition: TPropertyDefinition[T])
Conversion of a variable to a String
A sequence of Chars
toStr(m1.IntVariable)
toStr(m1.IntProperty)

Math Operators

Math Operator methods can be utilized to perform calculations, such as addition, subtraction, multiplication, and division. If there's a need to perform calculations on the values of a variable within the mapping before sending data to the target system, the following methods can be employed.

Method Description Example
add(Option[T], Double) Addition of a variable with a numeric data type and a Double value add(model.IntVariable, 2)
sub(Option[T], Double) Subtraction of a variable with a numeric data type and a Double value sub(model.IntVariable, 2.5)
mult(Option[T], Double) Multiplication of a variable with a numeric data type and a Double value mult(model.IntVariable, 3)
div(Option[T], Double) Division of a variable with a numeric data type and a Double value div(model.IntVariable, 3.5)
sqrt[T](value: Option[T]): Option[_] Square root of a variable with a numeric data type sqrt(model.DoubleVariable)
round[T](value: Option[T], digits: Int = 0): Option[_] Rounding of a variable with a numeric data type to specified decimal places (default: 0) round(model.DoubleVariable, 2)

String Operators

String Operator methods can be utilized to perform String manipulation.

Method Description Examples
concat(variable, variable) Concatenates two strings together. concat(myModel.myStringVariable, "World"), concat("Hello", myModel.myStringVariable), concat(myModel.myStringVariableA, myModel.myStringVariableB)
contains(variable, sequence) Checks and returns true if and only if this string contains the specified sequence of char values. contains(myModel.myStringVariable, "Hello"), contains(myModel.myStringVariable, myModel.myOtherStringVariable)
matches(variable, regex) Checks and returns true if the string matches the given regular expression. matches(myModel.myStringVariable, "Hello.*"), matches(myModel.myStringVariable, myModel.myRegExStringVariable)
replace(variable, target, replacement) Replaces the first occurrence of a substring within the string with the specified replacement. replace(myModel.myStringVariable, "T", "X"), replace(myModel.myStringVariable, myModel.search, "X"), replace(myModel.myStringVariable, myModel.search, myModel.replace)
replaceAll(variable, target, replacement) Replaces all occurrences of a substring within the string with the specified replacement. replaceAll(myModel.myStringVariable, "T", "X"), replaceAll(myModel.myStringVariable, myModel.search, "X"), replaceAll(myModel.myStringVariable, myModel.search, myModel.replace)
substring(variable, beginIndex) Extracts a string that is a substring of this string. The substring begins with the character at the specified index and extends to the end of this string. substring(myModel.myStringVariable, 3)
substring(variable, beginIndex, endIndex) Extracts a string that is a substring of this string. The substring begins at the specified beginIndex and extends to the character at index endIndex - 1. substring(myModel.myStringVariable, 3, 7)
toLowerCase(variable) Converts all characters in a string to lower case. toLowerCase(myModel.myStringVariable)
toUpperCase(variable) Converts all characters in a string to upper case. toUpperCase(myModel.myStringVariable)
trim(variable) Removes leading and trailing whitespace from a string / string variable. trim(myModel.myStringVariable)
strip(variable) Similar to trim, removes leading and trailing whitespace from a string. strip(myModel.myStringVariable)

Helpers

Additional helper methods that can be used to simplify the mapping process. They can be used to compare the value of a variable with a given value, or to map child variables from one complex variable to another.

Method Description Example
equals(variable, variable) Compares the value of a variable with a given value equals(myModel.myStringVariable, "Foo")
mapAndAssignChildren(complexVariable1, complexVariable2) Maps child variables from one complex variable to another mapAndAssignChildren(<source>, <target>). Variables must be of the same type and have the same id mapAndAssignChildren(m1.ComplexVariableDepth1, m2.ComplexVariableDepth1)

Time Conversions

Note

When dealing with OffsetDateTime, ZoneOffset.UTC is set as default. When formatting, the following formats are set as defaults:
ISO_DATE_TIME = yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
ISO_DATE = yyyy-MM-dd
ISO_TIME = HH:mm:ss.SSS'Z'

Method Description Example
longToOffsetDateTime(timestamp: Option[long], offset: ZoneOffset = ZoneOffset.UTC): OffsetDateTime Parses a Unix timestamp provided in a long variable to OffsetDateTime longToOffsetDateTime(Option(1732873920000)) → OffsetDateTime, longToOffsetDateTime(myModel.MyUnixTimestamp) → OffsetDateTime
longToLocalDateTime(timestamp: Option[long], offset: ZoneOffset = ZoneOffset.UTC): LocalDateTime Parses a Unix timestamp provided in a long variable to LocalDateTime longToLocalDateTime(Option(1601536800000)) → LocalDateTime, longToLocalDateTime(myModel.MyUnixTimestamp) → LocalDateTime
parseOffsetDateTime(dateTime: Option[String], formatter: DateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME): OffsetDateTime Parses a String to OffsetDateTime parseOffsetDateTime(Option("2024-11-29T08:32:00+01:00")) → OffsetDateTime
parseLocalDateTime(dateTime: Option[String], formatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME): LocalDateTime Parses a String to LocalDateTime parseLocalDateTime(Option("2024-12-13T10:15:30")) → LocalDateTime
formatOffsetDateTime(dateTime: Option[OffsetDateTime], formatter: DateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME): String Formats an OffsetDateTime formatOffsetDateTime(Option(OffsetDateTime)) → "2024-11-29T08:32:00+01:00"
formatLocalDateTime(dateTime: Option[LocalDateTime], formatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME): String Formats a LocalDateTime formatLocalDateTime(Option(LocalDateTime)) → "2024-12-13T10:15:30"
localDateTimeToOffsetDateTime(dateTime: Option[LocalDateTime], offset: ZoneOffset = ZoneOffset.UTC): OffsetDateTime Parses a LocalDateTime to OffsetDateTime localDateTimeToOffsetDateTime(Option(LocalDateTime)) → OffsetDateTime
localDateTimeToLong(dateTime: Option[LocalDateTime], offset: ZoneOffset = ZoneOffset.UTC): long Parses a LocalDateTime to long localDateTimeToLong(Option(LocalDateTime)) → 1601536800000
offsetDateTimeToLocalDateTime(dateTime: Option[OffsetDateTime]): LocalDateTime Parses an OffsetDateTime to LocalDateTime offsetDateTimeToLocalDateTime(Option(OffsetDateTime)) → 2024-12-13T10:15:30
offsetDateTimeToLong(date: Option[OffsetDateTime]): long Parses an OffsetDateTime to Long offsetDateTimeToLong(Option(OffsetDateTime)) → 1732873920000

Example of Date conversions:

Method Input Output
longToOffsetDateTime 1601536800000 2024-12-13T10:15:30+01:00
StringToLocalDateTime "2024-12-13T10:15:30" 2024-12-13T10:15:30
StringToOffsetDateTime "2024-11-29T08:32:00+01:00" 2024-11-29T08:32:00+01:00
1
2
3
4
5
SourceModel.SourceEvent/ComplexVar mapTo { event/variable =>
  Try {
    TargetModel.TargetEvent/ComplexVar := parsing method(event/variable.SourceVar)
  }
}
1
2
3
4
5
6
7
8
9
Equipment.DateTimeConversion mapTo { event =>
  Try {
    target.complexVar.Local2Long := localDateTimeToLong(event.Local2Long)
    target.complexVar.Long2Local := longToLocalDateTime(event.Long2Local)
    target.complexVar.String2Local := parseLocalDateTime(event.String2Local)
    target.complexVar.FormatLocal := formatLocalDateTime(event.FormatLocal)
    target.complexVar.Offset2Local := offsetDateTimeToLocalDateTime(event.Offset2Local)
  }
}

Loops (foreach)

In some use cases, it may be necessary to iterate through a collection if the Information Model contains a list or an array. In this case, call the items method on the list element of the Information Model, followed by foreach (line 13).

import java.time.LocalDateTime
import java.time.Instant
import java.time.ZoneId

equipment.FileEvent mapTo { event =>
  val newImportDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.systemDefault())
  db.MainDatabaseEvent.send(event1 => Try {
    event1.ImportDateTime := newImportDate
    event1.StepId := event.StepId

    event.analysisData.items.foreach { 

      case analysisDataType: ComplexCollectionVariableDefinition[AnalysisDataType] => {

        val analysisDataItem = event1.analysisDataTable.newItem

        analysisDataItem.name := analysisDataType.name
        analysisDataItem.length := analysisDataType.length
      }
    }
  })
}

Conditions (If - statements)

Within a rule, it's possible to implement conditions using Scala’s conditional expressions. If statements can be used to test a condition before executing the subsequent block. For example, this can be utilized to check if a certain condition is met before executing an event (line 3).

equipment.ActiveOrder.State mapTo { variable =>
  logger.info(s"Active order state: ${variable.value} - Processing Finished")
  if (variable.value == 3) {
    mes.NotifyOrderFinished.send(event => {
      Try{
        event.EquipmentId := equipment.EquipmentInformation.EquipmentType
        event.OrderNr := equipment.ActiveOrder.OrderInformation.OrderNo
        event.ProductNumber := equipment.ActiveOrder.OrderInformation.ProductNo
        event.QuantityOk := equipment.ActiveOrder.QuantityOk
        event.QuantityNOk := equipment.ActiveOrder.QuantityNOk
      }
    })
  }  
}

Exception Handling (Try/Catch)

Exception Handling is an integral part of the SMARTUNIFIER mapping logic. The mapTo and send callbacks expect a return value of the Scala type Try. If an exception occurs in one of the rules, SMARTUNIFIER logs the exception and displays a notification in the manager. Supported Communication Channels take further actions once an exception has occurred. For example, the File Reader Channel moves a file that initially triggered a rule into an error folder.

In the example below, a Try block is placed after each command and event call (lines 2 and 4).

database.update mapTo { (updateCommand, reply) =>
  Try {
    api.AmorphAPI.send(event =>
      Try {
        event.id := updateCommand.Identifier.Id
        event.name := updateCommand.Identifier.Name

        updateCommand.Status.items.foreach(item => {
          val statusItem = event.status.newItem
          statusItem.index := item.Index
          statusItem.value := itemValue
        })
      }  
    )
  }
}

Breaking out of Rules

You can break out of a rule by calling the Break() method in your code. Any code defined after the Break() method will not be executed.

Example 1: The Break() method can be used to stop the execution of the code if, for example, a variable value is not present (lines 2-4) but is needed later (line 9).

def rule_r1(): Unit = {
  if(model1.myVariable.isEmpty()) {
    Break()
  }

  //... mapping code here ...

  model1.myVariable.mapTo {
    variable => model2.myVariable := variable
  }
}

Example 2: Breaking out of a loop if the iterator does not have a next element (line 4) and calling Break() (line 5).

1
2
3
4
5
6
7
8
9
def rule_r2(): Unit = {
  val iterator = model1.myList.items.iterator
  while(iterator.hasNext){
    if(iterator.next().isEmpty()){
      Break()
    }
  }
  // ... mapping code here ...
}