davidusb-geek / emhass

emhass: Energy Management for Home Assistant, is a Python module designed to optimize your home energy interfacing with Home Assistant.
MIT License
260 stars 51 forks source link

Feature Request: Planning for heating/cooling systems #278

Open werdnum opened 1 month ago

werdnum commented 1 month ago

Heating/cooling systems have different dynamics, as 'integrating processes' (by analogy to the PID model concept) where you can pre-heat or pre-cool to ride out periods of higher demand.

It would be neat to have some way of doing this with EMHASS.

I have something like this implemented and working in a branch, which I want to clean up before sending a ~CL~ PR.

purcell-lab commented 1 month ago

Great news and quite a bit of interest, can you describe how your model is working and what parameters are required to be set.

I can see immediate application with my space heating and cooling (HVAC) systems. But also future application with my domestic and pool hot water heat pump systems.

werdnum commented 1 month ago

I am adding a new constraint to the linear model.

The temperature at time T is modelled as (temperature at time T-1) - (cooling_constant (indoor_temp - outdoor_temp)) + (heating_is_on heating_constant).

You pass in the start temperature, the desired temperature per timestep, and the forecast outdoor temperature per timestep.

I did some number crunching in my own home and determined values of 0.1 degrees per hour per degree for the cooling constant and 5.5 degrees per hour for the heating constant. That is, if I leave the heating off, the house cools down by 0.1 degrees per hour for every degree that the house is cooler than the ambient temperature outside. Similarly, if the heating is on, the house warms up by approximately one degree per hour.

Feel free to take a look at my branch if you want to find out more.

My configuration is as follows:

  def_load_config:
    - {}
    - {}
    - thermal_config:
        heating_rate: 5.0
        cooling_constant: 0.1
        overshoot_temperature: 24.0

And in Home Assistant:

rest_command:
  emhass_forecast:
    url: http://emhass.homeassistant.svc.cluster.local:5000/action/naive-mpc-optim
    method: post
    timeout: 300
    payload: |
      {% macro time_to_timestep(time) -%}
        {{ (((today_at(time) - now()) / timedelta(minutes=30)) | round(0, 'ceiling')) % 48 }}
      {%- endmacro -%}
      {%- set horizon = (state_attr('sensor.electricity_price_forecast', 'forecasts')|length) -%}
      {%- set heated_intervals = [[time_to_timestep("06:30")|int, time_to_timestep("07:30")|int], [time_to_timestep("17:30")|int, time_to_timestep("23:00")|int]] -%}
      {
        "prediction_horizon": {{ horizon }},
        "def_total_hours": [
          {{ max(0, float(states("input_number.pool_pump_required_run_time")) - float(states("sensor.pool_pump_run_time"))) | float }},
          {{ max(0, (states("input_number.car_target_soc")|int) - (states("sensor.car_battery_soc")|int)) * 64 / 100.0 / 7 | float }},
          0
        ],
        "def_end_timestep": [
          {{ time_to_timestep("23:59") }},
          {{ min(time_to_timestep("07:30"), time_to_timestep("16:30") ) }},
          0
        ],
        "load_cost_forecast":
          {{
          (
            (
              [states('sensor.general_price')|float(0)]
                + state_attr('sensor.electricity_price_forecast', 'forecasts')
              |map(attribute='per_kwh')
              |list
            )[:horizon]
          )
          }},
        "prod_price_forecast":
          {{
          (
            (
              [state_attr('sensor.general_price', 'spot_per_kwh')|float(0)]
                + state_attr('sensor.electricity_price_forecast', 'forecasts')
              |map(attribute='spot_per_kwh')
              |list
            )[:horizon]
          )
          }},
          "heater_start_temperatures": [0, 0, {{state_attr("climate.living", "current_temperature")}}],
          "heater_desired_temperatures": [[], [], [{% set comma = joiner(", ") %}
            {%- for i in range(horizon) -%}
              {%- set timestep = i -%}
              {{comma()}}
              {% for interval in heated_intervals if timestep >= interval[0] and timestep <= interval[1] -%}
                21
              {%- else -%}
                0
              {%- endfor -%}
            {% endfor %}]],
          "pv_power_forecast": [
          {% set comma = joiner(", ") -%}
          {%- for _ in range(horizon) %}{{comma()}}0{% endfor %}
          ],
          "outdoor_temperature_forecast": [
          {%- set comma = joiner(", ") -%}
          {%- for fc in weather_forecasts['weather.openweathermap'].forecast if (fc.datetime|as_datetime) > now() and (fc.datetime|as_datetime) - now() < timedelta(hours=24) -%}
            {%- if loop.index0 * 2 < horizon -%}
              {{comma()}}{{fc.temperature}}
              {%- if loop.index0 * 2 + 1 < horizon -%}
                {{comma()}}{{fc.temperature}}
              {%- endif -%}
            {%- endif -%}
          {%- endfor %}]
      }

