Skip to content

Scripting guide

Write your own apps for AWTRIX: small programs that draw on the display, fetch data and react to buttons.

New here?

How the display works shows where things sit on the display and which text moves by itself.

What you get

Your first script takes five steps in the web UI:

  1. Open the web UI in your browser and go to the Scripts tab.
  2. Press + to start a new app, and type Hello into the name field.
  3. Replace the text in the editor with this:

    class Hello
      def draw()
        clear()
        text(1, 6, "hi", 0x00FF00)
      end
    end
    
    return Hello()
    

    What the script shows on the display

  4. Press Save (or Ctrl-S).

  5. Press Show on the clock. The display shows hi in green.

Hello is an app in the rotation from now on, and it comes back after a reboot. The name you typed is its install name: AWTRIX saves the script under it and tells apps apart by it. See Install name.

Other ways in

  • Ready-made scripts: the AWTRIX Hub collects finished scripts from the community. Send one to your clock from its page. The web UI offers later versions as an update.
  • Step by step: the tutorials, starting with Tutorial 1: Draw something, grow one app from a blank display to a live weather chart.
  • Not a programmer? Build an app with AI gives you a prompt for a chatbot. Describe the app in plain words and paste the script it writes into the web UI.

How it behaves

  • A script is an app. It takes its turn in the rotation like a pushed app. Unlike a pushed app, it makes its own content, fetches its own data and stays on AWTRIX after a reboot.
  • Your app is a class. AWTRIX calls its methods, the hooks, at set moments. The file ends with return YourClass(), which hands AWTRIX the app to run.
  • draw() paints one frame, about 40 times a second, only while your app is shown. Every frame starts black, so draw() paints everything each time. Only draw() can paint: drawing calls in other hooks do nothing.
  • loop() runs about once a second, also while your app is hidden. Fetch, count and build your text there, and keep the result in a member such as self.label. draw() then only paints it.
  • on_show() runs each time your app appears, before its first frame. Reset a scroll position or a counter there. on_hide() runs when your app has left.
  • An error stops only your app. The display shows ERR: in red in its turn, and the Scripts tab shows the message next to the script. Fix the line it names and save again. See ERR: on the display.

How the display works explains where things sit and which text moves.

Keep a value between frames

The members of your class keep their values from one call to the next. Declare them with var at the top of the class, give them a starting value in init(), and change them in loop():

class Seconds
  var count, label

  def init()
    self.count = 0
    self.label = "0"
  end

  def loop()
    self.count += 1                   # about once a second
    self.label = str(self.count)      # build the text here ...
  end

  def draw()
    clear()
    text(1, 6, self.label, 0xFFFFFF)  # ... and only paint it here
  end
end

return Seconds()

What the script shows on the display

The number goes up once a second. draw() paints the same text about 40 times in that second. Keeping scripts small shows why the text is built in loop().

React to a button

on_button(btn) is called when a button is pressed while your app is shown. btn is "left", "select" or "right". Return true to keep the press for your app:

class Pages
  var page, label

  def init()
    self.page = 1
    self.label = "1"
  end

  def on_button(btn)
    if btn == "left"
      self.page -= 1
    elif btn == "right"
      self.page += 1
    else
      return false                    # select works as usual
    end
    self.label = str(self.page)
    return true                       # the display stays on this app
  end

  def draw()
    clear()
    text(1, 6, self.label, 0xFFFFFF)
  end
end

return Pages()

A press your app takes stays with it: left and right do not switch apps, select does not dismiss a notification, and a double press does not switch the display off. Each press your app takes also gives it its full time on the display again. Return nothing, false or nil, and the button works as usual. Button events adds holding and releasing.

What a script can do

Each topic has its own page in the menu. Find the row that matches the app you have in mind, and follow the link. In the editor, Ctrl-. lists every call.

