Vestaboard Integration

I’ve created a custom Vestaboard integration that allows you to post custom content to your Vestaboard from within Home Assistant. It uses the local API and also provides sensors for showing the current state of the board.

What is Vestaboard?
Vestaboard is a 6x22 characters connected split flap display for your home or office space. It comes with an app to post curated or custom messages to and an optional subscription called Vestaboard+ that allows for integration with custom services to push real-time messaging to it. But most importantly, it also comes with a local API (as well as a cloud-based one).

How do I use Vestaboard with Home Assistant?
There are two ways to use Vestaboard through Home Assistant. The easiest way is to install my custom integration and request local API access to your Vestaboard. The latter may take a couple of days, but is really the way to go.

The other option is to use the cloud API and some custom rest_commands. See the instructions below on how to set that up.

Cloud API
Go to the Vestaboard website, login with your account and create API credentials. You will get an API Key, API Secret and a Subscription ID from there. You will need them in a bit. Vestaboard provides more details on this process in it’s API documentation, which you can access by registering as a developer here. Or you just google for it if you don’t want to register.

Copy and paste the following rest_command into your configuration.yaml:

rest_command:
  vestaboard_message:
    url: "https://platform.vestaboard.com/subscriptions/{{ vestaboard_subscription_id}}/message"
    method: POST
    content_type: application/json
    headers:
      X-Vestaboard-Api-Secret: "{{vestaboard_api_secret}}"
      X-Vestaboard-Api-Key: "{{vestaboard_api_key}}"
    verify_ssl: true
    payload: >
      {% if text is defined %}
        {{ { "text": text }|to_json }}
      {% else %}
        {% set map = {
          ' ': 0,
          'A': 1,
          'B': 2,
          'C': 3,
          'D': 4,
          'E': 5,
          'F': 6,
          'G': 7,
          'H': 8,
          'I': 9,
          'J': 10,
          'K': 11,
          'L': 12,
          'M': 13,
          'N': 14,
          'O': 15,
          'P': 16,
          'Q': 17,
          'R': 18,
          'S': 19,
          'T': 20,
          'U': 21,
          'V': 22,
          'W': 23,
          'X': 24,
          'Y': 25,
          'Z': 26,
          '1': 27,
          '2': 28,
          '3': 29,
          '4': 30,
          '5': 31,
          '6': 32,
          '7': 33,
          '8': 34,
          '9': 35,
          '0': 36,
          '!': 37,
          '@': 38,
          '#': 39,
          '$': 40,
          '(': 41,
          ')': 42,
          '-': 44,
          '+': 46,
          '&': 47,
          '=': 48,
          ';': 49,
          ':': 50,
          "'": 52,
          '"': 53,
          '%': 54,
          ',': 55,
          '.': 56,
          '/': 59,
          '?': 60,
          '°': 62,
          '\xc1': 63,
          '\xc2': 64,
          '\xc3': 65,
          '\xc4': 66,
          '\xc5': 67,
          '\xc6': 68,
          '\xc7': 69,
          '~': 0
        } %}
        {% set l = lines %}
        {% set ns = namespace(l = [], m = []) %}
        {% for i in range(6) %}
          {% set s = "" if i >= l|length else l[i] %}
          {% set a = '{:<22}'.format(s)|upper|list %}
          {% set ns.l = [] %}
          {% for c in a %}
            {% set ns.l = ns.l + ([map[c if c in map else '?']]) %}
          {% endfor %}
          {% set ns.m = ns.m + [ns.l] %}
        {% endfor %}
        {{ {'characters': ns.m }|to_json }}
      {% endif %}

This rest command implements the Vestaboard message API in both it’s text and characters variant. You will need to provide the Vestaboard API key, Vestaboard API secret and Vestaboard subscription ID you’ve obtained earlier to the rest_command call. I recommend storing them each in the secrets.yaml file. In addition you also need to provide either a “text” argument or a “lines” argument.

The text argument is a simple string and Vestaboard will take care of laying it out and positioning it on the board for you.

The lines argument is expected to be a list of 6 strings. One for each row of the Vestaboard. Each string should not be longer than 22 characters. The rest_command implementation will truncate strings that are longer than that.

Examples:

service: rest_command.vestaboard_message
data:
  vestaboard_api_key: !secret vestaboard_api_key
  vestaboard_api_secret: !secret vestaboard_api_secret
  vestaboard_subscription_id: !secret vestaboard_subscription_id
  text: "The quick brown fox jumps over the lazy dog"
