EMHASS: An Energy Management for Home Assistant

I’m not using the ml forecaster.
Just a simple naive MPC method, with PV power forecast coming from forecast.solar, which I manually pass to EMHASS.
p_pv_forecast is actually created by EMHASS


Did you changed the set_use_pv parameter to True?

Yes

Hello, again the same problem with optimization for negative prices. I think there must be some bug in profit cost function, plus I still don’t understand such high negative P_hybrid_inverter values - where should the power go?

Here is the optimization json, just for case if anyone can find some problem…

    {
        "load_cost_forecast":[4.82, 4.47, 4.15, 3.77, 3.65, 3.45, 3.32, 3.35, 3.48, 3.62, 3.61, 3.47, 2.62, 1.56, 2.1, 0.59, -0.68, -2.15, -1.24, 0.04, 1.41, 2.74, 4.36, 4.9],
        "prod_price_forecast":[2.39, 2.1, 1.84, 1.53, 1.42, 1.26, 1.16, 1.18, 1.28, 1.4, 1.39, 1.27, 0.57, -0.3, -0.37, -1.11, -2.67, -3.37, -3.13, -1.56, -0.43, 0.16, 2.01, 2.46],
        "prediction_horizon":24,    
        "pv_power_forecast":[11, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1230, 2498, 3337, 3779, 4384, 5440, 6110, 6437, 6337, 5793, 4891, 4116, 3421, 1660],
        "load_power_forecast":[274, 420, 380, 220, 210, 210, 310, 300, 220, 200, 230, 280, 540, 360, 770, 640, 840, 790, 920, 760, 610, 500, 740, 260],
        "delta_forecast":2,
        "operating_hours_of_each_deferrable_load":[1,4,0,0,0],
        "def_current_state":[0,0,0,0,0],
        "soc_init":0.64,
        "soc_final":0.78,
        "battery_minimum_state_of_charge":0.4,
        "start_timesteps_of_each_deferrable_load":[0,0,0,0,0],
        "end_timesteps_of_each_deferrable_load":[4,0,0,0,0]
     }

Allowing the wallbox (deferrable_1) higher power values helped. Like the inverter now knows what to do with the energy overflow. But this is still strange behavior, the full power from the grid is not a must, it is an option, so I would expect only the usable power from grid will be used and scheduled in P_grid_ variables.The profit cost function tries to do the profit even when it is not possible.

After some time, again very bad optimization - no load scheduled during cheapest hour. Unusable when there are negative prices during the day. @davidusb I guess we can consider it as a bug?

Maybe something familiar I see:

Negative feed-in, EMHASS wants to import 4000 W (My upper limit) and there is plenty available for the power load and deferrable from the PV. Where do these 4000W go then considering the battery is already 100% charged. With these very negative prices I run my Huawei inverter with zero export setting, but EMHASS doesn’t ‘see’ this. EDIT: using pv curtailment makes it sensible for EMHASS, so I guess all is fine here :slight_smile:

Thanks for doing this - i will read through over the weekend and come back with any questions.

regards

Craig

The current ones i have are the SBP5000 units - i have 3 of them (one per phase) they are happy to export at about 4.6Kw and import at about 5Kw (they derate in the heat)

They have worked flawlessly for me for 4 years and are pushed hard as i use them for Grid Arbitrage

Initially i used them through the WIFI interface and the obsolete AA55 protocol (how the phone app communicates with them) - i suffer multiple failures of the WIFI adapters so in the end i moved to hard wired RS485 and modbus. My Node Red instance runs on a Virtual machine and talks TCP/IP to a 4 port RS485 to TCPIP converter (Modbus TCP to RTU)

Craig

1 Like

Now that the defaults for

weight_battery_discharge
weight_battery_charge

have been set to 0 (by default), what is the impact on the optimization result when using a battery?

        # Add more terms to the objective function in the case of battery use
        if self.optim_conf["set_use_battery"]:
            objective = objective + plp.lpSum(
                -0.001
                * self.timeStep
                * (
                    self.optim_conf["weight_battery_discharge"] * P_sto_pos[i]
                    - self.optim_conf["weight_battery_charge"] * P_sto_neg[i]
                )
                for i in set_I
            )

