HEADLESS SETUP on my ENERGICA SS9 Electric Motorcycle to push OBDII data to HOME ASSISTANT

One of the things I still had to fix on my Home Assistant setup monitoring my Energica electric motorcycle was to replace that Android phone with some headless device that could remain on the bike underneath the fairings gathering ride and charge data both at home and on route.

Hardware selection

For that I picked a Raspberry Pi (version 1 model B), it has only 2 USB ports, one for a WiFi dongle and the other one for a Bluetooth dongle. Fair enough, if you buy a Raspberry Pi 3 or 4 today it will have 4 USB ports and Wifi and Bluetooth build in. But this will work for me (for now).

The reason I’m going for an OBDII bluetooth dongle connection and not the built in BLE connection from Energica (again for now) is 2 fold. Firstly that BLE connection is somewhat secured and encrypted, nothing like this for the OBDII dongles reducing complexity a lot. And secondly the OBDII port is available as soon as the bike powers on, both from charging as from ignition. I haven’t 100% confirmed this but I think the BLE connection is only ON when the bike is powered ON by ignition. Oh and maybe a 3rd reason could be that OBDII is available for everyone, while BLE has been phased out by Energica.

The OBDII dongle I use is a cruise control branded OBDII bluetooth dongle. Despite common believe not all elm327 chipset OBD dongles will work for this since we need to execute Energica specific OBDII commands. If you only need ODO and temperature and some other default OBD standard PID values you might be good but for Energica specifics you’ll have to look for an, often more expensive, dongle that does support more.

Raspberry Pi Config and Python Code

I started with this python OBD library first. Installation of that and some bluetooth tools is done with these commands. Spoiler; I couldn’t get it working for my custom commands though.

sudo apt-get install pip
pip install obd
sudo apt-get install bluetooth bluez-utils blueman

For Energica specific OBDII details I have this writeup already. Looking at this python library I’ll need custom commands with OBDComand and then parse the value before passing it on. Something like:

import obd

connection = obd.OBD() # auto-connects to USB or RF port

#cmd = obd.commands.SPEED # select an OBD command (sensor)
cmd = obd.commands.OBDCommand("ATCRA200") # Energica specific command

response = connection.query(cmd) # send command & parse response

print(response.value) # returns unit-bearing values thanks to Pint
print(response.value.to("mph")) # some output formatting

Above command is what I can repeat in a loop to get current state, before that is done I need to setup the OBDII dongle connection with a sequence similar to this one

# OBDII dongle boot sequence (once should be fine)
OBDCommand("ATWS") # "warm start" this gives info on the used OBD dongle
OBDCommand("ATE0") # "echo OFF"
OBDCommand("ATSP6") # "set protocol"
OBDCommand("ATAT1") # "adaptive timing"
# most of the default obd commands won't work with headers ON
OBDCommand("ATH1") # "headers ON", false
OBDCommand("ATL0") # "linefeed OFF"
OBDCommand("ATS0") # "set timeout"

# Energica specific commands, always start with this
OBDCommand("ATCAF0") # "auto format OFF", false
OBDCommand("ATSH7DF") # "SH 7DF", true

# Charge state
# ex response 200 10 00 00 00 00 00 00 00
# first digit is charge state, 01=IDLE, 02=AC, 16=DC charging

# BMS state
# ex response 200 16 47 64 17 0C 01 FF FE

# Cell balance
# ex response 203 0F 40 43 2E 0F 36 0F 4D
# 2 last pairs are cell min and max voltage in mV, ex 0F35 = 3893 mV

That 200 command returns most of the data but also needs some more info on parsing. This should explain most using an example response. You can also check final code at the end of this article for parsing logic.

16  // 22 in dec = low batt temp
47  // 71 in dec matches SOC
64  // 100 in dec = SOH
17  // 23 in dec = high batt temp
0C  // see next (bPackV in V)
01  // 0C01 is 3084 /10 = 308.4 V packV value
FF  // see next (bPackI in A)
FE  // FFFE is -2 /10 = -0.2 A packI value 

In the next steps I discovered the custom command option from this OBDII python library isn’t going to work for these AT commands. So instead I started looking into working with the serial python interface directly.

pip install pyserial

Make the Raspberry Pi find my eml327 OBDII Bluetooth dongle

First challenge was to connect the raspberry pi with the OBDII dongle on the bike. I tried the promising OBDII library first as that one had several useful methods.

