In my first blog post about hacking my Tesla Powerwalls, I laid out all of the foundations and information about my home energy setup. You really need to read that blog post first as I'm going to be building on all of that work here, and assuming that you're familiar with everything in that post.

The final piece of the puzzle

In my previous post, I had focused on solving a particular problem. My Powerwalls would charge up overnight on the very cheap off-peak rate so that they could run my house during the day, avoiding the expensive peak rate. The problem I was coming across was that the batteries would hit 100% SoC overnight, start depleting during the day, but when the sun started shining, our solar array could fill the batteries back up to 100% SoC and then export excess solar production at an almost worthless cost. I created an automation to stop the batteries from charging once they hit 75% SoC, which left plenty of room for excess solar production to fill up the batteries during the day rather than exporting the energy. This was an improvement, but it wasn't an ideal solution, so I set out to resolve it.

My new export tariff

I've finally managed to get myself on a decent export tariff, meaning that I'm now being paid much more for energy that I export to the grid. I feel that energy I export to the grid should be paid for at the same rate as energy I import from the grid, but that's unfortunately not the case. I am, however, now being paid almost 4x more than I was before for energy export, and it still falls 50% short of what my import costs! My import tariff is still the same, and my export tariff is now much better.

Activity Time Cost
Import 05:30 to 23:30 £0.28/kWh
Import 23:30 to 05:30 £0.07/kWh
Export 00:00 to 23:59 £0.04/kWh £0.15/kWh

Of course, the fact that I can purchase electricity for £0.07/kWh overnight and then export it for £0.15/kWh is quite an attractive proposition, and something that I'd like to take advantage of. This is something known as load-shifting, or Load Management, where suppliers want to encourage people to move their usage into more desirable time periods, and is the reason for the very existence of the cheap overnight tariffs in the first place. I already have the batteries and solar array to allow me to do this, so all I needed was a solution to automate it.

Using Home Assistant and Teslemetry, again!

In my previous post, I used Teslemetry to control when the batteries could charge from the grid to introduce the ceiling of 75% SoC to leave room for excess solar production during the day. Now, though, I want to go one step further and charge the batteries to 100% SoC overnight, and slowly let them feed back to the grid during the day if we have excess capacity, along with excess solar production, to leave me with enough power to get back to the cheap rate at 23:30 again.

The Home Assistant automation to do this was surprisingly simple, so I will post the whole thing here, and then we can talk through what it does.

alias: Dynamic Battery Export Control
description: >-
  Adjust export setting and export mode display based on battery SoC and
  time-of-day
triggers:
  - minutes: /15
    trigger: time_pattern
