Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: Planning for heating/cooling systems #278

Closed
werdnum opened this issue May 10, 2024 · 21 comments
Closed

Feature Request: Planning for heating/cooling systems #278

werdnum opened this issue May 10, 2024 · 21 comments

Comments

@werdnum
Copy link
Contributor

werdnum commented May 10, 2024

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
Copy link
Contributor

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
Copy link
Contributor Author

werdnum commented May 10, 2024

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
Copy link
Contributor

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
Copy link
Contributor Author

werdnum commented May 10, 2024 via email

@davidusb-geek
Copy link
Owner

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:

  • Hard to find a trade-off between the number of configurable parameters and then the increased complexity to setup things when starting with EMHASS.
  • Don't forget to add some testing lines, to make this more easy to maintain and evolve in the future.

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
Copy link
Contributor Author

werdnum commented May 13, 2024

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:

def post_data(

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. #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
Copy link
Owner

... 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. #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
Copy link
Contributor

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

@borisvettorato
Copy link

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

@davidusb-geek
Copy link
Owner

Hi @werdnum, do you mind if I try to implement your thermal modeling proposition from your branch in the following EMHASS release? I'm planning on doing this very soon, so I can propose a PR to implement this using your branch changes and if you are willing to take a look and comment that PR it would be great. What do you think? A simple linear thermal model like this seems perfect for me

@werdnum
Copy link
Contributor Author

werdnum commented Jul 6, 2024 via email

@davidusb-geek
Copy link
Owner

Ok looking forward for your fixes. I'll take a look but at first glance it seems that you are not ignoring the constraint for def_total_hours[k]*self.optim_conf['P_deferrable_nom'][k] when the deferrable load has a thermal config. So this may cause un compatible unfeasible constraints. Of course converting this to penalties could be a workaround.

@werdnum
Copy link
Contributor Author

werdnum commented Jul 7, 2024

Ok looking forward for your fixes. I'll take a look but at first glance it seems that you are not ignoring the constraint for def_total_hours[k]*self.optim_conf['P_deferrable_nom'][k] when the deferrable load has a thermal config. So this may cause un compatible unfeasible constraints. Of course converting this to penalties could be a workaround.

I think I tried to make it a >= or ignore it, I forget which one I did.

I think my eventual vision would be that you have a range of options for your 'demand' side (i.e. whatever it is that tells the model not to just leave the load off all the time). A specific number of hours is one option, a constraint is another, and a third option that I was thinking about for EV charging is a benchmark price (i.e. my average cost to charge my EV is 27c/kWh, so run the charger whenever electricity is cheaper than that).

@davidusb-geek
Copy link
Owner

How are you configuring this?
In your original post you said:

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

This means that you have 3 deferrable loads and only the third one is a thermal load with those configuration options?
I'm trying to understand the concept of overshoot_temperature.
Also there are other missing paramters used in the code start_temperature, desired_temperature, etc., you are setting those up directly at runtime?

@davidusb-geek
Copy link
Owner

Also there are other missing paramters used in the code start_temperature, desired_temperature, etc., you are setting those up directly at runtime?

Ok, just answering myself for this part, I just saw in your first post that these are defined in the automation template

@werdnum
Copy link
Contributor Author

werdnum commented Jul 8, 2024 via email

@werdnum
Copy link
Contributor Author

werdnum commented Jul 8, 2024

I pushed my bugfixes to my branch - I think it was a merge conflict issue in the end.

Would still like to throw together some unit tests but haven't had the chance to really poke at this in some time (little kids, school holidays and so on)

@davidusb-geek
Copy link
Owner

Those unit tests will be welcomed when you have the time.
I have tested this and found some inconsistencies when defining the constraints.
The way that I made it work properly was as it is shown here in this banch: https://github.com/davidusb-geek/emhass/tree/davidusb-geek/dev/thermal_model

@davidusb-geek
Copy link
Owner

This model was released, the documentation is here: https://emhass.readthedocs.io/en/latest/thermal_model.html.
Feel free to test and open new issues if having problems implementing this.
I have tested and it works fine. I also added a unittest.
Closing this as completed for now

@werdnum
Copy link
Contributor Author

werdnum commented Jul 13, 2024 via email

@purcell-lab
Copy link
Contributor

Andrew, David, thank you both for this addition to EMHASS, this is providing a major functionality increase which has very broad application.

I might setup a Thermal Setup HOWTO in the Discussions so we can document our configurations and get a few use cases that can then be transferred to the documentation as configuration isn't working smoothly for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants