Monthly Archives: December 2023

Using Home Assistant Kasa Matter Plugs and a Victron MPPT VEDirect cable to deplete your batteries for next day

Wouldn’t it be nice to automate depleting your batteries at night so that you have unwasted electrons coming in next day from your solar panels? My setup is a Kasa matter plug for grid and a Kasa matter plug for solar going into a PDU as an automatic transfer switch. So at any point on my phone I could turn off the grid plug and let the batteries drain for next day, but that is manual labor so we should automate it with python.

I am going to do this on FreeBSD, but would work for Linux etc. as well by changing your serial port for VEDirect, so we’ll just make that a configuration option. Another reason I’m going to use home assistant for turning plug on or off, is they are matter plugs, the kp125m to be exact, so just using python-kasa or whatever is not going to work for matter plugs(not currently anyways). However we can use Home Assistant API calls to turn them off or on.

And I know there is going to be someone saying why not just use a Victron Cerbo GX instead and use a TCP port on network. Fair enough, just don’t have one yet, so I’ll just run the VEDirect cable to my servers USB port and do it that way for now.

Alright let’s get started, figuring out voltage max…

First thing we have to do is figure out a battery voltage max that we want to have for next day before sun comes out. So I’m going to do this by example so you can figure out yours. In my setup I have 280AH lifepov4 batteries @24V. Now I know right now it’s winter time and most I’m going to get a day is 2Kw a day off my 920watts of solar panels from my Victron history graphs on the app.

so math: 1Kw = 1000w / 24V = 41.6 amps (24V might actually be 26.blahV for absorption voltage, but whatever) So 41.6 x 2 = 83.33 amps for 2Kw. So I need to drain batteries 280ah – 83.33ah = 197 AH. Now that I know I need it at 197AH each day to not have wasted electrons, I need to look at a chart:

https://batteryfinds.com/lifepo4-voltage-chart-3-2v-12v-24v-48v/

Chart is in percentages, so now I need to convert that 197AH to a percentage so I can match up voltages with it. So let’s do that, 197AH/280AH*100=70.35%. Great now I have a percentage so I can cross reference that chart at 70% to see what my max voltage should be. According to chart, 70% is 26.4V. Perfect we are golden, but remember to adjust this for summertime when your getting 6-7 Kw a day instead!

Installing pre-requisite packages

For VEDirect we’ll use the vedirect module. For Home Assistant all we’ll need is pythons requests and json libraries to keep it simple. For FreeBSD:

pkg install py39-pip py39-requests
pip install vedirect

Figuring out what we need to code

So what I’m thinking is we need 2 python scripts, one to turn on or off the grid plug depending on the voltage, and a 2nd to turn it on no matter what before sun comes out. We will toss the 2 scripts in a cronjob , so first one runs every 5 minutes between 11pm and 6:50am, and 2nd one to run at 7am to turn on grid plug no matter what.

Now we will need 2 things from Home Assistant, so let’s login there. First we need an API long lived token for accessing the API, you can find this by clicking under developer tools and settings on your nav bar. Just click on your name at very bottom. Now scroll all way to bottom and click “create token”. Copy it and save it for later.

Next we need to know what Home Assistant actually calls our plug, not the name we gave it, also called the “entity”. For that just go to your home screen, click on your plug and go to settings icon. In there you should see something called “entity ID”. Copy that as well, that’s how we talk to home assistant.

Let’s code the 2nd script first as it should be shorter….

First Python script to turn on the plug

Let’s call this one ha_grid_on.py :

#!/usr/local/bin/python3.9

#pkg install py39-pip py39-requests

import requests
import json
import time

