The situation
I’m a programmer and rely on Syncthing to sync ~1.3 million project files and smartphone pictures/videos across 11 devices. So why not have all important Syncthing data and its status in Home Assistant, and even let it talk to me/alert me when things happen?
I use TTS alerts a lot (using PicoTTS), have quite a well-defined structure for the sound files (in config/www/alerts/
), and need (at least) bilingual spoken text. I also use the var custom component to store things like the default media player and the alerts base URI there. My HA already has inputs for TTS on/off and TTS language.
The plan
So I envisioned something like the following for “Syncthing at-a-glance” in HA:
- A Lovelace glance card like this:
- A Syncthing info sensor that aggregates all info I need from several REST calls into the attributes of one single sensor:
- Automations that could alert me when Syncthing goes down and/or is started again.
- A script that could read out essential Syncthing info to me via TTS, in a comprehensible way. (I can trigger these using an RF remote when at home.)
Preparing Syncthing
Syncthing offers a REST API, so integration into HA should be relatively simple. Syncthing’s GUI and REST API are by default restricted to the machine it runs on (localhost
), so we need to allow access from the network.
Allow remote access
Warning: If you do this, enable a username and strong password for Syncthing, otherwise this can be a security risk!
Open the Syncthing Web UI and enter Actions → Settings. In the GUI tab, overwrite the value 127.0.0.1:8384
with 0.0.0.0:8384
:
This will enable access to Syncthings’s Web UI as well as access to its REST API from other machines (i.e., not only the one Syncthing runs on).
While here, also enter a username and password, if you haven’t already.
Get the API key
In the Web UI, under Actions → Settings, open the General tab:
If you already see an API key here, don’t change it. If you have no API key yet, click Generate to generate a new one.
Copy the API key and place it into your secrets.yaml
file (fake key shown here):
syncthing_api_key: Ziegheizee7do7Je8uiMiepu8Abeevee
Try if it works
Using curl
in a terminal window, you can easily check if the REST API works. Let’s assume Syncthing runs on a machine called studio1
in the local network. Type the following command, substituting server name and API key with your own:
curl -X GET -H "X-API-Key: Ziegheizee7do7Je8uiMiepu8Abeevee" http://studio1:8384/rest/system/version
You should get a JSON response like the following:
{
"arch": "amd64",
"codename": "Fermium Flea",
"isBeta": false,
"isCandidate": false,
"isRelease": true,
"longVersion": "syncthing v1.6.1 \"Fermium Flea\" (go1.14.4 linux-amd64) [email protected] 2020-06-02 09:49:22 UTC",
"os": "linux",
"version": "v1.6.1"
}
Great, it worked! Now let’s switch to Home Assistant.
Prepare Home Assistant
Note: I used Home Assistant Core version 0.112.0 for this. Your mileage may vary.
Sensors
Let’s add some sensors we can later use in a Lovelace glance card, and in our automations.
binary_sensor.syncthing
This sensor will show if Syncthing is alive.
Add the following binary sensor to your configuration, under the binary_sensor:
section:
binary_sensor:
# Syncthing (REST, requires GUI address '0.0.0.0:8384' and API Key)
- platform: rest
name: syncthing
resource: http://studio1:8384/rest/system/ping
headers:
X-API-Key: !secret syncthing_api_key
value_template: '{{ value_json.ping == "pong" }}'
device_class: connectivity
sensor.syncthing_version
This sensor will get some version and OS info from Syncthing.
Add it to your configuration, this time under the sensor:
section:
sensor:
# Syncthing (REST, requires GUI address '0.0.0.0:8384' and API Key)
# Version information
- platform: rest
name: syncthing_version
resource: http://studio1:8384/rest/system/version
headers:
X-API-Key: !secret syncthing_api_key
json_attributes:
- version
- longVersion
- os
- arch
value_template: '{{ value_json.version }}'
scan_interval: 300
Adjust resource:
to reflect your server name.
You can also adjust the scan_interval:
, I have set it to every 5 minutes (300 seconds) here.
sensor.syncthing_connections
This sensor gets the connection data for all your known devices, plus some totals. It can generate quite a lot of data, so don’t update it too often!
This sensor also belongs under sensors:
, just put it after the previous one:
# Connection information
- platform: rest
name: syncthing_connections
resource: http://studio1:8384/rest/system/connections
headers:
X-API-Key: !secret syncthing_api_key
json_attributes:
- connections
- total
# Don't read sensor value (it's the OLD last one!) but update directly from JSON
#value_template: "{{ states.sensor.syncthing_connections.attributes.connections.values() | selectattr('connected') | list | length }}"
value_template: "{{ value_json.connections.values() | selectattr('connected') | list | length }}"
scan_interval: 300
Again, adjust resource:
and scan_interval:
as needed.
sensor.syncthing_status
This sensor gets some status information from Syncthing. It also generates lots of data, so don’t update it too often!
You can also put this sensor just below the previous one:
# Status information
- platform: rest
name: syncthing_status
resource: http://studio1:8384/rest/system/status
headers:
X-API-Key: !secret syncthing_api_key
json_attributes:
- myID
- startTime
- uptime
- alloc
# Don't read sensor value (it's the OLD last one!) but update directly from JSON
#value_template: "{{ (states.sensor.syncthing_status.attributes.uptime / 86400) | round(0) | int }}"
value_template: "{{ (value_json.uptime / 86400) | round(0) | int }}"
unit_of_measurement: 'd'
scan_interval: 300
Again, adjust resource:
and scan_interval:
as needed.
sensor.syncthing_info
I wanted an extra “dummy” sensor that aggregates all information from the other sensors as attributes in a nice way. So this sensor uses the template
platform to do that.
This sensor belongs under sensors:
, just place it below the above ones:
# Info "sensor" that aggregates info from above REST sensors
- platform: template
sensors:
syncthing_info:
# force update if any of these change
entity_id:
- binary_sensor.syncthing
- sensor.syncthing_version
- sensor.syncthing_status
- sensor.syncthing_connections
value_template: "{{ states('binary_sensor.syncthing') }}"
# aggregate some useful info from the other sensors
attribute_templates:
my_ID: "{{ state_attr('sensor.syncthing_status', 'myID') }}"
version: "{{ state_attr('sensor.syncthing_version', 'version') }}"
version_long: "{{ state_attr('sensor.syncthing_version', 'longVersion') }}"
os: "{{ state_attr('sensor.syncthing_version', 'os') }}"
arch: "{{ state_attr('sensor.syncthing_version', 'arch') }}"
# must subtract 1 because own machine is also in connections, albeit always disconnected
connections_total: "{{ state_attr('sensor.syncthing_connections', 'connections') | length - 1 }}"
# selectattr('connected') is the same as selectattr('connected', 'equalto', True)
connections_active: "{{ states.sensor.syncthing_connections.attributes.connections.values()|selectattr('connected')|list|length }}"
total_bytes_in: "{{ state_attr('sensor.syncthing_connections', 'total')['inBytesTotal'] }}"
total_bytes_out: "{{ state_attr('sensor.syncthing_connections', 'total')['outBytesTotal'] }}"
start_time: "{{ as_timestamp(state_attr('sensor.syncthing_status', 'startTime')) | timestamp_custom() }}"
uptime: "{{ states('sensor.syncthing_status') }}"
memory_usage: "{{ state_attr('sensor.syncthing_status', 'alloc') }}"
As you see, this sensor does some calculations: It calculates the number of connections total (minus one, because your own machine is also always in this list, and I want to see the connections going to the other Syncthing devices), the number of devices currently connected, and makes a nice datetime result to show when your Syncthing was last started.
The needed sensors are now configured. You might want to test them out before we move on. Remember you’ll need to restart HA every time when sensors are added, deleted, or modified!
Hint: If you want to make more sensors yourself, here is the documentation on Syncthing’s REST API.
Lovelace Glance Card
Let’s add a Syncthing Glance Card to our system. Add the sensors we defined to your new glance card:
You also might want to call up the code editor and give the entities nice names:
If all goes well, your new card should look somehow like this one:
Try playing around with it, click the sensors, and so forth!
Automation example
Now let’s add simple automations for alerts when Syncthing goes down/is started again. You can devise much more elaborated automations yourself: Now since we have the data, we could stop Syncthing if it goes beyond a monthly data allowance, have us notified if Syncthing stops unexpectedly, and so on … The sky’s the limit.
Here is an example I use (under automation:
, or in automations.yaml
):
# Syncthing on
- alias: "Syncthing on"
trigger:
platform: state
entity_id: binary_sensor.syncthing
to: 'on'
action:
- service: script.audio_alert
data:
audiofile: "syncthing-on.mp3"
# Syncthing off
- alias: "Syncthing off"
trigger:
platform: state
entity_id: binary_sensor.syncthing
to: 'off'
action:
- service: script.audio_alert
data:
audiofile: "syncthing-off.mp3"
The script will simply play an audio alert to a set of defined media players, in the language selected in my HA. You could use a notify
instead, for example.
A script to read out Syncthing info
I can trigger this via a RF remote when at home. It’s not as nerdy as it seems, because scripts like these can actually help visibly impaired persons.
I use a lot of building blocks that rely on each other, so you might have to adapt the script to your needs and requirements.
The idea is to get an output like this:
German TTS, British TTS
Prerequisites
var
custom component
I use the var
custom component to store some “global” variables like the default media player, the default language, the path to the audio files, etc like this:
# Custom variables component (in "custom_components/var/")
var:
audio_alerts_media_player:
friendly_name: "Default Media Player for Audio Alerts"
initial_value: media_player.signalpi1
audio_alerts_base_url:
friendly_name: "Base URL for audio alert messages"
initial_value: "https://has1.example.com/local/alerts"
audio_alerts_base_path:
friendly_name: "Base (internal) path for audio alerts"
initial_value: "/home/homeassistant/.homeassistant/www/alerts"
alerts
folder structure
My alerts
folder is in the config/www
folder, as follows:
www/
alerts/
de/
picotts-beep.wav
syncthing-off.mp3
syncthing-on.mp3
en/
picotts-beep.wav
syncthing-off.mp3
syncthing-on.mp3
You see the files have the same name, but are recorded in different languages. I make the same distinction by language for TTS commands.
input_boolean.audio_alerts
This is a boolean input to switch on/off any TTS alerts (like at night, or when I’m not at home).
input_boolean:
audio_alerts:
name: Audio Alerts
initial: on
icon: mdi:voice
input_select.audio_language
This serves as a language selector on my main HA page:
input_select:
audio_language:
name: 'Audio Language'
# first 2 chars will be used as subfolder name for pre-recorded audio alerts
# full name will be used as language input to the TTS engine
options:
- 'en-GB'
- 'de-DE'
PicoTTS
I use PicoTTS as my TTS engine, because it can run locally and is absolutely “cloud-free” and doesn’t need a working internet connection. Also, it allows volume and pitch changes as well as inclusion of a sound bit in a WAV file to be spoken. (The WAV file to include has to be RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 16000 Hz. This can easily be generated using tools like Audacity.)
Under Linux (Debian variants), it can be easily installed using something like:
sudo apt-get install libttspico-utils
After that, install the PicoTTS integration in HA and you’re done. My configuration is like this:
# Text to speech
tts:
- platform: picotts
language: 'en-GB'
base_url: https://has1.example.com
cache: false
Note: I haven’t found a way to make this work with hass.io, but it runs just great if using Home Assistant Core.
Finally, the script!
If you have above prerequisites, here is my script.say_syncthing
, to go into scripts.yaml
:
say_syncthing:
alias: 'Say Syncthing info'
description: 'Speak Syncthing info'
fields:
entity_id:
description: 'Entity Id of the media player to use'
example: 'media_player.signalpi1'
audiofile:
description: 'Name of introductory audio file to play. MUST be WAV, pcm-s16le, mono, 16000 Hz.'
example: 'picotts-beep.wav'
message:
description: 'The text message to speak (can be a template)'
example: "Syncthing is at version {version}."
language:
description: 'The language to speak in (defaults to tts: setting)'
example: 'en-GB'
sequence:
# only alert if "Audio Alerts" is on
- condition: state
entity_id: 'input_boolean.audio_alerts'
state: 'on'
# play text message
- service: tts.picotts_say
data_template:
entity_id: "{{ entity_id|default(states('var.audio_alerts_media_player'),true) }}"
language: "{{ language | default(states('input_select.audio_language'), true) }}"
message: >-
{% set language = language | default(states('input_select.audio_language'), true) %}
{% set lang = language[0:2] %}
{% if audiofile == '' %}
{% elif audiofile %}
<play file="{{ states('var.audio_alerts_base_path') }}/{{ lang }}/{{ audiofile }}"/>
{% else %}
<play file="{{ states('var.audio_alerts_base_path') }}/{{ lang }}/picotts-beep.wav"/>
{% endif %}
{% set state = states('sensor.syncthing_info') %}
{% if state == 'on' %}
{# Make "speakable" version number #}
{% set version = state_attr('sensor.syncthing_info', 'version')[1:] | replace('.', ' ') %}
{% set conn_total = state_attr('sensor.syncthing_info', 'connections_total') %}
{% set conn_active = state_attr('sensor.syncthing_info', 'connections_active') %}
{% set st = strptime(state_attr('sensor.syncthing_info','start_time'), '%Y-%m-%d %H:%M:%S') %}
{% set uptime = state_attr('sensor.syncthing_info', 'uptime') %}
{% set in_val = (state_attr('sensor.syncthing_info','total_bytes_in') | filesizeformat(binary=false)).split(' ')[0] %}
{% set in_unit = (state_attr('sensor.syncthing_info','total_bytes_in') | filesizeformat(binary=false)).split(' ')[1] %}
{% set out_val = (state_attr('sensor.syncthing_info','total_bytes_out') | filesizeformat(binary=false)).split(' ')[0] %}
{% set out_unit = (state_attr('sensor.syncthing_info','total_bytes_out') | filesizeformat(binary=false)).split(' ')[1] %}
{% set total_val = ((state_attr('sensor.syncthing_info','total_bytes_in')|int + state_attr('sensor.syncthing_info','total_bytes_out')|int) | filesizeformat(binary=false)).split(' ')[0] %}
{% set total_unit = ((state_attr('sensor.syncthing_info','total_bytes_in')|int + state_attr('sensor.syncthing_info','total_bytes_out')|int) | filesizeformat(binary=false)).split(' ')[1] %}
{% set mem_val = (state_attr('sensor.syncthing_info','memory_usage') | filesizeformat(binary=false)).split(' ')[0] %}
{% set mem_unit = (state_attr('sensor.syncthing_info','memory_usage') | filesizeformat(binary=false)).split(' ')[1] %}
{% if lang == 'de' %}
{% set in_val = in_val | replace('.',',') %}
{% set out_val = out_val | replace('.',',') %}
{% set total_val = total_val | replace('.',',') %}
{% set mem_val = mem_val | replace('.',',') %}
{# Make "speakable" German date #}
{% set start = "%s" % ( st.strftime('%d.%m., %H:%M') ) %}
{% set message = "Syncthing Version {version} aktiv, {conn_active} von {conn_total} Geräten verbunden. " %}
{% set message = message ~ "Speichernutzung {mem_val} {mem_unit}, " %}
{% set message = message ~ "eingehend {in_val} {in_unit}, ausgehend {out_val} {out_unit}, gesamt {total_val} {total_unit} " %}
{% set message = message ~ "seit {start} ({uptime} Tage)." %}
{% else %}
{# Make "speakable" British date #}
{% set start = "%s, at %s" % ( st.strftime('%d %B %Y'), st.strftime('%H:%M') | regex_replace(find='^\s*0+:00', replace='midnight', ignorecase=False) | regex_replace(find='^\s*0+:0?', replace='zero ', ignorecase=False) ) %}
{% set message = "Syncthing version {version} active, {conn_active} of {conn_total} devices connected. " %}
{% set message = message ~ "Memory usage {mem_val} {mem_unit}, " %}
{% set message = message ~ "{in_val} {in_unit} incoming, {out_val} {out_unit} outgoing, {total_val} {total_unit} total traffic " %}
{% set message = message ~ "since {start} ({uptime} days)." %}
{% endif %}
{% else %}
{% if lang == 'de' %}
{% set message = "Syncthing ist nicht in Betrieb." %}
{% else %}
{% set message = "Syncthing is not running." %}
{% endif %}
{% endif %}
{% set message = message.format(version=version, conn_total=conn_total, conn_active=conn_active, start=start, uptime=uptime, in_val=in_val, in_unit=in_unit, out_val=out_val, out_unit=out_unit, total_val=total_val, total_unit=total_unit, mem_val=mem_val, mem_unit=mem_unit) %}
<volume level="60">{{ message }}</volume>
The script uses some little tricks to make the output better understandable, for both English and German. Also, I reduce the output volume to 60%, because PicoTTS otherwise tends to produce WAVs that clip easily. The picotts-beep.wav
, which is included as a default start sound, is already appropriately replaygained to fit with the 60% volume TTS output.
Bonus script!
Did you really follow me until here? Wow! You deserve a bonus!
Since you already set up all these components, inputs and sensors, you now also get the script.audio_alert
for pre-recorded MP3 messages. This script uses the same var
components, the language selector and the audio alerts on/off switch!
Here we go, put it into your scripts.yaml
and enjoy!
# Audio Alert
# Plays audio file given by "audiofile" dummy variable to media player.
# If "audiofile" is empty/undefined, plays "cantdothat.mp3".
# If no "entity_id" is given, plays to "media_player.signalpi1"
audio_alert:
alias: 'Audio Alert'
description: 'Send an audio alert to media player'
fields:
entity_id:
description: 'Entity Id of the media player to use'
example: 'media_player.signalpi1'
audiofile:
description: 'Name of the audio file to play, including extension'
example: 'warning.mp3'
language:
description: 'Language for output (subfolder, can be either like "de" or "de-DE", only first 2 chars used)'
example: 'de-DE'
sequence:
# only alert if "Audio Alerts" is on (or it's the "audio off" message)
- condition: or
conditions:
- condition: state
entity_id: 'input_boolean.audio_alerts'
state: 'on'
- condition: template
value_template: "{{ audiofile == 'audio-alerts-off.mp3' }}"
# we might be playing another message already, so wait until finished (or max. 30s)
# - wait_template: "{{ states(entity_id|default('media_player.signalpi1',true)) in ['idle','paused','off'] }}"
# - wait_template: "{{ not is_state(entity_id|default('media_player.signalpi1',true), 'playing') }}"
# timeout: '00:00:30'
# play message to player (or "signalpi1") (which is synchronised with others in LMS)
- service_template: media_player.play_media
data_template:
entity_id: "{{ entity_id|default(states('var.audio_alerts_media_player'),true) }}"
media_content_type: music
media_content_id: "{{ states('var.audio_alerts_base_url') }}/{{ (language|default(states('input_select.audio_language'),true))[0:2] }}/{{ audiofile|default(['cantdothat.mp3','cantdothat-2.mp3']|random, true) }}"
Have fun!
Have lots of fun (and good results) experimenting with this and adapting it, and let me know what you did with it!