For The calls
Being an app draw() init() setup() loop() on_show() on_hide() on_button()
Skipping a turn or staying longer should_show() duration()
Running as a background script or from the device menu # @headless true, # @ondemand
Drawing clear() pixel() line() rect() rect_fill() circle() circle_fill() icon() width() height() rgb() hsv()
Writing text text() text_width() text_ink_width() font()
Text that moves or shades scroll_text() ramp_text()
Charts and bars bar_chart() line_chart() progress()
Animated backgrounds effect() overlay()
The clock and the calendar hour() minute() second() weekday() day() month() year() now_ms() epoch_ms()
Working with numbers num() round() clamp() min() max()
Doing something later timer.after() timer.every() timer.cancel()
Buttons on_button() on_button_event()
What the device measures sensor.temperature() sensor.humidity() sensor.pressure() sensor.light() sensor.battery() sensor.battery_volts()
Remembering across a reboot store.get() store.set()
Letting the user change something a # @config line, then store.get()
Handing values to another app shared.set() shared.get() shared.age() shared.keys()
Sharing code between apps a # @module file, then import
Fetching from the internet http.get() http.post() http.put() http.patch() http.delete() http.request()
Picking a value out of a reply re.search() re.match() re.matchall(), or json.load()
Home automation mqtt.publish() mqtt.subscribe()
Reading a Modbus device modbus.readHoldingRegisters() modbus.readInputRegisters() modbus.readCoils() modbus.readDiscreteInputs()
Making a noise sound.play() sound.stop() sound.playing() sound.can()
Music written as text sound.play({'song': text, 'loop': true})
Shipping the sounds you play MP3s in the script's own folder
Reacting to the music music.bands() music.level() music.beat() music.playing() music.station() music.title()
Interrupting with an alert notify()
Moving the rotation along rotation.show() rotation.next() rotation.previous() rotation.pause() rotation.resume() rotation.close()
Turning the display on and off display.power() display.is_on()
What the owner configured settings.get() settings.set() settings.apply_case()
Working out what went wrong log()
Which firmware is running version()
Shipping the icons you draw a # @icons line, then icon()
Naming the scripts yours needs a # @requires line
Saying which clocks it runs on # @needs and # @display lines
Getting it ready to share the header checklist

What needs an import. The drawing, time and number calls are plain functions. http, mqtt, store, shared, settings, sensor, display, sound, music, rotation, timer and re are ready to use. modbus, the general modules json, string and math, and your own modules need an import line at the top of the file.

Just enough Berry

Scripts are written in Berry, a small language that looks a bit like Python. You do not need to know it to start: copy the examples and change them. The full language guide is there when you want more.

Comments start with # and run to the end of the line.

Variables are made with var. They have no declared type. These are the kinds of value:

var count = 3                            # integer: a whole number
var temp = 21.5                          # real: a number with decimals
var name = "kitchen"                     # string: text
var ready = true                         # bool: true or false
var readings = [21, 23, 22]              # list: several values in order
var spec = {"text": "Hi", "hold": true}  # map: values stored under names
var empty = nil                          # nil: nothing yet, or no answer

You will also meet a range (0 .. width() - 1 below) and a function, which you can pass around like any value. See Callbacks. type(v) answers "int", "real", "string", "bool" or "nil". Lists and maps both answer "instance", so use isinstance(v, list) or isinstance(v, map) for those.

Do not name a variable like a built-in call. A variable called text hides the call text(), and the next text(…) in that method stops your app with type_error: 'string' value is not callable. Pick another name: var label = "21°".

Numbers calculate with +, -, *, / and % (the remainder). Whole numbers divide without a remainder: 7 / 2 is 3, and 50 / 100 is 0. Multiply before you divide (50 * 8 / 100 is 4), or make one of the numbers a real (50 / 100.0 is 0.5).

Text is joined with +. Turn a number into text with str() first:

text(1, 6, "room " + name, 0xFFFFFF)   # joining text: fine
text(1, 6, str(temp) + "°", 0xFFFFFF)  # a number needs str() first

format() puts numbers into a pattern. %d is a whole number, %02d a whole number with at least two digits, and %.1f a number with one decimal:

format("%d:%02d", 9, 5)     # "9:05"
format("%.1f°", 21.46)      # "21.5°"

Decisions use if … elif … else, closed with end. Compare with ==, !=, <, >= and so on. Combine with && (and) and || (or):

if temp >= 30
  text(1, 6, "HOT", 0xFF0000)
elif temp >= 18 && temp < 30
  text(1, 6, "ok", 0x00FF00)
else
  text(1, 6, "cold", 0x0000FF)
end

For a choice between two values, condition ? a : b answers a when the condition is true and b otherwise:

var label = temp >= 30 ? "HOT" : "ok"

Repetition uses for over a range or a list, or while with a condition. Both close with end:

for x : 0 .. width() - 1     # x runs 0, 1, ... to the right-hand edge
  pixel(x, 7, 0x202020)