def plug(address,token,entity,state):
   #returns on/off with "state=check" else 1
   if state == "on":
      url = "http://" + address + "/api/services/switch/turn_on"
      req = "requests.post(url, headers=headers, json=data)"
   elif state == "off":
      url = "http://" + address + "/api/services/switch/turn_off"
      req = "requests.post(url, headers=headers, json=data)"
   elif state == "check":
      url = "http://" + address + "/api/states/" + entity
      req = "requests.get(url, headers=headers)"
   else:
      print(f"SunSaturn# [FAIL] : Only off/on/check should be called")
      exit(1) #exit program no point doing anything
   #print(f"url is {url}")
   #print(f"req is {req}")
   data = {"entity_id": entity}
   #print(data['entity_id'])
   headers = {
      "Authorization": "Bearer "+ token,
      "content-type": "application/json",
   }
   try:
      response = eval(req)
   except Exception as e:
      print(f"SunSaturn# [FAIL] : Failed connect to home assistant: {e}")
      exit(1) #exit program no point doing anything   
   if not response.ok:
      print(f"SunSaturn# [FAIL] : Failed response code not 200: {response.text}")
      exit(1) #exit program no point doing anything
   check = json.loads(response.text)
   if state == "check":
      check = json.loads(response.text)
      return check['state']
   return 1


#CONFIG - EDIT ME
#######################
#HOME ASSISTANT
#HA assistant token
address='ha:8123'                     #Home Assistant API [IP:PORT]
                                      #HA token, create one in HA
token='eyJhbGciOiJIUzI1NiIsInR5dCI6IkpXVCJ9.eyJpc3MiOiIyYmQ1MDM0YTg2NjY0NDIzOWZlZjg2NmZiNGY4N2E7MyIsImlyuCI6MTcwMTk5MTU3OCwiZXhwIjoyMDE3MzUxNTc4fQ.swUTIoUFWbXEul3JuWRRiKpE-Ene-kKeM1ch8uNQF5o'
entity='switch.kasa_smart_wi_fi_plug' #grab entity of Grid Plug from settings for device on homepage of HA
#######################

plug(address,token,entity,"on") #last arguement can be 1 of on/off/check
print(f"Turned on Plug, checking if it's really on...")
time.sleep(1) #we need a sleep of 1 to give HA time to update
check = plug(address,token,entity,"check")
if check == "on":
   print(f"YEP")
else:
   print(f"NOPE")

Ok I stuffed most of logic in a function, also since all we care about is the script just failing anywhere it fails, we just stuff a exit(1) anywhere to stop execution instead of returning from function. Next we setup configuration variables to edit like the token and entity we got from HA already. Only other config we need is the IP:port. Adjust this for your own HA, I have just ha in /etc/hosts pointing to its real IP.

Next we turn the plug on, then do a check to see if it’s actually on as well. Pretty simple script, first one done.

2nd python script

Let’s called this one victron.py:

#!/usr/local/bin/python3.9

#pkg install py39-pip py39-requests
#pip install vedirect 




import vedirect
import requests
import json

def victron(serial_port, serial_baud):
   try:
      device = vedirect.VEDirect(serial_port,serial_baud)
   except Exception as e:
      print(f"SunSaturn# [FAIL] : Victron read bad data: {e}")
      exit(1) #exit program no point doing anything
   try:
      float(device.battery_volts)
   except Exception as e:
      print(f"SunSaturn# [FAIL] : Return type was not a float: {e}")
      exit(1) #exit program no point doing anything
   if not device.battery_volts > 0:
      print(f"SunSaturn# [FAIL] : Voltage not greater than 0!")
      exit(1) #exit program no point doing anything
   #print(dir(device))
   #print(type(device.battery_volts))
   return device.battery_volts


#http://ha:8123/api/states/switch.kasa_smart_wi_fi_plug
#http://ha:8123/api/services/switch/turn_on
#http://ha:8123/api/services/switch/turn_off
def plug(address,token,entity,state):
   #returns on/off with "state=check" else 1
   if state == "on":
      url = "http://" + address + "/api/services/switch/turn_on"
      req = "requests.post(url, headers=headers, json=data)"
   elif state == "off":
      url = "http://" + address + "/api/services/switch/turn_off"
      req = "requests.post(url, headers=headers, json=data)"
   elif state == "check":
      url = "http://" + address + "/api/states/" + entity
      req = "requests.get(url, headers=headers)"
   else:
      print(f"SunSaturn# [FAIL] : Only off/on/check should be called")
      exit(1) #exit program no point doing anything
   #print(f"url is {url}")
   #print(f"req is {req}")
   data = {"entity_id": entity}
   #print(data['entity_id'])
   headers = {
      "Authorization": "Bearer "+ token,
      "content-type": "application/json",
   }
   try:
      response = eval(req)
   except Exception as e:
      print(f"SunSaturn# [FAIL] : Failed connect to home assistant: {e}")
      exit(1) #exit program no point doing anything   
   if not response.ok:
      print(f"SunSaturn# [FAIL] : Failed response code not 200: {response.text}")
      exit(1) #exit program no point doing anything
   check = json.loads(response.text)
   if state == "check":
      check = json.loads(response.text)
      return check['state']
   return 1


