Quick start guide

This guide is meant to walk you through the steps needed to get started with the preheat_open toolbox.

Creating and setting up an API Key

This step is defined in the README.md file under “Installation” / “Configuring the toolbox”.

Here, for the sake of the example, a test API key is loaded and we’re setting the logging to only display warnings and errors (to avoid noise in the notebook)

[56]:
from preheat_open import set_api_key, set_logging_level, test

set_api_key(test.API_KEY)
set_logging_level("WARNING") # Accepts: ERROR, WARNING, INFO, DEBUG

Loading buildings and their data

First, start by importing the package:

[57]:
import preheat_open

Then, identify which buildings you have access to by running:

[58]:
preheat_open.available_buildings()[["locationId", "address", "city", "country"]]
[58]:
locationId address city country
0 2756 API test location Aalborg Denmark

Loading the structure of a building

Buildings are described in the PreHEAT API by an identifier called “location ID”. To create a building, you need to call the Building class constructor with the right location ID:

[59]:
b = preheat_open.Building(2756)

The measurement point of a building are divided into units and components.

A unit is a group of components that corresponds to a system (e.g. a main heat supply, or a circulation loop - which can also have sub-units), while a component is a single measurement point.

These units can be listed with the following calls:

[60]:
# For the weather unit (providing the weather forecasts)
# - has nearly the same functionalities as a building unit
b.weather
[60]:
weather(WeatherForecast)
[61]:
# All other units corresponding to building elements
b.units.keys()
[61]:
dict_keys(['main', 'coldWater', 'heating', 'hotWater', 'cooling', 'electricity', 'ventilation', 'heatPumps', 'indoorClimate', 'custom'])
[62]:
b.units["heating"]
[62]:
[heating(heating_primary)]

While it is possible to traverse down the unit dictionary to find units and components of interest, there is a SMARTER way of doing this.

Using the query_units(..) or short-hand qu(..) method – available on both the building object and the unit objects. The query_units method has the following prototype:

def query_units(self, unit_type=None, name=None, unit_id=None):
    ...
    return unit

Hence, it takes 3 user given arguments; unit_type, name and unit_id. Specifying unit_id has highest precedence – here the unit with this exact unit id is returned. Specifying unit_type filters units based on this unit type (e.g. heating, ventilation, …). Specifying name filters units based on their name. Note that names are not unique.

Both unit_type and name are given as strings and a strict string comparison is done by default. However, to facilitate more flexibility when querying units in a building, it is possible to also filter based on regular expressions, by prepending the string arguments with an ?, treating everything in the string after ? as the regular expression. Examples:

Examples:

b.query_units(unit_id=11912) # Returns specific unit by id
b.query_units("heating") # Returns only units of unit_type "heating"
b.query_units("?heating") # Returns units of e.g. unit_types "heating" and "heatingCoils"
b.query_units(name="VE01") # Returns units with the name equal "VE01"
b.query_units(name="?VE01") # Returns units with names e.g. VE012, VE01AB...
[63]:
b.query_units(unit_id=15312)
[63]:
[main(main_unit)]
[64]:
b.query_units("heating")
[64]:
[heating(heating_primary)]
[65]:
b.query_units("?h")
[65]:
[heating(heating_primary), hotWater(dhw_primary), heatPumps(heat_pump_1)]

Example of chaining, as the method is also available on the units returned:

[66]:
b.qu(name="custom_unit_1").qu("?control")
[66]:
control(control_unit_custom_1)
[67]:
b.qu("?hot")
[67]:
hotWater(dhw_primary)

The query functionality is available because the whole building is now encoded as a graph. The graph is a property under a Building object:

[68]:
b.get_unit_graph()
[68]:
<networkx.classes.multidigraph.MultiDiGraph at 0x7f5f4a36d460>

The graph is represented as a MultiDiGraph using the networkx package – now a dependency to this package.

Loading data from a building

Once you have loaded the structure of a building, you can start loading the data into the structure itself.

[69]:
# Select period to load
start_date = "2021-05-01 00:00"
end_date   = "2021-05-02 00:00"

Note, that these can also be given as datetime objects from the Python native datetime package.

Select the time resolution of the data that you want to load. Acceptable values are: hour, day, week, month, year, minute (5 minute) and raw).

[70]:
time_resolution = "hour"

Then, load the data in the building structure (this can take a long time for a large building, if you select a long period and a low time resolution).

[71]:
b.load_data(start_date, end_date, time_resolution)

You can clear all data loaded in a building by calling:

[72]:
b.clear_data()

Similarly, all these loading and clearing functions are implemented at unit level, which allows you to only load data for units rather than the whole building (which is obviously much faster):

[73]:
b.qu(name="main_unit").load_data(start_date, end_date, time_resolution)
[74]:
b.qu(name="main_unit").clear_data()

Accessing the data in the building

In order to run the following below, make sure that you have loaded data at building level

[75]:
b.load_data(start_date, end_date, time_resolution)

Once the data is loaded, you can access it by looking at the data field of the unit that you are interested in:

