Ulanzi Desktop Pixel Clock on Esphome

Written by blakadder on
Ulanzi Desktop Pixel Clock on Esphome

Guide on installing Esphome on Ulanzi TC001 pixel clock I reviewed and making it work with Home Assistant.

Full disclosure: This device is purchased with the help of your donations. Links in this article are affiliate links. By purchasing through them I also get a small commission that funds future projects!

Ulanzi Smart Pixel Clock is available from the following stores:

In my internet searches for everything Ulanzi and Awtrix pixel clock related I found lubeda’s awtrix python script for Home Assistant. I checked out lubeda’s profile, as you usually do as a low key internet stalker, and a repository called EsphoMaTrix was on the list. I don’t know if it was the camel case of the title or the description “A simple DIY status display with an 8x32 RGB LED panel implemented with esphome.io” that drew me in. Well, I know some Esphome and I like tight Home Assistant integrations so I put it on the “to try out” list. Meanwhile I also got a Twitter ping mentioning the same project.

Almost two months later I got to try it out and, lo and behold, there was even support of Ulanzi TC001, albeit in an unfinished stage. Sometimes you unintentionally wait and others do the bulk of the work for you.

This will be a great solution for a Home Assistant or standalone user, if you’re not on of those you might be better off trying out PixelIt_Ulanzi which was also mentioned to me on Twitter a couple of times.


This configuration will not work with new ESPhome versions and the component has been moved to V2 with a different configuration. Update for this configuration is in the works!

First you need to have Esphome. If you don’t, install it using the appropriate method for your situation. For me it was a Docker container on my small Celeron J3455 home server.

Open Esphome dashboard and start a new project. Paste the yaml.

  devicename: ulanzi
  matrix_pin: GPIO32 
  buzzer_pin: GPIO15
  battery_pin: GPIO34 
  ldr_pin: GPIO35 
  left_button_pin: GPIO26 
  mid_button_pin: GPIO27 
  right_button_pin: GPIO14 
  scl_pin: GPIO22 
  sda_pin: GPIO21 

  - source:
      type: git
      url: https://github.com/lubeda/EsphoMaTrix
    refresh: 60s 
    components: [ ehmtx ]   

  comment: "Ulanzi TC001"
  name: $devicename 
      - ds1307.read_time:
  board: esp32dev


  ssid: !secret wifi_ssid
  password: !secret wifi_password


  # adapt the filename to your local settings
  - file: Calcium.ttf
    id: ehmtx_font
    size: 16
    glyphs:  |
      !?"%‰()+*=,-_.:°µ²³0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnÖÄÜöäüopqrstuvwxyz€$@<>/

  id: rgb8x32
  time_component: ehmtx_time
  matrix_component: ehmtx_display
  clock_time: 5           # duration to display the clock after this time the date is display until next "show_screen"
  clock_interval: 60      # show the clock at least each x seconds
  screen_time: 6          # duration to display a screen or a clock/date sequence
  date_format: "%d.%m"    # defaults "%d.%m." (use "%m.%d." for the US)
  time_format: "%H:%M"    # defaults "%H:%M" (use "%I:%M%p" for the US)
  show_dow: false         # draw the day indicator on the bottom of the screen, defaults to true
  show_date: true         # show the date for show_screen - show_clock seconds otherwise only shows the clock for show_screen seconds, defaults to true
  week_start_monday: true # default monday is first day of week, false = sunday
  yoffset: 8              # the text is aligned BASELINE_LEFT, the baseline defaults to 6
  xoffset: 1              # the text is aligned BASELINE_LEFT, the left defaults to 1
  scroll_interval: 80    # the interval in ms to scroll the text (default=80), should be a multiple of the update_interval from the display (default: 16ms)
  frame_interval: 192     # the interval in ms to display the next anim frame (default=192), should be a multiple of the update_interval from the display (default: 16ms)
  font_id: ehmtx_font
    - id: ha
      lameid: 7956
    - id: tempc
      lameid: 2422
    - id: plug
      lameid: 403
    - id: humidity
      lameid: 51764
    - id: co2
      lameid: 30662

  - platform: status
    name: "$devicename Status"
  - platform: gpio
      number: $left_button_pin
      inverted: true
    name: "$devicename left button"
        - number.decrement: screen_brightness
  - platform: gpio
      inverted: true
      number: $mid_button_pin
      mode: INPUT_PULLUP
    name: "$devicename middle button"
        - switch.toggle: displaycontrol
  - platform: gpio
      number: $right_button_pin
      inverted: true
    name: "$devicename right button"
        - number.increment: screen_brightness
