Introduction#

What is TapHome Script?#

TapHome Script is a custom scripting language designed specifically for IoT and smart home automation. It allows you to:

  • Read and write data from Modbus devices
  • Communicate with web services via HTTP/REST APIs
  • Process MQTT messages
  • Parse JSON and XML data
  • Implement complex automation logic
  • Transform and calculate sensor values

Where is TapHome Script Used?#

  • Smart Rules - Automation triggers and conditions
  • Equations - Mathematical formulas for sensor values
  • Formulas - Data transformations
  • Modbus Communication - Reading/writing Modbus registers
  • Packet Parser - Custom protocol parsing for TCP/UDP/HTTP/MQTT

Key Features#

Case-insensitive - You can write IF or ifMulti-line scripts - Write complex logic across multiple lines ✅ Rich data types - Numbers, strings, dates, colors, collections ✅ Built-in functions - 70+ functions for common tasks ✅ Network protocols - HTTP, MQTT, FTP, Modbus support ✅ Data parsing - JSON, XML, text parsing built-in


Getting Started#

Your First Script#

Let’s start with a simple example:

# Read temperature from a Modbus sensor
temperature := MODBUSR(H, 100, BigEndianInt16)

# Check if it's too hot
IF temperature > 25
  RETURN(1)  # Turn on cooling
ELSE
  RETURN(0)  # Turn off cooling
END

What this does:

  1. Reads a 16-bit integer from Modbus holding register 100
  2. Compares the temperature to 25
  3. Returns 1 (on) or 0 (off) based on the condition

Script Execution Flow#

  1. Parse - TapHome reads your script and checks syntax
  2. Evaluate - The script runs line by line
  3. Return - The final result is returned to the system

Language Syntax#

Comments#

Comments help document your code. They start with # and continue to the end of the line.

# This is a comment
temperature := 22  # This is also a comment

# Comments must start on a new line
# Inline comments after code are NOT supported on the same line without #

Important: Comments must be on their own line or after # on any line.

Variables and Data Types#

Declaring Variables#

Automatic Type (VAR):

VAR temperature := 22.5
VAR deviceName := "Living Room"
VAR isActive := TRUE

Explicit Type:

int counter := 0
float voltage := 3.3
string message := "Hello"
bool enabled := FALSE

Supported Types:

  • Numbers: int, uint, float, double, int8, int16, int32, int64, uint8, uint16, uint32, uint64
  • Text: string, char
  • Logic: bool
  • Date/Time: datetime, timespan
  • Network: httprequest, httpresponse, receivedmessage
  • Binary: blob
  • Colors: rgbcolor, hsvcolor
  • Collections: collection<TYPE>

Variable Assignment#

# Simple assignment
x := 10

# Compound assignment
x += 5   # x = x + 5
x -= 3   # x = x - 3
x *= 2   # x = x * 2
x /= 4   # x = x / 4

Collections (Arrays)#

# Create collection
numbers := {1, 2, 3, 4, 5}
names := {"Alice", "Bob", "Charlie"}

# Access elements (0-based index)
first := GETAT(numbers, 0)  # Gets 1
last := GETAT(numbers, 4)   # Gets 5

# Modify elements
SETAT(numbers, 0, 10)  # Sets first element to 10

# Add elements
APPEND(numbers, 6)  # Adds 6 to the end

# Collection size
count := numbers.Length

Operators#

Arithmetic Operators#

a := 10 + 5   # Addition: 15
b := 10 - 5   # Subtraction: 5
c := 10 * 5   # Multiplication: 50
d := 10 / 5   # Division: 2
e := 10 MOD 3 # Modulo (remainder): 1

Comparison Operators#

x = 5      # Equal to
x != 5     # Not equal to
x <> 5     # Not equal to (alternative)
x > 5      # Greater than
x < 5      # Less than
x >= 5     # Greater than or equal to
x <= 5     # Less than or equal to

Logical Operators#

# AND - both conditions must be true
IF temperature > 20 AND humidity < 80
  # Do something
END

# OR - at least one condition must be true
IF error = 1 OR warning = 1
  # Handle issue
END

# NOT - negation
IF !isActive
  # Device is not active
END

# XOR - exactly one must be true
IF sensor1Active XOR sensor2Active
  # Only one sensor is active
END

Bitwise Operators#

# Bitwise AND
result := 0xFF & 0x0F  # Result: 0x0F

# Bitwise OR
result := 0x10 | 0x01  # Result: 0x11

# Bitwise XOR
result := 0xFF ^ 0x0F  # Result: 0xF0

# Bit shift left
result := 1 << 4       # Result: 16 (binary: 00010000)

# Bit shift right
result := 16 >> 2      # Result: 4  (binary: 00000100)

Control Structures#

IF/ELSEIF/ELSE/END#

IF temperature > 30
  RETURN("Too hot")
ELSEIF temperature > 20
  RETURN("Comfortable")
ELSEIF temperature > 10
  RETURN("Cool")
ELSE
  RETURN("Cold")
END

Nested IF:

IF temperature > 25
  IF humidity > 70
    RETURN("Hot and humid")
  ELSE
    RETURN("Hot and dry")
  END
END

WHILE Loop#

Repeats while condition is true (checks condition BEFORE execution):

counter := 0

WHILE counter < 5
  counter := counter + 1
LOOP

# counter is now 5

DO-WHILE Loop#

Repeats while condition is true (checks condition AFTER execution):

counter := 0

DO
  counter := counter + 1
LOOP WHILE counter < 5

# counter is now 5
# Executes at least once even if condition is initially false

BREAK - Exit Loop#

counter := 0

WHILE TRUE
  counter := counter + 1

  IF counter >= 10
    BREAK  # Exit the loop
  END
LOOP

CONTINUE - Skip to Next Iteration#

sum := 0
counter := 0

WHILE counter < 10
  counter := counter + 1

  # Skip odd numbers
  IF counter MOD 2 = 1
    CONTINUE
  END

  sum := sum + counter
LOOP

# sum contains only even numbers: 2+4+6+8+10 = 30

RETURN - Exit Script#

# Exit immediately and return a value
IF error = 1
  RETURN(NaN)  # Return "Not a Number" to indicate error
END

# Normal processing
result := temperature * 2
RETURN(result)

Data Types Reference#

DateTime#

Represents date and time with timezone support.

Creating DateTime:

# Current time (local)
now := NOW()

# Current time (UTC)
nowUtc := NOW(DateTimeKind.Utc)

# Specific date/time
dt := DATETIME(2024, 1, 15, 10, 30, 0, 0, DateTimeKind.Utc)
#             year  mon day  hr  min sec ms  kind

# Parse from string
dt := DATETIME("2024-01-15 10:30:00")

DateTime Properties:

dt := NOW()

