Tasmota/.doc_for_ai/BERRY_LANGUAGE_REFERENCE.md

41 KiB

Berry Language Reference for Tasmota

Note: this file is supposed to use as a reference manual for Generative AI in a compact form. For Claude AI it costs ~10k tokens.

Introduction

Berry is an ultra-lightweight, dynamically typed embedded scripting language designed for resource-constrained environments. The language primarily supports procedural programming, with additional support for object-oriented and functional programming paradigms. Berry's key design goal is to run efficiently on embedded devices with very limited memory, making the language highly streamlined while maintaining rich scripting capabilities.

Tasmota Integration: Berry is the next generation scripting for Tasmota, embedded by default in all ESP32 based firmwares (NOT supported on ESP8266). It is used for advanced scripting, superseding Rules, and enables building drivers, automations, and UI extensions.

Basic Information

Comments

# This is a line comment
#- This is a
   block comment
-#

Literals

Numerical Literals

40      # Integer literal
0x80    # Hexadecimal literal (integer)
3.14    # Real literal
1.1e-6  # Real literal with scientific notation

Boolean Literals

true    # Boolean true
false   # Boolean false

String Literals

'this is a string'
"this is a string"

String literals can be concatenated without operators:

s = "a" "b" "c"    # s == "abc"
s = "a"            # Multi-line strings
    "b"
    "c"            # s == "abc"

Escape sequences:

  • \a - Bell
  • \b - Backspace
  • \f - Form feed
  • \n - Newline
  • \r - Carriage return
  • \t - Horizontal tab
  • \v - Vertical tab
  • \\ - Backslash
  • \' - Single quote
  • \" - Double quote
  • \? - Question mark
  • \0 - Null character
  • \ooo - Character represented by octal number
  • \xhh - Character represented by hexadecimal number
  • \uXXXX - Unicode character (UTF-8 encoded)

Nil Literal

nil     # Represents no value

Identifiers

An identifier starts with an underscore or letter, followed by any combination of underscores, letters, or numbers. Berry is case-sensitive.

a
TestVariable
Test_Var
_init
baseClass
_

Keywords

if       elif     else     while     for      def
end      class    break    continue  return   true
false    nil      var      do        import   as
try      except   raise    static

Types and Variables

Built-in Types

Simple Types

  • nil: Represents no value
  • Integer: Signed integer (typically 32-bit)
  • Real: Floating-point number (typically 32-bit)
  • Boolean: true or false
  • String: Sequence of characters
  • Function: First-class value that can be called
  • Class: Template for instances
  • Instance: Object constructed from a class

Class Types

  • list: Ordered collection of elements
  • map: Key-value pairs collection
  • range: Integer range
  • bytes: Byte buffer

Variables

Variables are dynamically typed in Berry. They can be defined in two ways:

# Direct assignment (creates variable if it doesn't exist)
a = 1

# Using the var keyword
var a       # Defines a with nil value
var a = 1   # Defines a with value 1
var a, b    # Defines multiple variables
var a = 1, b = 2  # Defines multiple variables with values

Scope and Lifecycle

Variables defined in the outermost block have global scope. Variables defined in inner blocks have local scope.

var i = 0   # Global scope
do
    var j = 'str'  # Local scope
    print(i, j)    # Both i and j are accessible
end
print(i)    # Only i is accessible here

Expressions

Operators

Arithmetic Operators

  • - (unary): Negation
  • +: Addition or string concatenation
  • -: Subtraction
  • *: Multiplication
  • /: Division
  • %: Modulo (remainder)

Relational Operators

  • <: Less than
  • <=: Less than or equal to
  • ==: Equal to
  • !=: Not equal to
  • >=: Greater than or equal to
  • >: Greater than

Logical Operators

  • &&: Logical AND (short-circuit)
  • ||: Logical OR (short-circuit)
  • !: Logical NOT

Bitwise Operators

  • ~: Bitwise NOT
  • &: Bitwise AND
  • |: Bitwise OR
  • ^: Bitwise XOR
  • <<: Left shift
  • >>: Right shift

Assignment Operators

  • =: Simple assignment
  • +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=: Compound assignment
  • :=: Walrus assignment (assigns and returns value)

Domain and Subscript Operators

  • .: Member access
  • []: Subscript access

Conditional Operator

  • ? :: Ternary conditional

Concatenation Operators

  • +: String or list concatenation
  • ..: String concatenation or range creation

Operator Precedence (highest to lowest)

  1. () (grouping)
  2. () (function call), [] (subscript), . (member access)
  3. - (unary), !, ~
  4. *, /, %
  5. +, -
  6. <<, >>
  7. &
  8. ^
  9. |
  10. ..
  11. <, <=, >, >=
  12. ==, !=
  13. &&
  14. ||
  15. ? :
  16. =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
  17. :=

Statements

Expression Statements

a = 1       # Assignment statement
print(a)    # Function call statement

Block

A block is a collection of statements. It defines a scope.

do
    # This is a block
    var a = 1
    print(a)
end

Conditional Statements

