I just stumbled across this last night and had a few issues implementing as-is, so figured I’d add a few notes to maybe save someone in the future a couple of hours.
- This solution won’t work with the Home Assistant containers, because these don’t have a ~/.ssh folder, and if you create it, it isn’t persistent (it’ll disappear on update). Minor tweak, but the SSH command needs to specify a
known_hosts
file in a persistent location (IE: /config
)
- The telnet command (more accurately, the greps/sed that follow it) flat out didn’t work for me on the specific version of OpenVPN I’m running (OpenVPN 2.5.5 x86_64-pc-linux-gnu) I can’t really determine why the difference without seeing the differences in raw output, so YMMV for anyone deciding to implement this, and it may require some tweaking/experimentation, especially depending on requirements.
- I personally didn’t love how dispersed the scripts were, I would prefer have everything in one place, on one system with one update command. So that’s what I did. This way the only change needed on the OpenVPN server is configuring management. Everything else is handled in Home Assistant.
- I loathe the fact you need to restart Home Assistant to make changes to
shell_command
, and the easiest way to get around that is to have the command a shell script, so you can tweak it without restarting.
So, here’s my modified solution:
Since you installed an OpenVPN server, I’m assuming you’re capable of enabling management on openvpn, running ssh-key-gen
and ssh-copy-id
, etc. or at least Googling it. Once you have password-free SSH working, copy ~/.ssh/known_hosts
to wherever this lives in your setup. Also make sure your pub/private keys are there as well. I placed everything in /config/openvpn
, but the scripts should be agnostic to their location, as long as everything is placed together.
shell_command in configuration.yaml:
shell_command:
vpnstatus_csv: 'bash /config/openvpn/update.sh'
Update your install location (/config/openvpn/
) as appropriate.
update.sh:
#!/bin/bash
CURRENT_DIR=`dirname "$(realpath $0)"`
OPENVPN=<USER@HOST>
TELNET_CMD='{ echo "status"; sleep 1; echo "exit"; sleep 1;} | telnet localhost 7505 | grep CLIENT_LIST | sed "s/.*CLIENT_LIST,//g"'
ssh -o UserKnownHostsFile=$CURRENT_DIR/known_hosts -i $CURRENT_DIR/open_vpn_rsa $OPENVPN "( $TELNET_CMD )" > $CURRENT_DIR/vpn_status.csv
ssh -o UserKnownHostsFile=$CURRENT_DIR/known_hosts -i $CURRENT_DIR/open_vpn_rsa $OPENVPN "cat /etc/openvpn/server/easy-rsa/pki/index.txt | grep ^V | awk '{print $5}' | sed 's/.*=//'" > $CURRENT_DIR/vpn_clients.txt
python $CURRENT_DIR/ovpn_status.py
Change open_vpn_rsa
to the name of your ssh-key and <USER@HOST>
as appropriate (EG: [email protected]).
ovpn_status.py:
"""Add OpenVPN Status info to Home Assistant."""
import csv
import os
import requests
CSV_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), "vpn_status.csv")
CLIENT_LIST = os.path.join(os.path.dirname(os.path.realpath(__file__)), "vpn_clients.txt")
URL = "http://localhost:8123/api/states/sensor.{entity_id}"
HEADERS = {
"Authorization": "Bearer <LONG_LIVED_TOKEN>",
"content-type": "application/json",
}
def post_entity(entity_id, state, attributes):
"""Submits a status to Home Assistant."""
data = f'{{"state": "{state}", "unique_id": "openvpn_{entity_id}", "attributes": {attributes}}}'
result = requests.post(URL.format(entity_id=entity_id), data=data, headers=HEADERS, timeout=10)
if result.status_code not in (200, 201):
print("ERROR: vpn_clients - " + str(result.status_code))
def update_client_usages(results):
"""Submits individual client's usage (sent/received)."""
# Create the list of sensors, zero them out
client_list = {}
with open(CLIENT_LIST, encoding="utf8") as client_fh:
for line in client_fh:
client_list[line.rstrip()] = {"sent": 0, "received": 0}
# Update the usage statistics for any active sensors
for client in results:
client_list[client["Common Name"]] = {"sent": client["Bytes Sent"], "received": client["Bytes Received"]}
# Submit all the clients
for client, values in client_list.items():
unique_id = client.replace("-", "_")
attributes = (
'{{"state_class": "measurement", "icon": "mdi:vpn", "friendly_name": "'
+ client
+ ' Bytes {operation}", "unit_of_measurement": "B", "device_class": "data_size", "state_class": "total_increasing"}}'
)
post_entity(f"{unique_id}_bytes_sent", values["sent"], attributes.format(operation="Sent"))
post_entity(f"{unique_id}_bytes_received", values["received"], attributes.format(operation="Received"))
def main():
"""Main body."""
# Read the csv_file
with open(CSV_FILE, encoding="utf8") as csv_fh:
csv_reader = csv.DictReader(csv_fh, delimiter=",")
results = []
for line in csv_reader:
results.append(line)
update_client_usages(results)
# Submit primary entity status
attributes = (
'{"clients": '
+ str(results).replace("'", '"')
+ ', "state_class": "measurement", "icon": "mdi:vpn", "friendly_name": "VPN Clients", "unit_of_measurement": "client(s)"}'
)
post_entity("vpn_clients", len(results), attributes)
if __name__ == "__main__":
main()
Change <LONG_LIVED_TOKEN>
to your token.
You should be able to run bash /config/openvpn/update.sh
from a Terminal on the Home Assistant now and it’ll create the sensor etc. If you see any errors, it’s time to debug, and I would specifically look at the telnet status
command output format. You may need to tweak the greps/seds. Additionally you will probably need to make changes to the CSV parsing in the openvpn.py
When this succeeds I see:
bash-5.1# bash /config/openvpn/update.sh
Connection closed by foreign host.
bash-5.1#
Restart Home Assistant to get the shell_command changes loaded.
Once all that works, create the updater automation:
alias: "Timer: Update VPN Clients"
description: ""
trigger:
- platform: time_pattern
seconds: "30"
condition: []
action:
- service: shell_command.vpnstatus_csv
data: {}
mode: single
This will update every 60 seconds, reasonable for me, but, I’m assuming if you made it this far, you can tweak all this as appropriate.
As a final note, I would recommend running chmod 600 -R <install_dir>
on this once done. You have tokens, and private keys living here, it doesn’t hurt to be extra secure.
Edit April 11, 2023: I added sensors to track usage per client, this added the second ssh line to update.sh and the call to update_client_usages() in ovpn_status.py::main(). Additionally I did some heavy cleanup to the script to improve statistics and readability. This particular implementation will have persistent sensors for all configured clients, but if they aren’t in use, their usage is “0”.