year := dt.Year          # 2024
month := dt.Month        # 1-12
day := dt.Day            # 1-31
hour := dt.Hour          # 0-23
minute := dt.Minute      # 0-59
second := dt.Second      # 0-59
dayOfWeek := dt.DayOfWeek  # 0=Sunday, 6=Saturday

DateTime Operations:

# Add time
future := DATETIMEADD(NOW(), 0, 1, 5, 2, 30, 0, 0)
#                            yr mon day hr min sec ms
# Adds: 1 month, 5 days, 2 hours, 30 minutes

# Compare dates
IF dt1 > dt2
  # dt1 is later than dt2
END

TimeSpan#

Represents duration or time interval.

# Create from milliseconds
duration := TIMESPAN(5000)  # 5 seconds

# Create from components
duration := TIMESPAN(1, 2, 30, 45, 500)
#                   days hrs min sec  ms

# Properties
days := duration.Days
hours := duration.Hours
totalSeconds := duration.TotalSeconds

HttpRequest & HttpResponse#

Creating HTTP Request:

# Simple GET
req := HTTPREQUEST("/api/data")

# POST with body
req := HTTPREQUEST("/api/update", "POST", "{\"value\": 25}")

# Access properties
path := req.Path
method := req.Method
body := req.Body

Working with Response:

response := SENDHTTPREQUEST(req)

IF response.IsSuccess
  content := response.Content
  statusCode := response.StatusCode
ELSE
  error := response.ReasonPhrase
END

RGBColor & HSVColor#

RGB Color (0-255 per channel):

# Convert from HSV
rgb := HSVTORGB(180, 1.0, 1.0)  # Cyan

red := rgb.Red      # 0-255
green := rgb.Green  # 0-255
blue := rgb.Blue    # 0-255

HSV Color (Hue: 0-360, Saturation: 0-1, Value: 0-1):

# Convert from RGB
hsv := RGBTOHSV(255, 0, 0)  # Red

hue := hsv.Hue              # 0-360 degrees
saturation := hsv.Saturation # 0.0-1.0
value := hsv.Value          # 0.0-1.0 (brightness)

Collections#

Creating Collections:

# From literals
numbers := {1, 2, 3, 4, 5}

# Byte collection from hex
bytes := BYTECOLLECTION("01FF23A4")

# Empty byte collection
emptyBytes := BYTECOLLECTION(10)  # 10 bytes

Collection Operations:

# Get element
value := GETAT(numbers, 0)

# Set element
SETAT(numbers, 0, 100)

# Add element
APPEND(numbers, 6)

# Insert element
INSERT(numbers, 2, 999)  # Insert 999 at index 2

# Remove elements
REMOVEAT(numbers, 0, 2)  # Remove 2 elements starting at index 0

# Find element
index := INDEXOF(numbers, 3)  # Returns index or -1

# Copy sub-collection
sub := COPY(numbers, 1, 3)  # Copy 3 elements starting at index 1

# Sort
sorted := ORDER(5, 2, 8, 1, 9)  # Returns {1, 2, 5, 8, 9}

Built-in Functions#

Mathematical Functions#

MIN / MAX - Minimum and Maximum#

# Find minimum
min := MIN(5, 10, 3, 8)  # Returns 3

# From collection
temperatures := {22.5, 23.1, 21.8, 24.0}
lowest := MIN(temperatures)  # Returns 21.8

# Find maximum
max := MAX(5, 10, 3, 8)  # Returns 10
highest := MAX(temperatures)  # Returns 24.0

# Strict versions (return NaN if any value is NaN)
result := MINSTRICT(5, NaN, 10)  # Returns NaN
result := MAXSTRICT(5, NaN, 10)  # Returns NaN

Use Case - Temperature Monitoring:

# Read temperatures from 3 sensors
t1 := MODBUSR(H, 100, BigEndianInt16)
t2 := MODBUSR(H, 102, BigEndianInt16)
t3 := MODBUSR(H, 104, BigEndianInt16)

# Get average and extremes
average := AVG(t1, t2, t3)
highest := MAX(t1, t2, t3)
lowest := MIN(t1, t2, t3)

# Return average
RETURN(average)

AVG - Average#

# Calculate average
avg := AVG(10, 20, 30)  # Returns 20

# From collection
readings := {22.5, 23.1, 21.8, 24.0}
average := AVG(readings)  # Returns 22.85

# Strict version
result := AVGSTRICT(10, NaN, 30)  # Returns NaN

ROUND, CEIL, FLOOR - Rounding#

# Round to nearest integer
rounded := ROUND(3.7)  # Returns 4
rounded := ROUND(3.2)  # Returns 3

# Round up (ceiling)
up := CEIL(3.2)  # Returns 4

# Round down (floor)
down := FLOOR(3.7)  # Returns 3

ABS - Absolute Value#

# Get absolute value
abs := ABS(-15)  # Returns 15
abs := ABS(10)   # Returns 10

POWER, SQRT - Power and Square Root#

# Power
result := POWER(2, 8)   # 2^8 = 256
result := POWER(10, 2)  # 10^2 = 100

# Square root
result := SQRT(16)  # Returns 4
result := SQRT(2)   # Returns 1.414...

MOD - Modulo (Remainder)#

# Get remainder
remainder := MOD(10, 3)  # Returns 1 (10 / 3 = 3 remainder 1)
remainder := MOD(17, 5)  # Returns 2

# Check if number is even
IF MOD(number, 2) = 0
  # Number is even
END

SIGN - Sign of Number#

sign := SIGN(-10)  # Returns -1
sign := SIGN(0)    # Returns 0
sign := SIGN(10)   # Returns 1

LN, LOG - Logarithms#

# Natural logarithm (base e)
ln := LN(2.71828)  # Returns ~1

# Logarithm base 10
log := LOG(100)  # Returns 2 (10^2 = 100)

# Logarithm custom base
log := LOG(8, 2)  # Returns 3 (2^3 = 8)

RAND, RANDINT - Random Numbers#

# Random decimal 0.0 to 1.0
random := RAND()  # Returns e.g., 0.7234

# Random integer (min inclusive, max exclusive)
dice := RANDINT(1, 7)  # Returns 1-6
number := RANDINT(1, 101)  # Returns 1-100

Logical Functions#

IF - Ternary Conditional#

# IF(condition, valueIfTrue, valueIfFalse)
result := IF(temperature > 25, 1, 0)

# Without else (returns null if false)
result := IF(temperature > 25, 1)

# Nested IF
result := IF(temp > 30, "hot", IF(temp > 20, "warm", "cold"))

SWITCH - Multi-way Branch#

# SWITCH(variable, value1, result1, value2, result2, ..., default)
state := 2

message := SWITCH(state,
  1, "On",
  2, "Off",
  3, "Auto",
  "Unknown")

# Returns "Off"

Use Case - Modbus State Decoding:

# Read state register
stateRegister := MODBUSR(H, 200, UInt16)