# Simple if
if condition
    # code executed if condition is true
end

# if-else
if condition
    # code executed if condition is true
else
    # code executed if condition is false
end

# if-elif-else
if condition1
    # code executed if condition1 is true
elif condition2
    # code executed if condition1 is false and condition2 is true
else
    # code executed if both condition1 and condition2 are false
end

Iteration Statements

# while loop
while condition
    # code executed repeatedly while condition is true
end

# for loop (iterating over a container)
for variable: expression
    # code executed for each element in the container
end

# Examples
for i: 0..5
    print(i)    # Prints 0, 1, 2, 3, 4, 5
end

for item: ['a', 'b', 'c']
    print(item)  # Prints a, b, c
end

for key: map.keys()
    print(key, map[key])  # Iterates over map keys
end

Jump Statements

# break - exits the loop
while true
    if condition
        break
    end
end

# continue - skips to the next iteration
for i: 0..5
    if i == 3
        continue
    end
    print(i)  # Prints 0, 1, 2, 4, 5
end

Import Statement

import math              # Import math module
import hardware as hw    # Import hardware module as hw

Exception Handling

# Raising exceptions
raise 'my_error'                     # Raise an exception
raise 'my_error', 'error message'    # Raise with message

# Catching exceptions
try
    # code that might raise an exception
except 'my_error'
    # code executed if 'my_error' is raised
end

# Catching with exception variable
try
    # code that might raise an exception
except 'my_error' as e
    # e contains the exception value
end

# Catching with exception and message variables
try
    # code that might raise an exception
except 'my_error' as e, msg
    # e contains the exception value, msg contains the message
end

# Catching any exception
try
    # code that might raise an exception
except ..
    # code executed for any exception
end

# Catching multiple exception types
try
    # code that might raise an exception
except 'error1', 'error2' as e, msg
    # code executed if either 'error1' or 'error2' is raised
end

Functions

Function Definition

# Named function
def add(a, b)
    return a + b
end

# Anonymous function
add = def (a, b)
    return a + b
end

# Lambda expression (compact form)
add = / a, b -> a + b

Function with Variable Arguments

def print_all(a, b, *args)
    print(a, b)
    for arg: args
        print(arg)
    end
end

print_all(1, 2, 3, 4, 5)  # args will be [3, 4, 5]

Calling Functions with Dynamic Arguments

def sum(a, b, c)
    return a + b + c
end

call(sum, 1, 2, 3)        # Calls sum(1, 2, 3)
call(sum, 1, [2, 3])      # Calls sum(1, 2, 3)

Closures

def counter(start)
    var count = start
    return def()
        count += 1
        return count
    end
end

c = counter(0)
print(c())  # 1
print(c())  # 2

Object-Oriented Programming

Class Declaration

class Person
    var name, age
    
    def init(name, age)
        self.name = name
        self.age = age
    end
    
    def greet()
        print("Hello, my name is", self.name)
    end
end

Static Members

class MathUtils
    static var PI = 3.14159
    
    static def square(x)
        return x * x
    end
end

print(MathUtils.PI)           # Access static variable
print(MathUtils.square(5))    # Call static method

Inheritance

class Student : Person
    var school
    
    def init(name, age, school)
        super(self).init(name, age)  # Call parent constructor
        self.school = school
    end
    
    def greet()
        super(self).greet()          # Call parent method
        print("I study at", self.school)
    end
end

Instantiation and Method Calls

person = Person("John", 30)
person.greet()                # Hello, my name is John

student = Student("Alice", 20, "University")
student.greet()               # Hello, my name is Alice
                              # I study at University

Operator Overloading

class Vector
    var x, y
    
    def init(x, y)
        self.x = x
        self.y = y
    end
    
    def +(other)
        return Vector(self.x + other.x, self.y + other.y)
    end
    
    def tostring()
        return "Vector(" + str(self.x) + ", " + str(self.y) + ")"
    end
end

v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2
print(v3)  # Vector(4, 6)

Built-in Classes

List

# Creating lists
l1 = []                # Empty list
l2 = [1, 2, 3]         # List with elements
l3 = list()            # Empty list using constructor
l4 = list(1, 2, 3)     # List with elements using constructor

# Accessing elements
l2[0]                  # First element (1)
l2[-1]                 # Last element (3)

# Range-based access (slicing)
l2[1..3]               # Elements from index 1 to 3 inclusive [2, 3]
l2[1..]                # Elements from index 1 to end [2, 3]
l2[1..-1]              # Elements from index 1 to last element [2, 3]
l2[0..-2]              # All elements except the last one [1, 2]
l2[-2..-1]             # Last two elements [2, 3]

# Modifying lists
l2.push(4)             # Add element to end
l2.pop()               # Remove and return last element
l2.pop(1)              # Remove and return element at index 1
l2.insert(1, 5)        # Insert 5 at index 1
l2.remove(1)           # Remove element at index 1
l2.resize(5)           # Resize list to 5 elements (fills with nil)
l2.clear()             # Remove all elements