>>> connection = obd.OBD()
[obd.obd] No OBD-II adapters found
[obd.obd] Cannot load commands: No connection to car

Learned quickly that there was some manual bluetooth config required first. Mainly using bluetoothctl was what did it.

# adapters recognised and services running
pi@raspberrypi2:~ $ hciconfig
hci0:	Type: Primary  Bus: USB
	BD Address: 00:02:72:33:97:2C  ACL MTU: 1021:8  SCO MTU: 64:1
	RX bytes:1580 acl:0 sco:0 events:85 errors:0
	TX bytes:3070 acl:0 sco:0 commands:85 errors:0

# lsusb shows my bluetooth dongle properly, identified as
Bus 001 Device 004: ID 0a5c:21e8 Broadcom Corp. BCM20702A0 Bluetooth 4.0

# Using bluetoothctl I did get scan results
pi@raspberrypi2:~ $ sudo bluetoothctl
Agent registered
[CHG] Controller 00:02:72:33:97:2C Pairable: yes
[bluetooth]# scan on
Discovery started
[CHG] Controller 00:02:72:33:97:2C Discovering: yes
[NEW] Device 66:1E:11:00:F1:43 66-1E-11-00-F1-43

# I know from previous use that the mac address I'm looking for is 66:1E:11:00:F1:43 so I paired that one 
[bluetooth]# pair 66:1E:11:00:F1:43
Attempting to pair with 66:1E:11:00:F1:43
[DEL] Device 24:47:71:25:38:78 24-47-71-25-38-78
[DEL] Device 5E:2B:7B:63:98:BA 5E-2B-7B-63-98-BA
[DEL] Device 50:41:F8:2E:8C:28 50-41-F8-2E-8C-28
[CHG] Device 66:1E:11:00:F1:43 Connected: yes
Request PIN code
[agent] Enter PIN code: 1234
[CHG] Device 66:1E:11:00:F1:43 Modalias: usb:v05ACp0239d0644
[CHG] Device 66:1E:11:00:F1:43 UUIDs: 00001101-0000-1000-8000-00805f9b34fb
[CHG] Device 66:1E:11:00:F1:43 UUIDs: 00001200-0000-1000-8000-00805f9b34fb
[CHG] Device 66:1E:11:00:F1:43 ServicesResolved: yes
[CHG] Device 66:1E:11:00:F1:43 Paired: yes
Pairing successful

# I also learned I not only have to pair that device using bluetoothctl but also trust it & set up a device to connect to under /dev/
# from https://unix.stackexchange.com/questions/343592/is-it-possible-to-use-the-bluetoothctl-write-command-to-send-serial-data

sudo rfcomm bind 1 C9:5B:CE:A4:97:C7

# slowly getting there, already got more output from the initial lib that is documented here https://python-obd.readthedocs.io/en/latest/Connections/#querycommand-forcefalse

>>> connection = obd.OBD()
[obd.elm327] Failed to set baudrate
[obd.obd] Cannot load commands: No connection to car
>>> connection = obd.OBD("/dev/rfcomm1")
>>> connection.status()
'Car Connected'

# I get actual data now

>>> print(connection.query(obd.commands.SPEED))
0.0 kph
>>> print(connection.query(obd.commands.COOLANT_TEMP))
15 degC

# these are the supported commands out of the box https://python-obd.readthedocs.io/en/latest/Command%20Tables/

# and this is how to create your own https://python-obd.readthedocs.io/en/latest/Custom%20Commands/

# not all data is usefull so far

>>> print(connection.query(obd.commands.STATUS))
<obd.OBDResponse.Status object at 0xb5ea1610>
>>> print(connection.query(obd.commands.RPM))
0.0 revolutions_per_minute
>>> response = connection.query(obd.commands.INTAKE_TEMP)
[obd.obd] 'b'010F': Intake Air Temp' is not supported
>>> print(connection.query(obd.commands.COOLANT_TEMP))
15 degC
>>> print(connection.query(obd.commands.ELM_VOLTAGE))
12.5 volt
>>> print(connection.query(obd.commands.AMBIANT_AIR_TEMP))
13 degC
>>> print(connection.query(obd.commands.FUEL_TYPE))
>>> print(connection.query(obd.commands.TIME_SINCE_DTC_CLEARED))
22462.0 minute
>>> print(connection.query(obd.commands.HYBRID_BATTERY_REMAINING))
100.0 percent