Is Emhass ready for 12th of June?
Most European countries will change to quarterly prices instead of hourly.

EMHASS can support different timesteps.

I’m using it here (AU) with 5 minute timesteps.

You will likely need to reshape your optimisation payload to include 15 minute price/ cost forecasts.

Indeed 15 min timesteps works fine! I modified my setup a few weeks ago. Nordpool HACS integration is still at 60minutes, so I copy those values 4 times for now. Solcast is 30minutes, so that one is copied twice.

Can you perhaps share your templates as reference?

In sensors.yaml
- platform: template
  sensors:
    current_quarter_of_the_day:
      value_template: "{{ ((now().hour * 60 + now().minute) / 15) | round}}"
 
In template.yaml
  - name: solcast_48hrs_forecast_15minutes_interval
    state: none

    attributes:
      solar_power: >-
        {%- set power = state_attr('sensor.solcast_pv_forecast_forecast_today', 'detailedForecast')| map(attribute='pv_estimate') | list + state_attr('sensor.solcast_pv_forecast_forecast_tomorrow', 'detailedForecast')| map(attribute='pv_estimate') | list %}
        {%- set values_all = namespace(all=[]) %}
        {% for i in range(power | length) %}
          {%- set v = (power[i] | float |multiply(1000) ) | int(0) %}
          {%- set values_all.all = values_all.all + [ v ] %}
          {%- set values_all.all = values_all.all + [ v ] %}
        {%- endfor %} {{ (values_all.all)[:192] }} 
        
  - name: solcast_48hrs_forecast_15minutes_interval_current_quarter
    state: none
    attributes:
      solar_power: >-
        {{ state_attr('sensor.solcast_48hrs_forecast_15minutes_interval','solar_power')[states('sensor.current_quarter_of_the_day') |int:192] }}
        

  - name: solcast_48hrs_forecast_15minutes_interval_current_quarter_count
    state: >-
      {{ state_attr('sensor.solcast_48hrs_forecast_15minutes_interval_current_quarter','solar_power') | list | count }}
 
1 Like

Is there a way force a Huawei sun inverter (sun2000-6ktl-m1) to take power from the grid instead the PV installation with negative buying prices? Don’t think it’s possible but maybe I am missing something here. thx

On my huawei i use Discharge = 0w. And huawei_solar.set_maximum_feed_grid_power_percent = 0

Do you perhaps can also share the Nordpool template? Many thanks in advance


alias: Kontrollera matning till nätet baserat på elpris
description: Stoppar export vid negativt elpris, tillåter vid positivt elpris
triggers:
  - trigger: time_pattern
    hours: /1
actions:
  - choose:
      - conditions:
          - condition: numeric_state
            entity_id: sensor.nordpool_kwh_se3_sek_3_10_0
            below: -30
        sequence:
          - data:
              device_id: 877400bca09c5e673ff27b201c71d371
              power_percentage: 0
            action: huawei_solar.set_maximum_feed_grid_power_percent
      - conditions:
          - condition: numeric_state
            entity_id: sensor.nordpool_kwh_se3_sek_3_10_0
            above: -30
        sequence:
          - data:
              device_id: 877400bca09c5e673ff27b201c71d371
            action: huawei_solar.reset_maximum_feed_grid_power
mode: single

Sure no problem!
In template.yaml:

#the prices as of now
  - name: nordpool_upcoming_quarters
    state: none
    attributes:
      costprices: >-
        {%- set costprices = ((state_attr('sensor.nordpool_kwh_nl_eur_3_10_0', 'raw_today') | map(attribute='value') | list  + state_attr('sensor.nordpool_kwh_nl_eur_3_10_0', 'raw_tomorrow') | map(attribute='value') | list))[now().hour:][:24] %}
        {%- set values_all = namespace(all=[]) %}
        {% for i in range(costprices | length) %}
          {%- set v = (costprices[i] | float ) %}
          {%- set values_all.all = values_all.all + [ v ] %}
          {%- set values_all.all = values_all.all + [ v ] %}
          {%- set values_all.all = values_all.all + [ v ] %}
          {%- set values_all.all = values_all.all + [ v ] %}          
        {%- endfor %} {{ (values_all.all)[(states('sensor.index_of_current_quarter') | int):][:192] }} 