# Negative indices for modification
l2[-1] = 10            # Set last element to 10
l2[-2] += 5            # Add 5 to second-to-last element

# Other operations
l2.size()              # Number of elements
l2.concat()            # Join elements as string (no separator)
l2.concat(", ")        # Join elements with separator
l2.reverse()           # Reverse the list
l2.copy()              # Create a shallow copy
l2 + [4, 5]            # Concatenate lists (new list)
l2 .. 4                # Append element (modifies list)

# Finding elements
l2.find(3)             # Returns index of first occurrence or nil if not found

# Iterating over indices
for i: l2.keys()       # Iterate over list indices
    print(i, l2[i])
end

# Comparing lists
[1, 2] == [1, 2]       # true
[1, 2] != [1, 3]       # true

Map

# Creating maps
m1 = {}                # Empty map
m2 = {"key": "value"}  # Map with key-value pair
m3 = map()             # Empty map using constructor

# Accessing elements
m2["key"]              # Get value by key
m2.find("key")         # Get value by key (returns nil if not found)
m2.find("key", "default")  # Get value with default if not found

# Modifying maps
m2["new_key"] = "new_value"  # Add or update key-value pair
m2.insert("key", "value")    # Insert key-value pair (returns true if inserted, false if key exists)
m2.remove("key")             # Remove key-value pair

# Other operations
m2.size()              # Number of key-value pairs
m2.contains("key")     # Check if key exists
for k: m2.keys()       # Iterate over keys
    print(k, m2[k])
end

Range

# Creating ranges
r1 = 0..5              # Range from 0 to 5 (inclusive)
r2 = range(0, 5)       # Same using constructor
r3 = 10..              # Range from 10 to MAXINT

# Accessing properties
r1.lower()             # Lower bound (0)
r1.upper()             # Upper bound (5)
r1.incr()              # Increment (default 1)

# Modifying ranges
r1.setrange(1, 6)      # Change range bounds
r1.setrange(1, 10, 2)  # Change range bounds and increment

# Using in for loops
for i: 0..5
    print(i)           # Prints 0, 1, 2, 3, 4, 5
end

String Operations

# String indexing and slicing
s = "hello"
s[0]                   # "h"
s[1]                   # "e"
s[-1]                  # "o" (last character)
s[1..3]                # "ell" (characters from index 1 to 3)
s[1..]                 # "ello" (characters from index 1 to end)
s[1..-1]               # "ello" (characters from index 1 to last)
s[0..-2]               # "hell" (all characters except the last one)

Bytes

# Creating bytes objects
b1 = bytes()           # Empty bytes
b2 = bytes("1122AA")   # From hex string
b3 = bytes(10)         # Pre-allocated 10 bytes
b4 = bytes(-8)         # Fixed size 8 bytes

# Accessing bytes
b2[0]                  # First byte (0x11)
b2[1..2]               # Bytes from index 1 to 2

# Modifying bytes
b2[0] = 0xFF           # Set byte at index 0
b2.resize(10)          # Resize buffer
b2.clear()             # Clear all bytes

# Reading/writing structured data
b2.get(0, 2)           # Read 2 bytes as unsigned int (little endian)
b2.get(0, -2)          # Read 2 bytes as unsigned int (big endian)
b2.geti(0, 2)          # Read 2 bytes as signed int
b2.set(0, 0x1234, 2)   # Write 2-byte value
b2.add(0x1234, 2)      # Append 2-byte value

# Conversion
b2.tohex()             # Convert to hex string
b2.asstring()          # Convert to raw string
b2.tob64()             # Convert to base64 string
b2.fromhex("AABBCC")   # Load from hex string
b2.fromstring("Hello") # Load from raw string
b2.fromb64("SGVsbG8=") # Load from base64 string

File

# Opening files
f = open("test.txt", "w")  # Open for writing
f = open("test.txt", "r")  # Open for reading

# Writing to files
f.write("Hello, world!")   # Write string
f.write(bytes("AABBCC"))   # Write bytes

# Reading from files
content = f.read()         # Read entire file
line = f.readline()        # Read one line
raw_data = f.readbytes()   # Read as bytes

# File positioning
f.seek(10)                 # Move to position 10
pos = f.tell()             # Get current position
size = f.size()            # Get file size

# Closing files
f.flush()                  # Flush buffers
f.close()                  # Close file

Standard Libraries

String Module

import string

# String operations
string.count("hello", "l")         # Count occurrences (2)
string.find("hello", "lo")         # Find substring (3), returns -1 if not found
string.split("a,b,c", ",")         # Split by separator (["a", "b", "c"])
string.split("hello", 2)           # Split at position (["he", "llo"])

# Character operations
string.byte("A")                   # Get byte value (65)
string.char(65)                    # Get character from byte ('A')

# Case conversion
string.toupper("hello")            # Convert to uppercase ("HELLO")
string.tolower("HELLO")            # Convert to lowercase ("hello")

# String transformation
string.tr("hello", "el", "ip")     # Replace characters ("hippo")
string.replace("hello", "ll", "xx") # Replace substring ("hexxo")
string.escape("hello\n")           # Escape for C strings