#CONFIG - EDIT ME
#######################
#HOME ASSISTANT
#HA assistant token
address='ha:8123'                     #Home Assistant API [IP:PORT]
                                      #HA token, create one in HA
token='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIyYmQ1MDM0YTg2NjY0NDIzOWZlZjg2NmZiNGY4N2E1MyIsImlhdCI6MTcwMTk5MTU3OCwiZXhwIjoyMDE3MzUxNTc4fQ.swUTIoUFWbXEul3JuWRRiKpE-Ene-kKeM1ch8uNQF5o'
entity='switch.kasa_smart_wi_fi_plug' #grab entity of Grid Plug from settings for device on homepage of HA
#VICTRON
voltage_max=26.1                      #we don't want voltage higher than this for morning solar charging
serial_port='/dev/cuaU0'              #serial_port for VEDIRECT, on linux try "setserial -g /dev/ttyUSB[01]" to find your serial port
serial_baud='19200'                   #serial baud rate, 19200 seems recommended setting from Victron
#######################

#check voltage of battery
volts=victron(serial_port,serial_baud) #will exit program if any failures with exit(1)
#print(f"Volts is: {volts} ",type(volts))

#check if plug is on or off
check = plug(address,token,entity,"check") #last arguement can be 1 of on/off/check
print(f"Battery voltage is {volts}, checking what to do...")

if check == "on":
   #plug is on, check it we need to turn it off
   if volts > voltage_max:
      plug(address,token,entity,"off")
      print(f"Voltage is greater than {voltage_max} Turned off Grid Plug")
   else:
      print(f"Grid Plug is on and voltage less than {voltage_max} DOING NOTHING")
else:
   #plug is off, check it we need to turn it on
   if volts < voltage_max:
      plug(address,token,entity,"on")
      print(f"Voltage is less than {voltage_max} Turned on Grid Plug")
   else:
      print(f"Grid Plug is off and voltage greater than {voltage_max} DOING NOTHING")

Again we reuse the Home Assistant function, but add an extra function for Victron to pull battery voltage off the solar charge controller. New config options are max_voltage we figured out earlier, and serial_port and serial_baud rate. Adjust these to work with your setup. So what we are doing is checking if plug is on or off first, then depending on that we check voltage against voltage_max to decide to turn the plug on or off. Pretty simple 🙂

Now we are almost done….Personally I do everything as root(you may need to so you can access serial port), so I created a /root/cronjobs directory and stuck victron.py and ha_grid_on.py in there. Last thing to do is toss them in cron: “crontab -e”

# run every 15 minutes between 11pm and 6:59 am, suppress logging
*/15 23 * * * (/root/cronjobs/victron.py) > /dev/null 2>&1
*/15 0-6 * * * (/root/cronjobs/victron.py) > /dev/null 2>&1
#make sure grid plug on no matter what by 7am
0 7 * * * (/root/cronjobs/ha_grid_on.py) > /dev/null 2>&1

And don’t forget to “chmod 700 *.py” 🙂

Keep in mind you have to accommodate for “voltage rebound”, this happens when no load is present, battery voltage will jump up .3-.4, so you will want to lower your voltage for day 🙂 Unfortunately this means every 5 min its on solar, every 5 min on grid because of the voltage rebound. To compensate I use to run cron every 5 minutes, now I run every 15 minutes, long as not running a really high load should be fine.

And that’s it, all automated, never worry about it again 🙂 Of course adjust $max_voltage come summertime, but other than that, your golden 🙂 I’ll leave adjusting python script to actually use different max_voltages depending on the month as an exercise to the reader….

Till next time ….

SunSaturn