.. but obviously this has a lot that's specific to my setup.

purcell-lab commented 1 month ago

Nice,

I have done some similar calculations for my household and have a cooling factor of 1500 W/ deg C, but I should be able to convert to your schema. I also had a template sensor to calculate the expected Power requirements profile per timestep for my HVAC, but getting it included as a constraint was well beyond my abilities, which you seem to have resolved. https://community.home-assistant.io/t/running-devices-when-energy-is-cheaper-and-greener/380011/26?u=markpurcell

412518624_10161048683055281_976229614539365053_n

I have also been adjusting p_nom depending on the desired set point and total_hours based on the number of forecast temps above the setpoint.

A couple of questions:

Does your HVAC automation then change the setpoint to ramp up and ramp down, or are you controlling power consumption in a different fashion?

Is your weather_forecasts['weather.openweathermap'].forecast a local macro, I still haven't made the shift since they removed forecasts as attributes from the weather entities?

werdnum commented 1 month ago

On Fri, 10 May 2024 at 15:07, Mark Purcell @.***> wrote:

Nice,

I have done some similar calculations for my household and have a cooling factor of 1500 W/ deg C, but I should be able to convert to your schema. I also had a template sensor to calculate the expected Power requirements profile per timestep for my HVAC, but getting it included as a constraint was well beyond my abilities, which you seem to have resolved. https://community.home-assistant.io/t/running-devices-when-energy-is-cheaper-and-greener/380011/26?u=markpurcell

412518624_10161048683055281_976229614539365053_n.jpg (view on web) https://github.com/davidusb-geek/emhass/assets/79175134/4538e424-e7ab-4c3b-88b4-782a89aaedc9

I have also been adjusting p_nom depending on the desired set point and total_hours based on the number of forecast temps above the setpoint.

I see, I’ve just been ignoring total_hours and using “must have predicted temperature > X” as the constraint.

A couple of questions:

Does your HVAC automation then change the setpoint to ramp up and ramp down, or are you controlling power consumption in a different fashion?

Nothing so complex. I’m just turning the air conditioner on with a set point of 24 when the plan calls for heating, and turning it off when the planned use is 0 and I’m not home or upstairs.

This has so far been good enough for my use case of pre heating the house ahead of any price spikes.

Is your weather_forecasts['weather.openweathermap'].forecast a local macro, I still haven't made the shift since they removed forecasts as attributes from the weather entities?

This is a pain, I ended up including it in the overall automation

https://gist.github.com/werdnum/8d15a5dbcdf5c6e535bdcffb9888a7b5

davidusb-geek commented 1 month ago

Hi. Nice feature again! I always thought that a simple linear model would be more that enough for our use cases, but never took the time to actually implement this. Please go ahead with the PR and let's add this. Same comments as in #261:

Another side comment, I saw in your branch that you add the possibility to pass the outdoor temperature, this is good and a needed option, but there is also the option to use the readily available outdoor temperature when using the scrapper weather forecast method, the outdoor temperature is there in the DataFrame as temp_air column name.

werdnum commented 1 month ago

Thanks, @davidusb-geek.

One thing I haven't figured out how to implement yet is that I'd like to export the temperature forecast to HA as well - this would help setting the setpoint in automations, to keep the heating/cooling usage in sync with what EMHASS is planning.

I notice that post_data only wants the single data series (planned electricity usage), but I'd like to pass along the predicted temperature based on the plan as an attribute: https://github.com/davidusb-geek/emhass/blob/6abb8a13ee8bc9a38eb8e4a4523cfadd8031c2c8/src/emhass/retrieve_hass.py#L321

Wondering if your preference is to pass them along in the same dataframe, or as a separate parameter.

Relatedly, I am having a bit of trouble making heads or tails of this code

I'm wondering if this would be a simpler way of writing that method:

    @staticmethod
    def get_attr_data_dict(
        data_df: pd.DataFrame,
        idx: int,
        entity_id: str,
        unit_of_measurement: str,
        friendly_name: str,
        list_name: str,
        state: float,
    ) -> dict:
        list_df = data_df.copy().loc[data_df.index[idx] :]
        forecast_list = []
        for ts, row in list_df.itertuples():
            datum = {}
            datum["date"] = ts.isoformat()
            datum[entity_id.split("sensor.")[1]] = np.round(row[entity_id], 2)
            forecast_list.append(datum)
        data = {
            "state": "{:.2f}".format(state),
            "attributes": {
                "unit_of_measurement": unit_of_measurement,
                "friendly_name": friendly_name,
                list_name: forecast_list,
            },
        }
        return data

... if I do it that way, then it becomes much more straightforward to pass along that 'ancillary' data like predicted temperature.

Hard to find a trade-off between the number of configurable parameters and then the increased complexity to setup things when starting with EMHASS.

It's your call of course, but my idea here was to move the configuration format away from a bunch of parallel lists, which can be hard to manage, to a single list of dicts (in fact, you can see this in my overall design, because it was becoming unmanageable to put in so many parallel lists for all the different thermal configuration parameters).

Backwards compatibility is definitely a concern, but you could add in some logic to 'copy' the old-style parallel lists into the list of dicts so people don't have things break underneath them.

Overall my problem in terms of ease of use is that the example configuration has so many different mandatory fields that it's hard to keep track of everything (c.f. https://github.com/davidusb-geek/emhass/issues/262).

I don't want to inundate you with ideas/suggestions/questions/requests, but EMHASS is the first truly 'magical' home automation I've put together and I'd be excited to work with you on some of the ideas and pain points I've encountered.

davidusb-geek commented 1 month ago

... if I do it that way, then it becomes much more straightforward to pass along that 'ancillary' data like predicted temperature.

There are certainly better ways of implementing this. Your solution might be an option. If its more efficient, useful for what you want to achieve and even more readable then go ahead and update that. No problem for me. Just be careful to modify accordingly where this is used, most notable in command_line.py. And just test your modification using the unit testing platform.

Overall my problem in terms of ease of use is that the example configuration has so many different mandatory fields that it's hard to keep track of everything (c.f. https://github.com/davidusb-geek/emhass/issues/262).

We recently decided to go ahead with a single configuration file in the form of the json file obtained when using the add-on: the options.json. Those parallel list were inherited from the initial choice of using the yaml format.

I don't want to inundate you with ideas/suggestions/questions/requests, but EMHASS is the first truly 'magical' home automation I've put together and I'd be excited to work with you on some of the ideas and pain points I've encountered.

Like the suggestion about simplifying the configuration I am always open to new ideas to improve all this, make it more efficient, easy to configure and setup and add new features. All contributions are welcomed, given a prior discussion like we had here for heating/cooling systems.

gieljnssns commented 4 weeks ago

Can this be helpful to find the thermal model for your house? https://github.com/czagoni/darkgreybox

borisvettorato commented 3 weeks ago

oke, I would like to help to build the thermal model, I have a bit of experience with machine learning and python, but have no clue how to implement this in a program like emhass. This is the model I have so far with a mae of .11 over 72 hours and a mape of 0.005 for my own house. image