We recently got an electric vehicle and unsurprisingly our electricity usage has shot up - something like 125% so far. This is of course offset by not needing to buy gas, but the PG&E bill is starting to look eye watering.
PG&E offers an exciting and nearly impenetrable number of rate plans. Right now we're on E-TOU-C which PG&E says is the best choice for us. This is a time of use plan which makes a lot of sense - electricity is cheap off peak and expensive when it's in high demand. Running the dishwasher at the end of the day saves a few cents. Charging an EV at the right time is a big deal.
I decided to simulate our bill on each plan, with and without EV charging.
This turns out to be astonishingly complicated. There is probably a significant energy saving in having the billing systems sweat a bit less. It's not just peak vs. off peak, the rates are different for summer and winter. In some plans peak is a daily occurrence and in others it doesn't apply to weekends and holidays (raising the exciting sub investigation of what PG&E considers to be a holiday). Some plans have a daily use fee. Our plan has a discount for baseline usage, others do not.
That's all just for the conventional time of use plans. The EV plans introduce a 'part-peak' period so there are three different rates based on time of day. They also have different definitions of summer.
I had imagined a quick spreadsheet but this has turned into a python exercise. The notebook is included below. If you use this you'll need to estimate your average daily EV charging needs and also your baseline details. It uses a year of data downloaded from PG&E to run the simulation, so use the year before you started charging an EV. I think I've captured most of the details but I did take a shortcut with the baseline calculations - it uses calendar months instead of billing periods. PG&E billing periods range from 28-33 days, presumably because that will be cheaper in the long run.
It would be nice if PG&E had some kind of what-if modelling but I guess that's not in their best interests. Right now the web site says I should stick on E-TOU-C, which looks like a bad idea even based on the past year of usage. All of the plans are pretty close for me based on historical usage though. Adding an EV shows a huge difference. Off peak rates are a lot cheaper but in exchange the peak rates are much higher. I'll save a lot moving to the EV2 plan, which is what I've just done. It's not clear how you should choose between the different EV oriented plans without getting into this level of detail, but they are all better than the conventional time of use options if you have better things to do.
I evaluated the E-TOU-B, E-TOU-C and E-TOU-D time of use plans and the EV Rate A, EV Rate B, EV2 and E-ELEC plans for people with an EV or other qualifying electrical thing. The chart at the top of the post shows PG&E's estimates for the past year, my estimates and then my estimates with EV charging included.
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Best PG&E Electric Rate Plan for EV\n",
"\n",
"Actual rate plan data in [this](https://www.pge.com/assets/rates/tariffs/Res_Inclu_TOU_Current.xlsx) Excel file. [This](https://www.pge.com/en/account/rate-plans/find-your-best-rate-plan/tiered-rate-plan.html) page has some details on the E1 tiered rate plan. TOU holidays are defined [here](https://www.pge.com/tariffs/en/rate-information/tou-holidays.html).\n",
"\n",
"Most plans have some minimum daily delivery, ignoring this as they are similar and not driven by usage. The tiered plans (E-1) etc just start charing more once you use more and are designed for people who can't control when power is used so not analyzing these. The remaining plans have a discount based on when you draw power, so looking at E-TOU-B, E-TOU-C and E-TOU-D which all have subtle differences. Also EVA, EVB, EV2 and E-ELEC which are targeted at EV / other electrical technology usage.\n",
"\n",
"Baseline is need for E-TOU-C. Information is on page 3 of the bill. I'm in baseline territory T, and my heat source is B - Not Electric (makes sense, heating is gas). For the current month my baseline is 208 kWh which is 32 days at 6.5 kWh / day. It's 32 because the billing cycle is 7/19/24 to 8/19/24 for some reason. And baseline switches from summer to winter based on month, but the billing periods are parts of months... For now, I'm using a simplified model for this. \n",
"\n",
"I'm also ignoring the very strange California Climate Credit ([previously](https://ithoughthecamewithyou.com/post/california-climate-credit)) which is in some plans but not others. It's a one off discount and not usuage driven. "
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import calendar\n",
"from datetime import datetime"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plan Calculations\n",
"\n",
"Functions to calculate total dollars spent based on day of year, hour of day and an assumption for EV charging. "
]
},
{
"cell_type": "code",
"execution_count": 136,
"metadata": {},
"outputs": [],
"source": [
"# assumption for daily charging - this will just be added at 4am each day as that's always going to be of peak\n",
"dailyEvKwh = 17.8\n",
"\n",
"\n",
"# E-TOU-C has a baseline discount, the months and billing periods are complex and so will simmplify to calendar months...\n",
"# These figure are for territory T with gas heating.\n",
"etoucSummerDailyBaseline = 6.5\n",
"etoucWinterDailyBaseline = 7.5\n",
"etoucCurrentMonth = 99\n",
"etoucRemainingBaseline = 0;\n",
"\n",
"def isTouHoliday(date):\n",
" # off peak weekends\n",
" dateobj = datetime.strptime(date, '%Y-%m-%d')\n",
" if dateobj.weekday() in [5, 6]:\n",
" return True\n",
" \n",
" # off peak holidays \n",
" holidays = ['2024-01-01', '2024-02-19', '2024-03-11', '2024-05-01', '2024-05-27', '2024-07-04', '2024-09-02', '2024-11-01', '2024-11-04', '2024-11-11', '2024-11-28', '2024-12-25',\n",
" '2023-01-03', '2023-02-20', '2023-03-13', '2023-05-01', '2023-05-29', '2023-07-04', '2023-09-02', '2023-11-01', '2023-11-06', '2023-11-11', '2023-11-23', '2023-12-25']\n",
" return date in holidays\n",
"\n",
"def days_in_month(date):\n",
" dateobj = datetime.strptime(date, '%Y-%m-%d')\n",
" # Get the number of days in the month\n",
" _, num_days = calendar.monthrange(dateobj.year, dateobj.month)\n",
" return num_days\n",
"\n",
"def evChargeKwh(hour):\n",
" startHour = int(hour.split(':')[0])\n",
" if (startHour == 4):\n",
" return dailyEvKwh\n",
" else:\n",
" return 0\n",
" \n",
"def priceLookupThreeBand(date, hour, startPeak, endPeak, startPartPeak, endPartPeak, startSummer, endSummer, summerPeak, summerPartPeak, summerOffPeak, winterPeak, winterPartPeak, winterOffPeak):\n",
" dateobj = datetime.strptime(date, '%Y-%m-%d')\n",
" month = dateobj.month\n",
" startHour = int(hour.split(':')[0])\n",
"\n",
" price = 0\n",
"\n",
" if month in range(startSummer, endSummer):\n",
" if startHour in range(startPeak, endPeak):\n",
" price = summerPeak\n",
" elif startHour in range(startPartPeak, endPartPeak):\n",
" price = summerPartPeak\n",
" else:\n",
" price = summerOffPeak\n",
" else:\n",
" if startHour in range(startPeak, endPeak):\n",
" price = winterPeak\n",
" elif startHour in range(startPartPeak, endPartPeak):\n",
" price = winterPartPeak\n",
" else:\n",
" price = winterOffPeak\n",
"\n",
" return price\n",
"\n",
"def priceLookupTwoBand(date, hour, startPeak, endPeak, summerPeak, summerOffPeak, winterPeak, winterOffPeak, forceOffPeak):\n",
" dateobj = datetime.strptime(date, '%Y-%m-%d')\n",
" month = dateobj.month\n",
" startHour = int(hour.split(':')[0])\n",
"\n",
" # summer is June-September, Winter is October-May\n",
" price = 0\n",
" if startHour not in range(startPeak, endPeak) or forceOffPeak:\n",
" if month in range(6, 9):\n",
" price = summerOffPeak\n",
" else:\n",
" price = winterOffPeak\n",
" else:\n",
" if month in range(6, 9):\n",
" price = summerPeak\n",
" else:\n",
" price = winterPeak\n",
"\n",
" return price\n",
"\n",
"def etoub(date, hour, kwh):\n",
" price = priceLookupTwoBand(date, hour, 16, 20, 0.56943, 0.44637, 0.4328, 0.394, False)\n",
" return (kwh + evChargeKwh(hour)) * price\n",
"\n",
"def etouc(date, hour, kwh):\n",
" global etoucCurrentMonth\n",
" global etoucRemainingBaseline\n",
"\n",
" # reset baseline each month\n",
" dateobj = datetime.strptime(date, '%Y-%m-%d')\n",
" month = dateobj.month\n",
" if (month != etoucCurrentMonth):\n",
" # calculate days in month\n",
" daysInMonth = days_in_month(date)\n",
" if month in range(6, 9):\n",
" etoucRemainingBaseline = etoucSummerDailyBaseline * daysInMonth\n",
" else:\n",
" etoucRemainingBaseline = etoucWinterDailyBaseline * daysInMonth\n",
" etoucCurrentMonth = month\n",
"\n",
" # calculate discount\n",
" dicountedKwh = 0\n",
" if (kwh <= etoucRemainingBaseline):\n",
" dicountedKwh = kwh\n",
" etoucRemainingBaseline = etoucRemainingBaseline - kwh\n",
" else:\n",
" dicountedKwh = etoucRemainingBaseline\n",
" etoucRemainingBaseline = 0\n",
" discount = dicountedKwh * 0.09837\n",
"\n",
" price = priceLookupTwoBand(date, hour, 16, 20, 0.59342, 0.49042, 0.47926, 0.44926, False)\n",
" cost = (kwh + evChargeKwh(hour)) * price\n",
"\n",
" # less discount\n",
" cost = cost - discount\n",
"\n",
" # there is a daily meter fee on this plan, add this at 4am each day\n",
" startHour = int(hour.split(':')[0])\n",
" if (startHour == 4):\n",
" cost = cost + 0.25298\n",
"\n",
" return cost\n",
"\n",
"def etoud(date, hour, kwh):\n",
" forceOffPeak = isTouHoliday(date)\n",
" price = priceLookupTwoBand(date, hour, 17, 19, 0.55447, 0.41951, 0.46486, 0.42625, forceOffPeak)\n",
" return (kwh + evChargeKwh(hour)) * price\n",
"\n",
"def eva(date, hour, kwh):\n",
" price = 0\n",
" if isTouHoliday(date):\n",
" price = priceLookupThreeBand(date, hour, 15, 18, 15, 18, 5, 10, 0.69132, 0.44721, 0.33466, 0.50872, 0.37671, 0.30498)\n",
" else:\n",
" price = priceLookupThreeBand(date, hour, 14, 20, 7, 22, 5, 10, 0.69132, 0.44721, 0.33466, 0.50872, 0.37671, 0.30498)\n",
" return (kwh + evChargeKwh(hour)) * price\n",
"\n",
"def evb(date, hour, kwh):\n",
" price = 0\n",
" if isTouHoliday(date):\n",
" price = priceLookupThreeBand(date, hour, 15, 18, 15, 18, 5, 10, 0.68841, 0.4443, 0.33175, 0.50587, 0.37386, 0.30213)\n",
" else:\n",
" price = priceLookupThreeBand(date, hour, 14, 20, 7, 22, 5, 10, 0.68841, 0.4443, 0.33175, 0.50587, 0.37386, 0.30213)\n",
" cost = (kwh + evChargeKwh(hour)) * price\n",
" # there is a daily meter fee on this plan, add this at 4am each day\n",
" startHour = int(hour.split(':')[0])\n",
" if (startHour == 4):\n",
" cost = cost + 0.04928\n",
" return cost\n",
"\n",
"def ev2(date, hour, kwh):\n",
" price = priceLookupThreeBand(date, hour, 16, 21, 15, 23, 6, 9, 0.62402, 0.51353, 0.31151, 0.49691, 0.48021, 0.31151)\n",
" return (kwh + evChargeKwh(hour)) * price\n",
"\n",
"def eelec(date, hour, kwh):\n",
" price = priceLookupThreeBand(date, hour, 16, 21, 15, 23, 6, 9, 0.6028, 0.44092, 0.38424, 0.37129, 0.3492, 0.33534)\n",
" cost = (kwh + evChargeKwh(hour)) * price\n",
" # there is a daily meter fee on this plan, add this at 4am each day\n",
" startHour = int(hour.split(':')[0])\n",
" if (startHour == 4):\n",
" cost = cost + 0.49281\n",
" return cost"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"etoucCurrentMonth = 99 # reset to make sure we calculate the first month\n",
"\n",
"print(etoub('2024-09-01', '23:00', 0.67))\n",
"print(etouc('2024-09-01', '23:00', 0.67))\n",
"print(etoud('2024-09-01', '23:00', 0.67))\n",
"print(eva('2024-09-01', '23:00', 0.67))\n",
"print(evb('2024-09-01', '23:00', 0.67))\n",
"print(ev2('2024-09-01', '23:00', 0.67))\n",
"print(eelec('2024-09-01', '23:00', 0.67))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Simulate Bill\n",
"\n",
"Use real data to simulate possible bills. This uses a PG&E expert for the 12 months before I started charging an EV. So the simulation should show how the average existing usage will change cost based on the various plan rules while adding an off-peak EV charge each day."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"df = pd.read_csv('pge1yr.csv', skiprows=6)\n",
"df.head()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"etoucCurrentMonth = 99 # reset to make sure we calculate the first month\n",
"\n",
"# possible bills\n",
"etoubTotal = 0\n",
"etoucTotal = 0\n",
"etoudTotal = 0\n",
"evaTotal = 0\n",
"evbTotal = 0\n",
"ev2Total = 0\n",
"eelecTotal = 0\n",
"\n",
"# process historical data\n",
"for index, row in df.iterrows():\n",
" etoubTotal = etoubTotal + etoub(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n",
" etoucTotal = etoucTotal + etouc(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n",
" etoudTotal = etoudTotal + etoud(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n",
" evaTotal = evaTotal + eva(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n",
" evbTotal = evbTotal + evb(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n",
" ev2Total = ev2Total + ev2(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n",
" eelecTotal = eelecTotal + eelec(row['DATE'], row['START TIME'], row['USAGE (kWh)'])\n",
" \n",
"print(f\"E-TOU-B Total: ${etoubTotal:,.2f}\")\n",
"print(f\"E-TOU-C Total: ${etoucTotal:,.2f}\")\n",
"print(f\"E-TOU-D Total: ${etoudTotal:,.2f}\")\n",
"print(f\"EV Rate A Total: ${evaTotal:,.2f}\")\n",
"print(f\"EV Rate B Total: ${evbTotal:,.2f}\")\n",
"print(f\"EV-2 Total: ${ev2Total:,.2f}\")\n",
"print(f\"E-ELEC Total: ${eelecTotal:,.2f}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.5"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Add Comment
All comments are moderated. Your email address is used to display a Gravatar and optionally for notification of new comments and to sign up for the newsletter.