service: rest_command.vestaboard_message
data:
  vestaboard_api_key: !secret vestaboard_api_key
  vestaboard_api_secret: !secret vestaboard_api_secret
  vestaboard_subscription_id: !secret vestaboard_subscription_id
  lines:
    - ""
    - "\xc1 The quick brown fox"
    - "\xc1 jumps over the lazy"
    - "\xc1 dog."
    - "~- The Boston Journal."
    - ""

Since the Vestaboard characters API doesn’t work with ASCII character codes, the rest_command implementation will map each of the characters in the strings to their respective Vestaboard character codes. There’s also a set of special character mappings for the color characters, as you can see used above (\xc1). To use them in your string simply type out the special character codes \xc1 to \xc7. And since I was struggling with working around yaml string trimming I also mapped the “~” character to the Vestaboard whitespace character. If you need leading whitespace anywhere on your board, you can simply use the otherwise unused “~” character. For your convenience, I’ll list all the special character mappings here:

  • \xc1: red
  • \xc2: orange
  • \xc3: yellow
  • \xc4: green
  • \xc5: blue
  • \xc6: purple
  • \xc7: white
  • ~: (whitespace)

What can I do with this?
I have created an automation, that among date, time and an uplifting message will display various data from my smart home, like temperature, air quality, the state of the HVAC units in my home as well as the upcoming appointment on the family calendar. But your imagination really is the limit here!

Checkout my Vestaboard in action here: https://youtu.be/b-KxvMScREw

Where can I get one?
You can buy your own Vestaboard on their website. It’s a small Silicon Valley startup and the board itself is really well made and it looks stunning in any space. If you use the following referral link, you can safe $200 off the purchasing price and I will get a $200 referral bonus:

But feel free to skip the referral link if you don’t feel comfortable using it.

6 Likes

Would love to see the automation code that converts your weather and calendar to vestaboard strings, thanks!

Hey Keith,

Happy to share my automation. Note that it’s just a copy & paste and I didn’t make any effort to particularly clean it up or anything. But might be a good starting point.

alias: Update vestaboard
id: update_vestaboard
trigger:
  - platform: time_pattern
    seconds: 0
  - platform: state
    entity_id:
      - input_text.vestaboard_message
      - sensor.back_temperature
      - sensor.front_temperature
      - sensor.deck_weather_temperature
      - climate.back
      - climate.front
      - input_boolean.vestaboard
  - platform: state
    entity_id: calendar.family
condition:
  - condition: state
    entity_id: input_boolean.vestaboard
    state: 'on'