[76]:
b.weather.data
[76]:
Temperature Humidity WindDirection WindSpeed Pressure LowClouds MediumClouds HighClouds Fog WindGust DewPointTemperature Cloudiness Precipitation DirectSunPower DiffuseSunPower SunAltitude SunAzimuth DirectSunPowerVertical
2021-04-30 22:00:00+00:00 5.8 78.8 156.4 1.2 1015.6 0.0 4.4 0.6 0.0 1.5 2.3 5.0 0.0 0.000000 0.000000 NaN NaN 0.000000
2021-04-30 23:00:00+00:00 5.1 80.6 180.2 1.9 1015.6 0.0 4.1 0.0 0.0 2.3 1.9 4.1 0.0 0.000000 0.000000 NaN NaN 0.000000
2021-05-01 00:00:00+00:00 3.9 81.9 186.8 1.5 1015.6 0.3 12.8 0.0 0.0 2.4 0.9 13.1 0.0 0.000000 0.000000 NaN NaN 0.000000
2021-05-01 01:00:00+00:00 4.8 78.7 197.8 0.8 1015.1 38.4 86.1 3.2 0.0 1.9 1.3 88.3 0.0 0.000000 0.000000 NaN NaN 0.000000
2021-05-01 02:00:00+00:00 4.2 77.2 23.4 1.2 1015.3 47.2 89.3 0.0 0.0 1.8 0.5 90.8 0.0 0.000000 0.000000 NaN NaN 0.000000
2021-05-01 03:00:00+00:00 3.3 82.6 54.7 1.2 1015.3 51.1 80.5 0.0 0.0 2.0 0.5 86.5 0.0 0.000000 0.000000 -0.716680 59.526031 0.000000
2021-05-01 04:00:00+00:00 4.2 83.6 98.6 0.7 1015.5 27.8 85.0 0.0 0.0 1.8 1.5 85.7 0.0 0.019763 0.023165 6.539537 71.966761 0.172400
2021-05-01 05:00:00+00:00 4.3 82.0 174.6 1.3 1015.6 40.5 82.1 0.0 0.0 1.9 1.3 84.4 0.0 0.050701 0.062113 14.456781 84.255512 0.196657
2021-05-01 06:00:00+00:00 4.9 86.2 165.7 2.2 1015.7 59.6 96.9 0.0 0.0 2.7 2.7 99.8 0.0 0.010110 0.018462 22.594146 96.879615 0.024296
2021-05-01 07:00:00+00:00 7.1 78.6 117.2 0.8 1015.7 77.6 94.0 3.3 0.0 2.1 3.6 96.4 0.0 0.015037 0.027498 30.514436 110.438631 0.025513
2021-05-01 08:00:00+00:00 8.0 82.1 81.2 1.2 1015.9 82.8 91.0 0.0 0.0 1.4 5.0 96.4 0.0 0.016692 0.033256 37.717475 125.633204 0.021584
2021-05-01 09:00:00+00:00 10.8 70.0 110.6 2.5 1016.1 85.3 64.8 0.0 0.0 4.9 5.5 92.3 0.0 0.136448 0.160478 43.560361 143.130464 0.143483
2021-05-01 10:00:00+00:00 9.9 68.3 141.1 4.4 1016.1 91.5 90.7 0.0 0.0 7.6 4.3 99.4 0.0 0.057968 0.094465 47.267479 163.119974 0.053552
2021-05-01 11:00:00+00:00 8.0 82.2 117.5 3.5 1016.0 89.2 77.3 0.0 0.0 7.6 5.1 97.1 0.0 0.095120 0.139577 48.151766 184.652224 0.085192
2021-05-01 12:00:00+00:00 10.9 85.4 86.2 3.7 1015.9 84.1 47.0 4.4 0.0 5.9 8.5 91.8 0.0 0.177784 0.202455 46.019851 205.737173 0.171565
2021-05-01 13:00:00+00:00 12.9 65.0 119.4 3.0 1014.9 80.8 47.9 4.0 0.0 6.5 6.5 85.0 0.0 0.177142 0.178444 41.320773 224.713504 0.201489
2021-05-01 14:00:00+00:00 9.7 78.3 150.8 2.6 1014.7 68.9 51.1 20.1 0.0 9.9 6.1 84.6 0.0 0.139822 0.151979 34.824553 241.176661 0.200993
2021-05-01 15:00:00+00:00 11.2 86.4 97.5 2.5 1014.3 53.6 66.2 33.7 0.0 4.5 9.0 87.3 0.0 0.104156 0.130791 27.263143 255.596955 0.202118
2021-05-01 16:00:00+00:00 12.1 74.4 74.1 3.9 1014.4 52.7 83.4 2.1 0.0 6.9 7.7 89.3 0.0 0.029123 0.049149 19.213980 268.687411 0.083564
2021-05-01 17:00:00+00:00 10.6 72.8 88.3 3.6 1014.3 37.6 93.6 11.2 0.0 6.4 5.9 97.9 0.0 0.028694 0.046071 11.137588 281.110849 0.145747
2021-05-01 18:00:00+00:00 10.2 74.9 91.1 2.9 1014.4 48.3 68.7 37.4 0.0 5.4 5.9 89.3 0.0 0.006048 0.009421 3.499731 293.417266 0.098884
2021-05-01 19:00:00+00:00 8.4 81.8 104.5 2.3 1014.3 1.1 46.0 26.0 0.0 4.3 5.4 62.8 0.0 0.000000 0.000000 -3.698823 306.045595 0.000000
2021-05-01 20:00:00+00:00 7.1 89.9 87.3 2.2 1014.5 0.1 37.8 20.5 0.0 3.2 5.5 50.2 0.0 0.000000 0.000000 NaN NaN 0.000000
2021-05-01 21:00:00+00:00 6.7 92.9 84.0 2.0 1014.7 0.1 30.9 52.4 0.0 2.8 5.6 67.4 0.0 0.000000 0.000000 NaN NaN 0.000000
[77]:
b.qu("main").data
[77]:
supplyT returnT flow volume energy power
2021-04-30 22:00:00+00:00 45.936817 27.390571 0.033795 105107.0 210.214 0.003279
2021-04-30 23:00:00+00:00 45.561354 28.378000 0.044800 105119.0 210.238 0.002834