Using pyserial to retrieve data from Energica

Took some thinkering to handle the proper formats, you could also use more AT commands to drop formatting and headers but I limited as much as possible with below script

>>> import serial

>>> ser = serial.Serial("/dev/rfcomm1", 9600, timeout=1)
>>> ser.write(b"ATE0\r")
>>> print(ser.readline())
>>> ser.write(b"ATCAF0\r")
>>> ser.write(b"ATSH7DF\r")
>>> ser.write(b"ATCRA200\r")
>>> ser.write(b"001\r")
>>> print(ser.readline())
b'OK\r\r>OK\r\r>OK\r\r>200 0B 49 64 0B 0C 17 FF FE \r\r>'

So at this point it’s definitely working. Not sure about those numbers I receive that are printed in between the commands? Anyhow it’s the last command “001” that I need to repeat and parse the response from.

>>> ser.write(b"001\r")
>>> response = ser.readline()
>>> print(response)
b'200 0B 49 64 0B 0C 17 FF FE \r\r>'

# temp in hex value
>>> print(response[4:6])
# int value
>>> print(int(response[4:6],16))
11 # low batt temp
>>> print(int(response[7:9],16))
73 # soc
>>> print(int(response[10:12],16))
100 # soh
>>> print(int(response[13:15],16))
11 # high batt temp
>>> print(int(response[16:21].decode().replace(" ",""),16)/10)
309.5 # battery voltage
>>> print(int(response[22:27].decode().replace(" ",""),16)/10)
6553.4 # battery current draw

# that last one is -0.2 as a signed value, some info here for options to decode this https://stackoverflow.com/questions/6727875/hex-string-to-signed-int-in-python-3-2

>>> def two_complement(hexstr, bits):
...   value = int(hexstr,16)
...   if value &(1<<(bits-1)):
...     value -= 1 << bits
...   return value

>>> print(two_complement(response[22:27].decode().replace(" ",""),16)/10)

Push data using REST calls to MQTT broker running on HA

For pushing data to Home Assistant I remember some python examples in the documentation. More specifically with the Requests module in Python. A code example and a picture from the Raspberry Pi (model 3) that is running Home Assistant here. Make sure to have some backup if you ever put time in one of these.

from requests import post