actions:
  - variables:
      now_ts: "{{ now().timestamp() }}"
      today: "{{ now().date().isoformat() }}"
      start_ts: "{{ (today ~ 'T05:30:00') | as_datetime | as_timestamp }}"
      end_ts: "{{ (today ~ 'T23:30:00') | as_datetime | as_timestamp }}"
      end_for_target_ts: "{{ (today ~ 'T23:15:00') | as_datetime | as_timestamp }}"
      current_soc: "{{ states('sensor.shireburn_charge') | float(0) }}"
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ now_ts < start_ts or now_ts > end_ts }}"
        sequence:
          - target:
              entity_id: input_number.target_soc
            data:
              value: 100
            action: input_number.set_value
          - target:
              entity_id: input_text.export_mode_state
            data:
              value: No Export
            action: input_text.set_value
          - condition: template
            value_template: "{{ states('select.shireburn_allow_export') != 'never' }}"
          - target:
              entity_id: select.shireburn_allow_export
            data:
              option: never
            action: select.select_option
    default:
      - variables:
          total_secs: "{{ end_for_target_ts - start_ts }}"
          elapsed_secs: "{{ now_ts - start_ts }}"
          progress: >-
            {{ (elapsed_secs / total_secs) if elapsed_secs < total_secs else 1.0
            }}
          target_soc: "{{ 95 - (85 * (progress ** 1.5)) }}"
      - target:
          entity_id: input_number.target_soc
        data:
          value: "{{ target_soc | round(0) }}"
        action: input_number.set_value
      - choose:
          - conditions:
              - condition: template
                value_template: |
                  {% set hour = now().hour %} {% if hour < 23 %}
                    {{ current_soc > (target_soc + 5) }}
                  {% else %}
                    {{ current_soc > target_soc }}
                  {% endif %}
            sequence:
              - target:
                  entity_id: input_text.export_mode_state
                data:
                  value: Battery + Solar Export
                action: input_text.set_value
              - condition: template
                value_template: "{{ states('select.shireburn_allow_export') != 'battery_ok' }}"
              - target:
                  entity_id: select.shireburn_allow_export
                data:
                  option: battery_ok
                action: select.select_option
          - conditions:
              - condition: template
                value_template: "{{ current_soc < (target_soc - 5) }}"
            sequence:
              - target:
                  entity_id: input_text.export_mode_state
                data:
                  value: No Export
                action: input_text.set_value
              - condition: template
                value_template: "{{ states('select.shireburn_allow_export') != 'never' }}"
              - target:
                  entity_id: select.shireburn_allow_export
                data:
                  option: never
                action: select.select_option
        default:
          - target:
              entity_id: input_text.export_mode_state
            data:
              value: Solar Export
            action: input_text.set_value
          - condition: template
            value_template: "{{ states('select.shireburn_allow_export') != 'pv_only' }}"
          - target:
              entity_id: select.shireburn_allow_export
            data:
              option: pv_only
            action: select.select_option
mode: single

The automation runs every 15 minutes and first checks if we're in the off-peak window of 23:30 to 05:30, and if we are, it sets the Target SoC to 100% and that's it, we want the battery charging. If we're in the peak window of 05:30 to 23:30, then a few things happen.

The first thing it does is calculate how far through the peak window we are, which is used to calculate the Target SoC. Originally, I had a linear decline throughout the day which seemed like it should work just fine. If we were 25% of the way through the window, the Target SoC would be 75%, at 50% of the way through the window the Target SoC was 50%, and 75% of the way through the window, the Target SoC would be 25%.

Plotting that out it looks like the graph below, and it did work fine, but we had some evenings where our usage would spike quite high and there wasn't enough of a buffer left in the battery, leaving us short on battery power. I didn't want to have to worry about problems like this, and whilst it worked fine for the most part, it needed improving.

Target SoC with a linear decline

I changed the Target SoC to an exponential decline throughout the day, meaning it would hold a slightly higher SoC during the day, but then tail off quite hard at the end of the day, making the Target SoC curve look like this.

Target SoC with an exponential decline

This gave us much more breathing room if we had a sudden increase in usage during the evening, and if not, the batteries could just export at a more aggressive rate to reach the lower Target SoC. This worked much better for us, gave us the breathing room we needed, but still meant that we could export any spare capacity during the peak window.

Now that the Target SoC is established, we can decide on the export mode to keep the Actual SoC aligned with the Target SoC. For that, there are the following 3 options.

Condition 1: Battery SoC > (Target SoC + 5) = Export Battery + Solar
Condition 2: Battery SoC < (Target SoC - 5) = Export Nothing
Default: Export Solar Only

Condition 1: If we're more than 5% above target, we should export from the solar array and the batteries as we have spare capacity in the batteries. This will bring the Actual SoC down to the Target SoC faster as time goes by.

Condition 2: If we're more than 5% below target we should export nothing, as we're trying to preserve the battery capacity by using solar to power the house as much as possible. This reduces the load on the battery and allows the Target SoC to fall over time while holding the Actual SoC as high as possible.

Default: The aim here is to run the house on the batteries and export solar production to the grid, trying to maintain a steady decline in Actual SoC throughout the day that should align it with Target SoC.