# Decode state
stateText := SWITCH(stateRegister,
  0x0001, "Heating",
  0x0002, "Cooling",
  0x0004, "Fan Only",
  0x0008, "Off",
  "Error")

RETURN(stateText)

HYSTERESIS - Prevent Oscillation#

# HYSTERESIS(value, upperThreshold, lowerThreshold,
#            upperState, lowerState, previousState)

# Example: Thermostat control
# Turn ON when temp drops below 20°C
# Turn OFF when temp rises above 22°C
# Keep previous state between 20-22°C

heater := HYSTERESIS(temperature, 22, 20, 0, 1, heater)
#                               upper low  off on prev

RETURN(heater)

Explanation:

  • If temp > 22°C → returns 0 (off)
  • If temp < 20°C → returns 1 (on)
  • If 20°C ≤ temp ≤ 22°C → returns previous state (prevents rapid switching)

EQUALS - Compare Numbers with Tolerance#

# EQUALS(number1, number2, epsilon)
# Default epsilon: 0.000001

result := EQUALS(3.14159, 3.14160, 0.001)  # Returns TRUE

# Useful for floating point comparisons
IF EQUALS(sensorValue, 25.0, 0.1)
  # Sensor reads approximately 25.0
END

ISNAN, ISNULL - Check for Special Values#

# Check if value is NaN (Not a Number)
IF ISNAN(result)
  # Handle error - result is not a valid number
END

# Check if value is null
IF ISNULL(optionalValue)
  # Value was not provided
END

String and Text Functions#

LENGTH - Get String/Collection Length#

# String length
len := LENGTH("Hello")  # Returns 5

# Collection length
numbers := {1, 2, 3, 4}
count := LENGTH(numbers)  # Returns 4

# Also available as property
len := "Hello".Length  # Returns 5
count := numbers.Length  # Returns 4

TOSTRING - Convert to String#

# Convert number
str := TOSTRING(123)  # Returns "123"

# With formatting
str := TOSTRING(3.14159, "F2")  # Returns "3.14" (2 decimal places)

# Convert boolean
str := TOSTRING(TRUE)  # Returns "True"

TODOUBLE - Parse Number from String#

# Convert string to number
num := TODOUBLE("123.45")  # Returns 123.45

# Returns NaN if conversion fails
num := TODOUBLE("abc")  # Returns NaN

# Check for errors
num := TODOUBLE(textValue)
IF ISNAN(num)
  # Conversion failed
END

SPLIT - Split String#

# Split by comma
parts := SPLIT("apple,banana,orange", ",")
# Returns collection: {"apple", "banana", "orange"}

# Get individual parts
first := GETAT(parts, 0)  # "apple"

# Split CSV line
data := SPLIT("25.5,60,1013", ",")
temperature := TODOUBLE(GETAT(data, 0))  # 25.5
humidity := TODOUBLE(GETAT(data, 1))     # 60

INDEXOF - Find Substring#

# Find position of substring
pos := INDEXOF("Hello World", "World")  # Returns 6

# Returns -1 if not found
pos := INDEXOF("Hello", "xyz")  # Returns -1

# Check if string contains substring
IF INDEXOF(message, "ERROR") != -1
  # Message contains "ERROR"
END

REPLACE - Replace Substring#

# Replace all occurrences
result := REPLACE("Hello World", "World", "Universe")
# Returns "Hello Universe"

# Replace multiple times
result := REPLACE("foo foo foo", "foo", "bar")
# Returns "bar bar bar"

COPY - Extract Substring#

# COPY(string, start, length)
sub := COPY("Hello World", 0, 5)  # Returns "Hello"
sub := COPY("Hello World", 6, 5)  # Returns "World"

# Copy to end
sub := COPY("Hello World", 6)  # Returns "World"

APPEND, INSERT, REMOVEAT - String Manipulation#

# Append
result := APPEND("Hello", " World")  # Returns "Hello World"

# Insert
result := INSERT("Hello World", 5, " Beautiful")
# Returns "Hello Beautiful World"

# Remove
result := REMOVEAT("Hello World", 5, 6)  # Remove " World"
# Returns "Hello"

ENCODE, DECODE - Encoding#

# XML encoding
encoded := ENCODE("<tag>", "xml")  # Returns "&lt;tag&gt;"
decoded := DECODE("&lt;tag&gt;", "xml")  # Returns "<tag>"

# Base64 encoding
encoded := ENCODE("Hello", "base64")  # Returns "SGVsbG8="
decoded := DECODE("SGVsbG8=", "base64")  # Returns "Hello"

TOBYTES - Convert String to Bytes#

# Convert to bytes (default: ISO-8859-1)
bytes := TOBYTES("Hello")

# With encoding
bytes := TOBYTES("Hello", "utf-8")

# Use for binary protocols
data := TOBYTES("AT+CMD\r\n")
SENDDATA(data)

Collection Functions#

ORDER - Sort Collection#

# Sort ascending
sorted := ORDER(5, 2, 8, 1, 9)
# Returns {1, 2, 5, 8, 9}

# Sort descending
sorted := ORDERDESC(5, 2, 8, 1, 9)
# Returns {9, 8, 5, 2, 1}

# Strict versions (fail if NaN present)
sorted := ORDERSTRICT(5, NaN, 2)  # Returns NaN
sorted := ORDERDESCSTRICT(5, NaN, 2)  # Returns NaN

ORDERINDEX - Get Sort Indices#

# Get indices that would sort the data
values := {50, 20, 80, 10}
indices := ORDERINDEX(values)
# Returns {3, 1, 0, 2}
# Meaning: element at index 3 is smallest (10),
#          element at index 1 is next (20), etc.

# Access sorted values
first := GETAT(values, GETAT(indices, 0))  # Gets 10

Date and Time Functions#

NOW - Current Date/Time#

# Current local time
now := NOW()

# Current UTC time
nowUtc := NOW(DateTimeKind.Utc)

# Get components
hour := now.Hour
minute := now.Minute
dayOfWeek := now.DayOfWeek

DATETIME - Create Date/Time#

# From components (year, month, day, hour, min, sec, ms, kind)
dt := DATETIME(2024, 1, 15, 10, 30, 0, 0, DateTimeKind.Utc)

# Parse from string
dt := DATETIME("2024-01-15 10:30:00")

# From ticks
dt := DATETIME(638400000000000000)

DATETIMEADD - Add Time#

# DATETIMEADD(datetime, years, months, days, hours, mins, secs, ms)

# Add 1 day
tomorrow := DATETIMEADD(NOW(), 0, 0, 1, 0, 0, 0, 0)

# Add 2 hours and 30 minutes
later := DATETIMEADD(NOW(), 0, 0, 0, 2, 30, 0, 0)

# Subtract (use negative numbers)
yesterday := DATETIMEADD(NOW(), 0, 0, -1, 0, 0, 0, 0)

TIMESPAN - Duration#

# From milliseconds
duration := TIMESPAN(5000)  # 5 seconds

# From components (days, hours, minutes, seconds, milliseconds)
duration := TIMESPAN(0, 2, 30, 0, 0)  # 2 hours 30 minutes

# Access components
hours := duration.Hours
minutes := duration.Minutes
totalSeconds := duration.TotalSeconds

Bit and Byte Operations#

GETBIT, SETBIT - Bit Manipulation#

# Get bit at position (0-31)
bit := GETBIT(5, 0)  # Returns 1 (binary: 0101, bit 0 is 1)
bit := GETBIT(5, 1)  # Returns 0 (binary: 0101, bit 1 is 0)

# Set bit at position
result := SETBIT(0, 2, 1)  # Returns 4 (binary: 0100)
result := SETBIT(7, 1, 0)  # Returns 5 (binary: 0101)

Use Case - Read Multiple Flags:

# Read status register
status := MODBUSR(H, 300, UInt16)

# Extract individual flags
alarm := GETBIT(status, 0)     # Bit 0: Alarm
warning := GETBIT(status, 1)   # Bit 1: Warning
fault := GETBIT(status, 2)     # Bit 2: Fault
running := GETBIT(status, 3)   # Bit 3: Running

IF alarm = 1
  # Handle alarm
END

GETBITS, SETBITS - Multiple Bits#

# Get multiple bits
# GETBITS(number, startBit, numberOfBits)
value := GETBITS(0xFF, 4, 4)  # Returns 0x0F (bits 4-7)

# Set multiple bits
# SETBITS(number, startBit, numberOfBits, value)
result := SETBITS(0, 4, 4, 0x0F)  # Returns 0xF0

GETBYTE, SETBYTE - Byte Manipulation#

# Get byte at index (0-3 for 32-bit)
byte := GETBYTE(0x12345678, 0)  # Returns 0x78 (low byte)
byte := GETBYTE(0x12345678, 3)  # Returns 0x12 (high byte)

# Set byte at index
result := SETBYTE(0, 1, 0xFF)  # Returns 0xFF00

FROMBCD, TOBCD - BCD Conversion#

# Convert from BCD (Binary Coded Decimal)
decimal := FROMBCD(0x1234)  # Returns 1234

# Convert to BCD
bcd := TOBCD(1234)  # Returns 0x1234

Use Case - BCD Encoded Modbus:

# Some devices store numbers in BCD format
bcdValue := MODBUSR(H, 500, UInt16)
actualValue := FROMBCD(bcdValue)

RETURN(actualValue)

Color Functions#

HSVTORGB - Convert HSV to RGB#

# HSVTORGB(hue, saturation, value)
# Hue: 0-360 degrees
# Saturation: 0.0-1.0
# Value (brightness): 0.0-1.0

rgb := HSVTORGB(0, 1.0, 1.0)      # Red
rgb := HSVTORGB(120, 1.0, 1.0)    # Green
rgb := HSVTORGB(240, 1.0, 1.0)    # Blue

red := rgb.Red      # 0-255
green := rgb.Green  # 0-255
blue := rgb.Blue    # 0-255

Use Case - Color Temperature to RGB:

# User sets color wheel position (0-360)
hue := colorWheelPosition

# Convert to RGB for LED strip
rgb := HSVTORGB(hue, 1.0, 1.0)

# Write to Modbus RGB controller
MODBUSW(H, 1000, UInt8, rgb.Red, rgb.Green, rgb.Blue)

RGBTOHSV - Convert RGB to HSV#

# RGBTOHSV(red, green, blue)
# Red, Green, Blue: 0-255

hsv := RGBTOHSV(255, 0, 0)  # Red

hue := hsv.Hue              # 0-360
saturation := hsv.Saturation # 0.0-1.0
value := hsv.Value          # 0.0-1.0

Utility Functions#

DEWPOINT - Dew Point Calculation#

# Calculate dew point from temperature and humidity
# DEWPOINT(temperature_celsius, humidity_percent)

dewpoint := DEWPOINT(25, 60)
# Returns dew point temperature in Celsius

LINEAR - Linear Interpolation#

# LINEAR(input, x1, y1, x2, y2, [type])
# Maps input from range [x1, x2] to range [y1, y2]

# Convert 0-100% to 4-20mA
current := LINEAR(percent, 0, 4, 100, 20)

# Examples:
# percent=0   → 4mA
# percent=50  → 12mA
# percent=100 → 20mA

Types:

  • LinearType.DEFAULT - Clamp to bounds
  • LinearType.INFINITE - Extrapolate beyond bounds

ADDERROR, ADDWARNING, ADDINFO - Logging#

# Add error message
ADDERROR("Sensor offline")
ADDERROR(100, "Critical temperature")

# Add warning
ADDWARNING("Low battery")

# Add info
ADDINFO("System started")

Modbus Communication#

Overview#

Modbus is a widely used industrial protocol. TapHome Script provides three functions:

  • MODBUSR - Read from Modbus registers
  • MODBUSW - Write to Modbus registers
  • MODBUSWNE - Write only if value changed (optimization)

Register Types#

CodeNameDescriptionRead/Write
SCSingle CoilSingle on/off bitWrite only
CCoilOn/off bits (discrete outputs)Read/Write
SHSingle HoldingSingle 16-bit registerWrite only
HHolding Register16-bit read/write registersRead/Write
DDiscrete InputOn/off bits (read-only)Read only
AAnalog Input16-bit registers (read-only)Read only

Data Types#

TypeSizeRangeDescription
Int1616-bit-32,768 to 32,767Signed integer
UInt1616-bit0 to 65,535Unsigned integer
Int3232-bit-2,147,483,648 to 2,147,483,647Signed integer
UInt3232-bit0 to 4,294,967,295Unsigned integer
Float32-bitIEEE 754Floating point
Bool16-bit0 or 1Boolean
StringVariableTextCharacter string

Endianness#

Big Endian (most significant byte first) - Default for most devices:

  • BigEndianInt16, BigEndianUint16
  • BigEndianInt32, BigEndianUint32
  • BigEndianFloat

Little Endian (least significant byte first):

  • LittleEndianInt16, LittleEndianUint16
  • LittleEndianInt32, LittleEndianUint32
  • LittleEndianFloat

Byte Swap (swap bytes within each 16-bit register):

  • BigEndianInt32ByteSwap, LittleEndianInt32ByteSwap
  • BigEndianUint32ByteSwap, LittleEndianUint32ByteSwap
  • BigEndianFloatByteSwap, LittleEndianFloatByteSwap

MODBUSR - Read from Modbus#

Syntax:

MODBUSR(registerType, registerNumber, dataType)
MODBUSR(registerType, registerNumber, dataType, numberOfRegisters)  # For strings

Examples:

# Read 16-bit signed integer
temperature := MODBUSR(H, 100, BigEndianInt16)