mode: restart
action:
  service: rest_command.vestaboard_message
  data:
    vestaboard_api_secret: !secret vestaboard_api_secret
    vestaboard_api_key: !secret vestaboard_api_key
    vestaboard_subscription_id: !secret vestaboard_subscription_id
    lines:
      - >
        {% set time = now().strftime('%a %b %d %Y, %H:%M' )  %}
        {{ time }}
      - >
        {%
          set bc =
            "\xc4"
              if is_state_attr('climate.back', 'preset_mode','eco')
            else
            "\xc1"
              if is_state_attr('climate.back', 'hvac_action', 'heating')
            else
            "\xc5"
              if is_state_attr('climate.back', 'hvac_action', 'cooling')
            else
            "\xc6"
              if is_state_attr('climate.back', 'fan_mode', 'on')
            else
            "\xc7"
              if not is_state('climate.back', 'off')
            else
            " "
        %}
        {%
          set fc =
            "\xc4"
              if is_state_attr('climate.front', 'preset_mode','eco')
            else
            "\xc1"
              if is_state_attr('climate.front', 'hvac_action', 'heating')
            else
            "\xc5"
              if is_state_attr('climate.front', 'hvac_action', 'cooling')
            else
            "\xc6"
              if is_state_attr('climate.front', 'fan_mode', 'on')
            else
            "\xc7"
              if not is_state('climate.front', 'off')
            else
            " "
        %}
        {%
          set oc =
            "\xc4"
              if is_state('binary_sensor.home_target_venting_state', 'on')
            else
              "\xc1"
                if states('sensor.deck_weather_temperature')|float(0) >
                  states('sensor.dynamic_target_temperature_range_upper')|
                  float(0)
            else
            "\xc5"
              if states('sensor.deck_weather_temperature')|float(0) <
                states('sensor.dynamic_target_temperature_range_lower')|
                float(0)
            else
            "\xc7"
        %}
        {%
          set aqi =
            states('sensor.waqi_concord_contra_costa_california')|int
        %}
        {%
          set ac =
            "\xc6"
              if aqi > 200
            else
            "\xc1"
              if aqi > 150
            else
            "\xc2"
              if aqi > 100
            else
            "\xc3"
              if aqi > 50
            else
            "\xc4"
        %}
        {%
          set stats =
            "{1}{0}°, {3}{2}°, {5}{4}°, {7}AQI".format(
              states('sensor.back_temperature')|round(0, 'common', 0),
              bc,
              states('sensor.front_temperature')|round(0, 'common', 0),
              fc,
              states('sensor.deck_weather_temperature')|round(
                0,
                'common',
                0
              ),
              oc,
              states('sensor.waqi_concord_contra_costa_california'),
              ac
            )
        %}
        {{ stats }}
      - >
        {% set start = strptime(state_attr('calendar.family','start_time'), '%Y-%m-%d %H:%M:%S') %}
        {% set dt = now().replace(tzinfo=None) - start %}
        {% set c = "\xc1" if (is_state('calendar.family', 'on') or dt.days == 0) else "\xc3" if dt.days == -1 and dt.seconds > 23 * 60 * 60 else "\xc7" if dt.days < -1 else "\xc4" %}
        {% set cal_message = state_attr('calendar.family', 'message') %}
        {%
          set matches = cal_message | regex_findall(
            '(^.{0,16}$)|(^.{1,16})[ ]|(^.{17,}$)'
          )
        %}
        {% set cal_message_line1 = matches[0][0] or matches[0][1] or matches[0][2] %}
        {% set cal_message_line2 = cal_message[cal_message_line1|length+1:] %}
        {% set startf = start.strftime('%H:%M') if is_state_attr('calendar.family', 'all_day', false) else '~~24h' %}
        {% set line1_time = cal_message|length < 17 %}
        {%
          set cal_line1 =
            "{}{:<16}{}".format(
                c,
                cal_message_line1[:16],
                startf if line1_time else ""
              )
        %}
        {{ cal_line1 }}
      - >
        {% set start = strptime(state_attr('calendar.family','start_time'), '%Y-%m-%d %H:%M:%S') %}
        {% set cal_message = state_attr('calendar.family', 'message') %}
        {%
          set matches = cal_message | regex_findall(
            '(^.{0,16}$)|(^.{1,16})[ ]|(^.{17,}$)'
          )
        %}
        {% set cal_message_line1 = matches[0][0] or matches[0][1] or matches[0][2] %}
        {% set cal_message_line2 = cal_message[cal_message_line1|length+1:] %}
        {% set startf = start.strftime('%H:%M') if is_state_attr('calendar.family', 'all_day', false) else '~~24h' %}
        {% set line1_time = cal_message|length < 17 %}
        {%
          set cal_line2 =
            '~{:<16}{}'.format(
              cal_message_line2[:16],
              startf if not line1_time else ''
            )
        %}
        {{ cal_line2 }}
      - ""
      - >
        {% if not is_state('input_text.vestaboard_message', '') %}
          {% set message = states('input_text.vestaboard_message') %}
        {% elif is_state('calendar.tice_valley_message', 'on') %}
          {% set message = state_attr('calendar.tice_valley_message', 'message')[:-9] %}
        {% else %}
          {% set n = now() %}
          {% if (is_state('sensor.occupancy', 'Sleep')) or n.hour < 3 %}
            {% set message = "Good night." %}
          {% elif (n.hour < 11) %}
            {% set message = "Good morning!" %}
          {% elif (n.hour < 15) %}
            {% set message = "Have a great day!" %}
          {% elif (n.hour < 18) %}
            {% set message = "Good afternoon." %}
          {% else %}
            {% set message = "Good evening." %}
          {% endif %}
        {% endif %}
        {{ message.center(22, '~') }}
2 Likes

@dev0 What great work. Been wanting this for a while. Here is one for you for today. Worked like a charm this morning.