# Checking
string.startswith("hello", "he")   # Check prefix (true)
string.startswith("hello", "HE", true) # Case-insensitive check (true)
string.endswith("hello", "lo")     # Check suffix (true)
string.endswith("hello", "LO", true) # Case-insensitive check (true)

# Formatting
string.format("Value: %d", 42)     # Format string
format("Value: %.2f", 3.14159)     # Format (global function)
f"Value: {x}"                      # f-string format
f"Value: {x:.2f}"                  # f-string with format specifier
f"{x=}"                           # f-string debug format

Math Module

import math

# Constants
math.pi                            # Pi (3.14159...)
math.inf                           # Infinity
math.nan                           # Not a Number
math.imin                          # Smallest possible integer
math.imax                          # Largest possible integer

# Basic functions
math.abs(-5)                       # Absolute value (5)
math.floor(3.7)                    # Round down (3)
math.ceil(3.2)                     # Round up (4)
math.round(3.5)                    # Round to nearest (4)
math.min(1, 2, 3)                  # Minimum value (1)
math.max(1, 2, 3)                  # Maximum value (3)

# Exponential and logarithmic
math.sqrt(16)                      # Square root (4)
math.pow(2, 3)                     # Power (8)
math.exp(1)                        # e^x (2.71828...)
math.log(2.71828)                  # Natural logarithm (1)
math.log10(100)                    # Base-10 logarithm (2)

# Trigonometric
math.sin(math.pi/2)                # Sine (1)
math.cos(0)                        # Cosine (1)
math.tan(math.pi/4)                # Tangent (1)
math.asin(1)                       # Arc sine (pi/2)
math.acos(1)                       # Arc cosine (0)
math.atan(1)                       # Arc tangent (pi/4)
math.atan2(1, 1)                   # Arc tangent of y/x (pi/4)

# Angle conversion
math.deg(math.pi)                  # Radians to degrees (180)
math.rad(180)                      # Degrees to radians (pi)

# Random numbers
math.srand(42)                     # Seed random generator
math.rand()                        # Random integer

# Special checks
math.isinf(math.inf)               # Check if value is infinity (true)
math.isnan(math.nan)               # Check if value is NaN (true)

JSON Module

import json

# Parsing JSON
data = json.load('{"name": "John", "age": 30}')
print(data.name)                   # John

# Error handling with json.load
data = json.load('{"invalid": }')  # Returns nil on parsing error
if data == nil
    print("Invalid JSON")
end

# Generating JSON
person = {
    "name": "Alice",
    "age": 25,
    "hobbies": ["reading", "swimming"]
}
json_str = json.dump(person)       # Compact JSON
json_formatted = json.dump(person, "format")  # Formatted JSON

OS Module

import os

# Directory operations
os.getcwd()                        # Get current directory
os.chdir("/path/to/dir")           # Change directory
os.mkdir("/path/to/new/dir")       # Create directory
os.remove("/path/to/file")         # Delete file or directory
os.listdir()                       # List current directory
os.listdir("/path")                # List specific directory

# Path operations
os.path.isdir("/path")             # Check if path is directory
os.path.isfile("/path/file.txt")   # Check if path is file
os.path.exists("/path")            # Check if path exists
os.path.split("/path/file.txt")    # Split into ["/path", "file.txt"]
os.path.splitext("file.txt")       # Split into ["file", ".txt"]
os.path.join("path", "file.txt")   # Join into "path/file.txt"

# System operations
os.system("command")               # Execute system command
os.exit()                          # Exit interpreter

Global Module

import global

# Accessing globals
global_vars = global()             # List of all global variables
global.contains("var_name")        # Check if global exists
value = global.var_name            # Get global value
global.var_name = 42               # Set global value
value = global.("dynamic_name")    # Dynamic access by name

Introspect Module

import introspect

# Inspecting objects
members = introspect.members(obj)  # List of object members
value = introspect.get(obj, "attr") # Get attribute value
introspect.set(obj, "attr", value) # Set attribute value
name = introspect.name(obj)        # Get object name
is_method = introspect.ismethod(fn) # Check if function is method

# Module operations
mod = introspect.module("math")    # Import module dynamically

# Pointer operations (advanced)
ptr = introspect.toptr(addr)       # Convert int to pointer
addr = introspect.fromptr(ptr)     # Convert pointer to int

Error Handling

Standard Exceptions

  • assert_failed: Assertion failed
  • index_error: Index out of bounds
  • io_error: IO malfunction
  • key_error: Key error
  • runtime_error: VM runtime exception
  • stop_iteration: End of iterator
  • syntax_error: Syntax error
  • unrealized_error: Unrealized function
  • type_error: Type error

Raising Exceptions

raise "my_error"                   # Raise exception
raise "my_error", "message"        # Raise with message

Catching Exceptions

try
    # Code that might raise an exception
except "my_error"
    # Handle specific exception
end

try
    # Code that might raise an exception
except "error1", "error2"
    # Handle multiple exceptions
