Berry Scripting Language is now included in all Tasmota32 builds for ESP32 microcontrollers, offering an ultra-lightweight dynamically typed scripting language for embedded devices. The language supports advanced automations, drivers, and animations, with a REPL console and Visual Studio Code extension for development. Berry is optimized for low RAM usage and supports PSRAM on ESP32.
# Berry Scripting Language [~](#berry-scripting-language) Berry Scripting is included in all `tasmota32` builds. It is **NOT** supported on ESP82xx If you plan to code in Berry, you should enable `#define USE_BERRY_DEBUG` which will give you much more details when coding If you want to use Generative AI to generate Berry code, it is highly suggested to use [claude.ai](https://claude.ai/) and use the [Markdown Language Reference](https://tasmota.github.io/docs/_media/BERRY_LANGUAGE_REFERENCE.txt). This files consumes ~6000 input tokens. Useful resources: - First time user of Berry: [Berry Introduction (in 20 minutes of less)](../Berry-Introduction/) - Language fast reference PDF (7 pages) [Berry Short manual](https://tasmota.github.io/docs/_media/berry_short_manual.pdf) - Full language documentation [The Berry Script Language Reference Manual](https://berry.readthedocs.io/en/latest/source/en/Reference.html) - Tasmota extension of Berry, see below - Full examples in the [Berry Cookbook](../Berry-Cookbook/) If you're new to Berry, have a look at [Berry Introduction (in 20 minutes of less)](../Berry-Introduction/) ## Introduction to Berry[~](#introduction-to-berry) Berry is the next generation scripting for Tasmota. It is based on the open-source Berry project, delivering an ultra-lightweight dynamically typed scripting language designed for lower-performance embedded devices. Reference sheet Download [Berry Short Manual](https://tasmota.github.io/docs/_media/berry_short_manual.pdf) to get a list of basic functions and capabilities of Berry language Berry Scripting allows simple and also advanced extensions of Tasmota, for example: - simple scripting - advanced rules, beyond what is possible with native rules - advanced automations 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 I 2C drivers - build complete Tasmota drivers - integrate native libraries like `lvgl` see[LVGL](../LVGL/) ### About the Berry language[~](#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 MicroPython-eque syntax, supports 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[~](#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 Start[~](#quick-start) Click on *Configuration* then *Berry Scripting Console* and enjoy the colorful Berry console, also called REPL (Read-Eval-Print-Loop). Drag the bottom corner of each screen to change its size The console is not designed for big coding tasks but it's recommended to use a code editor when dealing with many, many lines of code. An extension for Visual Studio Code exists to make writing Berry scripts even easier with colored syntax. Download the entire [folder](https://github.com/berry-lang/berry/tree/master/tools/plugins/vscode/) and copy to VSCode extensions folder. ### REPL Console[~](#repl-console) Try typing simple commands in the REPL. Since the input can be multi-lines, press `Enter` twice or click "Run" button to run the code. Use `Up` and `Down` 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 which 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 ``` ## Save your Scripts[~](#save-your-scripts) Berry can autostart your scripts. See a short description in the Section about the filesystem: https://tasmota.github.io/docs/UFS/#autoexecbe Your can use the Filemanager to edit or save files with your berry scripts. ## Iterate without rebooting[~](#iterate-without-rebooting) Since v13.0.0.1 you can restart the entire Berry VM with a click in the Berry console. This feature requires to compile with `#define USE_BERRY_DEBUG` which is anyways highly recommended when coding in Berry. Be aware that restarting the Berry VM loses all context, and may generate negative side effects that we haven't yet identified. When restarting the VM, `autoexec.be` is ran again. Instead of using the Web UI, you can also use the `BrRestart` command which does not require `#define USE_BERRY_DEBUG` . ## Lights and Relays[~](#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 representing the state of each relays and light (light comes last). `tasmota.set_power(relay, onoff)` changes the state of a single relay/light. 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 | `boolean` Turns 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..255` Set the overall brightness. Be aware that the range is `0..255` and not `0..100` as Dimmer. | | hue | `int 0..360` Set the color Hue in degree, range 0..360 (0=red). | | sat | `int 0..255` Set the color Saturation (0 is gray). | | ct | `int 153..500` Set the white color temperature in mired, ranging from 153 (cold white) to 500 (warm white) | | rgb | `string 6 hex digits` Set the color as hex `RRGGBB` , changing color and brightness. | | channels | `array of int, ranges 0..255` Set 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` , `channels` , `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]} # changing 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]} ``` ## Rules[~](#rules) The rule function have the general form below where parameters are optional: ``` 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` | `map` Berry structured object of the message, decoded from JSON. If JSON was invalid, it contains the original string | Dimmer rule Define the function and add a rule to Tasmota where the function runs if Dimmer value is more than 50 ``` python > 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 function can be used with multiple triggers. 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: ``` python import string def rule_adc(value, trigger) var i=string.find(trigger,"#A") var tr=string.split(trigger,i+2) var 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 ``` python 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 ) ``` ### Multiple triggers AND logic[~](#multiple-triggers-and-logic) It is possible to combine multiple triggers in a AND logic as an array: ``` tasmota.add_rule(["ANALOG#A1>300","ANALOG#A1<500"], def (values) rule_adc_in_range(1,values) end ) ``` `300 < ANALOG#A1 < 500` Triggers can be of different types too: ``` tasmota.add_rule(["ANALOG#A1>300","BME280#Temperature>28.0"], def (values) rule_adc_and_temp(1,values) end ) ``` `ANALOG#A1>300` AND `BME280#Temperature>28.0` In that case, the value and trigger arguments passed to the rule function are also lists: ``` python def function_name(values:list_of_string, triggers:list_of_string, msg) end ``` `msg` remains unchanged. ### Teleperiod rules[~](#teleperiod-rules) Teleperiod rules are supported with a different syntax from Tasmota rules. Instead of using `Tele-` prefix, you must use `Tele#` . For example `Tele#ANALOG#Temperature1` instead of `Tele-ANALOG#Temperature1` ### Rules operators[~](#rules-operators) | Operator | Function | |---|---| String Operators | | `=` | equal to (used for string comparison) | `!==` | not equal to (used for string comparison) | `$<` | string starts with | `$>` | string ends with | `$\|` | string contains | `$!` | string is not equal to | `$^` | string do not contains | Numerical Operators | | `==` | equal to (used for numerical comparison) | `>` | greater than | `<` | lesser than | `!=` | number not equal to | `>=` | greater than or equal to | `<=` | lesser than or equal to | `\|` | Modulo division to this number is `0` (remainder=0) | ## Timers[~](#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>)` ``` python > def t() print("Booh!") end > tasmota.set_timer(5000, t) [5 seconds later] Booh! ``` Timers are scheduled roughly within 50 milliseconds ticks. This means that you cannot have better than 50 ms resolution, and `set_timer(0, <function>` ) will schedule the function 50 ms later. In certain cases, you need to defer a function to an immediate future; for such case use `tasmota.defer(<function>)` which will run the function typically within the next millisecond. #### A word on functions and closure[~](#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 Berry functions. `cron` recurrent calls[~](#cron-recurrent-calls) You can choose to run some function/closure at regular intervals specified as `cron` style format with the first field representing seconds. ``` python > def f() print("Hi") end > tasmota.add_cron("*/15 * * * * *", f, "every_15_s") Hi Hi # added every 15 seconds > tasmota.remove_cron("every_15_s") # cron stops ``` Like timers, you need to create a closure if you want to register a method of an instance. Example: ``` python class A var name def init(name) self.name = name end def p() print("Hi,", self.name) end end php > bob = A("bob") > bob.p() Hi, bob > tasmota.add_cron("*/15 * * * * *", /-> bob.p(), "hi_bob") Hi, bob Hi, bob Hi, bob > tasmota.remove_cron("hi_bob") # cron stops ``` You can get the timestamp for the next event by using `tasmota.next_cron(id)` which returns an epoch in seconds. ## Loading Filesystem[~](#loading-filesystem) Berry files can exist in 2 forms, either a source file (extension `.be` ) or a pre-compiled bytecode (extension `.bec` ). Pre-compiled are usually smaller and load slightly faster (although compilation is fast enough in most use cases). It's usually more flexible and simpler to use source code (`.be` ). You can upload Berry code in the filesystem using the ** Consoles - Manage File system** menu and load them at runtime. Make careful to use `*.be` extension for those files.To load a Berry file, use the `load(filename)` function where `filename` is the name of the file with `.be` or `.bec` extension; if the file has no extension '.be' is automatically appended. You don't need to prefix with `/` . A leading `/` will be added automatically if it is not present. ## Previous behavior before 13.4.0.3: 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. If a precompiled bytecode (extension `.bec` ) is present of more recent than the Berry source file, the bytecode is directly loaded which is faster than compiling code. You can eventually remove the `*.be` file and keep only `*.bec` file (even with `load("file.be")` . The loading behavior is as follows: `load("hello")` and`load("hello.be")` loads`hello.be` and tries`hello.bec` if the first does not exist. If both`hello.be` and`hello.bec` exist,`hello.bec` is deleted to avoid confusion between versions.`load("hello.bec")` loads only`hello.bec` and fails if only`hello.be` is present To compile to `.bec` use `tasmota.compile("hello.be")` . If all is good, it returns `true` and creates `hello.bec` . But beware that if you use `load()` the `.bec` file is deleted. Note: `tasmota.compile()` is different than native Berry `compile()` ## Creating a Tasmota Driver[~](#creating-a-tasmota-driver) You can easily create a complete Tasmota driver with Berry. A Driver responds to messages from Tasmota. For each message type, the method with the same name is called. Actually you can register any class as a driver, it does not need to inherit from `Driver` ; the call mechanism is based on names of methods that must match the name of the event to be called. Driver methods are called with the following parameters: `f(cmd, idx, payload, raw)` . `cmd` is a string, `idx` an integer, `payload` a Berry object representation of the JSON in `payload` (if any) or `nil` , `raw` is a string. These parameters are meaningful to a small subset of events: `every_second()` : called every second`every_50ms()` : called every 50ms (i.e. 20 times per second)`every_100ms()` : called every 100ms (i.e. 10 times per second)`every_250ms()` : called every 250ms (i.e. 4 times per second)`web_sensor()` : display sensor information on the Web UI`json_append()` : display sensor information in JSON format for TelePeriod reporting`web_add_button()` : (deprecated) synonym of`web_add_console_button()` `web_add_main_button()` ,`web_add_management_button()` ,`web_add_console_button()` ,`web_add_config_button()` : add a button to Tasmota's Web UI on a specific page`web_add_handler()` : called when Tasmota web server started, and the right time to call`webserver.on()` to add handlers`save_before_restart()` : called just before a restart`mqtt_data(topic, idx, data, databytes)` : called for MQTT payloads matching`mqtt.subscribe` .`idx` is zero, and`data` is normally unparsed JSON.`set_power_handler(cmd, idx)` : called whenever a Power command is made.`idx` is a combined index value, with one bit per relay or light currently on.`cmd` can be ignored.`display()` : called by display driver with the following subtypes:`init_driver` ,`model` ,`dim` ,`power` .`button_pressed(cmd, idx)` : called when a button is pressed. See[sample code](https://github.com/arendst/Tasmota/pull/21711#issuecomment-2198649833).`button_multi_pressed(cmd, idx)` : called for button multi-press. See[sample code](https://github.com/arendst/Tasmota/pull/21711#issuecomment-2198649833).`any_key(cmd, idx)` : called when an interaction with Button or Switch occurs.`idx` is encoded as follows:`device_save << 24 | key << 16 | state << 8 | device` See[sample code](https://github.com/arendst/Tasmota/pull/21711#issuecomment-2198649833). Then register the driver with `tasmota.add_driver(<driver>)` . There are basically two ways to respond to an event: **Example** Define a class and implement methods with the same name as the events you want to respond to. ``` python class MyDriver def every_second() # do something end end d1 = MyDriver() tasmota.add_driver(d1) ``` ## Fast Loop[~](#fast-loop) Beyond the events above, a specific mechanism is available for near-real-time events or fast loops (200 times per second, or 5ms). Special attention is made so that there is no or very little impact on performance. Until a first callback is registered, performance is not impacted and Berry is not called. This protects any current use from any performance impact. Once a callback is registered, it is called separately from Berry drivers to ensure minimal overhead. `tasmota.add_fast_loop(cl:function) -> nil` registers a callback to be called in fast loop mode. The callback is called without any parameter and does not need to return anything. The callback is called at each iteration of Tasmota event loop. The frequency is set to 200Hz or 5ms. Note: since v13.1.0.2, the frequency of `fast_loop` does not depend anymore on the value of the `Sleep <x>` command. `tasmota.remove_fast_loop(cl:function) -> nil` removes a previously registered function or closure. You need to pass the exact same closure reference. Warning, if you need to register a method from an instance, you need a closure: ``` python class my_driver def every_100ms() # called every 100ms via normal way end def fast_loop() # called at each iteration, and needs to be registered separately and explicitly end def init() # register fast_loop method tasmota.add_fast_loop(/-> self.fast_loop()) # variant: # tasmota.add_fast_loop(def () self.fast_loop() end) end end tasmota.add_driver(my_driver()) # register driver tasmota.add_fast_loop(/-> my_driver.fast_loop()) # register a closure to capture the instance of the class as well as the method ``` ## Tasmota Only Extensions[~](#tasmota-only-extensions) `log(msg:string [, level:int = 3]) -> string` [~](#logmsgstring-levelint-3-string) Logs a message to the Tasmota console. Optional second argument is log_level (0..4), default is `2` (matching build time `LOG_LEVEL_INFO` ). Example ``` > log("A") A ``` `load(filename:string) -> bool` [~](#loadfilenamestring-bool) Loads a Berry script from the filesystem, and returns true if loaded successfully, false if file not found, or raises an exception in runtime. Filename does not need to start with `/` , but needs to end with `.be` (Berry source code) or `.bec` (precompiled bytecode). If the `.be` extension is missing, it is automatically added. The behavior for `.bec` files changed in v13.4: - when loading `<file>.be` , the `.be` file is loaded in priority and any `.bec` file with same prefix is removed (to avoid inconsistencies). If no `.be` file is present, it tried to load `.bec` file. - when loading `<file>.bec` , only `.bec` files are loaded and `.be` is ignored. - to create `.bec` files, you need to use `tasmota.compile` (see below) `tasmota.compile(filename:string) -> bool` [~](#tasmotacompilefilenamestring-bool) Loads a `.be` file, compiles it and saves a `.bec` file containing the compiled bytecode. Note: `tasmota.compile` is different from Berry native `compile` function. `save(filename:string, f:closure) -> nil` [~](#savefilenamestring-fclosure-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. There is generally no need to use this function, it is used internally by `load()` . `tasmota` object[~](#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-used-to-retrieve-tasmota-configuration) #### Functions for time, timers or cron[~](#functions-for-time-timers-or-cron) #### Functions to create custom Tasmota command[~](#functions-to-create-custom-tasmota-command) #### Functions to add custom responses to JSON and Web UI to sensors[~](#functions-to-add-custom-responses-to-json-and-web-ui-to-sensors) See examples in the [Berry-Cookbook](../Berry-Cookbook/#adding-commands-to-tasmota) #### Functions to manage Relays and Lights[~](#functions-to-manage-relays-and-lights) #### Low-level access to Tasmota globals and settings.[~](#low-level-access-to-tasmota-globals-and-settings) *Use with care and only if you know what you are doing.* The construct is to use `tasmota.global` or `tasmota.settings` to read or write attributes. You can do bad things with these features | Value | Details | |---|---| | tasmota.global.sleep | Current sleep value | | tasmota.global.devices_present | Number of Power channels, e.g. having virtual relays | | tasmota.settings.sleep | Sleep value stored in flash | `mqtt` module[~](#mqtt-module) Use with `import mqtt` . Since v11.1.0.1, there is an easier way than registering a driver, and listening to `mqtt_data` event. You can now just attach a function or closure to a MQTT topic, and it does the magic for you. The function you attach to a topic pattern received **only** the matching MQTT messages, not all messages unlike `mqtt_data()` would. The function takes the same parameters as `mqtt_data()` : `topic` : full topic received from the broker`idx` : not used`payload_s` : payload as string, usually converted to JSON with`import json json.load(payload_s)` `payload_b` : payload as a binary payload, bytes() array the function should return `true` if the event was parsed or if the event should not trigger a Tasmota command. If you return `nil` or nothing, it is considered as `true` which is the usual behavior you want (i.e. not trigger a Tasmota command from random MQTT messages). `light` object[~](#light-object) Module `light` is automatically imported via a hidden `import light` command. `gpio` module[~](#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 physical 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 GPIO mode. Any internal error or using unsupported GPIO yields a Berry exception. ## 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.VL53LXX_XSHUT1` , `gpio.MAX7219CLK` , `gpio.MAX7219DIN` , `gpio.MAX7219CS` , `gpio.TFMINIPLUS_TX` , `gpio.TFMINIPLUS_RX` , `gpio.ZEROCROSS` , `gpio.HALLEFFECT` , `gpio.EPD_DATA` , `gpio.GPIO_INPUT` , `gpio.KEY1_PD` , `gpio.KEY1_INV_PD` , `gpio.SWT1_PD` , `gpio.I2S_OUT_DATA` , `gpio.I2S_OUT_CLK` , `gpio.I2S_OUT_SLCT` , `gpio.I2S_IN_DATA` , `gpio.I2S_IN_CLK` , `gpio.I2S_IN_SLCT` , `gpio.INTERRUPT` , `gpio.MCP2515_CS` , `gpio.HRG15_TX` , `gpio.VINDRIKTNING_RX` , `gpio.BL0939_RX` , `gpio.BL0942_RX` , `gpio.HM330X_SET` , `gpio.HEARTBEAT` , `gpio.HEARTBEAT_INV` , `gpio.SHIFT595_SRCLK` , `gpio.SHIFT595_RCLK` , `gpio.SHIFT595_OE` , `gpio.SHIFT595_SER` , `gpio.SOLAXX1_RTS` , `gpio.OPTION_E` , `gpio.SDM230_TX` , `gpio.SDM230_RX` , `gpio.ADC_MQ` , `gpio.CM11_TXD` , `gpio.CM11_RXD` , `gpio.BL6523_TX` , `gpio.BL6523_RX` , `gpio.ADE7880_IRQ` , `gpio.RESET` , `gpio.MS01` , `gpio.SDIO_CMD` , `gpio.SDIO_CLK` , `gpio.SDIO_D0` , `gpio.SDIO_D1` , `gpio.SDIO_D2` , `gpio.SDIO_D3` , `gpio.FLOWRATEMETER_SIGNAL` , `gpio.SENSOR_END` An H-bridge is an electronic circuit that switches the polarity of a voltage applied to a load. These circuits are often used in robotics and other applications to allow DC motors to run forwards or backwards. See the Berry cookbook for [H-bridge control](../Berry-Cookbook/#h-bridge-control) ### DAC GPIOs[~](#dac-gpios) DAC is limited to specific GPIOs: - ESP32: only GPIO 25-26 - ESP32-S2: only GPIO 17-18 - ESP32-C3: not supported Example ``` > gpio.pin_mode(25, gpio.DAC) # sets GPIO25 to a DAC pin > gpio.dac_voltage(25, 1250) # set voltage to 1250mV 1255 ``` ### I2S[~](#i2s) DAC can also be used via `Esp8266Audio` through the ESP32 I2S -> DAC bridge. ## Example ``` js class MP3_Player : Driver var audio_output, audio_mp3, fast_loop_closure def init() self.audio_output = AudioOutputI2S() self.audio_mp3 = AudioGeneratorMP3() self.fast_loop_closure = def () self.fast_loop() end tasmota.add_fast_loop(self.fast_loop_closure) end def play(mp3_fname) if self.audio_mp3.isrunning() self.audio_mp3.stop() end var audio_file = AudioFileSourceFS(mp3_fname) self.audio_mp3.begin(audio_file, self.audio_output) self.audio_mp3.loop() #- start playing now -# end def fast_loop() if self.audio_mp3.isrunning() if !self.audio_mp3.loop() self.audio_mp3.stop() tasmota.remove_fast_loop(self.fast_loop_closure) end end end end mp3_player = MP3_Player() mp3_player.play("/pno-cs.mp3") ``` `energy` module[~](#energy-module) The `energy` module provides ways to read current energy counters and values (if you're creating your own automation) or updating the energy counters (if you're writing a driver). It relies on a new Berry feature that provides a direct mapping between the internal `C` structure called `struct Energy` and the `energy` module in Berry. For example, if you want to read or update an energy value: ``` > energy.active_power 0 > energy.active_power = 460 > energy.active_power 460 # internally it updates the C value `Energy.active_power[0]` (float) ``` You don't need to do `import energy` since Tasmota does it for you at boot. The special `energy.read()` function dumps all current values to a single `map` . Be aware that the object is very long. Prefer accessing individual attributes instead. List of `energy` attributes that you can read or write: | Attribute | Type | Description | |---|---|---| | voltage | float | Voltage (V) for main phase | | voltage_phases | array of float | Voltage (V) as an array of phases | | current | float | Current (A) for main phase | | current_phases | array of float | Current (A) as an array of phases | | active_power | float | Active Power (W) for main phase | | active_power_phases | array of float | Active Power (W) as an array of phases | | reactive_power | float | Reactive Power (W) for main phase | | reactive_power_phases | array of float | Reactive Power (W) as an array of phases | | power_factor | float | Power Factor (no unit) for main phase | | power_factor_phases | array of float | Power Factor (no unit) as an array of phases | | frequency | float | Frequency (Hz) for main phase | | frequency_phases | array of float | Frequency (Hz) as an array of phases | | export_active | float | (kWh) | | export_active_phases | array of float | (kWh) | | start_energy | float | Total previous energy (kWh) | | daily | float | Daily energy (kWh) | | total | float | Total energy (kWh) | | today_delta_kwh | uint32 | (deca milli Watt hours) 5764 = 0.05764 kWh = 0.058 kWh | | today_offset_kwh | uint32 | (deca milli Watt hours) | | today_kwh | uint32 | (deca milli Watt hours) | | period | uint32 | (deca milli Watt hours) | | fifth_second | uint8 | | | command_code | uint8 | | | data_valid | uint8 | `0` if data is valid for main phase | | data_valid_phases | array of uint8 | `0` if data is valid as an array of phases | | phase_count | uint8 | Number of phases (1..8) | | voltage_common | bool | Use single voltage | | frequency_common | bool | Use single frequency | | use_overtemp | bool | Use global temperature as overtemp trigger on internal energy monitor hardware | | today_offset_init_kwh | bool | | | voltage_available | bool | Enable if voltage is measured | | current_available | bool | Enable if current is measured | | type_dc | bool | | | power_on | bool | | | power_history_0 power_history_1 power_history_2 | uint16 | | | power_steady_counter | uint8 | Allow for power on stabilization | | min_power_flag | bool | | | max_power_flag | bool | | | min_voltage_flag | bool | | | max_voltage_flag | bool | | | min_current_flag | bool | | | max_current_flag | bool | | | mplh_counter | uint16 | | | mplw_counter | uint16 | | | mplr_counter | uint8 | | | max_energy_state | uint8 | ### Energy driver in Berry[~](#energy-driver-in-berry) Since v14.2.0, it is possible to implement an Energy driver in pure Berry. The Berry driver is enabled when an `OPTION_A 9` GPIO is configured: - by default, the energy driver has zero consumption. - the berry code can is `energy.driver_enabled()` to check if the virtual Berry Energy driver is active (i.e.`OPTION_A 9` is configured) - the following values need to be configured: `energy.phase_count` (default`1` ),`energy.voltage` ,`energy.current` ,`energy.power_factor` (typically`1.0` or less),`energy.frequency` (default`nan` ) - the most important value is `energy.active_power` (in Watt) which is added to the daily power consumption Example test code in `autoexec.be` : ``` if energy.driver_enabled() energy.phase_count = 1 energy.voltage = 240 energy.power_factor = 1.0 energy.current = 1.5 energy.frequency = 50 energy.active_power = 360 end ``` `wire` object for I2C[~](#wire-object-for-i2c) 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. MPU6886 on bus 2 ``` > mpuwire = tasmota.wire_scan(0x68, 58) > mpuwire <instance: Wire()> ``` Low-level commands if you need finer control: `path` module[~](#path-module) A simplified version of `os.path` module of standard Berry which is disabled in Tasmota because we don't have a full OS. The default file-system is the ESP32 internal flash. If you have a SD card mounted, it is mapped to the `/sd/` subdirectory. Example: ``` python import path print(path.listdir("/sd/")) # outputs a list of filenames at the root dir of the SD card ``` `persist` module[~](#persist-module) Easy way to persist simple values in Berry and read/write any attribute. Values are written in JSON format in `_persist.json` file. Be aware that `persist` cannot detect any change in sub-objects like lists or maps; in such case you can call `persist.dirty()` to indicate that data needs to be saved. Example ``` python > import persist > persist.a = 1 > persist.b = "foobar" > print(persist) <instance: Persist({'a': 1, 'b': 'foobar'})> > persist.save() # save to _persist.json ``` `introspect` module[~](#introspect-module) Allows to do introspection on instances and modules, to programmatically list attributes, set and get them. ``` python > class A var a,b def f() return 1 end end > ins=A() > ins.a = "foo" > import introspect > introspect.members(ins) ['b', 'a', 'f'] > introspect.get(ins, "a") foo > introspect.set(ins, "a", "bar") bar > ins.a bar ``` `webclient` class[~](#webclient-class) Class `webclient` provides an implementation of an HTTP/HTTPS web client and make requests on the LAN or over the Internet. Features: - Support HTTP and HTTPS requests to IPv4 addresses and domain names, to arbitrary ports, via a full URL. - Support for HTTPS and TLS via BearSSL (which is much lighter than default mbedTLS) - HTTPS (TLS) only supports cipher ECDHE_RSA_WITH_AES_128_GCM_SHA256 which is both secure and widely supported - Support for URL redirections - Ability to set custom User-Agent - Ability to set custom headers - Ability to set Authentication header - Support for Chunked encoding response (so works well with Tasmota devices) - Support for `GET` ,`POST` ,`PUT` ,`PATCH` ,`DELETE` methods The current implementation is based on a fork of Arduino's HttpClient customized to use BearSSL Current limitations (if you need extra features please open a feature request on GitHub): - Payload sent to server ( `POST` ) can include either text or binary - Only supports text responses (html, json...) but not binary content yet (no NULL char allowed). However you can download binary content to the file-system with `write_file` - Maximum response size is 32KB, requests are dropped if larger - HTTPS (TLS) is in 'insecure' mode and does not check the server's certificate; it is subject to Man-in-the-Middle attack - No support for compressed response - this should not be a problem since the client does not advertise support for compressed responses Example ``` > cl = webclient() > cl.begin("http://ota.tasmota.com/tasmota32/release/") <instance: webclient()> > r = cl.GET() > print(r) 200 > s = cl.get_string() > print(s) Alternative firmware for ESP32 based devices with web UI, [.../...] ``` Example ``` > cl = webclient() > cl.begin("https://raw.githubusercontent.com/tasmota/autoconf/main/esp32/M5Stack_Fire_autoconf.zip") <instance: webclient()> > r = cl.GET() > print(r) 200 > cl.write_file("M5Stack_Fire_autoconf.zip") 950 ``` #### Managing redirects[~](#managing-redirects) HTTP redirects (301/302) are not followed by default. You can use `wc.set_follow_redirects(true)` to have redirects automatically followed for HEAD and GET. There is a default limit of 10 successive redirects, this prevents from infinite loops. For the examples, we use `http://ota.tasmota.com/tasmota32` which is redirected to `http://ota.tasmota.com/tasmota32/` Example ``` cl = webclient() cl.set_follow_redirects(true) cl.begin("http://ota.tasmota.com/tasmota32") r = cl.GET() print(r) s = cl.get_string() print(s) ``` Alternatively, you can manage yourself redirects and retrieve the `Location` header Example ``` cl = webclient() cl.set_follow_redirects(false) cl.collect_headers("Location") cl.begin("http://ota.tasmota.com/tasmota32") r = cl.GET() print(r) if r == 301 || r == 302 print("Location:", cl.get_header("Location")) elif r == 200 s = cl.get_string() print(s) end cl.close() ``` Main functions: Request customization: Static utility methods: | webclient static method | Parameters and details | |---|---| | url_encode | `(url:string) -> string` Encodes a string according to URL escape rules. Use before you use `begin()` | `webserver` module[~](#webserver-module) Module `webserver` provides functions to enrich Tasmota's Web UI. It is tightly linked to Tasmota page layout. Functions used to add UI elements like buttons to Tasmota pages, and analyze the current request. See above `Driver` to add buttons to Tasmota UI. Low-level functions if you want to display custom pages and content: Module `webserver` also defines the following constants: - Tasmota's web server states: `webserver.HTTP_OFF` ,`webserver.HTTP_USER` ,`webserver.HTTP_ADMIN` ,`webserver.HTTP_MANAGER` ,`webserver.HTTP_MANAGER_RESET_ONLY` - Tasmota's pages: `webserver.BUTTON_CONFIGURATION` ,`webserver.BUTTON_INFORMATION` ,`webserver.BUTTON_MAIN` ,`webserver.BUTTON_MANAGEMENT` ,`webserver.BUTTON_MODULE` - Methods received by handler: `webserver.HTTP_ANY` ,`webserver.HTTP_GET` ,`webserver.HTTP_OPTIONS` ,`webserver.HTTP_POST` See the [Berry Cookbook](../Berry-Cookbook/) for examples. `tcpclient` class[~](#tcpclient-class) Simple TCP client supporting string and binary transfers: - create an instance of the client with `var tcp = tcpclient()` - connect to the server `tcp.connect(address:string, port:int [, timeout_ms:int]) -> bool` Address can be numerical IPv4 or domain name. Returns`true` if the connection succeeded. Optional`timeout` in milliseconds. The default timeout is`USE_BERRY_WEBCLIENT_TIMEOUT` (2 seconds). - check if the socket is connected with `tcp.connected()` - send content with `tcp.write(content:string or bytes) -> int` . Accepts either a string or a bytes buffer, returns the number of bytes sent. It's your responsibility to resend the missing bytes - check if bytes are available for reading `tcp.available() -> int` . Returns`0` if nothing was received. This is the call you should make in loops for polling. - read incoming content as string `tcp.read() -> string` or as bytes`tcp.readbytes() -> bytes` . It is best to call`tcp.available()` first to avoid creating empty response objects when not needed - close the socket with `tcp.close()` Full example: ``` tcp = tcpclient() tcp.connect("192.168.2.204", 80) print("connected:", tcp.connected()) s= "GET / HTTP/1.0\r\n\r\n" tcp.write(s) print("available1:", tcp.available()) tasmota.delay(100) print("available1:", tcp.available()) r = tcp.read() tcp.close() print(r) ``` `tcpclientasync` class[~](#tcpclientasync-class) Variant of `tcpclient` using only non-blocking calls in full asynchronous mode. This allows to have multiple concurrent connections with fine-grained control over timeouts and no blocking of Tasmota. This is especially useful for Matter Border Router for ESP8266 Tasmota based devices via HTTP. All calls return immediately, so you need to poll the API periodically to send/receive data, and manage timeouts yourself. Typical sequence: - create an instance of the client with `var tcp = tcpclientasync()` - connect to the server `tcp.connect(address:string, port:int) -> bool` . Address should be numerical IPv4 or IPv6 if you want the call to return immediately (i.e. do DNS resolution ahead of time), otherwise a DNS resolution might take some time and fail. If DNS failed, this call returns`false` . - regularly call `connected()` waiting for`true` to detect when the connection is established. While`connected()` returns`nil` then connection is in-progress. If`connected()` changes to`false` then the connection was refused by the host. - if the connection is not established after a definite amount of time, you should declare 'timeout' and call `close()` - to send data: first call `listening()` to ensure that the socket is ready to send data. Note: the socket is always listening when the connection was just established. Then call`write()` to send you data (string or bytes), this call returns the actual amount of data sent; if it is lower than your content, you need to handle yourself re-sending the remaining data. Note: ensuring that you send less than the MTU should keep you from happening (~1430 bytes max). - to receive data: first call `available()` to check if some data is ready to be received. Then call`read()` or`readbytes()` to get the buffer as string or bytes. You can limit the amount of data received, but in such case, the extra data is discarded and lost. - regularly call `connected()` to check if the connection is still up - finally call `close()` to close the connection on your side and free resources. It is implicitly called if the connection was closed from the peer. Full example: ``` python def try_connect(addr, port) import string var tcp = tcpclientasync() var now = tasmota.millis() var r = tcp.connect(addr, port) print(string.format("Time=%5i state=%s", tasmota.millis()-now, str(tcp.connected()))) print(tcp.info()) tasmota.delay(50) print(string.format("Time=%5i state=%s", tasmota.millis()-now, str(tcp.connected()))) print(tcp.info()) tasmota.delay(150) print(string.format("Time=%5i state=%s", tasmota.millis()-now, str(tcp.connected()))) print(tcp.info()) tasmota.delay(500) print(string.format("Time=%5i state=%s", tasmota.millis()-now, str(tcp.connected()))) print(tcp.info()) return tcp end tcp = try_connect("192.168.1.19", 80) ``` `tcpserver` class[~](#tcpserver-class) Simple TCP server (socket) listening for incoming connection on any port. - create an instance of the `tcpserver` on a specific port with`s = tcpserver(8888)` - periodically call `s.hasclient()` to know if a new client has connected - if the previous returned `true` , call`var c = s.accept()` or`var c = s.acceptasync()` to accept the connection. It returns an instance of`tcpclient` or`tcpclientasync` ; it responds to the same APIs as outgoing TCP connection and allows text and binary transfers. - you can call `c.close()` to close the connection, or call`c.connected()` to know if it's still connected (i.e. the client hasn't closed the connection on their side) - close the server with `s.close()` . This will prevent the server from receiving any new connection, but existing connections are kept alive. Full example: ``` > s = tcpserver(8888) # listen on port 8888 > s.hasclient() false # in parallel connect on this port with `nc <ip_address> 8888` > s.hasclient() true # we have an incoming connection > c = s.accept() > c <instance: tcpclient()> # send 'foobar' from the client > c.read() foobar # send 'foobar2' again from the client > c.readbytes() bytes('666F6F626172320A') > c.close() # this closes the connection ``` `udp` class[~](#udp-class) Class `udp` provides ability to send and received UDP packets, including multicast addresses. You need to create an object of class `udp` . Such object can send packets and listen to local ports. If you don't specify a local port, the client will take a random source port. Otherwise the local port is used as source port. When creating a local port, you need to use `udp->begin(<ip>, <port)>` . If `<ip>` is empty string `""` then the port is open on all interfaces (wifi and ethernet). #### Sending udp packets[~](#sending-udp-packets) ``` > u = udp() > u.begin("", 2000) # listen on all interfaces, port 2000 true > u.send("192.168.1.10", 2000, bytes("414243")) # send 'ABC' to 192.168.1.10:2000, source port is 2000 true ``` #### Receive udp packets[~](#receive-udp-packets) You need to do polling on `udp->read()` . If no packet was received, the call immediately returns `nil` . ``` > u = udp() > u.begin("", 2000) # listen on all interfaces, port 2000 true > u.read() # if no packet received, returns `nil` > > u.read() # if no packet received, returns `nil` bytes("414243") # received packet as `bytes()` ``` #### Simple UDP server printing received packets[~](#simple-udp-server-printing-received-packets) ``` python class udp_listener var u def init(ip, port) self.u = udp() print(self.u.begin_multicast(ip, port)) tasmota.add_driver(self) end def every_50ms() import string var packet = self.u.read() while packet != nil tasmota.log(string.format(">>> Received packet ([%s]:%i): %s", self.u.remote_ip, self.u.remote_port, packet.tohex()), 2) packet = self.u.read() end end end # listen on port 2000 for all interfaces # udp_listener("", 2000) ``` #### Send and receive multicast[~](#send-and-receive-multicast) IPv4 example, using the `udp_listener` listener above. On receiver side: ``` udp_listener("224.3.0.1", 2000) ``` On sender side: ``` u = udp() u.begin_multicast("224.3.0.1", 2000) u.send_multicast(bytes().fromstring("hello")) # alternatively u = udp() u.begin("", 0) # send on all interfaces, choose random port number u.send("224.3.0.1", 2000, bytes().fromstring("world")) ``` The receiver will show: ``` >>> Received packet ([192.168.x.x]:2000): 68656C6C6F >>> Received packet ([192.168.x.x]:64882): 776F726C64 ``` This works the same with IPv6 using an address like "FF35:0040:FD00::AABB" `mdns` module[~](#mdns-module) Module `import mdns` support for mDNS (Multicast DNS, aka Bonjour protocol) announces. This is needed for Matter Wifi support. This feature requires `#define USE_DISCOVERY` compile option (not included in standard builds). Example (announce of a Matter Wifi device): ``` python import mdns mdns.start() mdns.add_service("_matterc","_udp", 5540, {"VP":"65521+32768", "SII":5000, "SAI":300, "T":1, "D":3840, "CM":1, "PH":33, "PI":""}) ``` ### Addressable LEDs (WS2812, SK6812)[~](#addressable-leds-ws2812-sk6812) There is native support for addressable LEDs via NeoPixelBus, with support for animations. Currently supported: WS2812, SK6812. Details are in [Berry LEDs](../Berry_Addressable-LED/) `serial` class[~](#serial-class) The `serial` class provides a low-level interface to hardware UART. The serial GPIOs don't need to be configured in the template. Example ``` # gpio_rx:4 gpio_tx:5 ser = serial(4, 5, 9600, serial.SERIAL_7E1) ser.write(bytes(203132)) # send binary 203132 ser.write(bytes().fromstring("Hello)) # send string "Hello" msg = ser.read() # read bytes from serial as bytes print(msg.asstring()) # print the message as string ``` Supported serial message formats: `SERIAL_5N1` , `SERIAL_6N1` , `SERIAL_7N1` , `SERIAL_8N1` , `SERIAL_5N2` , `SERIAL_6N2` , `SERIAL_7N2` , `SERIAL_8N2` , `SERIAL_5E1` , `SERIAL_6E1` , `SERIAL_7E1` , `SERIAL_8E1` , `SERIAL_5E2` , `SERIAL_6E2` , `SERIAL_7E2` , `SERIAL_8E2` , `SERIAL_5O1` , `SERIAL_6O1` , `SERIAL_7O1` , `SERIAL_8O1` , `SERIAL_5O2` , `SERIAL_6O2` , `SERIAL_7O2` , `SERIAL_8O2` `display` module[~](#display-module) The `display` module provides a simple API to initialize the Universal Display Driver with data provided as a string. It is used by `autoconf` mechanism. `uuid` module[~](#uuid-module) The `uuid` module allows to generate uuid4 random ids. ``` python > import uuid > uuid.uuid4() 1a8b7f78-59d8-4868-96a7-b7ff3477d43f ``` | Tasmota Function | Parameters and details | |---|---| | uuid4 | `uuid.uuid4() -> string` Generates a uuid4 random id as string. | `crc` module[~](#crc-module) The `crc` module allows to compute crc32/16/8 from bytes() arrays. ``` python > import crc > crc.crc32(0xFFFFFFFF, bytes("AABBCC")) -1091314015 > crc.crc16(0xFFFF, bytes("AABBCC")) 20980 > crc.crc8(0xFF, bytes("AABBCC")) 139 ``` `tasmota_log_reader` class[~](#tasmota_log_reader-class) The `tasmota_log_reader` class allows you to read and potentially parse the Tasmota logs. It keeps track of what logs were already read in the past and feeds you with new log lines if some are available. It is for example used by the LVGL `tasmota_log` widget to display logs on a display. Note: calling `tasmota_log_reader` can be expensive in string allocations, and adds pressure on the garbage collector. Use wisely. Example: ``` js var lr = tasmota_log_reader() # do this regularly var ret = lr.get_log(2) # read at log level 2 if ret != nil var lines = r.split('\n') # extract as a list of lines # do whatever you need end ``` `ULP` module[~](#ulp-module) The `ULP` module exposes the third computing unit of the ESP32, which is a simple finite state machine (FSM) that is designed to perform measurements using the ADC, temperature sensor and even external I2C sensors. This small ultra low power coprocessor can run in parallel to the main cores and in deep sleep mode, where it is capable to wake up the system, i.e. in reaction to sensor measurements. The binding to Berry consists of some lightweight wrapper functions and the communication with the main cores works by accessing the RTC_SLOW_MEM from both sides, which is the same way as in any other ESP32 ULP project. ``` python # simple LED blink example import ULP ULP.wake_period(0,500000) # off time ULP.wake_period(1,200000) # on time c = bytes("756c70000c006c00000000001000008000000000000000000000000010008072010000d0e5af2c72340040802705cc190005681d10008072e1af8c720100006821008072040000d0120080720800207004000068010005825c0000800405681d00000092680000800505681d0100009268000080000000b0") ULP.load(c) ULP.run() ``` More information (including suggestions for a toolchain) on the [ULP page](../ULP/). `re` regex module[~](#re-regex-module) Use with `import re` . There are two ways to use regex, first is to call directly the module which triggers a compilation of the regex at each call. The second one is to pre-compile the regex once into an object which is much more efficient if you need to use the regex multiple times. Any error in the compilation of the regex pattern yields an exception. ``` python > import re # first series are all-in-one, patterns are compiled on the fly # Returns the list of matches, or empty list of no match > re.search("a.*?b(z+)", "zaaaabbbccbbzzzee") ['aaaabbbccbbzzz', 'zzz'] # Returns the list of list of matches > re.searchall('<([a-zA-Z]+)>', '<abc> yeah <xyz>') [['<abc>', 'abc'], ['<xyz>', 'xyz']] # Returns the list of matches, or empty list of no match; must match from the beginning of the string. > re.match("a.*?b(z+)", "aaaabbbccbbzzzee") ['aaaabbbccbbzzz', 'zzz'] # Returns the number of chars matched instead of the entire match (saves memory) > re.match2("a.*?b(z+)", "aaaabbbccbbzzzee") [14, 'zzz'] # Returns the list of matches, or empty list of no match; there should not be any gaps between matches. > re.matchall('<([a-zA-Z]+)>', '<abc> yeah <xyz>') [['<abc>', 'abc']]) > re.matchall('<([a-zA-Z]+)>', '<abc><xyz>') [['<abc>', 'abc'], ['<xyz>', 'xyz']] # Returns the list of strings from split > re.split('/', "foo/bar//baz") ['foo', 'bar', '', 'baz'] # below are pre-compiled patterns, which is much faster if you use the # pattern multiple times # # the compiled pattern is a `bytes()` object that can be used # as a replacement for the pattern string > rb = re.compilebytes('<([a-zA-Z]+)>') # rb is compiled to bytes('1A0000000C0000000100000062030260FB7E00013C7E020302617A415A62F87E03013E7E017F') > re.searchall(rb, '<abc> yeah <xyz>') [['<abc>', 'abc'], ['<xyz>', 'xyz']] > rb = re.compilebytes("/") > rb bytes('0C000000070000000000000062030260FB7E00012F7E017F') > re.split(rb, "foo/bar//baz") ['foo', 'bar', '', 'baz'] > re.split(rb, "/b") ['', 'b'] ``` Note: for `match` and `search` , the first element in the list contains the global match of the pattern. Additional elements correspond to the sub-groups (in parenthesis). The regex engine is based on [re1.5](https://github.com/pfalcon/re1.5) also used in MicroPython. `crypto` module[~](#crypto-module) Module `import crypto` support for common cryptographic algorithms. Currently supported algorithms: - AES CTR 256 bits - requires `#define USE_BERRY_CRYPTO_AES_CTR` - AES GCM 256 bits - AES CCM 128 or 256 bits - AES CBC 128 bits - Elliptic Curve C25519 - requires `#define USE_BERRY_CRYPTO_EC_C25519` - Elliptic Curve P256 (secp256r1) - requires `#define USE_BERRY_CRYPTO_EC_P256` - HKDF key derivation with HMAC SHA256 - requires `#define USE_BERRY_CRYPTO_HKDF_SHA256` - HMAC SHA256 - MD5 - PKKDF2 with HMAC SHA256 key derivation - requires `#define USE_BERRY_CRYPTO_PBKDF2_HMAC_SHA256` - SHA256 - JWT RS256 (RSASSA-PKCS1-v1_5 with SHA256) - requires `#define USE_BERRY_CRYPTO_RSA` `crypto.AES_CTR` class[~](#cryptoaes_ctr-class) Encrypt and decrypt, using AES CTR (Counter mode) with 256 bits keys. Test vectors from [https://datatracker.ietf.org/doc/html/rfc4231](https://datatracker.ietf.org/doc/html/rfc4231) ``` python # Test case from https://www.ietf.org/rfc/rfc3686.txt import crypto key = bytes("F6D66D6BD52D59BB0796365879EFF886C66DD51A5B6A99744B50590C87A23884") iv = bytes("00FAAC24C1585EF15A43D875") cc = 0x000001 aes = crypto.AES_CTR(key) plain = bytes("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F") cipher = aes.encrypt(plain, iv, cc) assert(cipher == bytes("F05E231B3894612C49EE000B804EB2A9B8306B508F839D6A5530831D9344AF1C")) plain2 = aes.decrypt(cipher, iv, cc) assert(plain == plain2) ``` `crypto.AES_GCM` class[~](#cryptoaes_gcm-class) Encrypt, decrypt and verify, using AES GCM (Galois Counter Mode) with 256 bits keys. Example taken from [https://wizardforcel.gitbooks.io/practical-cryptography-for-developers-book/content/symmetric-key-ciphers/aes-encrypt-decrypt-examples.html](https://wizardforcel.gitbooks.io/practical-cryptography-for-developers-book/content/symmetric-key-ciphers/aes-encrypt-decrypt-examples.html) ``` python import crypto key = bytes('233f8ce4ac6aa125927ccd98af5750d08c9c61d98a3f5d43cbf096b4caaebe80') ciphertext = bytes('1334cd5d487f7f47924187c94424a2079656838e063e5521e7779e441aa513de268550a89917fbfb0492fc') iv = bytes('2f3849399c60cb04b923bd33265b81c7') authTag = bytes('af453a410d142bc6f926c0f3bc776390') # decrypt ciphertext with key and iv aes = crypto.AES_GCM(key, iv) plaintext = aes.decrypt(ciphertext) print(plaintext.asstring()) # 'Message for AES-256-GCM + Scrypt encryption' tag = aes.tag() print(tag == authTag) # true ``` `crypto.AES_CCM` class[~](#cryptoaes_ccm-class) Encrypt and decrypt, using AES CCM with 256 bits keys. Example from Matter: ``` # raw_in is the received frame raw_in = bytes("00A0DE009A5E3D0F3E85246C0EB1AA630A99042B82EC903483E26A4148C8AC909B12EF8CDB6B144493ABD6278EDBA8859C9B2C") payload_idx = 8 # unencrypted header is 8 bytes tag_len = 16 # MIC is 16 bytes p = raw[payload_idx .. -tag_len - 1] # payload mic = raw[-tag_len .. ] # MIC a = raw[0 .. payload_idx - 1] # AAD i2r = bytes("92027B9F0DBC82491D4C3B3AFA5F2DEB") # key # p = bytes("3E85246C0EB1AA630A99042B82EC903483E26A4148C8AC909B12EF") # a = bytes("00A0DE009A5E3D0F") n = bytes("009A5E3D0F0000000000000000") # nonce / IV # mic = bytes("8CDB6B144493ABD6278EDBA8859C9B2C") # expected cleartext clr = bytes("05024FF601001536001724020024031D2404031818290324FF0118") # method 1 - with distinct calls import crypto aes = crypto.AES_CCM(i2r, n, a, size(p), 16) cleartext = aes.decrypt(p) tag = aes.tag() assert(cleartext == clr) assert(tag == mic) # method 2 - single call raw = raw_in.copy() # copy first if we want to keep the encrypted version var ret = crypto.AES_CCM.decrypt1(i2r, n, 0, size(n), raw, 0, payload_idx, raw, payload_idx, size(raw) - payload_idx - tag_len, raw, size(raw) - tag_len, tag_len) assert(ret) assert(raw[payload_idx .. -tag_len - 1] == clr) ``` `crypto.AES_CBC` class[~](#cryptoaes_cbc-class) Encrypt and decrypt, using AES CBC with 128 bits keys. Example: ``` js var b = bytes().fromstring("hello world_____") # 16-byte aligned var key = bytes().fromstring("1122334455667788") # 16 bytes var iv = bytes().fromstring("8877665544332211") # 16 bytes print("data:",b.asstring()) # "hello world_____" import crypto aes = crypto.AES_CBC() aes.encrypt1(key, iv, b) print("cipher:",b) iv = bytes().fromstring("8877665544332211") aes.decrypt1(key, iv, b) print("decrypted data:",b.asstring()) # "hello world_____" ``` `crypto.EC_C25519` class[~](#cryptoec_c25519-class) Provides Elliptic Curve C25519 Diffie-Hellman key agreement. Requires `#define USE_BERRY_CRYPTO_EC_C25519` Example from test vectors [https://www.rfc-editor.org/rfc/rfc7748](https://www.rfc-editor.org/rfc/rfc7748): ``` python import crypto # alice side alice_priv_key = bytes("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a") alice_pub_key = bytes("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a") assert(crypto.EC_C25519().public_key(alice_priv_key) == alice_pub_key) # bob side bob_priv_key = bytes("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb") bob_pub_key = bytes("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f") assert(crypto.EC_C25519().public_key(bob_priv_key) == bob_pub_key) # shared key computed by alice ref_shared_key = bytes("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742") alice_shared_key = crypto.EC_C25519().shared_key(alice_priv_key, bob_pub_key) bob_shared_key = crypto.EC_C25519().shared_key(bob_priv_key, alice_pub_key) assert(alice_shared_key == ref_shared_key) assert(bob_shared_key == ref_shared_key) ``` `crypto.EC_P256` class[~](#cryptoec_p256-class) Provides Elliptic Curve Prime256 (secp256r1) Diffie-Hellman key agreement and various functions on P256 curve. Requires `#define USE_BERRY_CRYPTO_EC_P256` Example: ``` python import crypto priv = bytes("f502fb911d746b77f4438c674e1c43650b68285dfcc0583c49cd6ed88f0fbb58") p = crypto.EC_P256() pub = p.public_key(priv) assert(pub == bytes("04F94C20D682DA29B7E99985D8DBA6ABEA9051D16508742899835098B1113D3D749466644C47B559DB184556C1733C33E5788AE250B8FB45F29D4CF48FF752C1ED")) import crypto priv = bytes("4E832960415F2B5FA2B1FDA75C1A8F3C84BAEB189EDC47211EF6D27A21FC0ED8") p = crypto.EC_P256() pub = p.public_key(priv) assert(pub == bytes("042166AE4F89981472B7589B8D79B8F1244E2EEE6E0A737FFBFED2981DA3E193D6643317E054D2A924F2F56F1BF4BECA13192B27D8566AF379FBBF8615A223D899")) print("x=",pub[1..32]) print("y=",pub[33..65]) import crypto p = crypto.EC_P256() priv_A = bytes("f502fb911d746b77f4438c674e1c43650b68285dfcc0583c49cd6ed88f0fbb58") pub_A = bytes("04F94C20D682DA29B7E99985D8DBA6ABEA9051D16508742899835098B1113D3D749466644C47B559DB184556C1733C33E5788AE250B8FB45F29D4CF48FF752C1ED") priv_B = bytes("4E832960415F2B5FA2B1FDA75C1A8F3C84BAEB189EDC47211EF6D27A21FC0ED8") pub_B = bytes("042166AE4F89981472B7589B8D79B8F1244E2EEE6E0A737FFBFED2981DA3E193D6643317E054D2A924F2F56F1BF4BECA13192B27D8566AF379FBBF8615A223D899") shared_1 = p.shared_key(priv_A, pub_B) shared_2 = p.shared_key(priv_B, pub_A) assert(shared_1 == shared_2) ``` `crypto.HKDF_SHA256` class[~](#cryptohkdf_sha256-class) Provides HKDF using HMAC SHA256 key derivation. Turns 'ikm' (input keying material) of low entropy and creates a pseudo random key. Requires `#define USE_BERRY_CRYPTO_HKDF_SHA256` Test vectors from [https://www.rfc-editor.org/rfc/rfc5869](https://www.rfc-editor.org/rfc/rfc5869) ``` python import crypto # Test Case 1 hk = crypto.HKDF_SHA256() ikm = bytes("0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B0B") salt = bytes("000102030405060708090A0B0C") info = bytes("F0F1F2F3F4F5F6F7F8F9") k = hk.derive(ikm, salt, info, 42) assert(k == bytes("3CB25F25FAACD57A90434F64D0362F2A2D2D0A90CF1A5A4C5DB02D56ECC4C5BF34007208D5B887185865")) # Test Case 2 hk = crypto.HKDF_SHA256() ikm = bytes("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f") salt = bytes("606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf") info = bytes("b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff") k = hk.derive(ikm, salt, info, 82) assert(k == bytes("b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87")) # Test Case 3 hk = crypto.HKDF_SHA256() ikm = bytes("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b") salt = bytes() info = bytes() k = hk.derive(ikm, salt, info, 42) assert(k == bytes("8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8")) ``` `crypto.PBKDF2_HMAC_SHA256` class[~](#cryptopbkdf2_hmac_sha256-class) Provides PBKDF2 using HMAC SHA256 key derivation. Turns a password into a hash. Test vectors from [https://github.com/brycx/Test-Vector-Generation/blob/master/PBKDF2/pbkdf2-hmac-sha2-test-vectors.md](https://github.com/brycx/Test-Vector-Generation/blob/master/PBKDF2/pbkdf2-hmac-sha2-test-vectors.md) ``` python import crypto pb = crypto.PBKDF2_HMAC_SHA256() assert(pb.derive("password", "salt", 1, 20) == bytes('120fb6cffcf8b32c43e7225256c4f837a86548c9')) assert(pb.derive("password", "salt", 2, 20) == bytes('ae4d0c95af6b46d32d0adff928f06dd02a303f8e')) assert(pb.derive("password", "salt", 3, 20) == bytes('ad35240ac683febfaf3cd49d845473fbbbaa2437')) assert(pb.derive("password", "salt", 4096, 20) == bytes('c5e478d59288c841aa530db6845c4c8d962893a0')) assert(pb.derive("passwd", "salt", 1, 128) == bytes('55AC046E56E3089FEC1691C22544B605F94185216DDE0465E68B9D57C20DACBC49CA9CCCF179B645991664B39D77EF317C71B845B1E30BD509112041D3A19783C294E850150390E1160C34D62E9665D659AE49D314510FC98274CC79681968104B8F89237E69B2D549111868658BE62F59BD715CAC44A1147ED5317C9BAE6B2A')) ``` `crypto.SHA256` class[~](#cryptosha256-class) Provides SHA256 hashing function Example test vectors from [https://www.dlitz.net/crypto/shad256-test-vectors/](https://www.dlitz.net/crypto/shad256-test-vectors/) ``` python import crypto h = crypto.SHA256() # SHA256 of empty message assert(h.out() == bytes("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")) # (first 16 bytes of RC4 keystream where the key = 0) h.update(bytes("de188941a3375d3a8a061e67576e926d")) assert(h.out() == bytes("067c531269735ca7f541fdaca8f0dc76305d3cada140f89372a410fe5eff6e4d")) ``` `crypto.HMAC_SHA256` class[~](#cryptohmac_sha256-class) Provides HMAC SHA256 hashing function Test case from [https://datatracker.ietf.org/doc/html/rfc4231](https://datatracker.ietf.org/doc/html/rfc4231): ``` python import crypto key = bytes("4a656665") msg = bytes("7768617420646f2079612077616e7420666f72206e6f7468696e673f") h = crypto.HMAC_SHA256(key) h.update(msg) hmac = h.out() assert(hmac == bytes("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843")) ``` `crypto.RSA` class[~](#cryptorsa-class) Provides RSA core features, currently only JWT RS256 signing (RSASSA-PKCS1-v1_5 with SHA256) - requires `#define USE_BERRY_CRYPTO_RSA` Signing a full JWT token with RS256 ``` python import string import crypto # JWT requires base64url and not raw base64 # see https://base64.guru/standards/base64url # input: string or bytes def base64url(v) import string if type(v) == 'string' v = bytes().fromstring(v) end var b64 = v.tob64() # remove trailing padding b64 = string.tr(b64, '=', '') b64 = string.tr(b64, '+', '-') b64 = string.tr(b64, '/', '_') return b64 end # JWT header and claim var header = '{"alg":"RS256","typ":"JWT"}' var claim = '{"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}' var b64header = base64url(header) var b64claim = base64url(claim) assert(b64header == 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9') assert(b64claim == 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0') # `body` is the payload to sign with RS256 var body = b64header + '.' + b64claim assert(body == 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0') var private_key = '-----BEGIN PRIVATE KEY-----\n'+ 'MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj\n'+ 'MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu\n'+ 'NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ\n'+ 'qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg\n'+ 'p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR\n'+ 'ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi\n'+ 'VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV\n'+ 'laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8\n'+ 'sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H\n'+ 'mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY\n'+ 'dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw\n'+ 'ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ\n'+ 'DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T\n'+ 'N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t\n'+ '0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv\n'+ 't8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU\n'+ 'AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk\n'+ '48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL\n'+ 'DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK\n'+ 'xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA\n'+ 'mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh\n'+ '2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz\n'+ 'et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr\n'+ 'VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD\n'+ 'TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc\n'+ 'dn/RsYEONbwQSjIfMPkvxF+8HQ==\n'+ '-----END PRIVATE KEY-----\n' # public_key for reference but not actually used here var public_key = '-----BEGIN PUBLIC KEY-----\n'+ 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo\n'+ '4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u\n'+ '+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh\n'+ 'kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ\n'+ '0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg\n'+ 'cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc\n'+ 'mwIDAQAB\n'+ '-----END PUBLIC KEY-----\n' # read private_key as DER binary while (private_key[-1] == '\n') private_key = private_key[0..-2] end var private_key_DER = bytes().fromb64(string.split(private_key, '\n')[1..-2].concat()) # comparison with what was expected assert(private_key_DER.tob64() == '[binary]') # sign body var body_b64 = bytes().fromstring(body) var sign = crypto.RSA.rs256(private_key_DER, body_b64) var b64sign = base64url(sign) # check output assert(b64sign == 'NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ') # Final token: var jwt_token = payload + '.' + b64sign ``` `crypto.MD5` class[~](#cryptomd5-class) Provides MD5 hashing function. Test vector: ``` python import crypto h = crypto.MD5() t = bytes().fromstring("The quick brown fox jumps over the lazy dog") h.update(t) m = h.finish() assert(m == bytes("9e107d9d372bb6826bd81d3542a419d6")) ``` `flash` module[~](#flash-module) Warning: this is a low-level module used to read and write flash memory. You normally shouldn't need to use it. It is used internally by `partition_core` . Use with `import flash` . `img` class[~](#img-class) Thin wrapper for image data, that allows format conversions and is able to reduce memory reallocations in certain scenarios. Supports following image types, which integer values are equal to the enum `pixformat_t` of Espressif's webcam driver: - `img.RGB565` = 0 - `img.RGB888` = 5 - `img.JPEG` = 4 - `img.GRAYSCALE` = 3 Create an instance of an image with `var i = img()` . Memory will be released automatically by Berry's garbage collector after deletion of the instance. | img Function | Parameters and details | |---|---| | from_jpeg | `img.from_jpeg(jpeg_buffer:bytes[, type:img.type]) -> nil` Copy JPEG image as byte buffer to the buffer of an `img` instance. If optional image type is provided, this will be converted on the fly. This will not reallocate the image buffer, if the size and format does not change. | | from_buffer | `img.from_buffer(image_data:bytes,width:int:height:int,type:img.type) -> nil` Construct image from raw image data for the types `RGB565` , `RGB888` and `GRAYSCALE` . | | get_buffer | `img.get_buffer([descriptor:bytes]) -> image_data:bytes` Returns the raw image data for any supported type. For `RGB565` , `RGB888` and `GRAYSCALE` a descriptor can be provided to get a ROI (region of interest). | | convert_to | `img.convert_to(type:img.type) -> nil` Internal conversion of the image format. | | info | `img.info() -> map` Returns a map with some information about the current image. | The optional ROI descriptor is a representation of an affine matrix, which can be constructed in Berry: ``` # Describe ROI using an affine matrix (https://en.wikipedia.org/wiki/Affine_transformation#Image_transformation) # | scale_x shear_x translation_x | # | shear_y scale_y translation_y | # | 0 0 1 | - these are constants in this scope def roi_dsc(m) var d = bytes(-24) d.setfloat(0,m["scaleX"]) d.setfloat(4,m["shearX"]) d.setfloat(8,m["shearY"]) d.setfloat(12,m["scaleY"]) d.seti(16,m["transX"],2) d.seti(18,m["transY"],2) d.seti(20,m["width"],2) d.seti(22,m["height"],2) return d end ``` [ROI editor](../ROI-editor/) Example: ``` js # load jpg file into img var i = img() var f = open("j.jpg","r") i.from_jpg(f.readbytes(),img.RGB565) # i now holds image data with type RGB565 f.close() ``` `cam` module[~](#cam-module) Very small module to access a connected camera module with the purpose to have as much heap memory available as possible in comparison to the fully fledged webcam drivers for machine learning, but there are more possible applications. It is not intended to be a general replacement for the webcam drivers. | Tasmota Function | Parameters and details | |---|---| | cam.setup | `(mode:int) -> bool` Init camera hardware with the resolution (same value as command `wcresolution` ). | | cam.get_image | `([image:img[,type:img.type]]) -> bytes or nil` Takes a picture - without an additional option this is just a JPEG buffer. If an image instance is provided, the image data will go there. If an additional type is given, a conversion will happen on the fly. This will not lead to a memory reallocation, if there is no change for size and type of the image. | | cam.info | `() -> map` Shows info map with last current resolution and camera mode | Example: ``` # Simple "video player" for boards with a camera and a display scr = lv.scr_act() scr.set_style_bg_color(lv.color(lv.COLOR_BLUE), lv.PART_MAIN | lv.STATE_DEFAULT) # create a lv_img object as image view cam_view = lv.img(scr) cam_view.center() i = img() import cam cam.setup(5) # 240 x 240 cam.get_image(i,i.RGB565LE) def lv_img_dsc(image) var i = image.info() var dsc = bytes(24) dsc..0x19 # magic dsc..0x12 # cf RGB565 dsc.add(0,2) # flags dsc.add(i["width"],2) # width dsc.add(i["height"],2) # height dsc.add(i["width"] * 2,2) # stride dsc.add(0,2) # reserved dsc.add(i["size"],4) # size dsc.add(i["buf_addr"],4) # data dsc.add(0,4) # reserved print(dsc) return dsc end descriptor = lv_img_dsc(i) cam_view.set_src(descriptor) # bind cam_view to buffer of img def video() cam.get_image(i,i.RGB565LE) # this will just update the buffer, no reallocation cam_view.invalidate() tasmota.set_timer(20,/->video()) # aim for 50 Hz end video() ``` `BLE` module[~](#ble-module) Write drivers and applications for Bluetooth Low Energy supporting all 4 roles. More information here: [BLE module](../Bluetooth_MI32/#ble-module) ## Philips Hue emulation for Alexa[~](#philips-hue-emulation-for-alexa) Berry extends the native Hue/Alexa emulation and makes it possible to handle any number of virtual lights. You can easily define "virtual" lights in Berry, respond to commands from Alexa and send light status. It is up to you to define the final behavior. For example you could control some fancy devices, light strips or whatever takes on/off, dimmer or RGB commands. Your imagination is the limit. Hue emulation requires both `#define USE_EMULATION` and `#define USE_EMULATION_HUE` . Emulation must also be enabled with `Emulation 2` command. `light_state` class[~](#light_state-class) The core class is `light_state` which represents a virtual light. `light_state` general methods: `light_state` getters: | Attributes | Parameters and details | |---|---| | power | `(bool)` on/off state | | reachable | `(bool)` light is reachable | | type | `(int)` number of channels of the light | | bri | `(int)` brightness of the light (0..255) | | ct | `(int)` white temperature of the light (153..500) | | sat | `(int)` saturation of the light (0..255) | | hue | `(int)` hue of the light (0..360) | | hue16 | `(int)` hue as 16 bits (0..65535) | | r g b | `(int)` Red Green Blue channels (0..255) | | x y | `(float)` x/y color as floats (0.0 .. 1.0) | | mode_ct mode_rgb | `(bool)` light is in RGB or CT mode | | get | `get() -> map` returns the complete state of the light as a mapExample: `{'rgb': '1E285A', 'hue': 230, 'type': 5, 'power': false, 'bri': 90, 'mode_rgb': true, 'sat': 170, 'mode_ct': false, 'channels': [30, 40, 90, 0, 0]}` | `light_state` setters: | Methods | Parameters and details | |---|---| | set_power | `set_power(bool) -> nil` sets on/off state | | set_reachable | `set_reachable(bool) -> nil` sets the reachable state | | set_bri | `set_bri(int) -> nil` sets the brightness (0..255) | | set_ct | `set_ct(int) -> nil` sets the white temperature (153..500) | | set_sat | `set_sat(int) -> nil` sets the saturation (0..255) | | set_huesat | `set_huesat(hue:int, sat:int) -> nil` sets hue and saturation (0..360, 0..255) | | set_hue16sat | `set_hue16sat(hue16:int, sat:int) -> nil` sets hue16 and saturation (0..65535, 0..255) | | set_rgb | `set_rgb(r:int, g:int, b=int) -> nil` sets red/green/blue channels (0..255 x 3) | | set_xy | `set_xy(x:float, y:float) -> nil` sets color as x/y (0.0 .. 1.0 x 2) | `light_state` static helper functions: | Methods | Parameters and details | |---|---| | gamma8 | `gamma8(int) -> nil` applies gamma correction to 8 bits value (0..255) | | gamma10 | `gamma10(int) -> nil` applies gamma correction to 10 bits value (0..1023) | | reverse_gamma10 | `reverse_gamma10(int) -> nil` applies reverse gamma correction to 10 bits value (0..1023) | `hue_bridge` module[~](#hue_bridge-module) Use `import hue_bridge` and declare all the virtual lights. Example: ``` python # put this in `autoexec.be` import hue_bridge l1 = light_state(light_state.DIMMER) hue_bridge.add_light(11, l1, "Synthetic Dimmer", "V1", "Tasmota Factory") l2 = light_state(light_state.CT) hue_bridge.add_light(12, l2, "Synthetic CT", "V1", "Tasmota Factory") l5 = light_state(light_state.RGBCT) hue_bridge.add_light(15, l5, "Synthetic RGBCT") ``` When you start the Hue pairing, all virtual lights are advertised. You need to make sure that virtual lights are defined at each restart (in `autoexec.be` for example). `hue_bridge` functions: | Methods | Parameters and details | |---|---| | add_light | `add_light(id:int, light:instance of light_state, name:string [, model:string, manuf:strin]) -> light` Adds an virtual light to the Hue bridge. `id` = numerical identifier of the Hue light. Using low numbers avoids conflict with real lights from Tasmota`light` = instance of `light_state` handling the state and behavior of the light`name` = name of the light as displayed in the Alexa app (can be overridden in the app)`model` (opt) = name of the manufacturer model, defaults to "Unknown"`manuf` (opt) = name of the manufacturer, defaults to "Tasmota" | | remove_light | `remove_light(id:int) -> nil` Removes a light from the Hue bridge by hue id. | | light_to_id | `light_to_id(light:instance) -> int` converts a registered `light_instance` instance to its Hue id | ## Zigbee[~](#zigbee) For Zigbee coordinators, there is a Berry mapping that allows explore Zigbee configurations and devices. It also allows to intercept incoming message (low and high level) and transform messages before they reach the Tasmota layer. This is useful for non-standard Zigbee devices for which Zigbee plug-ins are not sufficient. Note: the following are only available when compiling with `#define USE_ZIGBEE` Internally, the Tasmota Zigbee engine calls `callBerryZigbeeDispatcher()` at key points to allow your Berry code to take over and change messages on-the-fly. `import zigbee` [~](#import-zigbee) First step is to use `import zigbee` which returns an instance (monad) of `zb_coord()` . | General methods | Parameters and details | |---|---| | started | `zigbee.started() -> bool or nil` Returns `true` if Zigbee successfully started, then all other Zigbee methods are available. This state is final and does not change.Returns `false` if Zigbee is still in initialization process. This state eventually changes to `true` or `nil` .Returns `nil` if Zigbee is not configured (no GPIO) or if initialization failed. This state is final and indicates a fatal error. | | info | `zigbee.info() -> map` returns a map with general configuration of the Zigbee coordinator.Format is identical to `ZbConfig` Example: `{'ext_pan_id': '0xCCCCCCCCA11A2233', 'tx_radio': 20, 'shortaddr': 0, 'longaddr': '0x00124B0026BAABBC', 'channel': 11, 'pan_id': 837, 'pan_id_hex': '0x0345', 'shortaddr_hex': '0x0000'}` | | size | `zigbee.size() -> int` returns the number of devices known by the coordinator | | iter | `zigbee.iter() -> iterator` Returns an iterator on all Zigbee devices Use compact implicit form: `for ze: zigbee print(ze) end` | | item [] | `zigbee.item(shortaddr:int | friendlyname:str) -> instance of zb_device` Returns the Zigbee device corresponding to short address `shortaddr` or to friendly name `friendlyname` .Returns an `index_error` exception if not found.You can use the compact syntax `zigbee[0xFAB6]` | | find | `zigbee.find(shortaddr:int | friendlyname:str) -> instance of zb_device` Returns the Zigbee device corresponding to short address `shortaddr` or to friendly name `friendlyname` .Contrary to the above, returns `nil` if not found (no exception). | | abort | `zigbee.abort() -> nil` aborts the initialization of Zigbee MCU. To be used when initialization of Zigbee failed | `zb_device` class[~](#zb_device-class) The class `zb_device` contains all known information about a paired Zigbee device (end-device or router). You can't create a `zb_device` from scratch, they most be retrieved from `zigbee` object. | General methods | Parameters and details | |---|---| | info | `info() -> attribute_list or nil` Returns the last known state for this device as an `attribute_list` This is equivalent of running `ZbInfo | `zb_device` instances can only be read, you can't change directly any attribute. | Instance Variables | Parameters and details | |---|---| | shortaddr | `shortaddr -> int` returns the 16 bits short address | | longaddr | `longaddr -> bytes` returns the long 64 bits address as 8 bytes (or all zeroes if unknown) | | name | `name -> string` returns the friendly name of the device or `0x....` hex name if no friendly name was defined using `ZbName` command | | reachable | `reachable -> bool` is the device reachable, i.e. did it respond last time we tried to contact them | | hidden | `hidden -> bool` is the device declared as hidden, i.e. not announced in Hue emulation | | router | `router -> bool` is the device known to be a router | | model | `model -> string` model of the device | | manufacturer | `manufacturer -> string` manufacturer name of the device | | lastseen | `lastseen -> int` timestamp (epoch) when the device was last seen | | lqi | `lqi -> int` radio strength and quality when the device was last seen | | battery | `battery -> int` percentage of battery, or `-1` if unknown of no battery | | battery_lastseen | `battery_lastseen -> int` timestamp (epoch) when the battery was last reported, or `-1` | Example: ``` python import zigbee # show all devices for device: zigbee print(device) end # # outputs: # <instance: zb_device(0x868E, 0x00124B001F841E41, name:'Bedroom', model:'TH01', manufacturer:'eWeLink')> # ... more devices # read one device by short address var device = zigbee[0x868E] print(device.longaddr) # bytes('411E841F004B1200') print(device.reachable) # false - because it's a sleep device print(device.router) # false - it's a sleepy device so not a router print(device.manufacturer, device.model) # eWeLink TH013000_g5xawfcq')> # example with a plug device = zigbee[0xC1BC] print(device.longaddr, device.reachable, device.router) # bytes('859F4E001044EF54') true false print(device.manufacturer, device.model) # LUMI lumi.plug.maeu01 ``` ### Changing Zigbee values on-the-fly[~](#changing-zigbee-values-on-the-fly) Whenever a Zigbee message is received (typically values of attributes), the Tasmota Zigbee engines generates events at key points which allow custom Berry code to intercept and change messages on-the-fly. Messages are sent in the following order: `frame_received` : (low-level) the raw Zigbee message is passed as`bytes` and attributes are not yet decoded. The`bytes` buffer can be modified and passed back to the Tasmota Zigbee engine.`attributes_raw` : (mid-level) Zigbee attributes are decoded but no transformation is applied yet. Attributes are only available in cluster/attribute format, names are not decoded and plug-ins are not yet applied. This is the perfect moment to change non-standard attributes and map them to standard ones.`attributes_refined` : (high-level) Attributes are mapped to their names (when possible) and all transformations are applied. This is the last chance to change values.`attributes_final` : (high-level) consolidated`attributes_refined` . It is triggered just before final and consolidated attributes are sent to MQTT. Zigbee typically waits for 350ms before sending attributes, so it can consolidate multiple sensors (like temperature + humidity + pressure) in a single MQTT message The format of methods are the following: `def <zigbee event>(event_type, frame, attr_list, idx)` | Argument | Description | |---|---| `event_type` | (string) can take values: `frame_received` , `attributes_raw` or `attributes_refined` | `frame` | (instance of `zcl_frame` ) low-level ZCL frameAlways present | `attr_list` | (instance of `XXX` ) list of attributes.This attribute is `nil` for `frame_received` , contains raw attributes in `attributes_raw` and refined attributes in `attributes_refined` | `idx` | (int 16 bits unsigned) contains the Zigbee short address | Example, if you want to dump all the traffic passed: ``` python import zigbee class my_zb_handler def frame_received(event_type, frame, attr_list, idx) print(f"shortaddr=0x{idx:04X} {event_type=} {frame=}") end def attributes_raw(event_type, frame, attr_list, idx) print(f"shortaddr=0x{idx:04X} {event_type=} {attr_list=}") end def attributes_refined(event_type, frame, attr_list, idx) print(f"shortaddr=0x{idx:04X} {event_type=} {attr_list=}") end def attributes_final(event_type, frame, attr_list, idx) print(f"shortaddr=0x{idx:04X} {event_type=} {attr_list=}") end end var my_handler = my_zb_handler() zigbee.add_handler(my_handler) # example of reading for a plug # # shortaddr=0xC1BC event_type=frame_received frame={'srcendpoint': 21, 'transactseq_set': 0, 'shortaddr': 49596, 'dstendpoint': 1, 'payload': bytes('5500003956CE8243'), 'shortaddr_hex': '0xC1BC', 'manuf': 0, 'payload_ptr': <ptr: 0x3ffccb5c>, 'need_response': 0, 'transactseq': 25, 'cmd': 1, 'direct': 0, 'cluster': 12, 'cluster_specific': 0, 'groupaddr': 0} # shortaddr=0xC1BC event_type=attributes_raw attr_list={"000C/0055":261.612,"Endpoint":21,"LinkQuality":21} # shortaddr=0xC1BC event_type=attributes_refined attr_list={"ActivePower":261.612,"(ActivePower)":"0B04/050B","Endpoint":21,"LinkQuality":21} # shortaddr=0xC1BC event_type=attributes_final attr_list={"ActivePower":261.612,"(ActivePower)":"0B04/050B","Endpoint":21,"LinkQuality":21} # to remove handler: # zigbee.remove_handler(my_handler) ``` The `attr_list` is of class `zcl_attribute_list` and can be accessed with `zigbee.zcl_attribute_list` . | Methods | Parameters and details | |---|---| | size | `size() -> int` Number of attributes in the list | | remove | `remove(index:int) -> nil` Remove the item at `index` | | item [x] | `item(index:int) -> instance` or `[index:int] -> instance` Retrieve attribute at `index` , or `nil` if none.Note: contrary to native `list` it does not throw an exception if the index if off bounds. | | new_head | `new_head(attribute:instance of zigbee.zcl_attribute_list) -> self` Adds a new attribute at the beginning (head) of the list | | new_tail | `new_tail(attribute:instance of zigbee.zcl_attribute_list) -> self` Adds a new attribute at the end (tail) of the list | Variables of `zcl_attribute_list` for the entire list and common to all attributes: | Attributes (read or write) | Details | |---|---| `groupaddr` | `uint16` group address if the message was multicast, or `nil` | `src_ep` | `uint8` source endpoint of the message | `lqi` | `uint8` lqi for the message received (link quality) | The `zcl_attribute_list` contains a list of `zcl_attribute` instance. | Attributes (read or write) | Details | |---|---| `cluster` | `uint16` ZCL cluster number | `attr_id` | `uint16` ZCL attribute id | `cmd` | `uint8` ZCL command number | `direction` | `0 or 1` ZCL direction of the message (to or from the coordinator) | `cmd_general` | `0 or 1` ZCL flag indicating a general command vs a cluster specific command | `key` | `string or nil` attribute name (if any) or `nil` | `val` | `any` ZCL value of the attribute, can be `int/float/string/bytes...` | `key_suffix` | `uint8` key suffix in case a same attribute is repeatedLike `Power1` , `Power2` ... | `manuf` | `uint16` ZCL manufacturer specific code or 0 if noneThis is typically indicating a proprietary attribute | `attr_multiplier` | `int` multiplier to be applied or `1` | `attr_divider` | `int` divider to be applied or `1` | `attr_base` | `int` offset to be applied or `0` | `attr_type` | `uint8` ZCL type byte for the received attribute | `zcl_attribute_list` methods: | Methods | Parameters and details | |---|---| | tomap | `tomap() -> map` Transforms main attributes as map (read-only): `cluster` , `attr_id` , `cmd` , `direction` , `key` , `val` | #### Changing attributes received[~](#changing-attributes-received) For events `attributes_raw` and `attributes_refined` , you receive an instance of `attr_list` which represents all the attributes received. This list can be modified according to specificities of devices, hence giving full liberty on decoding exotic protocols or manufacturers. The decoding is done in 2 steps: - `attributes_raw` contains individual attributes with their native raw values. Names are not yet matched, nor scale factors applied. This is where you want to decode non-standard protocols Example:`{"000C/0055":261.612,"Endpoint":21,"LinkQuality":21}` represents raw value from a plug; the value was decoded as float. - `attributes_refined` contains a similar list with additional decoding handled, any scale factor applied (like transforming integer temperature in 1/100 of Celsius to a`float` ), and human readable names attached. Example:`{"ActivePower":261.612,"(ActivePower)":"0B04/050B","Endpoint":21,"LinkQuality":21}` In this example, the attribute is`0B04/050B` is rename as`ActivePower` , but the original`0B04/050B` attribute cluster/id is still readable. We can see that the generic`000C/0055 (AnalogValue)` from`lumi.plug.maeu01` is replaced with`0B04/050B (ActivePower)` . #### Changing Zigbee frame, `zcl_frame` class[~](#changing-zigbee-frame-zcl_frame-class) The `zcl_frame` represents a low-level ZCL (Zigbee Cluster Library) structure before any decoding or specific processing. You generally prefer to modify a frame later on when attributes or commands are decoded. class `zcl_frame` : | Attributes (read or write) | Details | |---|---| `srcendpoint` | `uint8` source endpoint | `dtsendpoint` | `uint8` destination endpoint | `shortaddr` | `uint16` destination short address | `groupadddr` | `uint16` destination multicast group address (if shortaddr is 0xFFFE) | `cluster` | `uint16` cluster number | `cmd` | `uint8` ZCL command number | `cluster_specific` | `flag 0/1` is the command general or cluster specific | `manuf` | `uint16` manufacturer specific number (or 0x0000) | `needs_response` | `flag 0/1` does this frame needs a response | `payload` | `bytes()` bytes of the actual data (use with caution, can be read and changed) | | The following are rarely used flags | | `direct` | `flag 0/1` is the frame to be sent directly only (not routed) | `transactseq` | `uint8` transaction number (read only) | `transactseq_set` | `uint8` transaction number (write only - if you need to change it) | Example: ``` frame_received frame_received {'srcendpoint': 21, 'transactseq_set': 0, 'shortaddr': 49596, 'dstendpoint': 1, 'payload': bytes('550039D5787B43'), 'shortaddr_hex': '0xC1BC', 'manuf': 4447, 'payload_ptr': <ptr: 0x3ffd4d04>, 'need_response': 0, 'transactseq': 60, 'cmd': 10, 'direct': 0, 'cluster': 12, 'cluster_specific': 0, 'groupaddr': 0} nil 49596 ``` ## Compiling Berry[~](#compiling-berry) Berry is included if the following is defined in `user_config_override.h` : ``` #define USE_BERRY ``` Other options that can be changed: | Option | Description | |---|---| `#define USE_BERRY_PSRAM` | Use PSRAM to allocate memory instead of main RAM. If no PSRAM is connected, this option has no effect. Enabled by default | `#define USE_BERRY_DEBUG` | Provide additional information in case of a Berry exception, adding line number in the call chain. This feature adds ~8% of memory consumption to Berry compiled code. Disabled by default | `#define USE_WEBCLIENT` | Enable the `webclient` module allowing to do HTTP requests.Enabled by default | `#define USE_WEBCLIENT_HTTPS` | Adds support for HTTPS to `webclient` . This feature adds ~45KB of Flash space for TLS support.Disabled by default | `#define USE_BERRY_WEBCLIENT_USERAGENT "TasmotaClient"` | Specifies the default `User-Agent` field sent by `webclient` . Can be changed on a per request basis. | `#define USE_BERRY_WEBCLIENT_TIMEOUT 5000` | Specifies the default timeout in millisecond for `webclient` . Can be changed on a per request basis. | [Berry Cookbook](../Berry-Cookbook/)[~](#berry-cookbook) Find complete examples and use scenarios of Berry in the [Berry Cookbook](../Berry-Cookbook/)