# Read 16-bit unsigned integer
pressure := MODBUSR(H, 200, UInt16)

# Read 32-bit integer (uses 2 registers)
energy := MODBUSR(H, 300, BigEndianInt32)

# Read float (uses 2 registers)
voltage := MODBUSR(H, 400, LittleEndianFloat)

# Read boolean from coil
pump := MODBUSR(C, 50, Bool)

# Read string (10 registers = 20 characters max)
deviceName := MODBUSR(H, 1000, String, 10)

Real-World Example - Energy Meter:

# Read power (Float, registers 100-101)
power := MODBUSR(H, 100, BigEndianFloat)

# Read energy (UInt32, registers 200-201)
energy := MODBUSR(H, 200, BigEndianUint32)

# Read voltage (Int16, register 300)
voltage := MODBUSR(H, 300, Int16)

# Calculate total cost
cost := energy * 0.12  # $0.12 per kWh

RETURN(cost)

MODBUSW - Write to Modbus#

Syntax:

MODBUSW(registerType, registerNumber, dataType, value)
MODBUSW(registerType, registerNumber, dataType, value1, value2, ...)  # Multiple values

Examples:

# Write 16-bit integer
MODBUSW(H, 100, BigEndianInt16, 1234)

# Write 32-bit integer
MODBUSW(H, 200, BigEndianInt32, 1000000)

# Write float
MODBUSW(H, 300, BigEndianFloat, 25.5)

# Write boolean to coil
MODBUSW(C, 50, Bool, TRUE)

# Write multiple values (auto-increment registers)
MODBUSW(H, 1000, UInt16, 100, 200, 300, 400)
# Writes: register 1000=100, 1001=200, 1002=300, 1003=400

Real-World Example - Thermostat Control:

# Read current temperature
currentTemp := MODBUSR(H, 100, BigEndianInt16)

# Read setpoint
setpoint := MODBUSR(H, 200, BigEndianInt16)

# Calculate heating state (0=off, 1=on)
heating := HYSTERESIS(currentTemp, setpoint + 1, setpoint - 1, 0, 1, heating)

# Write heating command
MODBUSW(C, 50, Bool, heating)

RETURN(heating)

MODBUSWNE - Write if Not Equal#

Writes value only if it differs from the current register value. This reduces Modbus traffic.

Syntax:

MODBUSWNE(registerType, registerNumber, dataType, value)

Example:

# Only write if value changed
targetTemp := 22
MODBUSWNE(H, 200, Int16, targetTemp)

# This is more efficient than:
# MODBUSW(H, 200, Int16, targetTemp)  # Writes every time

Common Modbus Patterns#

Pattern 1: Read-Modify-Write#

# Read current value
current := MODBUSR(H, 100, UInt16)

# Modify
current := current + 10

# Write back
MODBUSW(H, 100, UInt16, current)

Pattern 2: Multi-Register Status#

# Read status register
status := MODBUSR(H, 300, UInt16)

# Extract individual flags using bit operations
alarm := GETBIT(status, 0)
warning := GETBIT(status, 1)
fault := GETBIT(status, 2)
running := GETBIT(status, 3)

# Return combined state
IF alarm = 1
  RETURN(-1)  # Alarm
ELSEIF warning = 1
  RETURN(0)   # Warning
ELSEIF running = 1
  RETURN(1)   # Running
ELSE
  RETURN(2)   # Stopped
END

Pattern 3: Batch Read#

# Read multiple sequential registers efficiently
temp1 := MODBUSR(H, 100, Int16)
temp2 := MODBUSR(H, 101, Int16)
temp3 := MODBUSR(H, 102, Int16)

# Calculate average
average := AVG(temp1, temp2, temp3)

RETURN(average)

Network Communication#

HTTP/REST API#

SENDHTTPREQUEST - Send HTTP Request#

Syntax:

SENDHTTPREQUEST(path)
SENDHTTPREQUEST(path, method)
SENDHTTPREQUEST(path, method, body)
SENDHTTPREQUEST(path, method, body, header1, header2, ...)
SENDHTTPREQUEST(httpRequestObject)

Examples:

# Simple GET request
response := SENDHTTPREQUEST("/api/data")

# POST with JSON body
response := SENDHTTPREQUEST("/api/update", "POST", "{\"value\": 25}")

# With headers
response := SENDHTTPREQUEST("/api/data", "GET", "",
  "Authorization: Bearer token123",
  "Content-Type: application/json")

# Using HttpRequest object
req := HTTPREQUEST("/api/data", "POST", "{\"temp\": 22}")
response := SENDHTTPREQUEST(req)

Working with Response:

response := SENDHTTPREQUEST("/api/temperature")

IF response.IsSuccess
  # Parse JSON response
  temperature := PARSEJSON(response.Content, "$.temperature")
  RETURN(temperature)
ELSE
  # Handle error
  ADDERROR("HTTP Error: " + TOSTRING(response.StatusCode))
  RETURN(NaN)
END

Real-World Example - Weather API:

# Get weather from API
response := SENDHTTPREQUEST("/api/weather?city=London", "GET")

IF response.IsSuccess
  # Parse JSON
  temp := PARSEJSON(response.Content, "$.main.temp")
  humidity := PARSEJSON(response.Content, "$.main.humidity")

  # Convert Kelvin to Celsius
  tempC := temp - 273.15

  RETURN(tempC)
ELSE
  RETURN(NaN)
END

HTTPREQUEST - Create Request Object#

# Create reusable request
req := HTTPREQUEST("/api/data", "POST", "{\"value\": 25}")

# Modify request
req.Method := "PUT"
req.Body := "{\"value\": 30}"

# Send request
response := SENDHTTPREQUEST(req)

MQTT#

MQTTPUBLISH - Publish MQTT Message#

Syntax:

MQTTPUBLISH(topic, data)

Examples:

# Publish string
MQTTPUBLISH("home/sensor/temperature", "22.5")

# Publish JSON
json := "{\"temp\": " + TOSTRING(temperature) + "}"
MQTTPUBLISH("home/sensor/data", json)

# Publish bytes
bytes := TOBYTES("binary data")
MQTTPUBLISH("device/raw/data", bytes)

Real-World Example - MQTT Bridge:

# Read from Modbus
temperature := MODBUSR(H, 100, BigEndianInt16)
humidity := MODBUSR(H, 102, BigEndianInt16)

# Create JSON payload
json := "{\"temperature\": " + TOSTRING(temperature) +
        ", \"humidity\": " + TOSTRING(humidity) + "}"

# Publish to MQTT
MQTTPUBLISH("sensors/room1/data", json)

RETURN(temperature)

FTP#

FTPDOWNLOAD - Download File#

# Download file as bytes
data := FTPDOWNLOAD("/config/settings.txt")

# Convert to string and parse
text := TOSTRING(data)
value := PARSETEXT(text, "Temperature=", "\n")