end

try
    # Code that might raise an exception
except "my_error" as e
    # e contains the exception value
end

try
    # Code that might raise an exception
except "my_error" as e, msg
    # e contains exception, msg contains message
end

try
    # Code that might raise an exception
except ..
    # Catch all exceptions
end

Error Handling Patterns

Many functions in Berry return nil to indicate errors rather than raising exceptions. This is common in functions that parse data or perform operations that might fail:

# JSON parsing
data = json.load('{"invalid": }')  # Returns nil on parsing error
if data == nil
    print("Invalid JSON")
end

# Map access
value = map.find("key")            # Returns nil if key doesn't exist
if value == nil
    print("Key not found")
end

# String operations
index = string.find("hello", "z")  # Returns -1 if substring not found
if index == -1
    print("Substring not found")
end

Assertions

assert(condition)                  # Raises assert_failed if condition is false
assert(condition, "message")       # Raises with custom message

Best Practices

Variable Naming

  • Use descriptive names for variables and functions
  • Use camelCase or snake_case consistently
  • Prefix private members with underscore (convention only)

Code Organization

  • Group related functions and classes
  • Use modules for logical separation
  • Keep functions small and focused

Memory Management

  • Be mindful of memory usage on embedded systems
  • Release resources when no longer needed
  • Use fixed-size buffers when appropriate

Error Handling

  • Use exceptions for exceptional conditions
  • Check return values for expected errors
  • Provide meaningful error messages

Performance

  • Avoid creating unnecessary objects
  • Reuse buffers when processing large data
  • Use native functions for performance-critical code

Tasmota-Specific Features

Tasmota-Specific Modules

Beyond standard Berry modules, Tasmota provides additional modules:

tasmota - Core integration module (automatically imported)

light - Light control (automatically imported)

mqtt - MQTT operations (import mqtt)

webserver - Web server extensions (import webserver)

gpio - GPIO control (import gpio)

persist - Data persistence (import persist)

path - File system operations (import path)

energy - Energy monitoring (automatically imported)

display - Display driver integration (import display)

crypto - Cryptographic functions (import crypto)

re - Regular expressions (import re)

mdns - mDNS/Bonjour support (import mdns)

ULP - Ultra Low Power coprocessor (import ULP)

uuid - UUID generation (import uuid)

crc - CRC calculations (import crc)

Tasmota Constants and Enums

# GPIO constants (gpio module)
gpio.INPUT, gpio.OUTPUT, gpio.PULLUP, gpio.PULLDOWN
gpio.HIGH, gpio.LOW
gpio.REL1, gpio.KEY1, gpio.LED1, gpio.I2C_SCL, gpio.I2C_SDA
# ... many more GPIO function constants

# Serial constants
serial.SERIAL_8N1, serial.SERIAL_7E1, etc.

# Webserver constants  
webserver.HTTP_GET, webserver.HTTP_POST, webserver.HTTP_OPTIONS, webserver.HTTP_ANY
webserver.HTTP_OFF, webserver.HTTP_USER, webserver.HTTP_ADMIN, webserver.HTTP_MANAGER
webserver.HTTP_MANAGER_RESET_ONLY
webserver.BUTTON_MAIN, webserver.BUTTON_CONFIGURATION, webserver.BUTTON_INFORMATION
webserver.BUTTON_MANAGEMENT, webserver.BUTTON_MODULE

Console and REPL

Access Berry console via ConfigurationBerry Scripting Console. The console supports:

  • Multi-line input (press Enter twice or click "Run")
  • Command history (arrow keys)
  • Colorful syntax highlighting
  • Berry VM restart with BrRestart command

File System and Loading

Berry files can be source (.be) or pre-compiled bytecode (.bec):

load("filename")        # Loads .be or .bec file
tasmota.compile("file.be")  # Compiles .be to .bec

Autostart: Place autoexec.be in filesystem to run Berry code at boot.

Tasmota Integration Functions

Core Tasmota Functions

# System information
tasmota.get_free_heap()     # Free heap bytes
tasmota.memory()            # Memory stats map
tasmota.arch()              # Architecture: "esp32", "esp32s2", etc.
tasmota.millis()            # Milliseconds since boot
tasmota.yield()             # Give time to low-level functions
tasmota.delay(ms)           # Block execution for ms milliseconds

# Commands and responses
tasmota.cmd("command")      # Execute Tasmota command
tasmota.resp_cmnd_done()    # Respond "Done"
tasmota.resp_cmnd_error()   # Respond "Error"
tasmota.resp_cmnd_str(msg)  # Custom response string
tasmota.resp_cmnd(json)     # Custom JSON response

# Configuration
tasmota.get_option(index)   # Get SetOption value
tasmota.read_sensors()      # Get sensor JSON string
tasmota.wifi()              # WiFi connection info
tasmota.eth()               # Ethernet connection info

Rules and Events

# Add rules (similar to Tasmota Rules but more powerful)
tasmota.add_rule("trigger", function)
tasmota.add_rule(["trigger1", "trigger2"], function)  # AND logic
tasmota.remove_rule("trigger")