service: rest_command.vestaboard_message
data:
  vestaboard_api_key: !secret vestaboard_api_key
  vestaboard_api_secret: !secret vestaboard_api_secret
  vestaboard_subscription_id: !secret vestaboard_subscription_id
    lines:
      - "\xc4\xc4\xc6\xc6\xc5\xc5\xc5\xc5\xc6\xc6\xc4\xc4\xc6\xc6\xc5\xc5\xc5\xc5\xc6\xc6\xc4\xc4"
      - "\xc3\xc4\xc4\xc6\xc6\xc5\xc5\xc6\xc6\xc4\xc4\xc4\xc4\xc6\xc6\xc5\xc5\xc6\xc6\xc4\xc4\xc3"
      - "\xc3\xc3\xc4\xc4\xc6\xc6H\xc6A\xc4P\xc3P\xc4Y\xc6\xc6\xc6\xc4\xc4\xc3\xc3"
      - "\xc1\xc3\xc3\xc4\xc4E\xc6A\xc4S\xc3T\xc3E\xc4R\xc6\xc4\xc4\xc3\xc3\xc1"
      - "\xc1\xc1\xc3\xc3\xc4\xc4\xc4\xc4\xc3\xc3\xc1\xc1\xc3\xc3\xc4\xc4\xc4\xc4\xc3\xc3\xc1\xc1"
      - "\xc5\xc1\xc1\xc3\xc3\xc4\xc4\xc3\xc3\xc1\xc1\xc1\xc1\xc3\xc3\xc4\xc4\xc3\xc3\xc1\xc1\xc5"
  mode: single
  icon: mdi:script-text-outline
1 Like

I’m having what looks like a payload issue. Using the examples you have above (thanks!)

I did notice a space in the {{ vestaboard_subscription_id}} above which I tried with/without just in case.

Here’s the msg I am getting:

Logger: homeassistant.components.rest_command
Source: components/rest_command/__init__.py:137
Integration: RESTful Command (documentation, issues)
First occurred: 1:33:07 PM (3 occurrences)
Last logged: 1:38:08 PM

Error. Url: https://platform.vestaboard.com/subscriptions//message. Status code 404. Payload: b'{"characters": [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]}'

I get the same msg if I try it with text or lines. Any ideas??

You need to set the vestaboard_subscription_id (and also the vestaboard_api_secret, vestaboard_api_key) variables/parameters. Make sure you do not have any typos / spelling mistakes.

And if you literally just copy + pasted my example, make sure you set the secrets in your secrets.yaml to your own subscription, api_key and api_secret values.

1 Like

Got it thanks! I needed to hard code the api/subscription/id/etc as my secrets wasn’t working for some reason. Looks like I need to tweak some other things but over this works great! Thanks for doing this. Would be great to see an official integration.

Vestaboard recently released a local API for it’s split-flap boards. If there’s interest I can update and share a version of the rest_command that uses the local API instead of the cloud platform API. Please “like”/“heart” this comment if you are interested in using the local API.

8 Likes

Thanks for kicking off Vestaboard support!

I had seen a note about local api coming; glad to see it has actually launched!

This rest_command will use Vestaboard Local API if it was enabled on your Vestaboard. To do so, please follow the steps outlined here.

rest_command:
   vestaboard_local_message:
    url: "http://{{vestaboard_host}}:7000/local-api/message"
    method: POST
    content_type: application/json
    verify_ssl: false
    headers:
      X-Vestaboard-Local-Api-Key: "{{vestaboard_local_api_key}}"
    payload: >
      {% set map = {
        ' ': 0,
        'A': 1,
        'B': 2,
        'C': 3,
        'D': 4,
        'E': 5,
        'F': 6,
        'G': 7,
        'H': 8,
        'I': 9,
        'J': 10,
        'K': 11,
        'L': 12,
        'M': 13,
        'N': 14,
        'O': 15,
        'P': 16,
        'Q': 17,
        'R': 18,
        'S': 19,
        'T': 20,
        'U': 21,
        'V': 22,
        'W': 23,
        'X': 24,
        'Y': 25,
        'Z': 26,
        '1': 27,
        '2': 28,
        '3': 29,
        '4': 30,
        '5': 31,
        '6': 32,
        '7': 33,
        '8': 34,
        '9': 35,
        '0': 36,
        '!': 37,
        '@': 38,
        '#': 39,
        '$': 40,
        '(': 41,
        ')': 42,
        '-': 44,
        '+': 46,
        '&': 47,
        '=': 48,
        ';': 49,
        ':': 50,
        "'": 52,
        '"': 53,
        '%': 54,
        ',': 55,
        '.': 56,
        '/': 59,
        '?': 60,
        '°': 62,
        '\xc1': 63,
        '\xc2': 64,
        '\xc3': 65,
        '\xc4': 66,
        '\xc5': 67,
        '\xc6': 68,
        '\xc7': 69,
        '~': 0
      } %}
      {% set l = lines %}
      {% set ns = namespace(l = [], m = []) %}
      {% for i in range(6) %}
        {% set s = "" if i >= l|length else l[i] %}
        {% set a = '{:<22}'.format(s)|upper|list %}
        {% set ns.l = [] %}
        {% for c in a %}
          {% set ns.l = ns.l + ([map[c if c in map else '?']]) %}
        {% endfor %}
        {% set ns.m = ns.m + [ns.l] %}
      {% endfor %}
      {{  ns.m |to_json }}