RETURN(value)

FTPUPLOAD - Upload File#

# Upload text file
data := "Temperature: 22.5\nHumidity: 60"
FTPUPLOAD("/logs/sensor.log", data, "write")

# Append to log file
timestamp := TOSTRING(NOW())
logEntry := timestamp + ": Temperature = 22.5\n"
FTPUPLOAD("/logs/sensor.log", logEntry, "append")

Real-World Example - Data Logger:

# Read sensors
temp := MODBUSR(H, 100, BigEndianInt16)
humidity := MODBUSR(H, 102, BigEndianInt16)

# Create CSV line
timestamp := TOSTRING(NOW())
csvLine := timestamp + "," + TOSTRING(temp) + "," + TOSTRING(humidity) + "\n"

# Append to FTP log file
FTPUPLOAD("/logs/data.csv", csvLine, "append")

RETURN(temp)

Parsing and Data Extraction#

PARSETEXT - Extract Text#

Syntax:

PARSETEXT(source, leftPattern)
PARSETEXT(source, leftPattern, rightPattern)

Examples:

# Extract number after pattern
text := "Temperature: 22.5°C"
temp := PARSETEXT(text, "Temperature: ", "°")
# Returns: 22.5 (as number)

# Extract without right pattern (reads until non-alphanumeric)
text := "Value=123 Units"
value := PARSETEXT(text, "Value=")
# Returns: 123

# Legacy format with multiple patterns
html := "<div>Temperature<span>22.5</span></div>"
temp := PARSETEXT(html, "<div>...Temperature...<span>", "</span>")
# Returns: 22.5

Real-World Example - Parse HTTP Response:

response := SENDHTTPREQUEST("/api/status")

# Extract value from plain text response
# Response: "Status: OK, Temperature: 22.5, Humidity: 60"
temp := PARSETEXT(response.Content, "Temperature: ", ",")
humidity := PARSETEXT(response.Content, "Humidity: ")

RETURN(temp)

PARSEJSON - Parse JSON#

Syntax:

PARSEJSON(jsonData, jsonPath)
PARSEJSON(jsonData, jsonPath, ignoreError)

JSONPath Examples:

  • $.temperature - Get top-level property
  • $.sensor.value - Get nested property
  • $.sensors[0].temp - Get array element
  • $.sensors[*].temp - Get all temps from array

Examples:

# Simple property
json := "{\"temperature\": 22.5}"
temp := PARSEJSON(json, "$.temperature")
# Returns: 22.5

# Nested object
json := "{\"sensor\": {\"name\": \"Room1\", \"value\": 22}}"
value := PARSEJSON(json, "$.sensor.value")
# Returns: 22

# Array element
json := "{\"sensors\": [{\"id\": 1, \"temp\": 20}, {\"id\": 2, \"temp\": 22}]}"
temp := PARSEJSON(json, "$.sensors[1].temp")
# Returns: 22

# With error handling
temp := PARSEJSON(json, "$.missing", true)
IF ISNULL(temp)
  # Property not found
  temp := 0
END

Real-World Example - Weather API:

# Get weather data
response := SENDHTTPREQUEST("/api/weather?city=London")

IF response.IsSuccess
  # Parse nested JSON
  # Response: {"main": {"temp": 295.15, "humidity": 60}, "weather": [{"description": "clear sky"}]}

  temp := PARSEJSON(response.Content, "$.main.temp")
  humidity := PARSEJSON(response.Content, "$.main.humidity")
  description := PARSEJSON(response.Content, "$.weather[0].description")

  # Convert Kelvin to Celsius
  tempC := temp - 273.15

  RETURN(tempC)
END

PARSEXML - Parse XML#

Syntax:

PARSEXML(xmlData, xpathExpression)

XPath Examples:

  • //temperature - Find temperature element anywhere
  • /root/sensor/value - Specific path from root
  • //sensor[@id='1']/value - Element with attribute
  • //sensor/@value - Get attribute value

Examples:

# Simple element
xml := "<root><temperature>22.5</temperature></root>"
temp := PARSEXML(xml, "//temperature")
# Returns: 22.5

# With attribute
xml := "<sensors><sensor id='1' value='20'/><sensor id='2' value='22'/></sensors>"
value := PARSEXML(xml, "//sensor[@id='2']/@value")
# Returns: 22

# Nested path
xml := "<root><device><sensor><value>100</value></sensor></device></root>"
value := PARSEXML(xml, "/root/device/sensor/value")
# Returns: 100

Real-World Example - SOAP API:

# Call SOAP service
soapBody := "<soap:Envelope><soap:Body><GetTemperature/></soap:Body></soap:Envelope>"
response := SENDHTTPREQUEST("/api/soap", "POST", soapBody,
  "Content-Type: text/xml")

IF response.IsSuccess
  # Parse SOAP response
  # Response: <soap:Envelope><soap:Body><GetTemperatureResponse><Temperature>22.5</Temperature></GetTemperatureResponse></soap:Body></soap:Envelope>

  temp := PARSEXML(response.Content, "//Temperature")

  RETURN(temp)
END

Common Integration Examples#

Example 1: Temperature Monitoring with Alerts#

# Read temperature from Modbus sensor
temperature := MODBUSR(H, 100, BigEndianInt16)

# Define thresholds
warningTemp := 30
criticalTemp := 35

# Check thresholds
IF temperature >= criticalTemp
  ADDERROR(101, "Critical temperature: " + TOSTRING(temperature) + "°C")
  RETURN(2)  # Critical state
ELSEIF temperature >= warningTemp
  ADDWARNING(100, "High temperature: " + TOSTRING(temperature) + "°C")
  RETURN(1)  # Warning state
ELSE
  RETURN(0)  # Normal state
END

Example 2: Multi-Sensor Average with Error Handling#

# Read three temperature sensors
t1 := MODBUSR(H, 100, BigEndianInt16)
t2 := MODBUSR(H, 102, BigEndianInt16)
t3 := MODBUSR(H, 104, BigEndianInt16)

# Check for errors (some Modbus devices return -32768 for errors)
IF t1 = -32768 OR ISNAN(t1)
  ADDERROR("Sensor 1 offline")
  t1 := NaN
END

IF t2 = -32768 OR ISNAN(t2)
  ADDERROR("Sensor 2 offline")
  t2 := NaN
END

IF t3 = -32768 OR ISNAN(t3)
  ADDERROR("Sensor 3 offline")
  t3 := NaN
END

# Calculate average (AVG ignores NaN values)
average := AVG(t1, t2, t3)

IF ISNAN(average)
  ADDERROR("All sensors offline")
  RETURN(NaN)
END

RETURN(average)

Example 3: HVAC Control with Hysteresis#

# Read current temperature
temperature := MODBUSR(H, 100, BigEndianInt16)

# Read setpoint (target temperature)
setpoint := MODBUSR(H, 200, BigEndianInt16)