url = "http://homeassistant.local:8123/api/services/mqtt/publish"
token = "ABCDEFGH"
headers = {
    "Authorization": "Bearer " + token,
    "content-type": "application/json",

response = post(url, json={"key": "value"}, headers=headers)

All things combined

Note that on startup your /dev/rfcomm1 connection has to be recreated, for me with the MAC address of my dongle that is done with below command creating /dev/rfcomm1. Something I should automate on boot really.

sudo rfcomm bind 1 66:1E:11:00:F1:43

Besides creating a script that runs on boot I would also recommend running a watchdog service on it so that it keeps running. It’s not like I’ve spent days and days to ensure a stable connection or anything like that.

import time
import serial
from requests import post

device = "/dev/rfcomm1"
haUrl = "http://homeassistant.local:8123/api/services/mqtt/publish"
haToken = "ABCDEFGH" # create one at HA > Your Profile > long term token

headers = {
    "Authorization": "Bearer " + haToken,
    "content-type": "application/json",

# Initial OBDII data connection
def initObd2Connection():
  print("Reset OBDII connection")
  global ser
  ser = serial.Serial(device, 9600, timeout=1)
  ser.write(b"ATWS\r")      # restart device
  time.sleep(1)             # give it some time to reboot
  ser.write(b"ATH1\r")      # headers ON

  # get ambient temp on init, standard OBDII PID is 0146
  temp = ser.readline()
  #pushData("energica/temp/ambient", int(temp[9:11],16)-40, "°C")
  pushData("energica/temp/ambient", int(temp[18:20],16)-40, "°C")

  # get ODO (distance since codes cleared, ODO not supported)
  odo = ser.readline()
  #pushData("energica/odo", int(odo[9:13],16), "km")
  pushData("energica/odo", int(response[18:23].decode().replace(" ",""),16), "km")

  ser.write(b"ATCAF0\r")    # auto format OFF, breaks default OBDII
  ser.write(b"ATSH7DF\r")   # filter on 7DF
  ser.write(b"ATCRA200\r")  # address 200
  ser.write(b"001\r")       # test fetch data
  ser.write(b"ATE0\r")      # echo OFF
  print("OBDII connection ready")

# helper to convert 2 complement signed
def two_complement(hexstr, bits):
  value = int(hexstr,16)
  if value &(1<<(bits-1)):
    value -= 1 << bits
  return value

# helper to format and push data
def handleResponse(response):
  print("Handling valid response data", response)
  #pushData("energica/temp", int(response[4:6],16), "°C")
  pushData("energica/soc", int(response[7:9],16), "%")
  #pushData("SOH", int(response[10:12],16), "%")
  pushData("energica/temp/battery", int(response[13:15],16), "°C")
  pushData("energica/voltage", int(response[16:21].decode().replace(" ",""),16)/10, "V")
  pushData("energica/current", two_complement(response[22:27].decode().replace(" ",""),16)/10, "A")
  response = None

# helper to push data to MQTT
def pushData(topic, payload, unit):
    jsonObj = {'topic': topic, 'payload': payload, 'retain': 1, 'qos': 1}
    print("Pushing payload", jsonObj) 
    response = post(haUrl, json=jsonObj, headers=headers)
    print("MQTT server response", response)
    print("Failed to push data to MQTT config")
  # print(topic, payload, unit) # TODO implement pushing data

# Initial OBDII data connection
  print("OBDII init failed")

# Loop to check for state of OBDII data connection
while True:
  try:                      # have some minimal err handling
    print("Send OBDII command")
    ser.write(b"001\r")     # gets Energica specific data
    response = ser.readline()
    length = len(response)
    print("Received response", response, "with length", length)
    if length >= 28: # only handle valid length responses 
    print("Failed to get valid OBDII data, delayed 30s")
    time.sleep(30)          # wait a bit longer and try full on reconn.

And some example output from running this script below. Some logging might have changed in between but the overall sequence should still be there.

pi@raspberrypi2:~ $ python energica-obd.py 
Reset OBDII connection
b'\r\rELM327 v1.5\r\r>'
b'001\r0B 48 64 0B 0C 17 FF FE \r\r>'
OBDII connection ready
send OBD command
received response b'200 0B 48 64 0B 0C 17 FF FE \r\r>' with length 31
Handling valid data b'200 0B 48 64 0B 0C 17 FF FE \r\r>'
Pushing payload {'topic': 'energica/soc', 'payload': 72, 'retain': 1, 'qos': 1}

Next steps

As always I have some next steps already. I should parse also 201, 202 and 203 Energica specific commands. While writing this I also just realised I don’t have an ODO value now in this script since that is part of the default OBDII commands. And maybe I should also explore my options to connect with that BLE connection of the bike from python. You’ll get an update on this blog as soon as I have more.

For running on boot I’ve found this documentation explaining you need to create a script that can be added using crontab. So something like this if you put the python code in a script named energica-obd.py, the script we’ll name launcher.sh in home directory. For a watchdog setup there is a python library but I haven’t checked details yet.

# launcher.sh

# link bluetooth device

sudo rfcomm bind 1 66:1E:11:00:F1:43

# execute python script

python /home/pi/energica/energica-obd.py 2> /home/pi/energica/energica-logs

Run with crontab

Enable to run the script by changing the file rights and adding it to crontab config (only to be done once) and edit crontab

chmod 755 launcher.sh
sudo crontab -e

Add below line in that file

@reboot sh /home/pi/launcher.sh >/home/pi/logs/cronlog 2>&1

Or run in detached screen

Might have to install screen first and install /etc/rc.local

sudo apt-get install screen
sudo vi /etc/rc.local

Now add the following content before the exit 0 line.

# Run a command as `pi` from the home folder 
# in a screen named `energica`
su - pi -c "screen -dmS energica ~/energica/launcher.sh"

The advantage of using screen detached is that you can have it running in it’s virtual screen session without having a user logged in to the system. Plus when a user logs in they can easily attach to the sessions available. Some example commands to get started with screen to finish this post.

# lists all current screen sessions
screen -ls 

# example output
There is a screen on:
	411.energica	(02/01/23 10:41:52)	(Detached)
1 Socket in /run/screen/S-pi.

# attach to a running screen session (r=resume)
screen -r 411

# then to exit a screen session w/o interrupting the script use CTRL+A followed by CTRL+D (d=detach)

Leave a Reply

Your email address will not be published. Required fields are marked *

Please reload

Please Wait