# Rule function signature
def rule_function(value, trigger, msg)
  # value: trigger value (%value% equivalent)
  # trigger: full trigger string
  # msg: parsed JSON map or original string
end

# Examples
tasmota.add_rule("Dimmer>50", def() print("Bright!") end)
tasmota.add_rule("ANALOG#A1>300", def(val) print("ADC:", val) end)

Timers and Scheduling

# Timers (50ms resolution)
tasmota.set_timer(delay_ms, function)
tasmota.remove_timer(id)
tasmota.defer(function)     # Run in next millisecond

# Cron scheduling
tasmota.add_cron("*/15 * * * * *", function, "id")
tasmota.remove_cron("id")
tasmota.next_cron("id")     # Next execution timestamp

# Time functions
tasmota.rtc()               # Current time info
tasmota.time_dump(timestamp) # Decompose timestamp
tasmota.time_str(timestamp)  # ISO 8601 string
tasmota.strftime(format, timestamp)
tasmota.strptime(time_str, format)

Device Control

# Relays and Power
tasmota.get_power()         # Array of relay states
tasmota.set_power(idx, state) # Set relay state

# Lights (use light module)
light.get()                 # Current light status
light.set({"power": true, "bri": 128, "hue": 120})

# Light attributes: power, bri (0-255), hue (0-360), sat (0-255), 
# ct (153-500), rgb (hex string), channels (array)

Custom Commands

# Add custom Tasmota commands
def my_command(cmd, idx, payload, payload_json)
  # cmd: command name, idx: command index
  # payload: raw string, payload_json: parsed JSON
  tasmota.resp_cmnd_done()
end

tasmota.add_cmd("MyCmd", my_command)
tasmota.remove_cmd("MyCmd")

Tasmota Drivers

Create complete Tasmota drivers by implementing event methods:

class MyDriver
  def every_second()     # Called every second
  end
  
  def every_50ms()       # Called every 50ms
  end
  
  def web_sensor()       # Add to web UI
    tasmota.web_send("{s}Sensor{m}Value{e}")
  end
  
  def json_append()      # Add to JSON teleperiod
    tasmota.response_append(',"MySensor":{"Value":123}')
  end
  
  def web_add_main_button()  # Add button to main page
    import webserver
    webserver.content_send("<button onclick='la(\"&myaction=1\");'>My Button</button>")
  end
  
  def button_pressed()   # Handle button press
  end
  
  def mqtt_data(topic, idx, data, databytes)  # Handle MQTT
  end
  
  def save_before_restart()  # Before restart
  end
end

# Register driver
driver = MyDriver()
tasmota.add_driver(driver)

Fast Loop

For near real-time events (200Hz, 5ms intervals):

def fast_function()
  # High-frequency processing
end

tasmota.add_fast_loop(fast_function)
tasmota.remove_fast_loop(fast_function)

GPIO Control

import gpio

# GPIO detection and control
gpio.pin_used(gpio.REL1)        # Check if GPIO is used
gpio.pin(gpio.REL1)             # Get physical GPIO number
gpio.digital_write(pin, gpio.HIGH)  # Set GPIO state
gpio.digital_read(pin)          # Read GPIO state
gpio.pin_mode(pin, gpio.OUTPUT) # Set GPIO mode

# PWM control
gpio.set_pwm(pin, duty, phase)  # Set PWM value
gpio.set_pwm_freq(pin, freq)    # Set PWM frequency

# DAC (ESP32 GPIO 25-26, ESP32-S2 GPIO 17-18)
gpio.dac_voltage(pin, voltage_mv)  # Set DAC voltage

# Counters
gpio.counter_read(counter)      # Read counter value
gpio.counter_set(counter, value) # Set counter value

I²C Communication

# Use wire1 or wire2 for I²C buses
wire1.scan()                    # Scan for devices
wire1.detect(addr)              # Check if device present
wire1.read(addr, reg, size)     # Read from device
wire1.write(addr, reg, val, size) # Write to device
wire1.read_bytes(addr, reg, size)  # Read as bytes
wire1.write_bytes(addr, reg, bytes) # Write bytes

# Find device on any bus
wire = tasmota.wire_scan(addr, i2c_index)

MQTT Integration

import mqtt

# MQTT operations
mqtt.publish(topic, payload, retain)
mqtt.subscribe(topic, function)  # Subscribe with callback
mqtt.unsubscribe(topic)
mqtt.connected()                 # Check connection status

# Callback function signature
def mqtt_callback(topic, idx, payload_s, payload_b)
  # topic: full topic, payload_s: string, payload_b: bytes
  return true  # Return true if handled
end

Web Server Extensions

import webserver

# In driver's web_add_handler() method
webserver.on("/my_page", def() 
  webserver.content_send("<html>My Page</html>")
end)

# Request handling
webserver.has_arg("param")      # Check parameter exists
webserver.arg("param")          # Get parameter value
webserver.arg_size()            # Number of parameters