In sensors.yaml:
# 0, 1, 2 or 3 depending on minutes in current hour
- platform: template
  sensors:
    index_of_current_quarter:
      friendly_name: "Index of current quarter"
      device_class: duration
  
      value_template: >-
        {{ ((now().minute / 15)) | int }}
 

@markpurcell Just got the email advising that I’m moving to 5 minute billing and I’m going overseas in a few days. Wouldn’t you know it.
I’ve implemented amber2mqtt and created the template below to replace my old one.

{
  {#- Forecast of production price (feed-in tariff) -#}
  "prod_price_forecast": {{
    [states('sensor.amber_5min_current_feed_in_price')|float(0)] +
    state_attr('sensor.amber_billing_interval_forecasts_feed_in_price', 'Forecasts')
    | selectattr('advanced_price_predicted','is_number')
    | map(attribute='advanced_price_predicted')
    | map('multiply',-1) 
    | list
  }},

  {#- Determine if it's within the seasonal demand tariff period (March–August or November–December) -#}
  {%- set current_month = now().month -%}
  {%- if current_month in [11, 12, 1, 2, 3, 6, 7, 8] -%}

  {#- Data Dictionary name inside if/then loop #}
  "load_cost_forecast": 
  {%- set current_time = now() -%}

  {#- Define demand tariff start and end times -#}
  {%- set demand_tariff_start = "15:00:00" -%}
  {%- set demand_tariff_end = "21:00:00" -%}

  {#- Convert start and end times to datetime objects for today -#}
  {%- set start_time = current_time.replace(hour=0, minute=0, second=0, microsecond=0) + 
    timedelta(hours=(demand_tariff_start.split(":")[0]|int), minutes=(demand_tariff_start.split(":")[1]|int)) 
  -%}
  {%- set end_time = current_time.replace(hour=0, minute=0, second=0, microsecond=0) + 
    timedelta(hours=(demand_tariff_end.split(":")[0]|int), minutes=(demand_tariff_end.split(":")[1]|int)) 
  -%}

  {#- Ensure the end time is after the start time (adjust for overnight scenarios) -#}
  {%- if end_time <= start_time -%}
    {%- set end_time = end_time + timedelta(days=1) %}
  {%- endif %}

  {#- Retrieve current and forecasted general price data -#}
  {%- set values = (
    [states('sensor.amber_5min_current_general_price')|float(0)] + 
    state_attr('sensor.amber_billing_interval_forecasts_general_price', 'Forecasts') 
    | selectattr('advanced_price_predicted','is_number')        
    | map(attribute='advanced_price_predicted')
    | list
    ) 
  -%}
  {%- set ns = namespace(x=[]) %}

    {#- Iterate through forecasted values and adjust pricing during demand tariff window
        If within demand tariff hours and price is below $1, apply a fixed cost of 1.0 
    -#}
  {%- for i in range(values|length) %}
    {%- set future_time = current_time + timedelta(minutes=i * 30) %}
    {%- if start_time <= future_time < end_time and values[i] < 1 %}
      {%- set ns.x = ns.x + [1.0] %}
    {%- else %}
      {%- set ns.x = ns.x + [values[i]] %}
    {%- endif %}
  {%- endfor %}

  {{- ns.x | tojson }},

  {%- else %}
  
    {#- Retrieve current and forecasted general price data if not in the demand tariff period -#}
  "load_cost_forecast": {{
    [states('sensor.amber_5min_current_general_price')|float(0)] + 
    state_attr('sensor.amber_billing_interval_forecasts_general_price', 'Forecasts') 
    | selectattr('advanced_price_predicted','is_number')        
    | map(attribute='advanced_price_predicted')
    | list
  }},
  {%- endif -%}
    
    {#- Forecast of consumption price (demand tariff) -#}
  "pv_power_forecast": {{
    ([states('sensor.sonnenbatterie_84324_production_w')|int(0)] +
    state_attr('sensor.solcast_pv_forecast_forecast_today', 'detailedForecast')|selectattr('period_start','gt',utcnow()) | map(attribute='pv_estimate')|map('multiply',1000)|map('int')|list +
    state_attr('sensor.solcast_pv_forecast_forecast_tomorrow', 'detailedForecast')|selectattr('period_start','gt',utcnow()) | map(attribute='pv_estimate')|map('multiply',1000)|map('int')|list
    )| tojson
  }},

    {#- Current and Forecast consumption power derived from previoud 24h of consumption -#}
  "load_power_forecast": {{
    ([states('sensor.house_power_consumption_less_deferrables')|int(0)] +
    states('input_text.fifo_buffer').split(',') | map('int') | list)
  }},

  {#- Prediction horizon (number of forecast intervals available) -#}
  "prediction_horizon": {{
     state_attr('sensor.amber_billing_interval_forecasts_feed_in_price', 'Forecasts')
    | selectattr('advanced_price_predicted','is_number') 
    | map(attribute='advanced_price_predicted')
    | list | count
  }},

    {#- Define the number of deferrable loads and their respective total hours -#}
  "num_def_loads": 2,
  "def_total_hours": [
    {%- if 'unny' in states('sensor.denistone_east_icon_descriptor_0') and states('sensor.cecil_st_general_price') | float(0) < 0.1 or states('sensor.cecil_st_general_price') | float(0) < 0.05 -%}
      {%- if is_state('sensor.season', 'winter') -%}
        {{2}}
      {%- elif is_state('sensor.season', 'summer') -%}
        {{4}}
      {%- else -%}
        {{3}}
      {%- endif -%}
    {%- else -%}
      {{0}}
    {%- endif -%},
    {%- if is_state('device_tracker.ynot_location_tracker', ['home']) -%}
      {%- if is_state('binary_sensor.ynot_charger', ['on']) -%}
        {{ ((states('number.ynot_charge_limit')|int(80)-(states('sensor.ynot_battery')|int(0)))/30*3)|round(0) }}
      {%- else -%} 
        0
      {%- endif -%}
    {%- else -%} 
      0
    {%- endif -%}
    ],

    {#- Define the nominal power of the deferrable loads -#}
  "P_deferrable_nom": [1300, {{ (states('input_number.ev_amps') | int(0) * 230)|int(0) }}],

    {#- Define the deferrable loads as semi-continuous -#}
  "treat_def_as_semi_cont": [1, 0],

    {#- Define the deferrable loads as constant -#}
  "set_def_constant": [0, 0],

    {#- Define the initial and final state of charge of the battery -#}
  "soc_init": {{ (states('sensor.sonnenbatterie_84324_state_charge_user')|int(0))/100 }},
  "soc_final": 0.08,

    {#- Define the alpha and beta values giving higher weightings to the current states to make system more reactive -#}
  "alpha": 1,
  "beta": 0,

    {#- Define the maximum charge and discharge power of the battery based on the current state of charge -#}
  {%- set battery_state = states('sensor.sonnenbatterie_84324_state_charge_user')|int(0) %}
  {%- if battery_state > 90 %}
    "battery_charge_power_max": 850,
    "battery_discharge_power_max": 3300
  {%- elif battery_state > 7 %}
    "battery_charge_power_max": 3300,
    "battery_discharge_power_max": 3300
  {%- else %}
    "battery_charge_power_max": 3300,
    "battery_discharge_power_max": 1200
  {%- endif %}
}

Haven’t had time to look into what else is required to manage 5 min billing. How does the 5 minute billing change things? What minimal changes can I get away with?