# example to switch to next screen
#        lambda: |-
#          id(rgb8x32)->skip_screen();
  level: INFO

# Enable Home Assistant API
    - service: alarm
        icon_name: string
        message: string
        lambda: |-
    - service: brightness
        brightness: int
        lambda: |-
    - service: icons
        lambda: |-
    - service: skip_screen
        lambda: |-
    - service: tuneplay
        tune: string
        - rtttl.play:
            rtttl: !lambda 'return tune;'

  - platform: template
    name: "$devicename brightness"
    id: screen_brightness
    min_value: 0
    max_value: 255
    update_interval: 1s
    step: 25
    lambda: |-
      return id(rgb8x32)->get_brightness();
      lambda: |-

  - platform: template
    name: "$devicename Display"
    id: displaycontrol
    icon: "mdi:power"
    restore_mode: ALWAYS_ON
    lambda: |-
      return id(rgb8x32)->show_display;
      lambda: |-
      lambda: |-

  - platform: sht3xd
      name: "$devicename Temperature"
      name: "$devicename Humidity"
    update_interval: 60s
  - platform: adc
    pin: $battery_pin
    name: "$devicename Battery"
    id: battery_voltage
    update_interval: 10s
    device_class: battery
    accuracy_decimals: 0
    attenuation: auto
      - sliding_window_moving_average:
          window_size: 15
          send_every: 15
          send_first_at: 1
      - multiply: 1.6
      - lambda: |-
          return ((x - 3) / 0.69 * 100.00);
    unit_of_measurement: '%'
  - platform: adc
    id: light_sensor
    name: "$devicename Illuminance"
    pin: $ldr_pin
    update_interval: 10s
    attenuation: auto
    unit_of_measurement: lx
    accuracy_decimals: 0
      - lambda: |-
          return (x / 10000.0) * 2000000.0 - 15 ;
  - platform: ledc
    pin: $buzzer_pin
    id: rtttl_out

  output: rtttl_out

  sda: 21
  scl: 22
  scan: true
  id: bus_a

  - platform: neopixelbus
    id: ehmtx_light
    type: GRB
    variant: WS2812
    pin: $matrix_pin
    num_leds: 256
    color_correct: [30%, 30%, 30%]
    name: "$devicename Light"
    restore_mode: ALWAYS_OFF
      lambda: |-
       lambda: |-

  - platform: homeassistant
  - platform: ds1307
    update_interval: never
    id: ehmtx_time
  - platform: addressable_light
    id: ehmtx_display
    addressable_light_id: ehmtx_light
    width: 32
    height: 8
    pixel_mapper: |-
      if (y % 2 == 0) {
        return (y * 32) + x;
      return (y * 32) + (31 - x);
    rotation: 0°
    update_interval: 16ms
    auto_clear_enabled: true
    lambda: |-

Before installing download the Calcium font and place it in the same location as the configuration yaml. Connect the Ulanzi via the USB-C port to the machine with Esphome. Click install and choose “Plug into..” and wait for it to complete.

If that is not possible you can choose “Manual download”, grab the built binary and flash it manually with esptool.py or ESPHome-Flasher.


This is just the skeleton configuration, you need to add (animated) icons you’re going to use. Or do a deep dive and adapt it how you want it, it’s just Esphome.

Buttons and Sensors

Buttons are configured to change brightness using left and right buttons and turn the display on or off with the middle button.


Initial configuration had raw ADC sensors for battery and voltage. I’ve set them up as a lux and battery percentage sensors to the best of my abilities. Can’t say they’re accurate since the “calibration” was a trial and error job but they look to be in the ballpark.


You can add icons and animated gifs via a file or url. Esphome will try to downscale the static images to 8x8 but the icons don’t turn out great most of the time. The easier way is to use the Lametric Developer website and poach from their library of thousands of appropriately sized icons, many of them animated.

Create an account, go to Create new app and click Select icon under Look and Feel and find the ones you want to use.

There’s a somewhat hidden feature in EsphoMaTrix to pull the icons directly just by its LameTric numeric id:

    - id: ha
      lameid: 7956

I don’t know for how long will this be possible and I strongly recommend to download the icons you intend to use and save them locally. Add the icon number to the url https://developer.lametric.com/content/apps/icon_thumbs/ and download it to the same folder as the configuration yaml file. Use with:

    - id: batman
      file: batman.gif


To have good readable fonts you should search for pixel fonts. I’ve found a great resource called Bitfontmaker2 with a large selection of fonts with open licenses like public domain or Creative Commons. Bitfontmaker2 also lets you create your own font or edit the existing one to add characters you might be missing.


Not all fonts will work but there’s a higher chance to find a suitable one here than on the random free font repositories.

Download the font and save it to the same folder as the configuration yaml file and change the filename in the yaml.

  - file: fonts/BetterPixels.ttf
    id: ehmtx_font
    size: 16
    glyphs:  |
      !?"%()+*=,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnÖÄÜöäüopqrstuvwxyz€$@<>/

Some fonts will require different sizes, most likely 8 or 12 and many fonts will just be too big to display properly. Some fonts will fit only with uppercase letters.

Depending on the font size and shape you might need to raise or lower yoffset: to make the entire font visible.

The glyphs: key just tells Esphome which characters will be saved to flash memory and used by the display. If a character is missing from the font with a defined glyph it will be displayed as a white outlined square. If a glyph isn’t defined that character it will be displayed as a white block. Adjust according to your use and font’s capabilities. Fonts on Bitfontmaker2 have a GlyphMap link to display all available characters.

I settled on Calcium because it looks nice and has a lot of included glyphs. Other contenders were Awesome as a large font or, appropriately named, smallfonts with symbols or DMD-Small as small font options. Small fonts are useful if you have day_of_week indicator enabled, make sure to set yoffset: 6. Honorable mention goes to Fallout, font used in my favourite games Fallout 1 and 2


Many of the basic EsphoMaTrix services are in the configuration. Read more about Services.

There’s a new service, particular to the Ulanzi TC001: tune service which plays RTTTL tunes on the built in buzzer.

RTTTL or Ring Tone Text Transfer Language was used to transfer ringtones to Nokia cellphones. Some of you will remember the ubiquitous Nokia ringtone in the late 90’s and nowadays you can play it directly from Home Assistant as a notification on your pixel clock.

Open your Home Assistant instance and show your service developer tools with a specific service selected.

find service ESPHome devicename_tuneplay and enter

service: esphome.ulanzi_tuneplay
  tune: 'NokiaTun:d=4,o=5,b=225:8e6,8d6,f#,g#,8c#6,8b,d,e,8b,8a,c#,e,2a'


As with any other service you can use it in an automation.

There’s more than ten thousand melodies to choose from downloadable from Picaxe.

Example Automation

I’ve just gone ahead and used the ehmtk [README.md] example because it’s simple and effective.

alias: Ulanzi Display Stuff
description: "Create screens on Ulanzi TC001"
  - platform: state
    entity_id: sensor.temptest_pms5003_pm2_5
    id: pm25
  - platform: state
    entity_id: sensor.temptest_2_sht4x_temperature
    id: tempc
  - platform: state
    entity_id: sensor.temptest_mhz19b_carbondioxide
    id: co2
condition: []
  - service: esphome.ulanzi_add_screen
      icon_name: "{{trigger.id}}"
      text: >-
        {{ trigger.to_state.state }}{{ trigger.to_state.attributes.unit_of_measurement }}
      lifetime: 2
      alarm: false
mode: queued
max: 10

To make this work you have to configure icons in yaml with id’s identical to the trigger id used.

What happens is this:

First you need to configure an icon with icon_name: pm25. Then, when the trigger with the id “pm25” happens, Home Assistant calls the _screen service with icon_name: pm25 (identical to the trigger id). This will create a screen pm25 with the icon configured and then insert the value and unit of measurement of that sensor.

Final product


This were just basic examples. Read more on all the services you can call from Home Assistant and different displays EsphoMaTrix has in their README.

Ulanzi Smart Pixel Clock is available from the following stores:

I’m looking forward to see what you’ve accomplished with this powerful component for Esphome. Share your screens and tag me on Twitter or Instagram.


Chief shenaniganeer!


comments powered by Disqus