emhass 0.13.2__tar.gz → 0.13.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {emhass-0.13.2 → emhass-0.13.4}/PKG-INFO +47 -23
- {emhass-0.13.2 → emhass-0.13.4}/README.md +45 -22
- {emhass-0.13.2 → emhass-0.13.4}/pyproject.toml +2 -1
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/command_line.py +6 -2
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/data/associations.csv +6 -1
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/data/config_defaults.json +5 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/forecast.py +52 -12
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/machine_learning_forecaster.py +0 -2
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/optimization.py +262 -126
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/retrieve_hass.py +11 -9
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/static/data/param_definitions.json +39 -2
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/utils.py +61 -21
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/web_server.py +10 -28
- {emhass-0.13.2 → emhass-0.13.4}/.gitignore +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/LICENSE +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/__init__.py +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/data/cec_inverters.pbz2 +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/data/cec_modules.pbz2 +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/img/emhass_icon.png +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/machine_learning_regressor.py +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/static/advanced.html +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/static/basic.html +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/static/configuration_list.html +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/static/configuration_script.js +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/static/img/emhass_icon.png +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/static/img/emhass_logo_short.svg +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/static/img/feather-sprite.svg +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/static/script.js +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/static/style.css +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/templates/configuration.html +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/templates/index.html +0 -0
- {emhass-0.13.2 → emhass-0.13.4}/src/emhass/templates/template.html +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: emhass
|
3
|
-
Version: 0.13.
|
3
|
+
Version: 0.13.4
|
4
4
|
Summary: An Energy Management System for Home Assistant
|
5
5
|
Project-URL: Homepage, https://github.com/davidusb-geek/emhass
|
6
6
|
Project-URL: Source, https://github.com/davidusb-geek/emhass
|
@@ -24,6 +24,7 @@ Requires-Python: <3.13,>=3.10
|
|
24
24
|
Requires-Dist: flask>=3.1.0
|
25
25
|
Requires-Dist: gunicorn>=23.0.0
|
26
26
|
Requires-Dist: h5py>=3.12.1
|
27
|
+
Requires-Dist: highspy>=1.10.0
|
27
28
|
Requires-Dist: numpy<2.3.0,>=2.0.0
|
28
29
|
Requires-Dist: pandas>=2.2.0
|
29
30
|
Requires-Dist: plotly>=6.0.0
|
@@ -117,17 +118,10 @@ Description-Content-Type: text/markdown
|
|
117
118
|
</div>
|
118
119
|
|
119
120
|
<br>
|
120
|
-
<p align="
|
121
|
-
|
122
|
-
</p>
|
123
|
-
<p align="center">
|
124
|
-
<a href="https://www.buymeacoffee.com/davidusbgeek" target="_blank">
|
125
|
-
<img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" >
|
126
|
-
</a>
|
121
|
+
<p align="left">
|
122
|
+
EMHASS is a Python module designed to optimize your home energy interfacing with Home Assistant.
|
127
123
|
</p>
|
128
124
|
|
129
|
-
EHMASS is a Python module designed to optimize your home energy interfacing with Home Assistant.
|
130
|
-
|
131
125
|
## Introduction
|
132
126
|
|
133
127
|
EMHASS (Energy Management for Home Assistant) is an optimization tool designed for residential households. The package uses a Linear Programming approach to optimize energy usage while considering factors such as electricity prices, power generation from solar panels, and energy storage from batteries. EMHASS provides a high degree of configurability, making it easy to integrate with Home Assistant and other smart home systems. Whether you have solar panels, energy storage, or just a controllable load, EMHASS can provide an optimized daily schedule for your devices, allowing you to save money and minimize your environmental impact.
|
@@ -196,12 +190,13 @@ _Note: Both EMHASS via Docker and EMHASS-Add-on contain the same Docker image. T
|
|
196
190
|
|
197
191
|
### Method 2) Running EMHASS in Docker
|
198
192
|
|
199
|
-
You can also install EMHASS using Docker as a container. This can be in the same machine as Home Assistant (if your running Home Assistant as a Docker container) or in a different distant machine.
|
193
|
+
You can also install EMHASS using Docker as a container. This can be in the same machine as Home Assistant (if your running Home Assistant as a Docker container) or in a different distant machine. The "share" folder is where EMHASS stores the config.json file. In the examples below adjust the "-v" volume mappings to reflect where your path to the local host directory needs to be mapped to.
|
194
|
+
To install first pull the latest image:
|
200
195
|
```bash
|
201
196
|
# pull Docker image
|
202
197
|
docker pull ghcr.io/davidusb-geek/emhass:latest
|
203
|
-
# run Docker image, mounting config.json and secrets_emhass.yaml from host
|
204
|
-
docker run --rm -it --restart always -p 5000:5000 --name emhass-container -v
|
198
|
+
# run Docker image, mounting the dir storing config.json and secrets_emhass.yaml from host
|
199
|
+
docker run --rm -it --restart always -p 5000:5000 --name emhass-container -v /emhass/share:/share/ -v /emhass/secrets_emhass.yaml:/app/secrets_emhass.yaml ghcr.io/davidusb-geek/emhass:latest
|
205
200
|
```
|
206
201
|
*Note it is not recommended to install the latest EMHASS image with `:latest` *(as you would likely want to control when you update EMHASS version)*. Instead, find the [latest version tag](https://github.com/davidusb-geek/emhass/pkgs/container/emhass) (E.g: `v0.2.1`) and replace `latest`*
|
207
202
|
|
@@ -215,7 +210,7 @@ cd emhass
|
|
215
210
|
# may need to set architecture tag (docker build --build-arg TARGETARCH=amd64 -t emhass-local .)
|
216
211
|
docker build -t emhass-local .
|
217
212
|
# run built Docker image, mounting config.json and secrets_emhass.yaml from host
|
218
|
-
docker run --rm -it -p 5000:5000 --name emhass-container -v
|
213
|
+
docker run --rm -it -p 5000:5000 --name emhass-container -v /emhass/share:/share -v /emhass/secrets_emhass.yaml:/app/secrets_emhass.yaml emhass-local
|
219
214
|
```
|
220
215
|
|
221
216
|
Before running the docker container, make sure you have a designated folder for emhass on your host device and a `secrets_emhass.yaml` file. You can get a example of the secrets file from [`secrets_emhass(example).yaml`](https://github.com/davidusb-geek/emhass/blob/master/secrets_emhass(example).yaml) file on this repository.
|
@@ -231,23 +226,23 @@ Latitude: 45.83
|
|
231
226
|
Longitude: 6.86
|
232
227
|
Altitude: 4807.8
|
233
228
|
EOT
|
234
|
-
docker run --rm -it --restart always -p 5000:5000 --name emhass-container -v
|
229
|
+
docker run --rm -it --restart always -p 5000:5000 --name emhass-container -v /emhass/share:/share -v /emhass/secrets_emhass.yaml:/app/secrets_emhass.yaml ghcr.io/davidusb-geek/emhass:latest
|
235
230
|
```
|
236
231
|
|
237
232
|
#### Docker, things to note
|
238
233
|
|
239
|
-
- You can create a `config.json` file prior to running emhass. *(obtain a example from: [config_defaults.json](https://github.com/davidusb-geek/emhass/blob/enhass-standalone-addon-merge/src/emhass/data/config_defaults.json)* Alteratively, you can insert your parameters into the configuration page on the EMHASS web server. (for EMHASS to auto create a config.json) With either option, the volume mount `-v
|
234
|
+
- You can create a `config.json` file prior to running emhass. *(obtain a example from: [config_defaults.json](https://github.com/davidusb-geek/emhass/blob/enhass-standalone-addon-merge/src/emhass/data/config_defaults.json)* Alteratively, you can insert your parameters into the configuration page on the EMHASS web server. (for EMHASS to auto create a config.json) With either option, the volume mount `-v /emhass/share:/share` should be applied to make sure your config is stored on the host device. (to be not deleted when the EMHASS container gets removed/image updated)*
|
240
235
|
|
241
236
|
- If you wish to keep a local, semi-persistent copy of the EMHASS-generated data, create a local folder on your device, then mount said folder inside the container.
|
242
237
|
```bash
|
243
238
|
#create data folder
|
244
239
|
mkdir -p ~/emhass/data
|
245
|
-
docker run -it --restart always -p 5000:5000 -e LOCAL_COSTFUN="profit" -v
|
240
|
+
docker run -it --restart always -p 5000:5000 -e LOCAL_COSTFUN="profit" -v /emhass/share:/share -v /emhass/data:/data -v /emhass/secrets_emhass.yaml:/app/secrets_emhass.yaml --name DockerEMHASS <REPOSITORY:TAG>
|
246
241
|
```
|
247
242
|
|
248
243
|
- If you wish to set the web_server's homepage optimization diagrams to a timezone other than UTC, set `TZ` environment variable on docker run:
|
249
244
|
```bash
|
250
|
-
docker run -it --restart always -p 5000:5000 -e TZ="Europe/Paris" -v
|
245
|
+
docker run -it --restart always -p 5000:5000 -e TZ="Europe/Paris" -v /emhass/share:/share -v /emhass/secrets_emhass.yaml:/app/secrets_emhass.yaml --name DockerEMHASS <REPOSITORY:TAG>
|
251
246
|
```
|
252
247
|
### Method 3) Legacy method using a Python virtual environment *(Legacy CLI)*
|
253
248
|
If you wish to run EMHASS optimizations with cli commands. *(no persistent web server session)* you can run EMHASS via the python package alone *(not wrapped in a Docker container)*.
|
@@ -319,12 +314,22 @@ Additional optimization strategies were developed later, that can be used in com
|
|
319
314
|
|
320
315
|
### Dayahead Optimization - Method 1) Add-on and docker standalone
|
321
316
|
|
322
|
-
|
317
|
+
We can use the `shell_command` integration in `configuration.yaml`:
|
323
318
|
```yaml
|
324
319
|
shell_command:
|
325
320
|
dayahead_optim: "curl -i -H \"Content-Type:application/json\" -X POST -d '{}' http://localhost:5000/action/dayahead-optim"
|
326
321
|
publish_data: "curl -i -H \"Content-Type:application/json\" -X POST -d '{}' http://localhost:5000/action/publish-data"
|
327
322
|
```
|
323
|
+
An alternative that will be useful when passing data at runtime (see dedicated section), we can use the the `rest_command` instead:
|
324
|
+
```yaml
|
325
|
+
rest_command:
|
326
|
+
url: http://127.0.0.1:5000/action/dayahead-optim
|
327
|
+
method: POST
|
328
|
+
headers:
|
329
|
+
content-type: application/json
|
330
|
+
payload: >-
|
331
|
+
{}
|
332
|
+
```
|
328
333
|
### Dayahead Optimization - Method 2) Legacy method using a Python virtual environment
|
329
334
|
|
330
335
|
In `configuration.yaml`:
|
@@ -387,8 +392,8 @@ In `automations.yaml`:
|
|
387
392
|
```
|
388
393
|
in configuration page/`config.json`
|
389
394
|
```json
|
390
|
-
|
391
|
-
|
395
|
+
"method_ts_round": "first"
|
396
|
+
"continual_publish": true
|
392
397
|
```
|
393
398
|
In this automation, the day-ahead optimization is performed once a day, every day at 5:30am.
|
394
399
|
If the `optimization_time_step` parameter is set to `30` *(default)* in the configuration, the results of the day-ahead optimization will generate 48 values *(for each entity)*, a value for every 30 minutes in a day *(i.e. 24 hrs x 2)*.
|
@@ -542,7 +547,7 @@ For users who wish to have full control of exactly when they would like to run a
|
|
542
547
|
|
543
548
|
in configuration page/`config.json` :
|
544
549
|
```json
|
545
|
-
|
550
|
+
"continual_publish": false
|
546
551
|
```
|
547
552
|
POST action :
|
548
553
|
```bash
|
@@ -666,6 +671,25 @@ curl -i -H 'Content-Type:application/json' -X POST -d '{"pv_power_forecast":[0,
|
|
666
671
|
curl -i -H 'Content-Type:application/json' -X POST -d '{"pv_power_forecast":[0, 70, 141.22, 246.18, 513.5, 753.27, 1049.89, 1797.93, 1697.3, 3078.93], "prediction_horizon":10, "soc_init":0.5,"soc_final":0.6,"operating_hours_of_each_deferrable_load":[1,3],"start_timesteps_of_each_deferrable_load":[0,3],"end_timesteps_of_each_deferrable_load":[0,6]}' http://localhost:5000/action/naive-mpc-optim
|
667
672
|
```
|
668
673
|
|
674
|
+
For a more readable option we can use the `rest_command` integration:
|
675
|
+
```yaml
|
676
|
+
rest_command:
|
677
|
+
url: http://127.0.0.1:5000/action/dayahead-optim
|
678
|
+
method: POST
|
679
|
+
headers:
|
680
|
+
content-type: application/json
|
681
|
+
payload: >-
|
682
|
+
{
|
683
|
+
"pv_power_forecast": [0, 70, 141.22, 246.18, 513.5, 753.27, 1049.89, 1797.93, 1697.3, 3078.93],
|
684
|
+
"prediction_horizon":10,
|
685
|
+
"soc_init":0.5,
|
686
|
+
"soc_final":0.6,
|
687
|
+
"operating_hours_of_each_deferrable_load":[1,3],
|
688
|
+
"start_timesteps_of_each_deferrable_load":[0,3],
|
689
|
+
"end_timesteps_of_each_deferrable_load":[0,6]
|
690
|
+
}
|
691
|
+
```
|
692
|
+
|
669
693
|
## A machine learning forecaster
|
670
694
|
|
671
695
|
Starting in v0.4.0 a new machine learning forecaster class was introduced.
|
@@ -680,7 +704,7 @@ Pull requests are very much accepted on this project. For development, you can f
|
|
680
704
|
|
681
705
|
Some problems may arise from solver-related issues in the Pulp package. It was found that for arm64 architectures (ie. Raspberry Pi4, 64 bits) the default solver is not available. A workaround is to use another solver. The `glpk` solver is an option.
|
682
706
|
|
683
|
-
This can be controlled in the configuration file with parameters `lp_solver` and `lp_solver_path`. The options for `lp_solver` are: 'PULP_CBC_CMD', 'GLPK_CMD' and 'COIN_CMD'. If using 'COIN_CMD' as the solver you will need to provide the correct path to this solver in parameter `lp_solver_path`, ex: '/usr/bin/cbc'.
|
707
|
+
This can be controlled in the configuration file with parameters `lp_solver` and `lp_solver_path`. The options for `lp_solver` are: 'PULP_CBC_CMD', 'GLPK_CMD', 'HiGHS', and 'COIN_CMD'. If using 'COIN_CMD' as the solver you will need to provide the correct path to this solver in parameter `lp_solver_path`, ex: '/usr/bin/cbc'.
|
684
708
|
|
685
709
|
|
686
710
|
## License
|
@@ -62,17 +62,10 @@
|
|
62
62
|
</div>
|
63
63
|
|
64
64
|
<br>
|
65
|
-
<p align="
|
66
|
-
|
67
|
-
</p>
|
68
|
-
<p align="center">
|
69
|
-
<a href="https://www.buymeacoffee.com/davidusbgeek" target="_blank">
|
70
|
-
<img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" >
|
71
|
-
</a>
|
65
|
+
<p align="left">
|
66
|
+
EMHASS is a Python module designed to optimize your home energy interfacing with Home Assistant.
|
72
67
|
</p>
|
73
68
|
|
74
|
-
EHMASS is a Python module designed to optimize your home energy interfacing with Home Assistant.
|
75
|
-
|
76
69
|
## Introduction
|
77
70
|
|
78
71
|
EMHASS (Energy Management for Home Assistant) is an optimization tool designed for residential households. The package uses a Linear Programming approach to optimize energy usage while considering factors such as electricity prices, power generation from solar panels, and energy storage from batteries. EMHASS provides a high degree of configurability, making it easy to integrate with Home Assistant and other smart home systems. Whether you have solar panels, energy storage, or just a controllable load, EMHASS can provide an optimized daily schedule for your devices, allowing you to save money and minimize your environmental impact.
|
@@ -141,12 +134,13 @@ _Note: Both EMHASS via Docker and EMHASS-Add-on contain the same Docker image. T
|
|
141
134
|
|
142
135
|
### Method 2) Running EMHASS in Docker
|
143
136
|
|
144
|
-
You can also install EMHASS using Docker as a container. This can be in the same machine as Home Assistant (if your running Home Assistant as a Docker container) or in a different distant machine.
|
137
|
+
You can also install EMHASS using Docker as a container. This can be in the same machine as Home Assistant (if your running Home Assistant as a Docker container) or in a different distant machine. The "share" folder is where EMHASS stores the config.json file. In the examples below adjust the "-v" volume mappings to reflect where your path to the local host directory needs to be mapped to.
|
138
|
+
To install first pull the latest image:
|
145
139
|
```bash
|
146
140
|
# pull Docker image
|
147
141
|
docker pull ghcr.io/davidusb-geek/emhass:latest
|
148
|
-
# run Docker image, mounting config.json and secrets_emhass.yaml from host
|
149
|
-
docker run --rm -it --restart always -p 5000:5000 --name emhass-container -v
|
142
|
+
# run Docker image, mounting the dir storing config.json and secrets_emhass.yaml from host
|
143
|
+
docker run --rm -it --restart always -p 5000:5000 --name emhass-container -v /emhass/share:/share/ -v /emhass/secrets_emhass.yaml:/app/secrets_emhass.yaml ghcr.io/davidusb-geek/emhass:latest
|
150
144
|
```
|
151
145
|
*Note it is not recommended to install the latest EMHASS image with `:latest` *(as you would likely want to control when you update EMHASS version)*. Instead, find the [latest version tag](https://github.com/davidusb-geek/emhass/pkgs/container/emhass) (E.g: `v0.2.1`) and replace `latest`*
|
152
146
|
|
@@ -160,7 +154,7 @@ cd emhass
|
|
160
154
|
# may need to set architecture tag (docker build --build-arg TARGETARCH=amd64 -t emhass-local .)
|
161
155
|
docker build -t emhass-local .
|
162
156
|
# run built Docker image, mounting config.json and secrets_emhass.yaml from host
|
163
|
-
docker run --rm -it -p 5000:5000 --name emhass-container -v
|
157
|
+
docker run --rm -it -p 5000:5000 --name emhass-container -v /emhass/share:/share -v /emhass/secrets_emhass.yaml:/app/secrets_emhass.yaml emhass-local
|
164
158
|
```
|
165
159
|
|
166
160
|
Before running the docker container, make sure you have a designated folder for emhass on your host device and a `secrets_emhass.yaml` file. You can get a example of the secrets file from [`secrets_emhass(example).yaml`](https://github.com/davidusb-geek/emhass/blob/master/secrets_emhass(example).yaml) file on this repository.
|
@@ -176,23 +170,23 @@ Latitude: 45.83
|
|
176
170
|
Longitude: 6.86
|
177
171
|
Altitude: 4807.8
|
178
172
|
EOT
|
179
|
-
docker run --rm -it --restart always -p 5000:5000 --name emhass-container -v
|
173
|
+
docker run --rm -it --restart always -p 5000:5000 --name emhass-container -v /emhass/share:/share -v /emhass/secrets_emhass.yaml:/app/secrets_emhass.yaml ghcr.io/davidusb-geek/emhass:latest
|
180
174
|
```
|
181
175
|
|
182
176
|
#### Docker, things to note
|
183
177
|
|
184
|
-
- You can create a `config.json` file prior to running emhass. *(obtain a example from: [config_defaults.json](https://github.com/davidusb-geek/emhass/blob/enhass-standalone-addon-merge/src/emhass/data/config_defaults.json)* Alteratively, you can insert your parameters into the configuration page on the EMHASS web server. (for EMHASS to auto create a config.json) With either option, the volume mount `-v
|
178
|
+
- You can create a `config.json` file prior to running emhass. *(obtain a example from: [config_defaults.json](https://github.com/davidusb-geek/emhass/blob/enhass-standalone-addon-merge/src/emhass/data/config_defaults.json)* Alteratively, you can insert your parameters into the configuration page on the EMHASS web server. (for EMHASS to auto create a config.json) With either option, the volume mount `-v /emhass/share:/share` should be applied to make sure your config is stored on the host device. (to be not deleted when the EMHASS container gets removed/image updated)*
|
185
179
|
|
186
180
|
- If you wish to keep a local, semi-persistent copy of the EMHASS-generated data, create a local folder on your device, then mount said folder inside the container.
|
187
181
|
```bash
|
188
182
|
#create data folder
|
189
183
|
mkdir -p ~/emhass/data
|
190
|
-
docker run -it --restart always -p 5000:5000 -e LOCAL_COSTFUN="profit" -v
|
184
|
+
docker run -it --restart always -p 5000:5000 -e LOCAL_COSTFUN="profit" -v /emhass/share:/share -v /emhass/data:/data -v /emhass/secrets_emhass.yaml:/app/secrets_emhass.yaml --name DockerEMHASS <REPOSITORY:TAG>
|
191
185
|
```
|
192
186
|
|
193
187
|
- If you wish to set the web_server's homepage optimization diagrams to a timezone other than UTC, set `TZ` environment variable on docker run:
|
194
188
|
```bash
|
195
|
-
docker run -it --restart always -p 5000:5000 -e TZ="Europe/Paris" -v
|
189
|
+
docker run -it --restart always -p 5000:5000 -e TZ="Europe/Paris" -v /emhass/share:/share -v /emhass/secrets_emhass.yaml:/app/secrets_emhass.yaml --name DockerEMHASS <REPOSITORY:TAG>
|
196
190
|
```
|
197
191
|
### Method 3) Legacy method using a Python virtual environment *(Legacy CLI)*
|
198
192
|
If you wish to run EMHASS optimizations with cli commands. *(no persistent web server session)* you can run EMHASS via the python package alone *(not wrapped in a Docker container)*.
|
@@ -264,12 +258,22 @@ Additional optimization strategies were developed later, that can be used in com
|
|
264
258
|
|
265
259
|
### Dayahead Optimization - Method 1) Add-on and docker standalone
|
266
260
|
|
267
|
-
|
261
|
+
We can use the `shell_command` integration in `configuration.yaml`:
|
268
262
|
```yaml
|
269
263
|
shell_command:
|
270
264
|
dayahead_optim: "curl -i -H \"Content-Type:application/json\" -X POST -d '{}' http://localhost:5000/action/dayahead-optim"
|
271
265
|
publish_data: "curl -i -H \"Content-Type:application/json\" -X POST -d '{}' http://localhost:5000/action/publish-data"
|
272
266
|
```
|
267
|
+
An alternative that will be useful when passing data at runtime (see dedicated section), we can use the the `rest_command` instead:
|
268
|
+
```yaml
|
269
|
+
rest_command:
|
270
|
+
url: http://127.0.0.1:5000/action/dayahead-optim
|
271
|
+
method: POST
|
272
|
+
headers:
|
273
|
+
content-type: application/json
|
274
|
+
payload: >-
|
275
|
+
{}
|
276
|
+
```
|
273
277
|
### Dayahead Optimization - Method 2) Legacy method using a Python virtual environment
|
274
278
|
|
275
279
|
In `configuration.yaml`:
|
@@ -332,8 +336,8 @@ In `automations.yaml`:
|
|
332
336
|
```
|
333
337
|
in configuration page/`config.json`
|
334
338
|
```json
|
335
|
-
|
336
|
-
|
339
|
+
"method_ts_round": "first"
|
340
|
+
"continual_publish": true
|
337
341
|
```
|
338
342
|
In this automation, the day-ahead optimization is performed once a day, every day at 5:30am.
|
339
343
|
If the `optimization_time_step` parameter is set to `30` *(default)* in the configuration, the results of the day-ahead optimization will generate 48 values *(for each entity)*, a value for every 30 minutes in a day *(i.e. 24 hrs x 2)*.
|
@@ -487,7 +491,7 @@ For users who wish to have full control of exactly when they would like to run a
|
|
487
491
|
|
488
492
|
in configuration page/`config.json` :
|
489
493
|
```json
|
490
|
-
|
494
|
+
"continual_publish": false
|
491
495
|
```
|
492
496
|
POST action :
|
493
497
|
```bash
|
@@ -611,6 +615,25 @@ curl -i -H 'Content-Type:application/json' -X POST -d '{"pv_power_forecast":[0,
|
|
611
615
|
curl -i -H 'Content-Type:application/json' -X POST -d '{"pv_power_forecast":[0, 70, 141.22, 246.18, 513.5, 753.27, 1049.89, 1797.93, 1697.3, 3078.93], "prediction_horizon":10, "soc_init":0.5,"soc_final":0.6,"operating_hours_of_each_deferrable_load":[1,3],"start_timesteps_of_each_deferrable_load":[0,3],"end_timesteps_of_each_deferrable_load":[0,6]}' http://localhost:5000/action/naive-mpc-optim
|
612
616
|
```
|
613
617
|
|
618
|
+
For a more readable option we can use the `rest_command` integration:
|
619
|
+
```yaml
|
620
|
+
rest_command:
|
621
|
+
url: http://127.0.0.1:5000/action/dayahead-optim
|
622
|
+
method: POST
|
623
|
+
headers:
|
624
|
+
content-type: application/json
|
625
|
+
payload: >-
|
626
|
+
{
|
627
|
+
"pv_power_forecast": [0, 70, 141.22, 246.18, 513.5, 753.27, 1049.89, 1797.93, 1697.3, 3078.93],
|
628
|
+
"prediction_horizon":10,
|
629
|
+
"soc_init":0.5,
|
630
|
+
"soc_final":0.6,
|
631
|
+
"operating_hours_of_each_deferrable_load":[1,3],
|
632
|
+
"start_timesteps_of_each_deferrable_load":[0,3],
|
633
|
+
"end_timesteps_of_each_deferrable_load":[0,6]
|
634
|
+
}
|
635
|
+
```
|
636
|
+
|
614
637
|
## A machine learning forecaster
|
615
638
|
|
616
639
|
Starting in v0.4.0 a new machine learning forecaster class was introduced.
|
@@ -625,7 +648,7 @@ Pull requests are very much accepted on this project. For development, you can f
|
|
625
648
|
|
626
649
|
Some problems may arise from solver-related issues in the Pulp package. It was found that for arm64 architectures (ie. Raspberry Pi4, 64 bits) the default solver is not available. A workaround is to use another solver. The `glpk` solver is an option.
|
627
650
|
|
628
|
-
This can be controlled in the configuration file with parameters `lp_solver` and `lp_solver_path`. The options for `lp_solver` are: 'PULP_CBC_CMD', 'GLPK_CMD' and 'COIN_CMD'. If using 'COIN_CMD' as the solver you will need to provide the correct path to this solver in parameter `lp_solver_path`, ex: '/usr/bin/cbc'.
|
651
|
+
This can be controlled in the configuration file with parameters `lp_solver` and `lp_solver_path`. The options for `lp_solver` are: 'PULP_CBC_CMD', 'GLPK_CMD', 'HiGHS', and 'COIN_CMD'. If using 'COIN_CMD' as the solver you will need to provide the correct path to this solver in parameter `lp_solver_path`, ex: '/usr/bin/cbc'.
|
629
652
|
|
630
653
|
|
631
654
|
## License
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "emhass"
|
3
|
-
version = "0.13.
|
3
|
+
version = "0.13.4"
|
4
4
|
description = "An Energy Management System for Home Assistant"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.10, <3.13"
|
@@ -29,6 +29,7 @@ dependencies = [
|
|
29
29
|
"pytz>=2024.2",
|
30
30
|
"requests>=2.32.2",
|
31
31
|
"h5py>=3.12.1",
|
32
|
+
"highspy>=1.10.0",
|
32
33
|
"pulp>=2.8.0",
|
33
34
|
"pyyaml>=6.0.2",
|
34
35
|
"tables>=3.10.0",
|
@@ -72,7 +72,9 @@ def retrieve_home_assistant_data(
|
|
72
72
|
if optim_conf.get("set_use_pv", True):
|
73
73
|
var_list.append(retrieve_hass_conf["sensor_power_photovoltaics"])
|
74
74
|
if optim_conf.get("set_use_adjusted_pv", True):
|
75
|
-
var_list.append(
|
75
|
+
var_list.append(
|
76
|
+
retrieve_hass_conf["sensor_power_photovoltaics_forecast"]
|
77
|
+
)
|
76
78
|
if not rh.get_data(
|
77
79
|
days_list, var_list, minimal_response=False, significant_changes_only=False
|
78
80
|
):
|
@@ -302,7 +304,8 @@ def set_input_data_dict(
|
|
302
304
|
else:
|
303
305
|
P_PV_forecast = pd.Series(0, index=fcst.forecast_dates)
|
304
306
|
P_load_forecast = fcst.get_load_forecast(
|
305
|
-
|
307
|
+
days_min_load_forecast=optim_conf["delta_forecast_daily"].days,
|
308
|
+
method=optim_conf["load_forecast_method"],
|
306
309
|
)
|
307
310
|
if isinstance(P_load_forecast, bool) and not P_load_forecast:
|
308
311
|
logger.error(
|
@@ -400,6 +403,7 @@ def set_input_data_dict(
|
|
400
403
|
else:
|
401
404
|
P_PV_forecast = pd.Series(0, index=fcst.forecast_dates)
|
402
405
|
P_load_forecast = fcst.get_load_forecast(
|
406
|
+
days_min_load_forecast=optim_conf["delta_forecast_daily"].days,
|
403
407
|
method=optim_conf["load_forecast_method"],
|
404
408
|
set_mix_forecast=set_mix_forecast,
|
405
409
|
df_now=df_input_data,
|
@@ -39,6 +39,7 @@ optim_conf,set_total_pv_sell,set_total_pv_sell
|
|
39
39
|
optim_conf,lp_solver,lp_solver
|
40
40
|
optim_conf,lp_solver_path,lp_solver_path
|
41
41
|
optim_conf,lp_solver_timeout,lp_solver_timeout
|
42
|
+
optim_conf,num_threads,num_threads
|
42
43
|
optim_conf,set_nocharge_from_grid,set_nocharge_from_grid
|
43
44
|
optim_conf,set_nodischarge_to_grid,set_nodischarge_to_grid
|
44
45
|
optim_conf,set_battery_dynamic,set_battery_dynamic
|
@@ -60,6 +61,10 @@ plant_conf,surface_azimuth,surface_azimuth,list_surface_azimuth
|
|
60
61
|
plant_conf,modules_per_string,modules_per_string,list_modules_per_string
|
61
62
|
plant_conf,strings_per_inverter,strings_per_inverter,list_strings_per_inverter
|
62
63
|
plant_conf,inverter_is_hybrid,inverter_is_hybrid
|
64
|
+
plant_conf,inverter_ac_output_max,inverter_ac_output_max
|
65
|
+
plant_conf,inverter_ac_input_max,inverter_ac_input_max
|
66
|
+
plant_conf,inverter_efficiency_dc_ac,inverter_efficiency_dc_ac
|
67
|
+
plant_conf,inverter_efficiency_ac_dc,inverter_efficiency_ac_dc
|
63
68
|
plant_conf,compute_curtailment,compute_curtailment
|
64
69
|
plant_conf,Pd_max,battery_discharge_power_max
|
65
70
|
plant_conf,Pc_max,battery_charge_power_max
|
@@ -68,4 +73,4 @@ plant_conf,eta_ch,battery_charge_efficiency
|
|
68
73
|
plant_conf,Enom,battery_nominal_energy_capacity
|
69
74
|
plant_conf,SOCmin,battery_minimum_state_of_charge
|
70
75
|
plant_conf,SOCmax,battery_maximum_state_of_charge
|
71
|
-
plant_conf,SOCtarget,battery_target_state_of_charge
|
76
|
+
plant_conf,SOCtarget,battery_target_state_of_charge
|
@@ -10,6 +10,7 @@
|
|
10
10
|
"lp_solver": "default",
|
11
11
|
"lp_solver_path": "empty",
|
12
12
|
"lp_solver_timeout": 45,
|
13
|
+
"num_threads": 0,
|
13
14
|
"set_nocharge_from_grid": false,
|
14
15
|
"set_nodischarge_to_grid": true,
|
15
16
|
"set_battery_dynamic": false,
|
@@ -115,6 +116,10 @@
|
|
115
116
|
1
|
116
117
|
],
|
117
118
|
"inverter_is_hybrid": false,
|
119
|
+
"inverter_ac_output_max": 1000,
|
120
|
+
"inverter_ac_input_max": 1000,
|
121
|
+
"inverter_efficiency_dc_ac": 1.0,
|
122
|
+
"inverter_efficiency_ac_dc": 1.0,
|
118
123
|
"compute_curtailment": false,
|
119
124
|
"set_use_battery": false,
|
120
125
|
"battery_discharge_power_max": 1000,
|
@@ -1,5 +1,3 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
|
3
1
|
import bz2
|
4
2
|
import copy
|
5
3
|
import json
|
@@ -216,8 +214,7 @@ class Forecast:
|
|
216
214
|
]
|
217
215
|
|
218
216
|
def get_cached_open_meteo_forecast_json(
|
219
|
-
self,
|
220
|
-
max_age: int | None = 30,
|
217
|
+
self, max_age: int | None = 30, forecast_days: int = 3
|
221
218
|
) -> dict:
|
222
219
|
r"""
|
223
220
|
Get weather forecast json from Open-Meteo and cache it for re-use.
|
@@ -235,10 +232,30 @@ class Forecast:
|
|
235
232
|
before it is discarded and a new version fetched from Open-Meteo.
|
236
233
|
Defaults to 30 minutes.
|
237
234
|
:type max_age: int, optional
|
235
|
+
:param forecast_days: The number of days of forecast data required from Open-Meteo.
|
236
|
+
One additional day is always fetched from Open-Meteo so there is an extra data in the cache.
|
237
|
+
Defaults to 2 days (3 days fetched) to match the prior default.
|
238
|
+
:type forecast_days: int, optional
|
238
239
|
:return: The json containing the Open-Meteo forecast data
|
239
240
|
:rtype: dict
|
240
241
|
|
241
242
|
"""
|
243
|
+
|
244
|
+
# Ensure at least 3 weather forecast days (and 1 more than requested)
|
245
|
+
if forecast_days is None:
|
246
|
+
self.logger.warning(
|
247
|
+
"Open-Meteo forecast_days is missing so defaulting to 3 days"
|
248
|
+
)
|
249
|
+
forecast_days = 3
|
250
|
+
elif forecast_days < 3:
|
251
|
+
self.logger.warning(
|
252
|
+
"Open-Meteo forecast_days is too low (%s) so defaulting to 3 days",
|
253
|
+
forecast_days,
|
254
|
+
)
|
255
|
+
forecast_days = 3
|
256
|
+
else:
|
257
|
+
forecast_days = forecast_days + 1
|
258
|
+
|
242
259
|
json_path = os.path.abspath(
|
243
260
|
self.emhass_conf["data_path"] / "cached-open-meteo-forecast.json"
|
244
261
|
)
|
@@ -287,10 +304,13 @@ class Forecast:
|
|
287
304
|
+ "shortwave_radiation_instant,"
|
288
305
|
+ "diffuse_radiation_instant,"
|
289
306
|
+ "direct_normal_irradiance_instant"
|
307
|
+
+ "&forecast_days="
|
308
|
+
+ str(forecast_days)
|
290
309
|
+ "&timezone="
|
291
310
|
+ quote(str(self.time_zone), safe="")
|
292
311
|
)
|
293
312
|
try:
|
313
|
+
self.logger.debug("Fetching data from Open-Meteo using URL: %s", url)
|
294
314
|
response = get(url, headers=headers)
|
295
315
|
self.logger.debug("Returned HTTP status code: %s", response.status_code)
|
296
316
|
response.raise_for_status()
|
@@ -349,7 +369,8 @@ class Forecast:
|
|
349
369
|
): # The scrapper option is being left here for backward compatibility
|
350
370
|
if not os.path.isfile(w_forecast_cache_path):
|
351
371
|
data_raw = self.get_cached_open_meteo_forecast_json(
|
352
|
-
self.optim_conf["open_meteo_cache_max_age"]
|
372
|
+
self.optim_conf["open_meteo_cache_max_age"],
|
373
|
+
self.optim_conf["delta_forecast_daily"].days,
|
353
374
|
)
|
354
375
|
data_15min = pd.DataFrame.from_dict(data_raw["minutely_15"])
|
355
376
|
data_15min["time"] = pd.to_datetime(data_15min["time"])
|
@@ -671,6 +692,7 @@ class Forecast:
|
|
671
692
|
alpha: float,
|
672
693
|
beta: float,
|
673
694
|
col: str,
|
695
|
+
ignore_pv_feedback: bool = False,
|
674
696
|
) -> pd.DataFrame:
|
675
697
|
"""A simple correction method for forecasted data using the current real values of a variable.
|
676
698
|
|
@@ -684,9 +706,15 @@ class Forecast:
|
|
684
706
|
:type beta: float
|
685
707
|
:param col: The column variable name
|
686
708
|
:type col: str
|
709
|
+
:param ignore_pv_feedback: If True, bypass mixing and return original forecast (used during curtailment)
|
710
|
+
:type ignore_pv_feedback: bool
|
687
711
|
:return: The output DataFrame with the corrected values
|
688
712
|
:rtype: pd.DataFrame
|
689
713
|
"""
|
714
|
+
# If ignoring PV feedback (e.g., during curtailment), return original forecast
|
715
|
+
if ignore_pv_feedback:
|
716
|
+
return df_forecast
|
717
|
+
|
690
718
|
first_fcst = alpha * df_forecast.iloc[0] + beta * df_now[col].iloc[-1]
|
691
719
|
df_forecast.iloc[0] = int(round(first_fcst))
|
692
720
|
return df_forecast
|
@@ -787,12 +815,14 @@ class Forecast:
|
|
787
815
|
# Extracting results for AC power
|
788
816
|
P_PV_forecast = mc.results.ac
|
789
817
|
if set_mix_forecast:
|
818
|
+
ignore_pv_feedback = self.params["passed_data"].get("ignore_pv_feedback_during_curtailment", False)
|
790
819
|
P_PV_forecast = Forecast.get_mix_forecast(
|
791
820
|
df_now,
|
792
821
|
P_PV_forecast,
|
793
822
|
self.params["passed_data"]["alpha"],
|
794
823
|
self.params["passed_data"]["beta"],
|
795
824
|
self.var_PV,
|
825
|
+
ignore_pv_feedback,
|
796
826
|
)
|
797
827
|
P_PV_forecast[P_PV_forecast < 0] = 0 # replace any negative PV values with zero
|
798
828
|
self.logger.debug("get_power_from_weather returning:\n%s", P_PV_forecast)
|
@@ -1410,14 +1440,22 @@ class Forecast:
|
|
1410
1440
|
forecast_out.index.name = "ts"
|
1411
1441
|
forecast_out = forecast_out.rename(columns={"load": "yhat"})
|
1412
1442
|
elif method == "naive": # using a naive approach
|
1413
|
-
|
1414
|
-
|
1443
|
+
# Old code logic (shifted timestamp problem)
|
1444
|
+
# mask_forecast_out = (
|
1445
|
+
# df.index > days_list[-1] - self.optim_conf["delta_forecast_daily"]
|
1446
|
+
# )
|
1447
|
+
# forecast_out = df.copy().loc[mask_forecast_out]
|
1448
|
+
# forecast_out = forecast_out.rename(columns={self.var_load_new: "yhat"})
|
1449
|
+
# forecast_out = forecast_out.iloc[0 : len(self.forecast_dates)]
|
1450
|
+
# forecast_out.index = self.forecast_dates
|
1451
|
+
# New code logic
|
1452
|
+
forecast_horizon = len(self.forecast_dates)
|
1453
|
+
historical_values = df.iloc[-forecast_horizon:]
|
1454
|
+
forecast_out = pd.DataFrame(
|
1455
|
+
historical_values.values,
|
1456
|
+
index=self.forecast_dates,
|
1457
|
+
columns=["yhat"]
|
1415
1458
|
)
|
1416
|
-
forecast_out = df.copy().loc[mask_forecast_out]
|
1417
|
-
forecast_out = forecast_out.rename(columns={self.var_load_new: "yhat"})
|
1418
|
-
# Force forecast_out length to avoid mismatches
|
1419
|
-
forecast_out = forecast_out.iloc[0 : len(self.forecast_dates)]
|
1420
|
-
forecast_out.index = self.forecast_dates
|
1421
1459
|
elif (
|
1422
1460
|
method == "mlforecaster"
|
1423
1461
|
): # using a custom forecast model with machine learning
|
@@ -1506,12 +1544,14 @@ class Forecast:
|
|
1506
1544
|
return False
|
1507
1545
|
P_Load_forecast = copy.deepcopy(forecast_out["yhat"])
|
1508
1546
|
if set_mix_forecast:
|
1547
|
+
# Load forecasts don't need curtailment protection - always use feedback
|
1509
1548
|
P_Load_forecast = Forecast.get_mix_forecast(
|
1510
1549
|
df_now,
|
1511
1550
|
P_Load_forecast,
|
1512
1551
|
self.params["passed_data"]["alpha"],
|
1513
1552
|
self.params["passed_data"]["beta"],
|
1514
1553
|
self.var_load_new,
|
1554
|
+
False, # Never ignore feedback for load forecasts
|
1515
1555
|
)
|
1516
1556
|
self.logger.debug("get_load_forecast returning:\n%s", P_Load_forecast)
|
1517
1557
|
return P_Load_forecast
|