# Response functions
webserver.content_send(html)    # Send HTML content
webserver.content_button()      # Standard button
webserver.html_escape(str)      # Escape HTML

Persistence

import persist

# Automatic persistence to _persist.json
persist.my_value = 123
persist.save()                  # Force save to flash
persist.has("key")              # Check if key exists
persist.remove("key")           # Remove key
persist.find("key", default)    # Get with default

Network Clients

HTTP/HTTPS Client

cl = webclient()
cl.begin("https://example.com/api")
cl.set_auth("user", "pass")
cl.add_header("Content-Type", "application/json")

result = cl.GET()               # or POST(payload)
if result == 200
  response = cl.get_string()
  # or cl.write_file("filename") for binary
end
cl.close()

TCP Client

tcp = tcpclient()
tcp.connect("192.168.1.100", 80)
tcp.write("GET / HTTP/1.0\r\n\r\n")
response = tcp.read()
tcp.close()

UDP Communication

u = udp()
u.begin("", 2000)               # Listen on port 2000
u.send("192.168.1.10", 2000, bytes("Hello"))

# Receive (polling)
packet = u.read()               # Returns bytes or nil
if packet
  print("From:", u.remote_ip, u.remote_port)
end

Serial Communication

ser = serial(rx_gpio, tx_gpio, baud, serial.SERIAL_8N1)
ser.write(bytes("Hello"))       # Send data
data = ser.read()               # Read available data
ser.available()                 # Check bytes available
ser.flush()                     # Flush buffers
ser.close()                     # Close port

Cryptography

import crypto

# AES encryption
aes = crypto.AES_GCM(key_32_bytes, iv_12_bytes)
encrypted = aes.encrypt(plaintext)
tag = aes.tag()

# Hashing
crypto.SHA256().update(data).finish()  # SHA256 hash
crypto.MD5().update(data).finish()     # MD5 hash

# HMAC
crypto.HMAC_SHA256(key).update(data).finish()

File System Operations

import path

path.exists("filename")         # Check file exists
path.listdir("/")              # List directory
path.remove("filename")        # Delete file
path.mkdir("dirname")          # Create directory
path.last_modified("file")     # File timestamp

Regular Expressions

import re

# Pattern matching
matches = re.search("a.*?b(z+)", "aaaabbbzzz")  # Returns matches array
all_matches = re.searchall('<([a-zA-Z]+)>', html)  # All matches
parts = re.split('/', "path/to/file")  # Split string

# Compiled patterns (faster for reuse)
pattern = re.compilebytes("\\d+")
matches = re.search(pattern, "abc123def")

Energy Monitoring

# Read energy values
energy.voltage                  # Main phase voltage
energy.current                  # Main phase current  
energy.active_power            # Active power (W)
energy.total                   # Total energy (kWh)

# Multi-phase access
energy.voltage_phases[0]       # Phase 0 voltage
energy.current_phases[1]       # Phase 1 current

# Berry energy driver (with OPTION_A 9 GPIO)
if energy.driver_enabled()
  energy.voltage = 240
  energy.current = 1.5
  energy.active_power = 360    # This drives energy calculation
end

Display Integration

import display

# Initialize display driver
display.start(display_ini_string)
display.started()              # Check if initialized
display.dimmer(50)             # Set brightness 0-100
display.driver_name()          # Get driver name

# Touch screen updates
display.touch_update(touches, x, y, gesture)

Advanced Features

ULP (Ultra Low Power) Coprocessor

import ULP

ULP.wake_period(0, 500000)     # Configure wake timer
ULP.load(bytecode)             # Load ULP program
ULP.run()                      # Execute ULP program
ULP.set_mem(addr, value)       # Set RTC memory
ULP.get_mem(addr)              # Get RTC memory

mDNS Support

import mdns

mdns.start("hostname")         # Start mDNS
mdns.add_service("_http", "_tcp", 80, {"path": "/"})
mdns.stop()                    # Stop mDNS

Error Handling Patterns

Many Tasmota functions return nil for errors rather than raising exceptions:

# Check return values
data = json.load(json_string)
if data == nil
  print("Invalid JSON")
end

# Wire operations
result = wire1.read(addr, reg, 1)
if result == nil
  print("I2C read failed")
end

Best Practices for Tasmota

  1. Memory Management: Use tasmota.gc() to monitor memory usage
  2. Non-blocking: Use timers instead of delay() for long waits
  3. Error Handling: Always check return values for nil
  4. Persistence: Use persist module for settings that survive reboots
  5. Performance: Use fast_loop sparingly, prefer regular driver events
  6. Debugging: Enable #define USE_BERRY_DEBUG for development

Tasmota Extensions to Standard Modules

bytes class extensions

b = bytes("1122AA")               # From hex string
b = bytes(-8)                     # Fixed size buffer
b.tohex()                         # To hex string  
b.tob64()                         # To base64
b.fromhex("AABBCC")              # Load from hex
b.fromb64("SGVsbG8=")            # Load from base64
b.asstring()                      # To raw string

Common Tasmota Berry Patterns

