Tracking the Charge of an Electronic Vehicle with Home Assistant and WiCAN-OBD2

Home Automation Mar 23, 2024

The Problem

After relocating within biking distance of my workplace, my partner and I made the bold decision to sell both of our cars and opt for a single fully-electric vehicle. Our choice fell upon a 2019 Volkswagen e-Golf, a decision that initially seemed flawless until I encountered the frustrating lack of remote battery monitoring capabilities.

Despite CarNet being a built-in feature of the 2019 model, its usefulness was abruptly curtailed in 2021 with the nationwide sunset of 3G services. Why Volkswagen chose to rely on a deprecated protocol for a brand-new vehicle, three years after its discontinuation announcement, remains a baffling mystery. As one user aptly expressed it:

💬
"Absolutely asinine to have a 2019 model vehicle lose a service in ~2 years time." - Reihenmotor5, 2021

The Solution

Undeterred, I pressed on in my quest for a non-subscription solution to monitor our car's charging status. It dawned on me that OBD readers could offer a wealth of data for electric vehicles, including both the state of charge (in percentage) and its status (true or false).

After some research, I settled on the WiCAN-OBD2 by MeatPi, a smart investment available through Mouser for just $39. This one-time purchase stood out as a more cost-effective option compared to the subscription-based alternatives recommended by VW. Installing the device was a breeze, as car manufacturers typically anticipate owners requiring access to the OBD port throughout the vehicle's lifespan. As an added perk, since the WiCAN remains plugged into the car at all times, it doubles as a reliable indicator of your car's presence at home.

The WiCAN seamlessly connects to your WiFi network and transmits messages via an MQTT server. Once configured, it effortlessly establishes and terminates connections upon your arrival and departure from home. With the hardware in place, attention turns to tinkering with the software to tailor its functions to your preferences.

The GitHub repository for the device has a bevy of information about integrating it with Home Assistant. For more specific examples, check the Issues and Discussions sections where users are sharing their solutions and asking questions. Through these and additional sources of documentation you should be able to suss out the messages that need to be sent in order to receive the data you require.

I settled on using NodeRed to mange the messages as I prefer a visual flow when editing and refining my services.
⚠️
While there are numerous statistics reported through the OBD protocol, nearly all of them require the motor to be running or the charger to be connected.

It seems that (for most vehicles) the State of Charge (SoC) will always be reported when actively refilling the battery, regardless of the motor being running. That doesn't stop you from sniffing some of the more informational stats while the car is pulling in to the driveway/garage, though!


Bonus: A Lovelace card to conversationally display this information

I spent a significant amount of time creating the cards shown in the header image of this post. They're shown on all the smart displays throughout the house and provide a good reminder to plug in the car if it's been forgotten. I'll post it here along with the requirements needed to use it.

Requirements

type: custom:button-text-card
large: false
title: |
  [[[
    const carPluggedIn = states['input_select.car_plugged_in_v2'].state;
    const carTrackerState = states['device_tracker.car'].state;

    if (carTrackerState !== 'home') {
      return "The car is on an adventure!";
    } else {
      const carTrackerLastChanged = new Date(states['sensor.nominal_change_history'].attributes.changes['device_tracker.car']);
      const currentTime = new Date();

      if (carPluggedIn === 'Charging') {
        return "The car started charging " + relativeTime(new Date(states['sensor.nominal_change_history'].attributes.changes['input_select.car_plugged_in_v2'])) + " and has a " + states['sensor.battery_soc'].state + "% charge!";
      } else if (carPluggedIn === 'Unplugged') {
        return "The car is unplugged and has " + states['sensor.battery_soc'].state + "% charge!";
      } else if (carPluggedIn === 'Fully Charged') {
        return "The car finished charging " + relativeTime(new Date(states['sensor.nominal_change_history'].attributes.changes['binary_sensor.car_plugged_in'])) + " ago. It's all juiced and ready to go with " + states['sensor.battery_soc'].state + "%!";
      }
    }

    function relativeTime(previousTime) {
      const currentTime = new Date();
      const elapsed = currentTime - previousTime;
      const seconds = Math.floor(elapsed / 1000);
      const minutes = Math.floor(seconds / 60);
      const hours = Math.floor(minutes / 60);
      const days = Math.floor(hours / 24);

      if (days > 0) {
        return days + " day" + (days > 1 ? 's' : '') + " ago";
      } else if (hours > 0) {
        return hours + " hour" + (hours > 1 ? 's' : '') + " ago";
      } else if (minutes > 0) {
        return minutes + " minute" + (minutes > 1 ? 's' : '') + " ago";
      } else {
        return seconds + " second" + (seconds > 1 ? 's' : '') + " ago";
      }
    }
  ]]]
icon: |
  [[[
    const carPluggedIn = states['input_select.car_plugged_in_v2'].state;
    const carTrackerState = states['device_tracker.car'].state;

    if (carTrackerState !== 'home') {
      return "mdi:road";
    } else {
      if (carPluggedIn === 'Charging') {
        return "mdi:battery-charging-" + Math.floor(states['sensor.battery_soc'].state / 10) * 10;
      } else if (carPluggedIn === 'Fully Charged') {
        return "mdi:battery";
      } else if (carPluggedIn === 'Unplugged') {
        return "mdi:car-emergency";
      }
    }
  ]]]
icon_size: 38
background_color: |
  [[[
    const carPluggedIn = states['input_select.car_plugged_in_v2'].state;
    const carTrackerState = states['device_tracker.car'].state;

    if (carTrackerState !== 'home') {
      return "orange";
    } else {
      if (carPluggedIn === 'Charging') {
        return "green";
      } else if (carPluggedIn === 'Fully Charged') {
        return "blue";
      } else if (carPluggedIn === 'Unplugged') {
        return "red";
      }
    }
  ]]]
icon_animation: |
  [[[
    const carPluggedIn = states['input_select.car_plugged_in_v2'].state;
    const carTrackerState = states['device_tracker.car'].state;

    if (carTrackerState === 'home' && carPluggedIn === 'Unplugged') {
      return "spin";
    } else {
      return null;
    }
  ]]]
card_mod:
  style: |
    .icon-container {
        width: 50px !important;
        height: 50px !important;
    }
    .icon-container ha-icon.spin {
      animation: wobbling 1s linear infinite alternate none running !important;
    }
    @keyframes wobbling {
      0% {
        transform: rotate(-45deg) translateY(-2px);
      }
      100% {
        transform: rotate(+45deg) translateY(-2px);
      }
    }

Tags