If you look at the graphs above for Actual SoC, you can see the step changes every 15 minutes when the automation runs and changes the export mode to adjust the Actual SoC as needed, keeping things well aligned! One thing I am keeping in mind is that we're in summer right now, so our solar production is quite good and our energy usage is probably on the lower end of what's typical. As we approach autumn and then winter, I'm wondering if I might need to make the Target SoC curve a little steeper, to hold a higher charge during the day and tail off more aggressively in the evening, to give us a little more breathing room. Only time will tell on that one, but if there's a change required, I will come back and update this post with a note.

Seeing it in action

Now that I've run this automation for over a week, it's had a chance to run through days where we have exceptionally low solar production (British Summer), and we export very little, to days like yesterday where we have amazing solar production all day (one day in British Summer), and as a result, we see some great export rates too!

The top graph shows our export for the day and you can clearly see the Solar Bell (the solar production bell curve) from sunrise to sunset. Dotted throughout the day you can also see the occasional spikes in our export, where the automation has detected excess capacity in the batteries and released it into the grid, causing a spike in our 30 minute export window. You can then clearly see the exponential decline for battery capacity kicking in as we release excess battery capacity late into the evening. The bottom graph then shows our import, which looks very different, hauling in that cheap power overnight and not importing any energy during the day. If you want to see those graphs as kWh instead of cost, here they are.

Maximum import, maximum export

Once we hit the cheap tariff at 23:30, the heavy import begins. Not only do our Powerwalls begin charging, capable of pulling in a total of 15kW, we have quite a few other systems that make use of the off-peak rate too. If either one of our two EVs are plugged in, they will begin charging at 7kW, the hot tub only heats and filters the water overnight, so that fires up using almost 4kW, and a variety of smaller appliance like the tumble dryer might be waiting too, using a few more kW of power.

This means that we can quite easily hit our maximum import limit of 24kW, at which point the Powerwalls will automatically throttle their import to prevent us from exceeding the limit and blowing our 100A main fuse (100A x 240v = 24,000W). Given that we have a 24kW import limit, you'd think that we have a 24kW export limit too, but, we don't... For reasons known only to them, our Distribution Network Operator determined that we are only able to export at a maximum rate of 3.869kW, which you can see on the negative portion of the graph during the day where we hit the flat bottom of our maximum export rate.

Side note: Yes, I know our nominal voltage on the grid in the UK is 230v after we harmonised with Europe, but in reality, it's often much higher. My 100A fuse is also BS1361 rated, meaning it can realistically carry 120A indefinitely, and last for prolonged periods at 125A. Even at our minimum tolerance on grid voltage of 207v, we'd still only be pulling 116A at 24kW (24,000w / 207v = 116A). Here's the grid voltage for the same time window shown in the above graph to show what I mean.

It is interesting to see what effect solar PV has on the peak voltage, getting us quite high at 255v, and then at the opposite end of the scale under heavy load, we can dip all the way down to 233v!

Can we control the Powerwall charge rate?

Just before we wrap up, there is one final thing that I haven't yet found a solution for that I would like to resolve, so if anyone has any ideas, please let me know in the comments. When the Powerwalls start charging from the grid, they charge at their absolute maximum possible rate of 5kW per battery, giving us our total import rate of 15kW into our 3 batteries. Each battery has a usable capacity of 13.5kWh, so, ignoring efficiency for easy maths, at maximum charging rate they can go from 0% to 100% in 2 hours and 45 minutes.

It seems unnecessary to charge at their absolute maximum rate which will have them sat at 100% SoC for more than half of our off-peak window, and have charged them at full power when it wasn't needed. I'd much prefer to be able to set them to charge at 2.5kW per battery, which will still allow them to go from 0% to 100% during our off-peak window. Charging them more slowly is better for the batteries in the long run due to the lower charge rate, and, they will spend less time at 100% SoC which is also better for their health. If I had dynamic control of the charge rate I could even calculate what rate was required for them to hit 100% SoC right as the off-peak window ends!

At present, I can't see any way to do this, either setting a static import limit per battery or a dynamic limit based on SoC and time remaining. I know I can restrict the overall site import limit with the Tesla One app, but that's not useful here as it isn't flexible and could result in no import into the batteries if something else is using the import! I'm a little stuck on this one, do you have any ideas?