jasonacox / pypowerwall

Python API for Tesla Powerwall and Solar Power Data
MIT License
123 stars 21 forks source link

Powerflow background is transparent, not black. #34

Closed venturanc closed 1 year ago

venturanc commented 1 year ago

When I access x.x.x.x:8675/ for the powerflow animation, the background of the page is transparent, and I can't see the current battery percentage without highlighting the text on the page.

Did I miss a configuration step?

image

If I look at the page source and change the CSS background to black, it looks great, but then I think it autorefreshes and goes back to background=transparent.

jasonacox commented 1 year ago

Hi @venturanc!

How are you running the proxy? Docker or direct via command line? There are some envrionmental settings that change the color settings (see https://github.com/jasonacox/pypowerwall/tree/main/proxy/web):

# default
PW_STYLE=clear

# others
PW_STYLE=black
PW_STYLE=white
PW_STYLE=dakboard
PW_STYLE=grafana

Also, try this: http://x.x.x.x:8675/example.html - it should render an iFrame example like this:

image
venturanc commented 1 year ago

Ahh! Thanks! I'm running the docker version. The example html link does work too. Sorry that I missed that. I'll close this issue that was my user error.

BTW, thanks so much for developing this! I'm using this to get Tesla Powerwall info into Smartthings for home automation (switching off devices if grid outage, reminders to do laundry/dishes when the powerwall is full and excess solar production etc.)

I also have a python script that gets the grid power usage every 2 minutes from pypowerwall during solar production time and if my car is charging, it uses Teslapy to adjust the car charging speed to try and maintain minimal to no grid exports to avoid non-bypassable charges, and it stops charging at 4pm when the 4-9 peak period starts to not drain the powerwall. it was pretty easy to modify the TesSense project and get the data from pypowerwall instead of Sense!

jasonacox commented 1 year ago

Thanks @venturanc ! I would love to see some of your scripts if you are ever willing to share. Post them here if it is easy or we have a section where cool automation contributions can be added: https://github.com/jasonacox/pypowerwall/tree/main/tools - No pressure. Thanks for the kind words. 🙏

venturanc commented 1 year ago

Sure @jasonacox - I did a writeup about how to use PyPowerwall with SmartThings here:

I need to spend some more time cleaning this up, and perhaps add a web API call to start/stop the solar charging, and maybe tie it all together with pypowerwall with a docker-compose file, and get it published to my own github (after I figure out how to have it read/use the environmental variables to get the tesla login data and input a tesla refresh token, lat/lon and py-powerwall_IP variables - I'll refer to your code on the environmental variables stuff), but here's the quick and dirty functional python script:

"""
 Adapted by Nate Carroll 03/2023 from TesSense w/ SenseLink  -Randy Spencer 2023 Version 9.7
 Python charge monitoring utility for those who have a Tesla Powerwall.
 Uses jasonacox/pypowerwall docker container to get realtime stats from the local gateway for Production 
 and Grid Utilization of electricity to control
 your main Tesla's AC charging amps to charge only with excess production.
 Simply plug in your car, update your info below, and run this in python.

 Added: checking of location of Tesla to be sure it's charging at home
 Added: tracks cabin temp and local chargers, vents the car if it gets too hot 
 - removed due to Tesla removal of Vent option from API
"""

username = 'elon@tesla.com' # Fill in Tesla login email address/account
lat, lon  = ##.###, -###.###        # Fill in Location where charging will occur (shown at startup)
pypowerwall_IP = '10.x.x.x:8675'    # Fill in IP address and port for pypowerwall docker container

RedTxt, BluTxt, NormTxt = '\033[31m', '\033[34m', '\033[m'
RedBG, GrnBG, NormBG = '\033[101m', '\033[102m', '\033[0m'

import datetime, asyncio
import logging, sys#, json
# pip3 install teslapy
import teslapy
#/c Set stdout as logging handler
root_log = logging.getLogger()
root_log.setLevel(logging.WARNING) # WARNING or INFO or DEBUG
handler = logging.StreamHandler(sys.stdout)

def printerror(error, err) :                               # Error message with truncated data
    print(str(err).split("}")[0], "}\n", datetime.datetime.now().strftime( "%a %I:%M %p" ), error)

def printmsg(msg) :                                        # Timestamped message
    print( " ", datetime.datetime.now().strftime( "%a %I:%M %p" ), msg )

def PrintUpdate(chargedata, fast) :                        # Display stats at every % change
    print( "\nLevel:",
        chargedata['battery_level'], "%, Limit",
        chargedata['charge_limit_soc'], "%,",
        chargedata['charge_rate'], "MPH",
        chargedata['charger_voltage'], "Volts",
        chargedata['charge_energy_added'], "kWh added,")
    if fast : print("Rate:",
        chargedata['charger_power'], "KWs",
        chargedata['conn_charge_cable'],
        chargedata['fast_charger_type'],
        chargedata['minutes_to_full_charge'], "Minutes remaining\n" )
    else : print(chargedata['charger_actual_current'], "of a possible",
        chargedata['charge_current_request_max'], "Amps,",
        chargedata['time_to_full_charge'], "Hours remaining\n" )

def PrintTemp(car) :
    #Tesla removed vent API in USA due to NTHSA ~01/2023 - causes API error
    #if car.get_vehicle_data()['climate_state']['inside_temp'] > 40 : # 104°F
    #    if not car.get_vehicle_data()['vehicle_state']['fd_window'] : # Not Open
    #        Vent(car, 'vent')
    #else :
    #    if car.get_vehicle_data()['vehicle_state']['fd_window'] :    # Open
    #        Vent(car, 'close')
    print(car.temp_units(car.get_vehicle_data()['climate_state']['inside_temp']), end='')
    if car.get_vehicle_data()['climate_state']['fan_status'] : print(car.get_vehicle_data()['climate_state']['fan_status'], end='')
    if car.get_vehicle_data()['climate_state']['cabin_overheat_protection_actively_cooling'] : print(car.get_vehicle_data()['climate_state']['cabin_overheat_protection_actively_cooling'], end='')

def SendCmd(car, cmd, err) :                               # Start or Stop charging
    try :
        car.command(cmd)
    except teslapy.VehicleError as e :
        print(err)
        printmsg(e)

def SetAmps(car, newrate, err) :                           # Increase or decrease charging rate
    try :
        car.command('CHARGING_AMPS', charging_amps = newrate)
    except teslapy.VehicleError as e : printerror("V: "+err, e)
    except teslapy.HTTPError as e: printerror("H: "+err, e)

def SetCharging(car, newrate, msg) :
    print(msg, "charging to", newrate, "amps")
    if newrate == 2 : newrate = 1                          # For API a newrate of 3=3, 2=3, 1=2
    SetAmps(car, newrate, "Failed to change")              #  so to set to 2 newrate must be 1
    if newrate < 5 :                                       # if under 5 amps you need to send it twice:
        SetAmps(car, newrate, "Failed to change 2")

def StartCharging(car) :
    try :                                                  # Collect new data from Tesla
        state = car.get_vehicle_data()['charge_state']['charging_state']
    except teslapy.HTTPError as e:
        printerror("Tesla failed to update, please wait a minute...", e)
        return
    print(GrnBG + "Starting" + NormBG + " charge at 2 Amps")     # Underlined
    if state != "Charging" :
        SendCmd(car, 'START_CHARGE', "Won't start charging")
        SetAmps(car, 1, "Won't start charging 2")
        SetAmps(car, 1, "Won't start charging 3")

def StopCharging(car) :
    print( RedBG + "Stopping" + NormBG + " charge" )            # Underlined
    SendCmd(car, 'STOP_CHARGE', "Failed to stop")

def SuperCharging(chargedata) :                            # Loop while DC Fast Charging
    if chargedata['fast_charger_present']:
        printmsg("DC Fast Charging...")
        PrintUpdate(chargedata, 1)
        return(True)

def Wake(car) :
    printmsg("Waking...")
    try : car.sync_wake_up()
    except teslapy.VehicleError as e :
        printerror("Failed to wake", e)
        return(False)
    return(True)

def update_powerwall(): #get site data on solar/grid from local Powerwall gateway via pypowerwall docker container
    instant_stats = requests.get('http://'+pypowerwall_IP+'/aggregates')
    return (instant_stats.json())

def UpdateSense() :  # Update Powerwall and charger voltage
    global power_diff, volts
    try :
        power_diff = update_powerwall()['site']['instant_power']
        volts = int(vehicles[0].get_vehicle_data()['charge_state']['charger_voltage'])
    except :
        printmsg(RedTxt + "Powerwall data timeout or cannot get charger voltage" + NormTxt)
        power_diff = 0
        return(True)
    else :
        #volts = int(car.get_vehicle_data()['charge_state']['charger_voltage'])
        power_diff = int(update_powerwall()['site']['instant_power'])*-1

def Vent(car, command) :
    try :  car.command('WINDOW_CONTROL', command = command, lat=lat, lon=lon)
    except teslapy.VehicleError as e : printmsg("Window_Control Failed " + str(e))
    else:  print(RedTxt + "Windows will now", command + NormTxt)

async def TesSense() :
    rate = newrate = limit = level = lastime = fullORunplugged = 0
    minrate = 2                                            # Minimum rate you can set the charger to

    retry = teslapy.Retry(total=3, status_forcelist=(500, 502, 503, 504))

    with teslapy.Tesla(username, retry=retry, timeout=30) as tesla:
        if not tesla.authorized:
            print('Use browser to login. Page Not Found will be shown at success.')
            print('Open this URL: ' + tesla.authorization_url())
            tesla.fetch_token(authorization_response=input('Enter URL after authentication: '))
        vehicles = tesla.vehicle_list()

        print("Starting connection to", vehicles[0].get_vehicle_summary()['display_name'], end='')
        cardata = vehicles[0].get_vehicle_data()
        try:
            print("... [", round(cardata['drive_state']['latitude'], 3), round(cardata['drive_state']['longitude'], 3), "]")
        except: pass
        #print(' last seen ' + vehicles[0].last_seen(), end='') #last seen timestamp in future error
        if vehicles[0]['charge_state']['battery_level']:
            print(' at ' + str(vehicles[0]['charge_state']['battery_level']) + '% SoC\n')
        else: print('\n')

        while (True):                                      # Main loop with night time carve out
            if vehicles[0].get_vehicle_summary()['in_service'] :
                print("Sorry. Currently this car is in for service")
                exit()

            if datetime.datetime.now().time().hour < 8 or datetime.datetime.now().time().hour >= 16 :
                printmsg(BluTxt + "Nighttime" + NormTxt +", Sleeping until next hour...")
                if 16 <= datetime.datetime.now().time().hour <= 17 :
                    StopCharging(vehicles[0])
                    printmsg(BluTxt + "4-9pm" + NormTxt +", Stop charging...") #4-9pm peak rate when PowerWall is powering house, so don't charge car and drain powerwall
                await asyncio.sleep(60 * (60 - datetime.datetime.now().time().minute))
                continue

            if UpdateSense() :                             # Collect new data from Energy Monitor
                await asyncio.sleep(20)                    # Error: Return to top of order
                continue

            minwatts = minrate * volts                     # Calc minwatts needed to start charging

            if not vehicles[0].available() :               # Car is sleeping
                if power_diff > minwatts and not fullORunplugged :
                    if Wake(vehicles[0]):                  # Initial daytime wake() also, to get status
                        rate = newrate = 0                 # Reset rate as things will have changed
                        continue
                    else:
                        print("Wake error. Sleeping 20 minutes and trying again")
                        await asyncio.sleep(1200)          # Give the API a chance to find the car
                        continue
                else :
                    if fullORunplugged == 1 : print("Full-", end='')
                    elif fullORunplugged == 2 : print("Unplugged-", end='')
                    print("Sleeping, free power is", power_diff, "watts" )
                    if fullORunplugged :
                        printmsg(" Wait twenty minutes...")
                        await asyncio.sleep(1200)
                        continue

            else :                                         # Car is awake
                try :
                    cardata = vehicles[0].get_vehicle_data() # Collect new data from Tesla
                    chargedata = cardata['charge_state']
                except teslapy.HTTPError as e:
                    printerror("Tesla failed to update, please wait a minute...", e)
                    await asyncio.sleep(60)                # Error: Return to top of order
                    continue

                if SuperCharging(chargedata) :             # Display any Supercharging or DCFC data
                    await asyncio.sleep(120)               # Loop while Supercharging back to top
                    continue

                if 'latitude' in cardata['drive_state'] :  # Prevent remote charging issues
                    if round(cardata['drive_state']['latitude'], 3) != lat and \
                       round(cardata['drive_state']['longitude'], 3) != lon :
                        print(round(cardata['drive_state']['latitude'], 3), \
                             round(cardata['drive_state']['longitude'], 3), end='')
                        printmsg(' Away from home. Wait 5 minutes')
                        fullORunplugged = 2                 # If it's not at home, it's not plugged in nor full
                        await asyncio.sleep(300)
                        continue
                else :
                    print(RedTxt + 'Error: No Location' + NormTxt)

                if not chargedata['charging_state'] == "Charging" :   # Not charging, check if need to start

                    if power_diff > minwatts and not fullORunplugged: # Minimum free watts to start charge
                        if chargedata['battery_level'] >= chargedata['charge_limit_soc'] :
                            print("Full Battery, power at", power_diff, "watts" )
                            fullORunplugged = 1
                        elif chargedata['charging_state'] == "Disconnected":
                            print(RedTxt + "Please plug in" + NormTxt + ", power at", power_diff, "watts" )
                            fullORunplugged = 2
                        else :                             # Plugged in and battery is not full so
                            StartCharging(vehicles[0])

                    else :
                        print( "Not Charging, free power is at", power_diff, "watts" )

                else :                                     # Charging, update status
                    if chargedata['battery_level'] < chargedata['charge_limit_soc'] :
                        fullORunplugged = 0                # Mark it as NOT full and AS plugged-in

                    if  level != chargedata['battery_level'] or limit != chargedata['charge_limit_soc'] :
                        level, limit = chargedata['battery_level'], chargedata['charge_limit_soc']
                        PrintUpdate(chargedata, 0)         # Display charging info every % change

                    rate = chargedata['charger_actual_current']
                    newrate = min(rate + int(power_diff/volts), chargedata['charge_current_request_max'])

                    print( "Charging at", rate, "amps, with", power_diff, "watts surplus" )

                    if newrate < minrate :                 # Stop charging as there's no free power
                        StopCharging(vehicles[0])
                        newrate = 0
                    elif newrate > rate :                  # Charge faster with any surplus
                        SetCharging(vehicles[0], newrate, "Increasing")
                    elif newrate < rate :                  # Charge slower due to less availablity
                        SetCharging(vehicles[0], newrate, "Slowing")

            if lastime != vehicles[0].get_vehicle_data()['climate_state']['timestamp'] :
                lastime = vehicles[0].get_vehicle_data()['climate_state']['timestamp']
                PrintTemp(vehicles[0])                     # Display cabin temp and fan use
            printmsg(" Wait two minutes...")               # Message after every complete loop
            await asyncio.sleep(120)                       # Could use variable to change frequency of updates, but 2 minutes seems reasonable without hitting Tesla API frequently enough to cause lockout

#run the main program
try:
    await TesSense()
except KeyboardInterrupt:
    print("\n\n Interrupt received, stopping TesSense\n")
jasonacox commented 1 year ago

This is awesome! Thanks @venturanc 🙏

Do you mind if I add this to our /tools section (also happy to add a link to your github location once you have it)? Or you could submit a PR youself to get listed as a pypowerwall contributor. 😉

venturanc commented 1 year ago

I'd be honored to submit a PR to /tools and contribute! Give me just a few days to sort everything out and hopefully will get it in soon.

jasonacox commented 1 year ago

Thanks @venturanc !