Sending control signals

The API can be used to send control signals to the building.

This requires that: - you have activated the write permission in your API key (this can be made on your user profile) - Neogrid has given you the right to control the building that you want to control

Identifying the unit to be controlled

First, you need to select the ControlUnit that you want to control (adjust the proposal below to fit the building that you are controlling) :

[78]:
control_unit = b.query_units("control")[0]

Loading previous schedules

You can load schedules for the control unit in a given time period by calling the following method:

[79]:
# Select start and end time
start_time = "2020-01-01T00:00:00+02:00"
end_time   = "2020-01-01T12:00:00+02:00"

# Load the values of the schedule
schedule_data = control_unit.get_schedule(start_time, end_time)
[80]:
# Print the schedule
print(schedule_data)
                           value operation
startTime
2019-12-31 22:00:00+00:00    0.0    NORMAL
2019-12-31 22:01:00+00:00    1.0    NORMAL
2019-12-31 22:02:00+00:00    0.0    NORMAL
2019-12-31 22:03:00+00:00    1.0    NORMAL
2019-12-31 22:04:00+00:00    0.0    NORMAL
2019-12-31 22:05:00+00:00    1.0    NORMAL

Creating the control schedule and sending it

Then you need to create a schedule, structured in a dataframe format. A schedule consists in a dataframe containing: - startTime: this is the sequence of times when the gateway will need to apply a new value to the system - value: this is the sequence of the value that will be applied by the gateway at the corresponding step in “startTime” (note that the last value is a fallback value) - operation: this is the operation mode, in most cases, simply leave it set to “NORMAL” for each step.

Important notes: - As the control is happening over the internet, it is important to account for a risk of temporary loss of connection between your control and the gateway. Therefore, it is better to send a schedule of the several hours ahead. - Whenever a new schedule is received, the system forgets all previously received scheduled values starting from the first timestep of the new schedule. - The last value of the schedule is treated as a fallback, which will be applied ‘ad vitam aeternam’ by the gateway until a new value is received. It is therefore important that it is a safe and satisfactory default value.

[81]:
import pandas as pd

# Choose the number of points in your schedule
N_points_schedule = 6

# Built time range
t_range = pd.date_range("2020-01-01T00:00:00+02:00", "2020-01-01T00:05:00+02:00", N_points_schedule)
start_times = [pd.to_datetime(t) for t in t_range]

# Choose the fallback values and other values
fallback_value = 1
schedule_values = [0, 1, 0, 1, 0, fallback_value]

# Build schedule
schedule = {
    "value": schedule_values,
    "startTime": start_times,
    "operation": len(schedule_values) * ["NORMAL"],
}

# Convert to Pandas DataFrame
schedule_df = pd.DataFrame(schedule)
schedule_df.set_index("startTime", inplace=True)

Check that your schedule is acceptable:

[82]:
print(schedule_df)
                           value operation
startTime
2020-01-01 00:00:00+02:00      0    NORMAL
2020-01-01 00:01:00+02:00      1    NORMAL
2020-01-01 00:02:00+02:00      0    NORMAL
2020-01-01 00:03:00+02:00      1    NORMAL
2020-01-01 00:04:00+02:00      0    NORMAL
2020-01-01 00:05:00+02:00      1    NORMAL

The schedule is then sent using the following method of the control unit object.

(WARNING: this line overwrites existing control values, only use is after agreement with the building owner and having checked that your schedule is acceptable)

[83]:
test = control_unit.request_schedule(schedule_df)
/home/pvf/code/preheat_open/preheat_open/logging.py:47: UserWarning: Warning: you are trying to control an unit that is not activated
                (id=15357 / details: [unit: control_unit_custom_1 / building: [2756] API test location])
  warnings.warn(msg)
WARNING:preheat_open.control_unit:Warning: you are trying to control an unit that is not activated
                (id=15357 / details: [unit: control_unit_custom_1 / building: [2756] API test location])

Check the response to the request (a 403 means that you do not have permission to write).

[84]:
print(test)
<Response [200]>