Documentation
Templates (file format)
Require: Symcon >= 7.0
The "ModBus Device" instance can export its complete configuration as a template and import it again. A template is a JSON file containing all addresses, virtual addresses, required variable profiles, the byte order and the polling settings. A device that has been set up once can thus be set up on other systems with a few clicks or shared with other users.
Many ready-made templates can be found in our community: Show templates
This page describes the file format completely, so that templates can also be created directly from a manufacturer's data sheet or generated by a script.
Import and export
| Action | Description |
|---|---|
| Export template | Creates a JSON file from the current configuration. All used profiles that do not start with "~" are exported as well. |
| Import template | Replaces addresses, virtual addresses, byte order and polling settings in the configuration form. The changes are only saved with "Apply". Missing profiles are created in the process, existing profiles are not modified. |
The file is checked before the import. If it does not contain the three keys "Addresses", "VirtualAddresses" and "Profiles", the import is aborted with the message "This is not a valid ModBus template!". If a profile already exists with different settings, a hint is shown (e.g. "2 profiles do not match!"). In this case the existing profile is used.
The import overwrites the existing configuration of the instance. Variables whose ident no longer occurs in the new configuration are deleted when applying - including their archive data.
Structure
A template is a JSON object with the following keys:
| Key | Type | Required | Description |
|---|---|---|---|
| Addresses | Array | Yes | List of Modbus addresses, see Addresses |
| VirtualAddresses | Array | Yes | List of virtual addresses, see Virtual addresses, may be empty |
| Profiles | Object | Yes | Variable profiles created on import, see Profiles, may be empty |
| ByteOrder | Number | No | Byte order of the instance, see Byte order |
| Requests | Object | No | Polling settings, see Polling |
The order of the keys is arbitrary. Unknown keys are ignored. A minimal, valid template with a single address looks like this:
{
"Addresses": [
{
"Active": true,
"Name": "Battery state of charge",
"Ident": "battery_soc",
"Translation": [
{ "Language": "de", "Name": "Batterieladezustand" }
],
"DataType": 2,
"ReadFunctionCode": 4,
"ReadAddress": 33139,
"WriteFunctionCode": 0,
"WriteAddress": 0,
"Factor": 0,
"Length": 0,
"ByteOrder": -1,
"Profile": "~Battery.100"
}
],
"VirtualAddresses": [],
"Profiles": {},
"ByteOrder": 0,
"Requests": {
"Type": 0,
"Interval": 5000,
"DataBlocks": []
}
}
Addresses
Each entry in "Addresses" describes one value of the device. A variable is created below the instance for each active entry.
| Field | Type | Description |
|---|---|---|
| Active | Bool | Defines whether the variable is created and the value is read. Inactive entries remain in the template and can be activated by the user later. |
| Name | String | Name of the variable. An English name translated via "Translation" is recommended. |
| Ident | String | Ident of the variable. If the field is empty, the ident is generated automatically, see Ident. |
| Translation | Array | Translations of the name as a list of objects with "Language" (e.g. "de") and "Name". |
| DataType | Number | Data type of the register, see Data types. |
| ReadFunctionCode | Number | Function code for reading: 0 (do not read), 1, 2, 3 or 4, see Function codes. |
| ReadAddress | Number | Read address (start register or coil), see Addressing. |
| WriteFunctionCode | Number | Function code for writing: 0 (do not write), 5, 6, 15 or 16. If a function code is set, the variable gets a standard action. |
| WriteAddress | Number | Write address. Usually identical to the read address. |
| Factor | Number | Factor the read value is multiplied with. 0 means "no factor", see Factor. |
| Length | Number | Strings only: length in bytes (2 bytes per register). 0 for all other data types. |
| ByteOrder | Number | Byte order of this address. -1 uses the setting of the instance, see Byte order. |
| Profile | String | Name of the variable profile or empty. The profile type must match the variable type, see Data types. |
All fields should always be specified completely and with the correct JSON type. Numbers must not be given as a string ("3") or as null, otherwise applying the configuration fails. The exception is "Active": if it is missing, the address is considered active.
Older exports sometimes contain additional fields like "SwapBytes" or "CustomFactor". These are ignored and can be omitted. The value of a custom factor is always stored in the field "Factor".
Data types in templates
In the template the data type is stored as a number. For compatibility reasons the numbers are not sorted by size.
| DataType | Shown in the console | Registers | Variable type (without factor) |
|---|---|---|---|
| 0 | BOOL | 1 bit | Boolean |
| 1 | UINT8 (MSB) | 1 | Integer |
| 12 | UINT8 (LSB) | 1 | Integer |
| 2 | UINT16 | 1 | Integer |
| 3 | UINT32 | 2 | Integer |
| 11 | UINT64 | 4 | Float |
| 4 | INT8 (MSB) | 1 | Integer |
| 13 | INT8 (LSB) | 1 | Integer |
| 5 | INT16 | 1 | Integer |
| 6 | INT32 | 2 | Integer |
| 8 | INT64 | 4 | Float |
| 7 | FLOAT32 | 2 | Float |
| 9 | FLOAT64 | 4 | Float |
| 10 | STRING (PLAIN) | Length/2 | String |
| 14 | STRING (HEX) | Length/2 | String |
- MSB/LSB: UINT8/INT8 read a whole register and use the high-order (MSB) or the low-order byte (LSB). This way two 8 bit values can be read from one register with two entries on the same address.
- 64 bit: INT64 and UINT64 are mapped to a float variable because Symcon also supports 32 bit systems.
- STRING (PLAIN): The bytes are interpreted as text. Spaces and null bytes at the beginning and end are removed.
- STRING (HEX): The bytes are shown as a hex string (upper case), e.g. "0A1B". This is useful for bit fields, version numbers or MAC addresses.
- Factor: As soon as a factor other than 0 is set, the variable is always created as float - also for integer data types and also for the factor 1.
The type of a profile must match the variable type. A UINT16 register without factor results in an integer variable and can therefore not get the float profile "~Watt", for example. If an integer value should get a float profile, the factor 1 can be set.
Function codes and addresses
| Function code | Direction | Name | Allowed data types |
|---|---|---|---|
| 1 | Read | Read Coils | BOOL |
| 2 | Read | Read Discrete Inputs | BOOL |
| 3 | Read | Read Holding Registers | all except BOOL |
| 4 | Read | Read Input Registers | all except BOOL |
| 5 | Write | Write Single Coil | BOOL |
| 15 | Write | Write Multiple Coils | BOOL |
| 6 | Write | Write Single Register | 8/16 bit types only |
| 16 | Write | Write Multiple Registers | all except BOOL |
Invalid combinations are rejected with an error message when applying, e.g. "Non-Bit values must use function Read Holding Registers/Read Input Registers" or "Writing in only one register is not possible for multi register values like Int32/UInt32, Int64/UInt64, Float32/Float64, String".
Addressing: "ReadAddress" and "WriteAddress" are the addresses that are actually transmitted in the Modbus telegram, starting at 0. Many data sheets use the classic notation with a prefix instead (e.g. 40001 for the first holding register). In this case the prefix has to be subtracted, see Function codes. Other manufacturers already specify the addresses directly (e.g. Solis with 33000 for an input register). If in doubt, a test with a known value like the serial number or the grid frequency helps.
Byte order
The byte order is defined for the whole instance in the top-level key "ByteOrder". Single addresses can override it with their own "ByteOrder" field. The value -1 uses the setting of the instance. The examples show how the 32 bit value 0x11223344 is transmitted by the device:
| ByteOrder | Name | Transmitted bytes | Typical description in the data sheet |
|---|---|---|---|
| -1 | Inherited from device | ----------------- | address level only |
| 0 | Big-Endian (Standard) | 11 22 33 44 | "High word first", "ABCD", Modbus standard |
| 1 | Little-Endian | 44 33 22 11 | "DCBA" |
| 2 | Big-Endian (Byte Swap) | 22 11 44 33 | "BADC" |
| 3 | Little-Endian (Byte Swap) | 33 44 11 22 | "Low word first", "Word swap", "CDAB" |
For 16 bit values only 1 and 2 have an effect (bytes swapped within the register). The byte order has no meaning for BOOL values.
Factor
The raw value is multiplied by "Factor" before it is written into the variable. When writing, the value is divided by the factor first. The factor is always a multiplier: a division by 10 is specified as 0.1.
| Data sheet | Factor |
|---|---|
| Unit 0.1 V | 0.1 |
| Unit 0.01 Hz | 0.01 |
| Unit 10 W | 10 |
| Wh, wanted in kWh | 0.001 |
| no factor | 0 |
The factor must be 0 for BOOL and string addresses. If the device provides its own scale factor in a register (e.g. SunSpec "Scale Factor"), it can be applied using a virtual address.
Ident
The ident identifies the variable permanently. If "Ident" is empty, it is generated from data type, function code and read address: A[DataType][ReadFunctionCode]_[ReadAddress], e.g. "A_7_3100". For virtual addresses the name is used, with all characters except letters, digits and underscore replaced by "".
Without a fixed ident, the ident changes as soon as data type, function code or address (or the name for virtual addresses) are modified. The old variable is then deleted including its archive data and a new one is created. Templates should therefore always set a fixed, meaningful ident like "battery_soc". Each ident must only occur once - also not between addresses and virtual addresses.
The ident is also used in the scripts of the virtual addresses to access the values.
Virtual addresses
Virtual addresses calculate a variable with PHP from the values of the other addresses or distribute a written value to one or more addresses.
| Field | Type | Description |
|---|---|---|
| Active | Bool | Defines whether the variable is created and the scripts are executed. |
| Name | String | Name of the variable |
| Ident | String | Ident of the variable. If the field is empty, the ident is generated from the name. |
| Translation | Array | Translations of the name, like for addresses |
| VariableType | Number | 0 = Boolean, 1 = Integer, 2 = Float, 3 = String |
| Profile | String | Name of the variable profile or empty. The profile type must match the VariableType. |
| ReadAction | String | PHP code to calculate the value, empty for no calculation |
| WriteAction | String | PHP code for writing. If it is set, the variable gets a standard action. |
The scripts only contain the body of a function, i.e. without <?php. Line breaks are given as \n (or \r\n) in JSON.
ReadAction: Is executed after each poll. $VALUES contains all address values as an array with the ident as key. Virtual addresses are calculated in order and their result is added to $VALUES as well - a virtual address can therefore access the results of the previous ones. The return value is written into the variable. If null is returned, the variable stays unchanged. The return value must match the VariableType.
return ($VALUES['pv_voltage_1'] ?? 0) * ($VALUES['pv_current_1'] ?? 0);
WriteAction: Is executed when the variable is switched. $VALUE contains the new value, $VALUES the current values of all readable addresses. An array with the ident as key and the value to be written is returned. For each entry the action of the corresponding address is executed, i.e. including factor and data type. If null is returned, nothing is written.
// Only change bits 0-3 of a bit field, keep all other bits $current = $VALUES['control_register'] ?? 0; return ['control_register' => ($current & ~0x0F) | ($VALUE & 0x0F)];
The operator ?? 0 prevents errors as long as an address has not been read yet or is inactive. If a script outputs errors, they can be found in the debug of the instance under the ident of the virtual address.
Values are only written if they change or the variable is older than 60 seconds. This applies to read addresses as well as virtual addresses.
Profiles
"Profiles" is an object with the profile name as key. Profiles whose name starts with "~" are system profiles. They are not exported and do not have to be contained in the template. Custom profiles should get a unique prefix (e.g. "Vendor.Name") so that they do not collide with profiles of other templates.
| Field | Type | Description |
|---|---|---|
| Type | Number | 0 = Boolean, 1 = Integer, 2 = Float, 3 = String |
| Prefix | String | Prefix |
| Suffix | String | Suffix, including a leading space, e.g. " W" |
| MinValue | Number | Minimum value |
| MaxValue | Number | Maximum value |
| StepSize | Number | Step size. For switchable variables it defines the display as slider. |
| Digits | Number | Number of decimal places |
| Icon | String | Name of the icon or empty |
| Associations | Array | Associations, each with "Value", "Name", "Icon" and "Color" (-1 = no color) |
All fields must be specified. The JSON type of "Value" in the associations should match the profile type (true/false for Boolean, whole number for Integer).
"Profiles": {
"Vendor.OperatingMode": {
"Type": 1,
"Prefix": "",
"Suffix": "",
"MinValue": 0.0,
"MaxValue": 0.0,
"StepSize": 0.0,
"Digits": 0,
"Icon": "Information",
"Associations": [
{ "Value": 0, "Name": "Standby", "Icon": "", "Color": -1 },
{ "Value": 1, "Name": "Running", "Icon": "", "Color": 65280 }
]
}
}
Only missing profiles are created on import. An existing profile with the same name is not modified, even if it differs.
Polling
The key "Requests" contains the polling settings:
| Field | Type | Description |
|---|---|---|
| Type | Number | 0 = Single addresses, 1 = Data blocks |
| Interval | Number | Polling interval in milliseconds for the type "Single addresses". 0 disables polling. |
| DataBlocks | Array | Data blocks for the type "Data blocks" |
Single addresses: In each interval every active address with a read function code is polled individually. This is simple, but creates a lot of requests for many addresses. Especially with Modbus RTU or slow data loggers the time is often not sufficient.
Data blocks: Each data block reads a contiguous range with a single request and has its own interval. Afterwards all addresses that are completely within the block and use the same function code are updated. This way measured values can be read every 5 seconds and meter readings only every minute, for example.
| Field | Type | Description |
|---|---|---|
| Function | Number | Function code 1, 2, 3 or 4 |
| Address | Number | Start address |
| Quantity | Number | Number of registers or coils (at most 125 registers) |
| Poller | Number | Interval in milliseconds |
"Requests": {
"Type": 1,
"Interval": 5000,
"DataBlocks": [
{ "Function": 4, "Address": 33049, "Quantity": 10, "Poller": 5000 },
{ "Function": 4, "Address": 33161, "Quantity": 20, "Poller": 60000 },
{ "Function": 3, "Address": 43110, "Quantity": 1, "Poller": 10000 }
]
}
With the type "Data blocks", addresses that are not within any data block are not updated. A data block should only cover registers that the device actually supports - many devices answer a request with a gap in the register range with an error (ILLEGAL_DATA_ADDRESS) for the whole block.
Checklist
- All three required keys "Addresses", "VirtualAddresses" and "Profiles" are present, even if they are empty.
- Each address contains all fields with the correct JSON type (no strings or null instead of numbers).
- Each address and virtual address has a fixed, unique ident.
- Names are English and have a German translation in "Translation".
- Addresses are specified as they are transmitted in the telegram (without 30001/40001 prefix).
- Function code and data type match (BOOL only with 1, 2, 5, 15; multi register values not with 6).
- The profile type matches the variable type (a factor other than 0 always results in float).
- Custom profiles have a unique prefix and are completely contained in "Profiles".
- Rarely needed values are included with "Active": false instead of being omitted.
- Data blocks are used for many addresses and every active address is within a block.
- The template has been imported, applied and exported again once. The export must contain the same addresses.