Berry Cookbook~
Adding commands to Tasmota~
It is very easy to dynamically add a command to Tasmota with Berry.
Trivial example~
Let's start with the most simple command.
Let's define a command BrGC
that triggers a garbage collection and returns the memory allocated by Berry. We first define the function:
def br_gc()
var allocated = tasmota.gc() #- trigger gc and return allocated memory -#
import string
tasmota.resp_cmnd(string.format('{"BrGc":%i}', allocated))
end
And register the function:
tasmota.add_cmd('BrGc', br_gc)
Then in Tasmota Console:
brgc
21:04:30.369 CMD: brgc
21:04:30.376 RSL: stat/tasmota_923B34/RESULT = {"BrGc":5767}
General form of the custom command function~
The custom command function have the general form below where parameters are optionals:
def function_name(cmd, idx, payload, payload_json)
...
end
Parameter | Description |
---|---|
cmd | string name of the command in lower case. Can be used if same function is used for multiple similar commands for example. |
idx | Command's index is the unsigned integer (optionally) added at the end of the command name before the parameters (like Demo1 ). Default to 1 if not specified. |
payload | string of the command line as without any parsing. |
payload_json | if the payload is a valid JSON, it is converted into a Berry map object. |
More complete example~
In this example, we will create a new command called LightGold
that turns the light on and sets it to color gold #FFD700. This command accepts an optional JSON payload with the argument Dimmer
ranging from 0..100.
First we define a new Berry function with the logic. This function takes 4 arguments:
cmd
: the command name (with same case as it was registered). This is useful if you want to share the same code in multiple commands. Herecmd
isLightGold
idx
: the command index used, default to 1.payload
: the raw payload of the command as stringpayload_json
: the payload parsed as JSON, ornil
if the payload is not JSON
Example:
- command
lightgold
:cmd
=LightGold
,idx
=1,payload
=""
,payload_json
=nil
- command
LIGHTGOLD2
:cmd
=LightGold
,idx
=2,payload
=""
,payload_json
=nil
- command
lightgold not sure
:cmd
=LightGold
,idx
=1,payload
='not sure'
,payload_json
=nil
- command
lightgold {"value":"some"}
:cmd
=LightGold
,idx
=1,payload
='{"value":"some"}'
,payload_json
={'value':'some'}
In Berry, arguments are always optional, so you don't need to define them if you don't need them.
def light_gold(cmd, idx, payload, payload_json)
var dimmer = 50 #- default brightness to 50% -#
var bri
# parse payload
if payload_json != nil && payload_json.find("Dimmer") != nil # does the payload contain a 'dimmer' field
dimmer = int(payload_json.find("Dimmer"))
end
# set_light expects a brightness in range 0..255
bri = tasmota.scale_uint(dimmer, 0, 100, 0, 255)
# build the payload for set_light
var light_payload = {'power':true, 'rgb':'FFD700', 'bri':bri}
#- set the light values -#
tasmota.set_light(light_payload)
# report the command as successful
tasmota.resp_cmnd_done()
end
Finally you need to register the command:
tasmota.add_cmd('LightGold', light_gold)
Example (in Tasmota console, not Berry console):
lightgold
20:53:28.142 CMD: lightgold
20:53:28.151 RSL: stat/tasmota_923B34/RESULT = {"POWER":"ON"}
20:53:28.153 RSL: stat/tasmota_923B34/POWER = ON
20:53:28.160 RSL: stat/tasmota_923B34/RESULT = {"LightGold":"Done"}
lightgold {"Dimmer":20}
20:54:16.837 CMD: lightgold {"Dimmer":20}
20:54:16.848 RSL: stat/tasmota_923B34/RESULT = {"LightGold":"Done"}
Responding to commands~
Tasmota expects that you send a response to commands. You can use the following methods:
tasmota.resp_cmnd_done()
: report command asDone
(including translated versions)tasmota.resp_cmnd_error()
: report command asError
tasmota.resp_cmnd_failed()
: report command asFailed
tasmota.resp_cmnd_str(<msg>)
: report an arbitrary stringtasmota.resp_cmnd(<json>)
: report a custom JSON message (not prefixed by command name).
Adding a button to the Main menu~
Adding a button to the e.g. main menu can be achieved by using the message type web_add_main_button()
.
The method to be performed, when the user clicks the button is achieved by using the web_sensor()
method checking for the presence of an argument and a possible value assigned to the argument. The class provides the necessary methods to read the arguments:
webserver.has_arg(arg_name:string)
: -> boolean, checks if an argument with this name existswebserver.arg_size()
: -> integer, returns the number of argumentswebserver.arg(arg_name:string or arg_index:int)
: -> string, returns the value of the argument either by name or by position number [0..arg_size()-1]. If an argument has multiple values, you need to iterate using ints to get all valueswebserver.arg_name(arg_index:int)
-> string, get the name of argument by index [0..arg_size()-1]
Additionally the webserver class provides a new function of sending information to the Web UI by using the following methods
webserver.content_send(content:string)
webserver.content_send_style(content:string)
Let's see an example implementation of button methods in a Driver class
import webserver # import webserver class
class MyButtonMethods
def myOtherFunction(myValue)
#- do something -#
end
#- create a method for adding a button to the main menu -#
def web_add_main_button()
webserver.content_send("<p></p><button onclick='la(\"&m_toggle_main=1\");'>Toggle Main</button>")
end
#- create a method for adding a button to the configuration menu-#
def web_add_config_button()
#- the onclick function "la" takes the function name and the respective value you want to send as an argument -#
webserver.content_send("<p></p><button onclick='la(\"&m_toggle_conf=1\");'>Toggle Conf</button>")
end
#- As we can add only one sensor method we will have to combine them besides all other sensor readings in one method -#
def web_sensor()
if webserver.has_arg("m_toggle_main")
print("button pressed")
end
if webserver.has_arg("m_toggle_conf") # takes a string as argument name and returns a boolean
# we can even call another function and use the value as a parameter
var myValue = int(webserver.arg("m_toggle_conf")) # takes a string or integer(index of arguments) to get the value of the argument
self.myOtherFunction(myValue)
end
end
end
d1 = MyButtonMethods()
tasmota.add_driver(d1)
Creating an I2C driver~
Berry Scripting provides all necessary primitives for a complete I2C driver.
Step by step approach~
We will explore the different steps to write an I2C driver, and will take the MPU6886 as an example. The native driver already exists, and we'll rewrite it in Berry code.
Step 1: detect the device
I2C device are identified by address, only one device per address is allowed per I2C physical bus. Tasmota32 supports up to 2 I2C buses, using wire1
or wire2
objects.
To simplify device detection, we provide the convenience method tasmota.scan_wire()
. The first argument is the device address (0x68 for MPU6886). The optional second argument is the I2C Tasmota index, allowing to selectively disable some device families. See I2CDevice
command and page XXX. The index number for MPU6886 is 58.
class MPU6886
var wire # contains the wire object if the device was detected
def init()
self.wire = tasmota.wire_scan(0x68, 58)
end
end
self.wire
contains a reference to wire1
if the device was detected on I2C bus 1, a reference to wire2
if the device was detected on bus 2, or nil
if the device was not detected, or if I2C index 58 was disabled through I2CEnable
.
Step 2: verify the device
To make sure the device is actually an MPU6886, we check its signature by reading register 0x75. It should respond 0x19 (see datasheet for MPU6886).
[...]
if self.wire
var v = self.wire.read(0x68,0x75,1)
if v != 0x19 return end #- wrong device -#
[...]
Step 3: initialize the device
We write a series of values in registers to configure the device as expected (see datasheet).
[...]
self.wire.write(0x68, 0x6B, 0, 1)
tasmota.delay(10)
self.wire.write(0x68, 0x6B, 1<<7, 1) # MPU6886_PWR_MGMT_1
tasmota.delay(10)
self.wire.write(0x68, 0x6B, 1<<0, 1) # MPU6886_PWR_MGMT_1
tasmota.delay(10)
self.wire.write(0x68, 0x1C, 0x10, 1) # MPU6886_ACCEL_CONFIG - AFS_8G
tasmota.delay(1)
self.wire.write(0x68, 0x1B, 0x18, 1) # MPU6886_GYRO_CONFIG - GFS_2000DPS
tasmota.delay(1)
self.wire.write(0x68, 0x1A, 0x01, 1) # MPU6886_CONFIG
tasmota.delay(1)
self.wire.write(0x68, 0x19, 0x05, 1) # MPU6886_SMPLRT_DIV
tasmota.delay(1)
self.wire.write(0x68, 0x38, 0x00, 1) # MPU6886_INT_ENABLE
tasmota.delay(1)
self.wire.write(0x68, 0x1D, 0x00, 1) # MPU6886_ACCEL_CONFIG2
tasmota.delay(1)
self.wire.write(0x68, 0x6A, 0x00, 1) # MPU6886_USER_CTRL
tasmota.delay(1)
self.wire.write(0x68, 0x23, 0x00, 1) # MPU6886_FIFO_EN
tasmota.delay(1)
self.wire.write(0x68, 0x37, 0x22, 1) # MPU6886_INT_PIN_CFG
tasmota.delay(1)
self.wire.write(0x68, 0x38, 0x01, 1) # MPU6886_INT_ENABLE
tasmota.delay(100)
[...]
We also pre-compute multiplier to convert raw values to actual values:
[...]
self.gres = 2000.0/32768.0
self.ares = 8.0/32678.0
print("I2C: MPU6886 detected on bus "+str(self.wire.bus))
[...]
Step 4: read sensor value
We will detail here the acceleration senor; gyroscope works similarly and is not further detailed.
Reading the x/y/z sensor requires to read 6 bytes as a bytes()
object
var b = self.wire.read_bytes(0x68,0x3B,6)
Each value is 2 bytes. We use bytes.get(offset,size)
to extract 2-bytes values at offsets 0/2/4. The size is -2
to indicate that values are encoded in Big Endian instead of Little Endian.
var a1 = b.get(0,-2)
Finally the read value is unsigned 16 bits, but the sensor value is signed 16 bits. We convert 16 bits unsigned to 16 bits signed.
if a1 >= 0x8000 a1 -= 0x10000 end
We then repeat for y and z:
def read_accel()
if !self.wire return nil end #- exit if not initialized -#
var b = self.wire.read_bytes(0x68,0x3B,6)
var a1 = b.get(0,-2)
if a1 >= 0x8000 a1 -= 0x10000 end
var a2 = b.get(2,-2)
if a2 >= 0x8000 a2 -= 0x10000 end
var a3 = b.get(4,-2)
if a3 >= 0x8000 a3 -= 0x10000 end
self.accel = [a1 * self.ares, a2 * self.ares, a3 * self.ares]
return self.accel
end
Step 5: read sensor every second
Simply override every_second()
def every_second()
if !self.wire return nil end #- exit if not initialized -#
self.read_accel()
self.read_gyro()
end
Step 6: display sensor value in Web UI
You need to override web_sensor()
and provide the formatted string. tasmota.web_send_decimal()
sends a string to the Web UI, and converts decimal numbers according to the locale settings.
Tasmota uses specific markers:
{s}
: start of line{m}
: separator between name and value{e}
: end of line
#- display sensor value in the web UI -#
def web_sensor()
if !self.wire return nil end #- exit if not initialized -#
import string
var msg = string.format(
"{s}MPU6886 acc_x{m}%.3f G{e}"..
"{s}MPU6886 acc_y{m}%.3f G{e}"..
"{s}MPU6886 acc_z{m}%.3f G{e}"..
"{s}MPU6886 gyr_x{m}%i dps{e}"..
"{s}MPU6886 gyr_y{m}%i dps{e}"..
"{s}MPU6886 gyr_z{m}%i dps{e}",
self.accel[0], self.accel[1], self.accel[2], self.gyro[0], self.gyro[1], self.gyro[2])
tasmota.web_send_decimal(msg)
end
Step 7: publish JSON TelePeriod sensor value
Similarly to Web UI, publish sensor value as JSON.
#- add sensor value to teleperiod -#
def json_append()
if !self.wire return nil end #- exit if not initialized -#
import string
var ax = int(self.accel[0] * 1000)
var ay = int(self.accel[1] * 1000)
var az = int(self.accel[2] * 1000)
var msg = string.format(",\"MPU6886\":{\"AX\":%i,\"AY\":%i,\"AZ\":%i,\"GX\":%i,\"GY\":%i,\"GZ\":%i}",
ax, ay, az, self.gyro[0], self.gyro[1], self.gyro[2])
tasmota.response_append(msg)
end
Full example~
The code can be loaded manually with copy/paste, or stored in flash and loaded at startup in autoexec.be
as load("mpu6886.be")
. Alternatively it can be loaded with a Tasmota native command or rule:
Br load("mpu6886.be")
See code example below for MPU6886:
#-
- Example of I2C driver written in Berry
-
- Support for MPU6886 device found in M5Stack
- Alternative to xsns_85_mpu6886.ino
-#
class MPU6886
var wire #- if wire == nil then the module is not initialized -#
var gres, ares
var accel, gyro
def init()
self.wire = tasmota.wire_scan(0x68, 58)
if self.wire
var v = self.wire.read(0x68,0x75,1)
if v != 0x19 return end #- wrong device -#
self.wire.write(0x68, 0x6B, 0, 1)
tasmota.delay(10)
self.wire.write(0x68, 0x6B, 1<<7, 1) # MPU6886_PWR_MGMT_1
tasmota.delay(10)
self.wire.write(0x68, 0x6B, 1<<0, 1) # MPU6886_PWR_MGMT_1
tasmota.delay(10)
self.wire.write(0x68, 0x1C, 0x10, 1) # MPU6886_ACCEL_CONFIG - AFS_8G
tasmota.delay(1)
self.wire.write(0x68, 0x1B, 0x18, 1) # MPU6886_GYRO_CONFIG - GFS_2000DPS
tasmota.delay(1)
self.wire.write(0x68, 0x1A, 0x01, 1) # MPU6886_CONFIG
tasmota.delay(1)
self.wire.write(0x68, 0x19, 0x05, 1) # MPU6886_SMPLRT_DIV
tasmota.delay(1)
self.wire.write(0x68, 0x38, 0x00, 1) # MPU6886_INT_ENABLE
tasmota.delay(1)
self.wire.write(0x68, 0x1D, 0x00, 1) # MPU6886_ACCEL_CONFIG2
tasmota.delay(1)
self.wire.write(0x68, 0x6A, 0x00, 1) # MPU6886_USER_CTRL
tasmota.delay(1)
self.wire.write(0x68, 0x23, 0x00, 1) # MPU6886_FIFO_EN
tasmota.delay(1)
self.wire.write(0x68, 0x37, 0x22, 1) # MPU6886_INT_PIN_CFG
tasmota.delay(1)
self.wire.write(0x68, 0x38, 0x01, 1) # MPU6886_INT_ENABLE
tasmota.delay(100)
self.gres = 2000.0/32768.0
self.ares = 8.0/32678.0
print("I2C: MPU6886 detected on bus "+str(self.wire.bus))
end
end
#- returns a list of 3 axis, float as g acceleration -#
def read_accel()
if !self.wire return nil end #- exit if not initialized -#
var b = self.wire.read_bytes(0x68,0x3B,6)
var a1 = b.get(0,-2)
if a1 >= 0x8000 a1 -= 0x10000 end
var a2 = b.get(2,-2)
if a2 >= 0x8000 a2 -= 0x10000 end
var a3 = b.get(4,-2)
if a3 >= 0x8000 a3 -= 0x10000 end
self.accel = [a1 * self.ares, a2 * self.ares, a3 * self.ares]
return self.accel
end
#- returns a list of 3 gyroscopes, int as dps (degree per second) -#
def read_gyro()
if !self.wire return nil end #- exit if not initialized -#
var b = self.wire.read_bytes(0x68,0x43,6)
var g1 = b.get(0,-2)
if g1 >= 0x8000 g1 -= 0x10000 end
var g2 = b.get(2,-2)
if g2 >= 0x8000 g2 -= 0x10000 end
var g3 = b.get(4,-2)
if g3 >= 0x8000 g3 -= 0x10000 end
self.gyro = [int(g1 * self.gres), int(g2 * self.gres), int(g3 * self.gres)]
return self.gyro
end
#- trigger a read every second -#
def every_second()
if !self.wire return nil end #- exit if not initialized -#
self.read_accel()
self.read_gyro()
end
#- display sensor value in the web UI -#
def web_sensor()
if !self.wire return nil end #- exit if not initialized -#
import string
var msg = string.format(
"{s}MPU6886 acc_x{m}%.3f G{e}"..
"{s}MPU6886 acc_y{m}%.3f G{e}"..
"{s}MPU6886 acc_z{m}%.3f G{e}"..
"{s}MPU6886 gyr_x{m}%i dps{e}"..
"{s}MPU6886 gyr_y{m}%i dps{e}"..
"{s}MPU6886 gyr_z{m}%i dps{e}",
self.accel[0], self.accel[1], self.accel[2], self.gyro[0], self.gyro[1], self.gyro[2])
tasmota.web_send_decimal(msg)
end
#- add sensor value to teleperiod -#
def json_append()
if !self.wire return nil end #- exit if not initialized -#
import string
var ax = int(self.accel[0] * 1000)
var ay = int(self.accel[1] * 1000)
var az = int(self.accel[2] * 1000)
var msg = string.format(",\"MPU6886\":{\"AX\":%i,\"AY\":%i,\"AZ\":%i,\"GX\":%i,\"GY\":%i,\"GZ\":%i}",
ax, ay, az, self.gyro[0], self.gyro[1], self.gyro[2])
tasmota.response_append(msg)
end
end
mpu6886 = MPU6886()
tasmota.add_driver(mpu6886)
LVGL Touchscreen with 3 Relays~
#- start LVGL and init environment -#
lv.start()
hres = lv.get_hor_res() # should be 240
vres = lv.get_ver_res() # should be 320
scr = lv.scr_act() # default screean object
f20 = lv.montserrat_font(20) # load embedded Montserrat 20
f28 = lv.montserrat_font(28) # load embedded Montserrat 28
#- Backgroun -#
scr.set_style_local_bg_color(lv.OBJ_PART_MAIN, lv.STATE_DEFAULT, lv_color(0x000066)) # backgroun in dark blue #000066
#- Upper state line -#
stat_line = lv_label(scr)
if f20 != nil stat_line.set_style_local_text_font(lv.OBJ_PART_MAIN, lv.STATE_DEFAULT, f20) end
stat_line.set_long_mode(lv.LABEL_LONG_SROLL) # auto scrolling if text does not fit
stat_line.set_width(hres)
stat_line.set_align(lv.LABEL_ALIGN_LEFT) # align text left
stat_line.set_style_local_bg_color(lv.OBJ_PART_MAIN, lv.STATE_DEFAULT, lv_color(0x000088)) # background #000088
stat_line.set_style_local_bg_opa(lv.OBJ_PART_MAIN, lv.STATE_DEFAULT, lv.OPA_COVER) # 100% background opacity
stat_line.set_style_local_text_color(lv.OBJ_PART_MAIN, lv.STATE_DEFAULT, lv_color(0xFFFFFF)) # text color #FFFFFF
stat_line.set_text("Tasmota")
stat_line_height = stat_line.get_height()
#- display wifi strength indicator icon (for professionals ;) -#
stat_line.set_style_local_pad_right(lv.OBJ_PART_MAIN, lv.STATE_DEFAULT, stat_line_height + 1)
wifi_bars = lv_wifi_bars(stat_line)
wifi_bars.set_style_local_bg_color(lv.OBJ_PART_MAIN, lv.STATE_DEFAULT, lv_color(lv.BLACK))
wifi_bars.set_height(stat_line_height)
wifi_bars.set_width(stat_line_height)
wifi_bars.set_x(stat_line.get_width() - stat_line_height)
#- create a style for the buttons -#
btn_style = lv_style()
btn_style.set_radius(lv.STATE_DEFAULT, 10) # radius of rounded corners
btn_style.set_bg_opa(lv.STATE_DEFAULT, lv.OPA_COVER) # 100% background opacity
if f28 != nil btn_style.set_text_font(lv.STATE_DEFAULT, f28) end
btn_style.set_bg_color(lv.STATE_DEFAULT, lv_color(0x33BBFF)) # background color #1FA3EC (Tasmota Blue)
btn_style.set_border_color(lv.STATE_DEFAULT, lv_color(0x0000FF)) # border color #0000FF
#btn_style.set_bg_color(lv.STATE_FOCUSED, lv_color(0x0000FF)) # background color when pressed #0000FF
#btn_style.set_border_color(lv.STATE_FOCUSED, lv_color(0xFFFFFF)) # border color when pressed #FFFFFF
btn_style.set_text_color(lv.STATE_DEFAULT, lv_color(0x000000)) # text color #FFFFFF
#- enabled -#
btn_style.set_bg_color(lv.STATE_CHECKED, lv_color(0x0000FF)) # background color #1FA3EC (Tasmota Blue)
btn_style.set_text_color(lv.STATE_CHECKED, lv_color(0xFFFFFF)) # text color #FFFFFF
btn_style.set_outline_width(lv.STATE_FOCUSED, 0) # remove focus outline, not needed with touchscreen
#- register buttons -#
var btns = [] # relay buttons are added to this list to match with Tasmota relays
#- simple function to find the index of an element in a list -#
def findinlist(l, x)
for i:0..size(l)-1
if l[i] == x
return i
end
end
end
#- callback function when a button is pressed -#
#- checks if the button is in the list, and react to EVENT_VALUE_CHANGED event -#
def btn_event_cb(o, event)
var btn_idx = findinlist(btns, o)
if btn_idx != nil && event == lv.EVENT_VALUE_CHANGED
var val = o.get_state() < lv.BTN_STATE_CHECKED_RELEASED # true if checked, false if unchecked
tasmota.set_power(btn_idx, !val) # toggle the value
end
end
#- create a button object, set style, register callback and add to global list -#
#- you still need to re-position the button -#
def create_btn_relay(label)
var btn, btn_label
btn = lv_btn(scr)
btn.set_pos(30, 30)
btn.set_size(hres - 60, 60)
btn.add_style(lv.OBJ_PART_MAIN, btn_style)
btn.set_checkable(true) # enable toggle mode
btn_label = lv_label(btn)
btn_label.set_text(label)
btn.set_event_cb(btn_event_cb) # set callback to update Tasmota relays
btns.push(btn) # append button to the list
return btn
end
#- create 3 buttons -#
var btn1 = create_btn_relay("Relay 1")
btn1.set_y(30)
var btn2 = create_btn_relay("Relay 2")
btn2.set_y(100)
var btn3 = create_btn_relay("Relay 3")
btn3.set_y(170)
#- update the buttons values according to internal relays status -#
def btns_update()
var power_list = tasmota.get_power() # get a list of booleans with status of each relay
for b:btns
var state = b.get_state()
var power_state = (size(power_list) > 0) ? power_list.pop(0) : false # avoid exception if less relays than buttons
if state != lv.BTN_STATE_PRESSED && state != lv.BTN_STATE_CHECKED_PRESSED # update only if the button is not currently being pressed
b.set_state(power_state ? lv.BTN_STATE_CHECKED_RELEASED : lv.BTN_STATE_RELEASED)
end
end
end
#- update every 500ms -#
def btns_update_loop()
btns_update()
tasmota.set_timer(500, btns_update_loop)
end
btns_update_loop() # start
# If you change the style after creating the button, you need to update objects:
def btns_refresh_style()
for b:btns b.refresh_style(lv.OBJ_PART_MAIN, lv.STYLE_PROP_ALL) end
end
# Button states, read and set with:
# btn1.get_state() or btn1.set_state(lv.BTN_STATE_CHECKED_RELEASED)
# Ex:
# btn1.set_state(lv.BTN_STATE_RELEASED)
# btn1.set_state(lv.BTN_STATE_CHECKED_RELEASED)
#- Here are the states for buttons -#
# BTN_STATE_RELEASED
# BTN_STATE_PRESSED
# BTN_STATE_DISABLED
# BTN_STATE_CHECKED_RELEASED
# BTN_STATE_CHECKED_PRESSED
# BTN_STATE_CHECKED_DISABLED
Multi-Zone Heating Controller~
This project is a multi-zone heating controller written entirely in berry. It demonstrates the use of the persist module for saving/loading data; the webserver module for creating a custom "Manage Heating" user interface; dynamic loading of HTML from the file system; subscribing to a variety of rule triggers (using tasmota.add_rule); the implementation of custom commands (using tasmota.add_cmnd). It also makes good use of time functionaity (via tasmota.rtc, tasmota.time_dump, tasmota.set_timer and tasmota.strftime). The project also includes an LCD I2C driver for running a basic 20x4 display. The entire driver is implemented using just the tasmota.wire_scan method.
https://github.com/Beormund/Tasmota32-Multi-Zone-Heating-Controller
Ethernet Network Flipper~
Used on board with Ethernet. If both Wi-Fi and Ethernet are active, turn off Wi-Fi. Place code in autoexec.be
to execute on boot. You can call the function from Berry console any time with netflip()
.
def netflip()
var eth = tasmota.eth().find('ip') != nil #1
if tasmota.wifi().find('ip') != nil == eth #2
tasmota.cmd('Wifi ' .. (eth ? 0 : 1)) #3
end
end
tasmota.set_timer(30000,netflip) #4
- store variable "eth" with Ethernet status - "true" if Ethernet IP exists and "false" if not
- check if wifi status is true and compare to eth status
- send command
Wifi
with parameter depending on eth variable...
is to concatenate a string. See Berry manual - set a timer to execute the netflip function 30000ms (30 seconds) after loading
autoexec.be
For newer Berry versions, there is an improved version:
# Ethernet Network Flipper - checks every 30 seconds if ethernet if up
# if Ethernet is up, Wifi is turned off to avoid interference with Zigbee
# if Ethernet is down, Wifi is turned back on to allow fallback connection
def netflip()
var eth = tasmota.eth('up') #1
if tasmota.wifi('up') == eth #2
tasmota.cmd('Wifi ' + (eth ? '0' : '1')) #3
end
tasmota.set_timer(30000,netflip) #4
end
tasmota.set_timer(30000,netflip) #5
- store variable "eth" with Ethernet status - "true" if Ethernet IP exists and "false" if not
- check if wifi and eth are both up or both down
- send command
Wifi
with parameter depending on eth variable, turn Wifi on if eth is down, turn Wifi off if eth is up - set a timer to execute the netflip function every 30000ms (30 seconds)
- set a timer to execute the netflip function 30000ms (30 seconds) after loading
autoexec.be
TMP117 Driver~
Call function at intervals~
This small helper function allows you to call a function at stable intervals, automatically correcting in case of latency or other deviations. Not suitable for very short intervals; while the delay interval is in milliseconds for consistency with the standard tasmota.set_timer
, it would normally be seconds multiplied by 1000, like 60000 for every minute.
def set_timer_modulo(delay,f,id)
var now=tasmota.millis()
tasmota.set_timer((now+delay/4+delay)/delay*delay-now, def() set_timer_modulo(delay,f,id) f() end, id)
end
H-bridge control~
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.
You can typically use 2 PWM channels to pilot a H-bridge, under the condition that both channels are never active at the same time; otherwise you may destroy your device. This means that phasing must be calculated so that one pulse started once the other pulse is inactive, and the sum of both dutys must not exceed 100%.
The following Berry function ensures appropriate management of H-bridge:
#
# H_bridge class in Berry to pilot a H-bridge device
#
class H_bridge
var gpio1, gpio2
var max
# init(phy_gpio1, phy_gpio2) - initialize H-bridge with the 2 GPIOs used to control it
def init(gpio1, gpio2)
self.gpio1 = gpio1
self.gpio2 = gpio2
self.max = 1023 # max value of duty
end
# set the value of both PWM values
def set(v1, v2)
if v1 < 0 v1 = 0 end
if v2 < 0 v2 = 0 end
if v1 + v2 > self.max
raise "value_error", "the sum of duties must not exceed 100%"
end
import gpio
gpio.set_pwm(self.gpio1, v1, 0)
gpio.set_pwm(self.gpio2, v2, v1) # dephase by value v1
end
end
Example of use:
var hbridge = H_bridge(12, 13) # use GPIO12 and GPIO13
hbridge.set(100,200) # set values to 102/1023 and 204/1023, i.e. 10% and 20%
hbridge.set(100,950) # set values to 102/1023 and 950/1023, i.e. 10% and 93%
BRY: Exception> 'value_error' - the sum of duties must not exceed 100%
Flash to file~
This is an example of dumping the content of the internal flash of the ESP32 and write the content in the file system that you can download back to your PC.
The example below dumps the contant of the safeboot partition.
def flash_to_file(filename, addr, len)
import flash
import string
var f = open(filename, "wb")
try
# Do 4KB chunks
while len > 0
var chunk = 512
if len < chunk chunk = len end
var b = flash.read(addr, chunk)
print(string.format("0x%06X - %s (%i)", addr, str(b), chunk))
f.write(b)
b = nil
addr += chunk
len -= chunk
end
f.close()
except .. as e,m
f.close()
end
end
flash_to_file("safe_boot_flashed.bin", 0x10000, 768320)
Tank Sensor~
Tank Sensor for fuel-oil volume measurement using a VL53L1X or SR04 sensor
https://github.com/trlafleur/Tasmota-Tank-Sensor
Home Assistant Controls for Tasmota~
This is a Tasmota Berry Script library to greatly simplify the process of exposing Home Assistant controls (e.g. Pull-down Lists, Number Sliders, Sensors, etc.) from a Tasmota device - and handling the communication between both sides.
Build MQTT topic string based on FullTopic configuration~
This code bit illustrates how you can create a topic string in the form of the FullTopic specification. Details like whech %prefix% you want and what last level of the topic is obviously variable.
var topic = string.replace(string.replace(
tasmota.cmd('FullTopic',true)['FullTopic'],
'%topic%', tasmota.cmd('Topic',true)['Topic']),
'%prefix%', tasmota.cmd('Prefix',true)['Prefix3'])
+ 'SENSOR'
Wake-on-LAN~
The code below sends WoL (Wake on Lan) packets so you can wake up a device located on the same LAN.
def send_wake_on_lan(mac, broadcast_ip)
import string
u = udp()
u.begin("", 0)
var payload = bytes("FFFFFFFFFFFF")
var mac_bytes = bytes().fromhex(string.tr(mac, ":", ""))
for i:1..16
payload += mac_bytes
end
#print(payload)
return u.send(broadcast_ip, 9, payload)
end
send_wake_on_lan("84:CC:A8:64:B7:68", "192.168.2.255")
Other resources~
For Tasmota the're many Berry scripts available which can be found in the links below.
https://github.com/arendst/Tasmota/tree/development/tasmota/berry http://sfromis.strangled.net/tasmota/berry/github-repositories https://github.com/tasmota/Berry_playground/tree/main