Simple Sensor Driver

class MySensor
  var wire, addr
  
  def init()
    self.addr = 0x48
    self.wire = tasmota.wire_scan(self.addr, 99)  # I2C index 99
    if self.wire
      print("MySensor found on bus", self.wire.bus)
    end
  end
  
  def every_second()
    if !self.wire return end
    var temp = self.wire.read(self.addr, 0x00, 2)  # Read temperature
    self.temperature = temp / 256.0  # Convert to Celsius
  end
  
  def web_sensor()
    if !self.wire return end
    import string
    var msg = string.format("{s}MySensor Temp{m}%.1f °C{e}", self.temperature)
    tasmota.web_send_decimal(msg)
  end
  
  def json_append()
    if !self.wire return end
    import string
    var msg = string.format(',"MySensor":{"Temperature":%.1f}', self.temperature)
    tasmota.response_append(msg)
  end
end

sensor = MySensor()
tasmota.add_driver(sensor)

Custom Command with JSON Response

def my_status_cmd(cmd, idx, payload, payload_json)
  import string
  var response = {
    "Uptime": tasmota.millis(),
    "FreeHeap": tasmota.get_free_heap(),
    "WiFi": tasmota.wifi("rssi")
  }
  tasmota.resp_cmnd(json.dump(response))
end

tasmota.add_cmd("MyStatus", my_status_cmd)

MQTT Automation

import mqtt

def handle_sensor_data(topic, idx, payload_s, payload_b)
  var data = json.load(payload_s)
  if data && data.find("temperature")
    var temp = data["temperature"]
    if temp > 25
      tasmota.cmd("Power1 ON")  # Turn on fan
    elif temp < 20  
      tasmota.cmd("Power1 OFF") # Turn off fan
    end
  end
  return true
end

mqtt.subscribe("sensors/+/temperature", handle_sensor_data)

Web UI Button with Action

class WebButton
  def web_add_main_button()
    import webserver
    webserver.content_send("<p><button onclick='la(\"&toggle_led=1\");'>Toggle LED</button></p>")
  end
  
  def web_sensor()
    import webserver
    if webserver.has_arg("toggle_led")
      # Toggle GPIO2 (built-in LED on many ESP32 boards)
      var pin = 2
      var current = gpio.digital_read(pin)
      gpio.digital_write(pin, !current)
      print("LED toggled to", !current)
    end
  end
end

button = WebButton()
tasmota.add_driver(button)

Scheduled Task with Persistence

import persist

class ScheduledTask
  def init()
    if !persist.has("task_count")
      persist.task_count = 0
    end
    # Run every 5 minutes
    tasmota.add_cron("0 */5 * * * *", /-> self.run_task(), "my_task")
  end
  
  def run_task()
    persist.task_count += 1
    print("Task executed", persist.task_count, "times")
    
    # Do something useful
    var sensors = tasmota.read_sensors()
    print("Current sensors:", sensors)
    
    persist.save()  # Save counter to flash
  end
end

task = ScheduledTask()

HTTP API Client

class WeatherAPI
  var api_key, city
  
  def init(key, city_name)
    self.api_key = key
    self.city = city_name
    tasmota.add_cron("0 0 * * * *", /-> self.fetch_weather(), "weather")
  end
  
  def fetch_weather()
    var cl = webclient()
    var url = f"http://api.openweathermap.org/data/2.5/weather?q={self.city}&appid={self.api_key}"
    
    cl.begin(url)
    var result = cl.GET()
    
    if result == 200
      var response = cl.get_string()
      var data = json.load(response)
      if data
        var temp = data["main"]["temp"] - 273.15  # Kelvin to Celsius
        print(f"Weather in {self.city}: {temp:.1f}°C")
        
        # Store in global for other scripts to use
        import global
        global.weather_temp = temp
      end
    end
    cl.close()
  end
end

# weather = WeatherAPI("your_api_key", "London")

Rule-based Automation

# Advanced rule that combines multiple conditions
tasmota.add_rule(["ANALOG#A0>500", "Switch1#State=1"], 
  def(values, triggers)
    print("Both conditions met:")
    print("ADC value:", values[0])
    print("Switch state:", values[1])
    tasmota.cmd("Power2 ON")  # Activate something
  end
)

# Time-based rule
tasmota.add_rule("Time#Minute=30", 
  def()
    if tasmota.rtc()["hour"] == 18  # 6:30 PM
      tasmota.cmd("Dimmer 20")  # Dim lights for evening
    end
  end
)

Best Practices and Tips

  1. Always check for nil returns from Tasmota functions
  2. Use timers instead of delay() to avoid blocking Tasmota
  3. Implement proper error handling in I²C and network operations
  4. Use persist module for settings that should survive reboots
  5. Test memory usage with tasmota.gc() during development
  6. Use fast_loop sparingly - it runs 200 times per second
  7. Prefer driver events over polling when possible
  8. Use f-strings for readable string formatting
  9. Import modules only when needed to save memory
  10. Use tasmota.wire_scan() instead of manual I²C bus detection