end

Lists: [] makes an empty one, push() adds, remove(i) deletes by position, size() counts, l[0] is the first entry and l[-1] the last:

var readings = []
readings.push(21)
readings.push(23)
text(1, 6, str(readings[-1]), 0xFFFFFF)   # the newest one

Maps: read a value with find(). It answers nil when the key is missing. m["key"] raises an error on a missing key, so use it only for keys you wrote yourself. Maps also pass settings to notify() and effect():

var spec = {"text": "Hi", "hold": true}
var t = spec.find("text")                 # "Hi"
var pic = spec.find("icon")               # nil: absent, not an error
var shown = spec.find("icon", "none")     # "none": your own fallback

Modules add calls that are not built in. Load one with import at the top of the file, above the class. What needs an import lists them:

import json

var data = json.load("{\"temp\": 21.5}")   # a map: {"temp": 21.5}

Errors you expect are caught with try … except … end. The script keeps running, and the lines after except decide what happens instead:

var label
try
  label = str(data["current"]["temp"])     # [] raises an error on a missing key
except .. as err, msg
  log(msg)
  label = "--"
end

except .. catches every error. err and msg are its name and message. Without try, an error stops your app and the display shows ERR:.

Every if, for, while, def and class needs its own end. When saving reports a syntax_error, look for a missing end first. The line the error names can be further down.

Callbacks

Some calls take a function and call it later: http.get() when the answer arrives, timer.after() when the time is up, mqtt.subscribe() for every message. Such a function is a callback. There are two ways to write one:

# short: / arguments -> one expression
timer.after(3000, / -> self.reset())

# long: def (arguments) ... end, for several lines
mqtt.subscribe("home/door", def (topic, payload)
  self.door = payload
  self.changed = true
end)
  • A callback written inside a method of your app can use self, your app, so it can change your app's members.
  • The short form holds one expression. Usually it hands everything on to a method of your app: / body, status -> self.on_body(body, status).
  • Write callbacks where you start something: in setup(), loop() or a button handler. Never write them in draw().

Good to know

  • Whole numbers divide without a remainder: 50 / 100 is 0. Multiply before you divide, or write 100.0.
  • Do not name a variable like a built-in call. A variable called text hides text(), and the next call to it fails. Use another name, such as label.
  • Build lists, maps and text outside draw(). One built in draw() is built again 40 times a second. Build it in init(), loop() or a callback, and keep it in a member.
  • Drawing works only in draw(). A text() in loop() or setup() shows nothing and raises no error. Keep the value in a member and paint it in draw().
  • A syntax_error when you save usually means a missing end. The line it names can be further down.

Details

The lifecycle

The hooks are methods on your class. Only draw() is required. Add the others when you need them.

Method When it is called Can draw?
init() once, when AWTRIX creates your app no
setup() once, right after the app loads, before the first frame no
loop() about once a second, whether or not your app is shown no
draw() every frame (~40/s) while your app is shown yes
on_show() your app has just appeared no
on_hide() your app has just left the display no
on_button(btn) a button was pressed while your app is shown. btn is "left", "select" or "right". Return true to take the press no
on_button_event(btn, event) a press, hold or release of a button your app took. See Button events no
should_show() the rotation moves on to your app. Return false to skip this turn. See Skip a turn no
duration() your app appears. Return milliseconds to set how long it stays. See Stay longer no
  • init() or setup()? Give members their starting values in init(). Do first fetches and logging in setup(). Your store is ready in both.
  • After a reboot, the apps start their first loop() a couple of seconds apart, so they do not all fetch in the same second.
  • width(), height() and the text measurements work in every hook.
  • A misspelled built-in call (clesr()) shows up when you save, as a syntax_error. Your own methods are looked up when they are called, so self.helper() may use a method defined further down the class.
  • Only the app shown is asked about a button. If on_button() raises an error, the press goes through as usual, so a broken app cannot lock the buttons.
  • The class name is yours. Two scripts may both use class App.
  • should_show(), duration() and the header lines that keep an app out of the rotation are in Several apps together.

The editor

Built-in calls are shown in their own color, so a misspelled pixel stays plain. Ctrl-. lists the calls, Ctrl-S saves, Ctrl-/ comments lines in or out, and Tab and Shift-Tab indent a selection. The status line shows the cursor position and the size of the script. Every button of the Scripts tab is in The web UI.