Berry Scripting Language~
This feature is experimental, ESP32 only and included in all ESP32 pre-compiled builds
If you compile your own version, make sure the following is defined:
#define USE_BERRY
See full examples in the Berry Cookbook
Introduction to Berry~
Berry is the next generation scripting for Tasmota. It is based on the open-source Berry project, deliveting an ultra-lightweight dynamically typed embedded scripting language. It is designed for lower-performance embedded devices.
Berry Scripting allows simple and advanced extension of Tasmota, for example:
- simple scripting and advanced Rules
- advanced rules, beyond what is possible with native rules
- advanced automation
Berry Scripting takes it one step further and allows to build dynamic extensions to Tasmota, that would previously require native code:
- build light animations
- build I2C drivers
- build complete Tasmota drivers
- integrate native libraries like
lvgl(coming soon)
About the Berry language~
Berry has the following advantages:
- Lightweight: A well-optimized interpreter with very little resources. Ideal for use in microprocessors.
- Fast: optimized one-pass bytecode compiler and register-based virtual machine.
- Powerful: supports imperative programming, object-oriented programming, functional programming.
- Flexible: Berry is a dynamic type script, and it's intended for embedding in applications. It can provide good dynamic scalability for the host system.
- Simple: simple and natural syntax, support garbage collection, and easy to use FFI (foreign function interface).
- RAM saving: With compile-time object construction, most of the constant objects are stored in read-only code data segments, so the RAM usage of the interpreter is very low when it starts.
Tasmota port~
Berry Scripting in only supported on Tasmota32 for ESP32. The RAM usage starts at ~10kb and will be later optimized. Berry uses PSRAM on ESP32 if available (PSRAM is external RAM attached to Esp32 via SPI, it is slower but larger than internal RAM.
Quick Tutorial~
Make sure you compile Tasmota32 with #define USE_BERRY.
You should see similare lines in the Tasmota logs:
00:00:00.098 BRY: Berry initialized, RAM used=10002
00:00:00.264 BRY: No 'autoexec.be' file
Click on Configuration then Berry Scripting Console and enjoy the colorful Berry console, also called REPL (Read-Eval-Print-Loop).

Getting familiar with the REPL~
Try typing simple commands in the REPL. Since the input can be multi-lines, press 'Enter' twice to run the code. Use Up/Down arrows to navigate through history of previous commands.
> 1+1
2
> 2.0/3
0.666667
> print('Hello Tasmota!')
Hello Tasmota!
Note: Berry's native print() command displays text in the Berry Console and in the Tasmota logs. To log with finer control, you can also use the log() function, but it will not display in the Berry Console.
> print('Hello Tasmota!')
log('Hello again')
Hello Tasmota!
Meanwhile the Tasmota log shows:
tasmota.cmd("Dimmer 60") {"POWER":"ON","Dimmer":60,"Color":"996245","HSBColor":"21,55,60","Channel":[60,38,27]} The light is bright
The rule function have the general form below where parameters are optionals: ```python def function_name(value, trigger, msg) ... end
| Parameter | Description |
|---|---|
value | The value of the trigger. Similar to %value% in native rules. |
trigger | string of the trigger with all levels. Can be used if the same function is used with multiple triggers. |
msg | string of the message that triggered the rule. If it is a JSON, it has to be explicitly converted to a map object with json.load(msg). |
Example:
> def dimmer_over_50()
print("The light is bright")
end
tasmota.add_rule("Dimmer>50", dimmer_over_50)
> tasmota.cmd("Dimmer 30")
{"POWER":"ON","Dimmer":30,"Color":"4D3223","HSBColor":"21,55,30","Channel":[30,20,14]}
> tasmota.cmd("Dimmer 60")
{"POWER":"ON","Dimmer":60,"Color":"996245","HSBColor":"21,55,60","Channel":[60,38,27]}
The light is bright
The same fucntion can be used with multiple triggers.
Example if the function to process an ADC input should be triggered both by the tele/SENSOR message and the result of a Status 10 command:
tasmota.add_rule("ANALOG#A1", rule_adc_1)
tasmota.add_rule("StatusSNS#ANALOG#A1", rule_adc_1)
Or if the same function is used to process similar triggers:
import string
def rule_adc(value, trigger)
i=string.find(trigger,"#A")
tr=string.split(trigger,i+2)
adc=number(tr[1])
print("value of adc",adc," is ",value)
end
tasmota.add_rule("ANALOG#A1",rule_adc)
tasmota.add_rule("ANALOG#A2",rule_adc)
Another way to address the same using anonymous functions created dynamically
def rule_adc(adc, value)
print("value of adc",adc," is ",value)
end
tasmota.add_rule("ANALOG#A1",def (value) rule_adc(1,value) end )
tasmota.add_rule("ANALOG#A2",def (value) rule_adc(2,value) end )
A word on functions and closure~
Berry is a functional language, and includes the very powerful concept of a closure. In a nutshell, it means that when you create a function, it can capture the values of variables when the function was created. This roughly means that it does what intuitively you would expect it to do.
When using Rules or Timers, you always pass a Berry functions.
Timers~
Berry code, when it is running, blocks the rest of Tasmota. This means that you should not block for too long, or you may encounter problems. As a rule of thumb, try to never block more than 50ms. If you need to wait longer before the next action, use timers. As you will see, timers are very easy to create thanks to Berry's functional nature.
All times are in milliseconds. You can know the current running time in milliseconds since the last boot:
> tasmota.millis()
9977038
Sending a timer is as easy as tasmota.set_timer(<delay in ms>,<function>)
Example:
> def t() print("Booh!") end
> tasmota.set_timer(5000, t)
[5 seconds later]
Booh!
Lights and Relays~
Berry provides complete support for Relays and Lights.
You can control individual Relays or lights with tasmota.get_power() and tasmota.set_power().
tasmota.get_power() returns an array of booleans represnting the state of each relays and light (light comes last).
tasmota.set_power(relay, onoff) changes the state of a single relay/light.
Example (2 relays and 1 light):
> tasmota.get_power()
[false, true, false]
> tasmota.set_power(0, true)
true
> tasmota.get_power()
[true, true, false]
For light control, light.get() and light.set accept a structured object containing the following arguments:
| Attributes | Details |
|---|---|
| power | booleanTurns the light off or on. Equivalent to tasmota.set_power(). When brightness is set to 0, power is automatically set to off. On the contrary, you need to specify power:true to turn the light on. |
| bri | int range 0..255Set the overall brightness. Be aware that the range is 0..255 and not 0..100 as Dimmer. |
| hue | int 0..360Set the color Hue in degree, range 0..360 (0=red). |
| sat | int 0..255Set the color Saturation (0 is grey). |
| ct | int 153..500Set the white color temperature in mireds, ranging from 153 (cold white) to 500 (warm white) |
| rgb | string 6 hex digitsSet the color as hex RRGGBB, changing color and brightness. |
| channels | array of int, ranges 0..255Set the value for each channel, as an array of numbers |
When setting attributes, they are evaluated in the following order, the latter overriding the previous: power, ct, hue, sat, rgb, channles, bri.
# set to yellow, 25% brightness
> light.set({"power": true, "hue":60, "bri":64, "sat":255})
{'bri': 64, 'hue': 60, 'power': true, 'sat': 255, 'rgb': '404000', 'channels': [64, 64, 0]}
# set to RGB 000080 (blue 50%)
> light.set({"rgb": "000080"})
{'bri': 128, 'hue': 240, 'power': true, 'sat': 255, 'rgb': '000080', 'channels': [0, 0, 128]}
# set bri to zero, also powers off
> light.set({"bri": 0})
{'bri': 0, 'hue': 240, 'power': false, 'sat': 255, 'rgb': '000000', 'channels': [0, 0, 0]}
# chaning bri doesn't automatically power
> light.set({"bri": 32, "power":true})
{'bri': 32, 'hue': 240, 'power': true, 'sat': 255, 'rgb': '000020', 'channels': [0, 0, 32]}
# set channels as numbers (purple 12%)
> light.set({"channels": [32,0,32]})
{'bri': 32, 'hue': 300, 'power': true, 'sat': 255, 'rgb': '200020', 'channels': [32, 0, 32]}
Loading code from filesystem~
You can upload Berry code in the filesytem and load them at runtime. Just be careful to use the *.be extensions.
To load a Berry file, use the load(filename) function. It takes a filename and must end by .be or .bec.
Note: you don't need to prefix with /. A leading / will be added automatically if it is not present.
When loading a Berry script, the compiled bytecode is automatically saved to the filesystem, with the extension .bec (this is similar to Python's .py/.pyc mechanism). The save(filename,closure) function is used internally to save the bytecode.
Currently the precompiled is not loaded unless you explicitly use load("filename.bec") extension, this may change in the future.
Reference~
Below are the Tasmota specific functions and modules implemented on top of Berry.
Extensions to native Berry~
log(msg:string [, level:int = 3]) -> string~
Logs a message to the Tasmota console. Optional second argument is log_level (0..4), default is 2 LOG_LEVEL_INFO.
Example:
> log("A")
A
load(filename:string) -> int~
Loads a Berry script from the filesystem, and returns an error code; 0 means no error. Filename does not need to start with /, but needs to end with .be (Berry source code) or .bec (precompiled bytecode).
When loading a source file, the precompiled bytecode is saved to filesystem using the .bec extension.
save(filename:string, f:closure) -> nil~
Internally used function to save bytecode. It's a wrapper to the Berry's internal API be_savecode(). There is no check made on the filename.
Note: there is generally no need to use this function, it is used internally by load().
tasmota object~
A root level object called tasmota is created and contains numerous functions to interact with Tasmota.
Functions used to retrieve Tasmota configuration
Functions to create custom Tasmota command.
See examples in the Berry-Cookbook
Functions to manage Relay/Lights
light object~
Module light is automatically imported via a hidden import light command.
gpio module~
This module allows to retrieve the GPIO configuration set in the templates. You need to distinguish between logical gpio (like PWM, or I2C) and physical gpio which represent the GPIO number of the physicla pin. gpio.pin() transforms a logical gpio to a physical gpio, or -1 if the logical gpio is not set.
Currently there is limited support for GPIO: you can only read/write in digital mode and set the PGIO mode.
Here are the possible values for Tasmota GPIOS:
gpio.NONE, gpio.KEY1, gpio.KEY1_NP, gpio.KEY1_INV, gpio.KEY1_INV_NP, gpio.SWT1, gpio.SWT1_NP, gpio.REL1, gpio.REL1_INV, gpio.LED1, gpio.LED1_INV, gpio.CNTR1, gpio.CNTR1_NP, gpio.PWM1, gpio.PWM1_INV, gpio.BUZZER, gpio.BUZZER_INV, gpio.LEDLNK, gpio.LEDLNK_INV, gpio.I2C_SCL, gpio.I2C_SDA, gpio.SPI_MISO, gpio.SPI_MOSI, gpio.SPI_CLK, gpio.SPI_CS, gpio.SPI_DC, gpio.SSPI_MISO, gpio.SSPI_MOSI, gpio.SSPI_SCLK, gpio.SSPI_CS, gpio.SSPI_DC, gpio.BACKLIGHT, gpio.OLED_RESET, gpio.IRSEND, gpio.IRRECV, gpio.RFSEND, gpio.RFRECV, gpio.DHT11, gpio.DHT22, gpio.SI7021, gpio.DHT11_OUT, gpio.DSB, gpio.DSB_OUT, gpio.WS2812, gpio.MHZ_TXD, gpio.MHZ_RXD, gpio.PZEM0XX_TX, gpio.PZEM004_RX, gpio.PZEM016_RX, gpio.PZEM017_RX, gpio.SAIR_TX, gpio.SAIR_RX, gpio.PMS5003_TX, gpio.PMS5003_RX, gpio.SDS0X1_TX, gpio.SDS0X1_RX, gpio.SBR_TX, gpio.SBR_RX, gpio.SR04_TRIG, gpio.SR04_ECHO, gpio.SDM120_TX, gpio.SDM120_RX, gpio.SDM630_TX, gpio.SDM630_RX, gpio.TM1638CLK, gpio.TM1638DIO, gpio.TM1638STB, gpio.MP3_DFR562, gpio.HX711_SCK, gpio.HX711_DAT, gpio.TX2X_TXD_BLACK, gpio.TUYA_TX, gpio.TUYA_RX, gpio.MGC3130_XFER, gpio.MGC3130_RESET, gpio.RF_SENSOR, gpio.AZ_TXD, gpio.AZ_RXD, gpio.MAX31855CS, gpio.MAX31855CLK, gpio.MAX31855DO, gpio.NRG_SEL, gpio.NRG_SEL_INV, gpio.NRG_CF1, gpio.HLW_CF, gpio.HJL_CF, gpio.MCP39F5_TX, gpio.MCP39F5_RX, gpio.MCP39F5_RST, gpio.PN532_TXD, gpio.PN532_RXD, gpio.SM16716_CLK, gpio.SM16716_DAT, gpio.SM16716_SEL, gpio.DI, gpio.DCKI, gpio.CSE7766_TX, gpio.CSE7766_RX, gpio.ARIRFRCV, gpio.ARIRFSEL, gpio.TXD, gpio.RXD, gpio.ROT1A, gpio.ROT1B, gpio.ADC_JOY, gpio.SSPI_MAX31865_CS1, gpio.HRE_CLOCK, gpio.HRE_DATA, gpio.ADE7953_IRQ, gpio.SOLAXX1_TX, gpio.SOLAXX1_RX, gpio.ZIGBEE_TX, gpio.ZIGBEE_RX, gpio.RDM6300_RX, gpio.IBEACON_TX, gpio.IBEACON_RX, gpio.A4988_DIR, gpio.A4988_STP, gpio.A4988_ENA, gpio.A4988_MS1, gpio.OUTPUT_HI, gpio.OUTPUT_LO, gpio.DDS2382_TX, gpio.DDS2382_RX, gpio.DDSU666_TX, gpio.DDSU666_RX, gpio.SM2135_CLK, gpio.SM2135_DAT, gpio.DEEPSLEEP, gpio.EXS_ENABLE, gpio.TASMOTACLIENT_TXD, gpio.TASMOTACLIENT_RXD, gpio.TASMOTACLIENT_RST, gpio.TASMOTACLIENT_RST_INV, gpio.HPMA_RX, gpio.HPMA_TX, gpio.GPS_RX, gpio.GPS_TX, gpio.HM10_RX, gpio.HM10_TX, gpio.LE01MR_RX, gpio.LE01MR_TX, gpio.CC1101_GDO0, gpio.CC1101_GDO2, gpio.HRXL_RX, gpio.ELECTRIQ_MOODL_TX, gpio.AS3935, gpio.ADC_INPUT, gpio.ADC_TEMP, gpio.ADC_LIGHT, gpio.ADC_BUTTON, gpio.ADC_BUTTON_INV, gpio.ADC_RANGE, gpio.ADC_CT_POWER, gpio.WEBCAM_PWDN, gpio.WEBCAM_RESET, gpio.WEBCAM_XCLK, gpio.WEBCAM_SIOD, gpio.WEBCAM_SIOC, gpio.WEBCAM_DATA, gpio.WEBCAM_VSYNC, gpio.WEBCAM_HREF, gpio.WEBCAM_PCLK, gpio.WEBCAM_PSCLK, gpio.WEBCAM_HSD, gpio.WEBCAM_PSRCS, gpio.BOILER_OT_RX, gpio.BOILER_OT_TX, gpio.WINDMETER_SPEED, gpio.KEY1_TC, gpio.BL0940_RX, gpio.TCP_TX, gpio.TCP_RX, gpio.ETH_PHY_POWER, gpio.ETH_PHY_MDC, gpio.ETH_PHY_MDIO, gpio.TELEINFO_RX, gpio.TELEINFO_ENABLE, gpio.LMT01, gpio.IEM3000_TX, gpio.IEM3000_RX, gpio.ZIGBEE_RST, gpio.DYP_RX, gpio.MIEL_HVAC_TX, gpio.MIEL_HVAC_RX, gpio.WE517_TX, gpio.WE517_RX, gpio.AS608_TX, gpio.AS608_RX, gpio.SHELLY_DIMMER_BOOT0, gpio.SHELLY_DIMMER_RST_INV, gpio.RC522_RST, gpio.P9813_CLK, gpio.P9813_DAT, gpio.OPTION_A, gpio.FTC532, gpio.RC522_CS, gpio.NRF24_CS, gpio.NRF24_DC, gpio.ILI9341_CS, gpio.ILI9341_DC, gpio.ILI9488_CS, gpio.EPAPER29_CS, gpio.EPAPER42_CS, gpio.SSD1351_CS, gpio.RA8876_CS, gpio.ST7789_CS, gpio.ST7789_DC, gpio.SSD1331_CS, gpio.SSD1331_DC, gpio.SDCARD_CS, gpio.ROT1A_NP, gpio.ROT1B_NP, gpio.ADC_PH, gpio.BS814_CLK, gpio.BS814_DAT, gpio.WIEGAND_D0, gpio.WIEGAND_D1, gpio.NEOPOOL_TX, gpio.NEOPOOL_RX, gpio.SDM72_TX, gpio.SDM72_RX, gpio.TM1637CLK, gpio.TM1637DIO, gpio.PROJECTOR_CTRL_TX, gpio.PROJECTOR_CTRL_RX, gpio.SSD1351_DC, gpio.XPT2046_CS, gpio.CSE7761_TX, gpio.CSE7761_RX, gpio.VL53L0X_XSHUT1, gpio.MAX7219CLK, gpio.MAX7219DIN, gpio.MAX7219CS, gpio.TFMINIPLUS_TX, gpio.TFMINIPLUS_RX, gpio.ZEROCROSS, gpio.HALLEFFECT, gpio.EPD_DATA, gpio.INPUT, gpio.SENSOR_END
wire object~
Berry Scripting provides 2 objects wire1 and wire2 to communicate with both I2C buses.
Use wire1.scan() and wire2.scan() to scan both buses:
> wire1.scan()
[]
> wire2.scan()
[140]
You generally use tasmota.wire_scan() to find a device and the corresponding I2C bus.
Example with MPU6886 on bus 2:
> mpuwire = tasmota.wire_scan(0x68, 58)
> mpuwire
<instance: Wire()>
The following are low-level commands if you need finer control: