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
316 stars 64 forks source link

Feature Request: Time windows for deferrable loads #123

Closed sti0 closed 10 months ago

sti0 commented 1 year ago

Some deferrable loads should operate in a specific time windows. E.g. a water heater should operate in the next X hours even when this is not the cost-efficient time window. Another example will be a washing machine which should operate on a sunny day in the morning so the clothes can try in the sun in the afternoon. But if its not a sunny day it should end at least on 8pm so we can hang up the clothes before we go to sleep.

To achieve this and add some more dynamic for every single deferrable load the peak_period_hours parameter should accessable within the runtimeparams (or something similar).

davidusb-geek commented 1 year ago

This is already possible, you can pass all these parameters at runtime: https://emhass.readthedocs.io/en/latest/intro.html#passing-other-data

sti0 commented 1 year ago

But not the peak hours per deferrable load. These are not mentioned in the docs.

gieljnssns commented 1 year ago

I think the easiest way should be if we could define a different prediction_horizon per deferrable load

michaelpiron commented 10 months ago

I started working on this. I'm introducing a new parameter in the optimizer, called "def_end_timestep", defined as "The timestep before which each deferrable load should consume their energy." The optimizer will get an extra constraint stating that the deferrable load should not consume any energy after the specified timestep:

# Treat deferrable loads constraints
        for k in range(self.optim_conf['num_def_loads']):
            # Total time of deferrable load -> unchanged
            constraints.update({"constraint_defload{}_energy".format(k) :
                plp.LpConstraint(
                    e = plp.lpSum(P_deferrable[k][i]*self.timeStep for i in set_I),
                    sense = plp.LpConstraintEQ,
                    rhs = def_total_hours[k]*self.optim_conf['P_deferrable_nom'][k])
                })
            # Ensure deferrable loads consume energy before def_end_timestep
            constraints.update({"constraint_defload{}_end_timestep".format(k) :
                plp.LpConstraint(
                    e = plp.lpSum(P_deferrable[k][i]*self.timeStep for i in range(def_end_timestep[k], n)),
                    sense = plp.LpConstraintEQ,
                    rhs = 0)
                })

I have the "happy flow" scenario working, now working on edge cases/exception handling. Once mature enough, I'll make a pull request for this.

davidusb-geek commented 10 months ago

Thanks @michaelpiron Defined like this it is finally not a window, right? Another thing is to integrate this in a manner that by default this is deactivated, so no more constraint is added to the LP problem. And if the user need this feature then activate it somehow.

michaelpiron commented 10 months ago

Hi @davidusb-geek, indeed, in my proposal, the user just defines the end_timestep. The start_timestep is always step 0. But I could easily allow a start_timestep parameter to be specified as well, in a second iteration.

The way I'm writing it now, is that I allow the user to specify a def_end_timestep list in the API request of the MPC optimizer.

Example: "def_end_timestep": [3,0]
This is the case of two deferrable loads, whereby the first load should operate in the first three timesteps of the optimization window. The second load can use the complete optimization window (cf the value of 0).

In code:

# Treat deferrable loads constraints
        for k in range(self.optim_conf['num_def_loads']):
            # Total time of deferrable load
            constraints.update({"constraint_defload{}_energy".format(k) :
                plp.LpConstraint(
                    e = plp.lpSum(P_deferrable[k][i]*self.timeStep for i in set_I),
                    sense = plp.LpConstraintEQ,
                    rhs = def_total_hours[k]*self.optim_conf['P_deferrable_nom'][k])
                })
            # Ensure deferrable loads consume energy before def_end_timestep
            if def_end_timestep[k] > 0:
                # If the available timeframe (between now and def_end_timestep) is < number of timesteps to meet the hours to operate (def_total_hours), enlarge the timeframe.
                if def_end_timestep[k] < def_total_hours[k]/self.timeStep:
                    def_end_timestep[k] = ceil(def_total_hours[k]/self.timeStep)
                    self.logger.warning("Available timeframe for deferrable load %s is shorter than the specified number of hours to operate. Enlarging timeframe to def_total_hours.", k)

                constraints.update({"constraint_defload{}_end_timestep".format(k) :
                    plp.LpConstraint(
                        e = plp.lpSum(P_deferrable[k][i]*self.timeStep for i in range(def_end_timestep[k], n)),
                        sense = plp.LpConstraintEQ,
                        rhs = 0)
                    })

I also started to foresee a corresponding field in the add-on config UI, similar to the number of operating hours: image

Happy to receive your feedback.

michaelpiron commented 10 months ago

Submitted PR #153

davidusb-geek commented 10 months ago

Comments on #153

michaelpiron commented 10 months ago

Continued working on the PR, adding as well the start_timesteps. As such it is possible to define time windows for deferrable loads.

michaelpiron commented 10 months ago

PR #153 just got merged into main branch. This feature request can therefore be closed