Turn your tuya ev charger dynamic, load balancing or solar controlled

I wanted to share a project of mine, i took inspiration from many contributors, took pieces of code, modified them. you know how it goes. So i thought it would be time to give back a little.

I wrote an automation 2 actually that work together to control a cheap tuya ev charger from home assistant and have it take commands to either load balance, solar charge or shutdown.

The automation relies on a few sensors, a couple of helpers, some simple calculations, a smart socket with power metering and some tuya scenes (i haven’t been able to get the charger into localtuya yet.)

To operate it needs an input_number helper. one automation fills the value of that input number and the other takes that input number and translates it to a command for the charger (if change is needed).

These cheaper tuya ev chargers do not like big swings in power settings (they shut down and don’t start back easily) so the automation steps it up or down depending on the need.

The first script takes commands and sends them out if needed to the charger via a tuya scene:

alias: "Laadamperes regelen "
if:
  - condition: numeric_state
    entity_id: sensor.tesla_lader_hws_vermogen
    above: 100
  - condition: template
    value_template: >
      {% set laadstroom_keuze = states('input_number.echt_amperage') | float %}
      {% set laad_amperage = states('input_number.laad_amperage') | float %} {{
      laadstroom_keuze in [6, 8, 10, 13, 16] and laad_amperage in [6, 8, 10, 13,
      16] }}
then:
  - choose:
      - conditions:
          - condition: template
            value_template: >-
              {{ states('input_number.echt_amperage') | float <
              states('input_number.laad_amperage') | float }}
        sequence:
          - choose:
              - conditions:
                  - condition: template
                    value_template: "{{ states('input_number.echt_amperage') | float == 6 }}"
                sequence:
                  - entity_id: scene.ev_charger_8a
                    action: scene.turn_on
              - conditions:
                  - condition: template
                    value_template: "{{ states('input_number.echt_amperage') | float == 8 }}"
                sequence:
                  - entity_id: scene.ev_charger_10a
                    action: scene.turn_on
              - conditions:
                  - condition: template
                    value_template: "{{ states('input_number.echt_amperage') | float == 10 }}"
                sequence:
                  - entity_id: scene.ev_charger_13a
                    action: scene.turn_on
              - conditions:
                  - condition: template
                    value_template: "{{ states('input_number.echt_amperage') | float == 13 }}"
                sequence:
                  - entity_id: scene.ev_charger_16a
                    action: scene.turn_on
      - conditions:
          - condition: template
            value_template: >-
              {{ states('input_number.echt_amperage') | float >
              states('input_number.laad_amperage') | float }}
        sequence:
          - choose:
              - conditions:
                  - condition: template
                    value_template: "{{ states('input_number.echt_amperage') | float == 16 }}"
                sequence:
                  - entity_id: scene.ev_charger_13a
                    action: scene.turn_on
              - conditions:
                  - condition: template
                    value_template: "{{ states('input_number.echt_amperage') | float == 13 }}"
                sequence:
                  - entity_id: scene.ev_charger_10a
                    action: scene.turn_on
              - conditions:
                  - condition: template
                    value_template: "{{ states('input_number.echt_amperage') | float == 10 }}"
                sequence:
                  - entity_id: scene.ev_charger_8a
                    action: scene.turn_on
              - conditions:
                  - condition: template
                    value_template: "{{ states('input_number.echt_amperage') | float == 8 }}"
                sequence:
                  - entity_id: scene.ev_charger_6a
                    action: scene.turn_on
      - conditions:
          - condition: template
            value_template: >-
              {{ states('input_number.echt_amperage') | float ==
              states('input_number.laad_amperage') | float }}
        sequence:
          - entity_id: scene.none
            action: scene.turn_on

The pacing is done by waiting on helper input_number.echt_amperage. This number is generated by taking the power reading of the smart plug (sensor.tesla_lader_hws_vermogen) to which the charger is connected dividing it by the voltage and rounding it off to a set number of options (6 8 10 13 and 16, which are the possibe settings of the charger) to give a rough estimate of the amp draw. (a change in amps towards the setpoint indicates the car received and accepted the order so the charger can continue stepping up or down)
that script looks like this :

alias: laad amperes meting stekker (stap voor stap op/af schalen)
target:
  entity_id: input_number.echt_amperage
data:
  value: >-
    {% set calculated_value = (states('sensor.tesla_lader_hws_vermogen') |
    float(0)) / 240 %}

    {% if calculated_value is not none %}
      {% if calculated_value < 6 %}
        6
      {% elif calculated_value < 8 %}
        8
      {% elif calculated_value < 10 %}
        10
      {% elif calculated_value < 13 %}
        13
      {% elif calculated_value < 16 %}
        16
      {% else %}
        16
      {% endif %}
    {% else %}
      0  # Fallback value if calculation fails
    {% endif %}
action: input_number.set_value
enabled: true

this is triggered every 10 seconds when the charger is on it makes it so the charger controller script steps up or down 1 step from the current setting.

input_number.laad_amperage is what signals the need for a shift up or down. it is the value we want to “step toward” and maintain for the moment.

That is generated by this script for load balancing:

alias: dynamisch naar max
target:
  entity_id: input_number.laad_amperage
data:
  value: >-
    {% set max_belasting_p1 = states('input_number.max_belasting_p1') | float(0)
    %} {% set p1_vermogen = states('sensor.p1_meter_vermogen') | float(0) %} {%
    set tesla_vermogen = states('sensor.tesla_lader_hws_vermogen') | float(0) %}

    {% set calculated_value = ((max_belasting_p1 - p1_vermogen) +
    tesla_vermogen) / 250 %}

    {% if calculated_value is not none %}
      {% if calculated_value < 6 %}
        6
      {% elif calculated_value < 8 %}
        8
      {% elif calculated_value < 10 %}
        10
      {% elif calculated_value < 13 %}
        13
      {% elif calculated_value < 14 %}
        16
      {% else %}
        16
      {% endif %}
    {% else %}
      6  # Fallback value if calculation fails, set to 6A
    {% endif %}
action: input_number.set_value
enabled: true

input_number.max_belasting_p1 is a load limiter, it makes sure that if some big appliance kicks in there’s still room on the main power connection to sustain it while the charger steps down the ev charging. it takes 10 seconds per step so it needs a bit of a buffer. it is set to 6kw in my situation.

solar controlled charging is done by :

alias: Zon laden naar aanbod
target:
  entity_id: input_number.laad_amperage
data:
  value: >-
    {% set calculated_value =
    (states('input_number.beschikbare_solar_voor_thunder') | float(0) - 300) /
    240 %}

    {% if calculated_value is not none %}
      {% if calculated_value < 6 %}
        6
      {% elif calculated_value < 8 %}
        8
      {% elif calculated_value < 10 %}
        10
      {% elif calculated_value < 13 %}
        13
      {% elif calculated_value < 16 %}
        16
      {% else %}
        16
      {% endif %}
    {% else %}
      6  # Fallback value if calculation fails, set to 6A
    {% endif %}
action: input_number.set_value
enabled: true

this input number combines all loads that are controlled by solar and the main meter value. essentially making sure the ev charger will “push out” all other solar controlled functions by hogging the solar. (which causes the other solar functions to step down or turn of). this is off course a choice. you can prioritise is fits your situation. in my case the number generation looks like this:

target:
  entity_id: input_number.beschikbare_solar_voor_thunder
data:
  value: >-
    {% set calculated_value = 0 - (states('sensor.p1_meter_vermogen') |
    float(0)) + (states('sensor.zonne_boiler_hws_vermogen') | float(0)) + 
    (states('sensor.accu_pakket_hws_vermogen') | float(0)) + 
    (states('sensor.tesla_lader_hws_vermogen') | float(0))%} {% set
    clamped_value = [0, [calculated_value, 5000] | min] | max %} {{
    clamped_value | round(0, 'floor') | int }}
action: input_number.set_value
alias: beschikbaar solar voor Thunder (eis alle zonnestroom op)

The solar calculation is done every 3 minutes

The charger order execution (script sending out scenes) is triggered by changes in the input_numbers it relies on.

the other calculations are done every ten seconds

In my case the charging itself gets triggered by an app that selects the best times to charge the car based on power grid demand and my departure time. I get a financial reward for handing over that control.

That makes charging at night cheap and charging during the day profitable. As long as home assistant controls the current (through the tuya ev charger) and the sun is shining I get paid to charge my own car with my own solar energy. A free charge and some spare change for my efforts.

I hope this helps someone, it would have helped me :slight_smile:

1 Like

Great work, David. Thanks a lot For sharing.