# Calculate deadband (±1°C around setpoint)
upperLimit := setpoint + 1
lowerLimit := setpoint - 1

# Heating control (on below lower, off above upper)
heating := HYSTERESIS(temperature, upperLimit, lowerLimit, 0, 1, heating)

# Cooling control (on above upper, off below lower)
cooling := HYSTERESIS(temperature, upperLimit, lowerLimit, 1, 0, cooling)

# Write outputs to Modbus
MODBUSW(C, 50, Bool, heating)   # Heating coil
MODBUSW(C, 51, Bool, cooling)   # Cooling coil

# Return status: -1=cooling, 0=idle, 1=heating
IF cooling = 1
  RETURN(-1)
ELSEIF heating = 1
  RETURN(1)
ELSE
  RETURN(0)
END

Example 4: Energy Meter with Cost Calculation#

# Read energy (kWh) from meter
energy := MODBUSR(H, 100, BigEndianFloat)

# Read current power (W)
power := MODBUSR(H, 200, BigEndianFloat)

# Define electricity rates ($/kWh)
peakRate := 0.25
offPeakRate := 0.12

# Get current hour
now := NOW()
hour := now.Hour

# Determine rate (peak: 7am-11pm, off-peak: 11pm-7am)
rate := IF(hour >= 7 AND hour < 23, peakRate, offPeakRate)

# Calculate cost
cost := energy * rate

# Store cost back to Modbus (for display)
MODBUSW(H, 300, BigEndianFloat, cost)

# Log to MQTT
json := "{\"energy\": " + TOSTRING(energy) +
        ", \"power\": " + TOSTRING(power) +
        ", \"cost\": " + TOSTRING(cost) + "}"
MQTTPUBLISH("meter/data", json)

RETURN(cost)

Example 5: REST API Integration with Retry#

# Try to get data from REST API
maxRetries := 3
retry := 0
success := FALSE

WHILE retry < maxRetries AND !success
  response := SENDHTTPREQUEST("/api/sensor/123")

  IF response.IsSuccess
    # Parse temperature from JSON
    temperature := PARSEJSON(response.Content, "$.temperature", true)

    IF !ISNULL(temperature)
      success := TRUE
      RETURN(temperature)
    END
  END

  retry := retry + 1

  IF !success
    # Wait before retry (not real sleep, just example)
    ADDWARNING("API retry " + TOSTRING(retry))
  END
LOOP

# All retries failed
ADDERROR("Failed to get temperature from API")
RETURN(NaN)

Example 6: Data Logger to FTP#

# Read sensors
temperature := MODBUSR(H, 100, BigEndianInt16)
humidity := MODBUSR(H, 102, BigEndianInt16)
pressure := MODBUSR(H, 104, BigEndianInt16)

# Get timestamp
now := NOW()
timestamp := TOSTRING(now.Year) + "-" +
             TOSTRING(now.Month) + "-" +
             TOSTRING(now.Day) + " " +
             TOSTRING(now.Hour) + ":" +
             TOSTRING(now.Minute)

# Create CSV line
csvLine := timestamp + "," +
           TOSTRING(temperature) + "," +
           TOSTRING(humidity) + "," +
           TOSTRING(pressure) + "\n"

# Upload to FTP (append mode)
FTPUPLOAD("/logs/sensor_data.csv", csvLine, "append")

RETURN(temperature)

Example 7: Modbus to MQTT Bridge#

# Read all sensors from Modbus
temp := MODBUSR(H, 100, BigEndianInt16)
humidity := MODBUSR(H, 102, BigEndianInt16)
co2 := MODBUSR(H, 104, UInt16)
pressure := MODBUSR(H, 106, BigEndianFloat)

# Read device status
statusReg := MODBUSR(H, 200, UInt16)
alarm := GETBIT(statusReg, 0)
warning := GETBIT(statusReg, 1)
fault := GETBIT(statusReg, 2)

# Build JSON payload
json := "{" +
        "\"temperature\": " + TOSTRING(temp) + "," +
        "\"humidity\": " + TOSTRING(humidity) + "," +
        "\"co2\": " + TOSTRING(co2) + "," +
        "\"pressure\": " + TOSTRING(pressure) + "," +
        "\"alarm\": " + TOSTRING(alarm) + "," +
        "\"warning\": " + TOSTRING(warning) + "," +
        "\"fault\": " + TOSTRING(fault) +
        "}"

# Publish to MQTT
MQTTPUBLISH("building/room1/sensors", json)

RETURN(temp)

Example 8: Linear Scaling (4-20mA to Engineering Units)#

# Read 4-20mA signal from analog input
# (Assuming Modbus device returns mA as integer, e.g., 40-200 for 4.0-20.0mA)
analogInput := MODBUSR(A, 0, UInt16)

# Convert to mA
currentMA := analogInput / 10.0

# Validate 4-20mA range
IF currentMA < 3.5 OR currentMA > 20.5
  ADDERROR("Analog input out of range: " + TOSTRING(currentMA) + "mA")
  RETURN(NaN)
END

# Scale to engineering units
# Example: 4-20mA represents 0-100°C
temperature := LINEAR(currentMA, 4, 0, 20, 100)

RETURN(temperature)

Best Practices#

1. Error Handling#

Always handle potential errors:

# Check for NaN
value := MODBUSR(H, 100, Int16)
IF ISNAN(value)
  ADDERROR("Failed to read register")
  RETURN(NaN)
END

# Check HTTP success
response := SENDHTTPREQUEST("/api/data")
IF !response.IsSuccess
  ADDERROR("HTTP error: " + TOSTRING(response.StatusCode))
  RETURN(NaN)
END

# Check JSON parsing
temp := PARSEJSON(json, "$.temperature", true)
IF ISNULL(temp)
  ADDWARNING("Temperature not found in JSON")
  temp := 0  # Default value
END

2. Use Comments#

Document your logic:

# Read temperature from Modbus sensor on holding register 100
temperature := MODBUSR(H, 100, BigEndianInt16)

# Convert from 0.1°C resolution to 1°C
temperature := temperature / 10

# Apply hysteresis to prevent oscillation
# ON when temp drops below 20°C, OFF when rises above 22°C
heating := HYSTERESIS(temperature, 22, 20, 0, 1, heating)

3. Validate Input Ranges#

# Read sensor value
value := MODBUSR(H, 100, Int16)

# Validate range (sensor spec: -40 to 125°C)
IF value < -40 OR value > 125
  ADDERROR("Sensor reading out of range: " + TOSTRING(value))
  RETURN(NaN)
END

4. Use Constants#

Define constants at the top for easy maintenance:

# Configuration
tempRegister := 100
setpointRegister := 200
heatingCoil := 50
upperDeadband := 1
lowerDeadband := 1

# Read values
temperature := MODBUSR(H, tempRegister, BigEndianInt16)
setpoint := MODBUSR(H, setpointRegister, BigEndianInt16)