Make sure to call the rest command providing the data vestaboard_host (set to the IP address of your Vestaboard) and vestaboard_local_api_key (obtained as a result of enabling Local API on your Vestaboard)

I’ve made a little update to the rest_command to handle characters that Vestaboard cannot display better. Before, it would simply skip characters it cannot display, which would lead to lines that were too short. Vestaboard would reject such requests and not display them at all. Instead, characters that cannot be displayed will now be replaced by a question mark character.

I had built REST post messages by hand previously. This integration is much better thankyou.

I tried it but get an error. I put the code into my configuration file and my subscriptionid, apikey and apisecret in my secrets file. I get the following error when calling the sample code

Source: helpers/template.py:419
First occurred: 4:43:38 PM (9 occurrences)
Last logged: 4:46:31 PM

* Template variable warning: 'vestaboard_subscription_id' is undefined when rendering 'https://platform.vestaboard.com/subscriptions/{{ vestaboard_subscription_id}}/message'
* Template variable warning: 'vestaboard_api_secret' is undefined when rendering '{{vestaboard_api_secret}}'
* Template variable warning: 'vestaboard_api_key' is undefined when rendering '{{vestaboard_api_key}}

Any ideas?

When calling the rest_command you need to provide them as arguments to the rest_command.

Also see the usage examples in my original post.

Yep did that in the sample code. checked all the variable names to ensure they matched. vestaboard_api_secret, etc

service: rest_command.vestaboard_message
data:
  vestaboard_api_key: !secret vestaboard_api_key
  vestaboard_api_secret: !secret vestaboard_api_secret
  vestaboard_subscription_id: !secret vestaboard_subscription_id
  text: "The quick brown fox jumps over the lazy dog"

do I need to define the variables somewhere?

The checking you did includes adding them secrets.yaml file and reloading Home Assistant?

The error message states that the variable within the rest_command execution is undefined. That either means passing of the variable from the service call to the rest_command isn’t working or the value in the service call is already undefined (i.e. they are not correctly retrieved from the secrets.yaml file).

These are the two only things I can think of. The invocation itself, assuming the values are correctly set in the secrets.yaml file and loaded, looks correct.

Yes put it in the secrets file and restarted HA

See below for what is in secrets file. Tried with and without quotes

IFTTT_key: 12345
X-Vestaboard-Api-Key: abcde
X-Vestaboard-Api-Secret: fghij
vestaboard_subscription_id: "klmnop"
vestaboard_api_key: "qrstuv"
vestaboard_api_secret: "wxyz1234567890"


Interestingly if I try and use your same code in the developer tools to call service with the actual secrets in the code in quotes it works. My coding skills are rudimentary at best so clearly not not strong enough to work this one out

eg


service: rest_command.vestaboard_message
data:
  vestaboard_api_key: "qrstuv"
  vestaboard_api_secret: "wxyz1234567890"
  vestaboard_subscription_id: "klmnop"
  text: "The quick brown fox jumps over the lazy dog"

I ran into this as well. The issue is that the developer tools is unable to pull the values from the secrets.yaml file. If you instead put the service call in an automation, you should find that it works as expected.

1 Like

This sounds familiar and would make sense.

This is not done done, but I’ve started working on an actual Home Assistant integration that can be used to post messages to Vestaboards instead of having to add custom rest_commands to your yaml configuration.

Right now, the integration is still a custom_integration and not exactly robust, but it does work if installed correctly. You can clone it and check it out here: GitHub - DanielBaulig/vestaboard: Vestaboard Home Assistant integration

Please provide any feedback you may have (that’s not already outlined in the issues section).

1 Like

thankyou. I am currently away from my HA but I will try that.