# Control logic
heating := HYSTERESIS(temperature,
                      setpoint + upperDeadband,
                      setpoint - lowerDeadband,
                      0, 1, heating)

# Write output
MODBUSW(C, heatingCoil, Bool, heating)

5. Minimize Modbus Traffic#

Use MODBUSWNE for values that don’t change often:

# Bad - writes every time
setpoint := 22
MODBUSW(H, 200, Int16, setpoint)  # Writes even if already 22

# Good - writes only if changed
setpoint := 22
MODBUSWNE(H, 200, Int16, setpoint)  # Writes only if different

6. Use Meaningful Variable Names#

# Bad
x := MODBUSR(H, 100, Int16)
y := MODBUSR(H, 200, Int16)
z := HYSTERESIS(x, y + 1, y - 1, 0, 1, z)

# Good
temperature := MODBUSR(H, 100, Int16)
setpoint := MODBUSR(H, 200, Int16)
heating := HYSTERESIS(temperature, setpoint + 1, setpoint - 1, 0, 1, heating)

7. Keep Scripts Simple#

Break complex logic into smaller Smart Rules:

# Instead of one huge script handling everything,
# create separate Smart Rules for:
# - Temperature monitoring (one rule)
# - Alarm checking (one rule)
# - Data logging (one rule)
# - MQTT publishing (one rule)

8. Test Thoroughly#

Test edge cases:

# Test with:
# - Normal values
# - Out of range values
# - NaN / null values
# - Network failures
# - Device offline scenarios

Troubleshooting#

Common Issues#

Issue 1: NaN Results#

Problem: Script returns NaN unexpectedly

Causes:

  • Modbus read failed
  • Division by zero
  • Invalid data type conversion
  • Math operation on NaN

Solution:

value := MODBUSR(H, 100, Int16)

IF ISNAN(value)
  ADDERROR("Modbus read failed")
  RETURN(0)  # or appropriate default
END

Issue 2: Modbus Data Type Mismatch#

Problem: Wrong values from Modbus

Cause: Incorrect endianness or data type

Solution: Try different combinations:

# Try Big Endian first (most common)
value := MODBUSR(H, 100, BigEndianInt32)

# If wrong, try Little Endian
value := MODBUSR(H, 100, LittleEndianInt32)

# If still wrong, try ByteSwap
value := MODBUSR(H, 100, BigEndianInt32ByteSwap)

Issue 3: JSON Parsing Fails#

Problem: PARSEJSON returns null

Causes:

  • Invalid JSON
  • Wrong JSONPath
  • Element doesn’t exist

Solution:

# Use ignore error flag
value := PARSEJSON(json, "$.temperature", true)

IF ISNULL(value)
  ADDWARNING("Temperature not found in JSON")
  # Log actual JSON for debugging
  ADDINFO("JSON: " + json)
  value := 0
END

Issue 4: HTTP Request Fails#

Problem: HTTP requests timeout or fail

Causes:

  • Network connectivity
  • Wrong URL
  • Server down
  • Firewall blocking

Solution:

response := SENDHTTPREQUEST("/api/data")

IF !response.IsSuccess
  ADDERROR("HTTP " + TOSTRING(response.StatusCode) + ": " + response.ReasonPhrase)

  # Check specific errors
  IF response.StatusCode = 404
    ADDERROR("API endpoint not found")
  ELSEIF response.StatusCode = 401
    ADDERROR("Authentication required")
  ELSEIF response.StatusCode = 500
    ADDERROR("Server error")
  END

  RETURN(NaN)
END

Issue 5: Infinite Loop#

Problem: Script hangs

Cause: WHILE loop never exits

Solution: Always have exit condition and counter:

maxIterations := 100
counter := 0

WHILE condition AND counter < maxIterations
  # ... your code ...
  counter := counter + 1
LOOP

IF counter >= maxIterations
  ADDERROR("Loop exceeded maximum iterations")
END

Debugging Tips#

  1. Use logging functions:
ADDINFO("Temperature: " + TOSTRING(temperature))
ADDINFO("Setpoint: " + TOSTRING(setpoint))
  1. Check intermediate values:
value := MODBUSR(H, 100, Int16)
ADDINFO("Raw Modbus value: " + TOSTRING(value))

scaled := value / 10
ADDINFO("Scaled value: " + TOSTRING(scaled))
  1. Validate data types:
# Check if value is number
IF ISNAN(value)
  ADDERROR("Value is NaN")
END

# Check if value is null
IF ISNULL(value)
  ADDERROR("Value is null")
END
  1. Test small parts first:
# Comment out complex logic
# Test Modbus read first
temp := MODBUSR(H, 100, Int16)
RETURN(temp)

# Then add logic incrementally

Appendix A: Quick Reference#

Operators#

CategoryOperators
Arithmetic+ - * / MOD
Comparison= != <> < > <= >=
LogicalAND OR XOR !
Bitwise& `
Assignment:= += -= *= /= &= `

Control Structures#

StructureSyntax
IFIF condition ... ELSEIF ... ELSE ... END
WHILEWHILE condition ... LOOP
DO-WHILEDO ... LOOP WHILE condition
BREAKBREAK
CONTINUECONTINUE
RETURNRETURN(value)

Common Functions#

FunctionPurpose
MIN(...)Minimum value
MAX(...)Maximum value
AVG(...)Average
ROUND(x)Round to integer
ABS(x)Absolute value
HYSTERESIS(...)Prevent oscillation
IF(cond, t, f)Ternary if
SWITCH(...)Multi-way branch
LENGTH(x)String/collection length
TOSTRING(x)Convert to string
TODOUBLE(x)Parse number

Modbus Functions#

FunctionPurpose
MODBUSR(type, reg, dataType)Read Modbus
MODBUSW(type, reg, dataType, val)Write Modbus
MODBUSWNE(type, reg, dataType, val)Write if changed

Network Functions#

FunctionPurpose
SENDHTTPREQUEST(...)HTTP request
MQTTPUBLISH(topic, data)MQTT publish
FTPDOWNLOAD(path)FTP download
FTPUPLOAD(path, data, mode)FTP upload
PARSEJSON(json, path)Parse JSON
PARSEXML(xml, xpath)Parse XML
PARSETEXT(text, left, right)Extract text

Appendix B: Data Type Quick Reference#

TypeDescriptionExample
int32-bit signed integer42
float32-bit floating point3.14
double64-bit floating point3.14159
boolBooleanTRUE, FALSE
stringText string"Hello"
datetimeDate and timeDATETIME(2024, 1, 15, 10, 30, 0)
timespanDurationTIMESPAN(0, 2, 30, 0, 0)
collectionArray{1, 2, 3}

Support and Resources#

For more information:

  • Check the TapHome documentation
  • Contact TapHome support
  • Visit the TapHome community forum