Skip to content

Commit

Permalink
Now instantiating the thermostat with sensitivity configuration (#322)
Browse files Browse the repository at this point in the history
Sensitivity controller is not yet active.
  • Loading branch information
climategadgets committed Jun 22, 2024
1 parent 65a7fe1 commit b02a3b5
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 10 deletions.
4 changes: 2 additions & 2 deletions docs/configuration/zones.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ Positive `p` and `i` values are used for cooling mode, negative for heating. `li
### sensitivity
Defines how strongly the zone will react to setpoint changes. The algorithm is explained [here](https://www.emathhelp.net/en/calculators/differential-equations/half-life-calculator).

* `half-life` defines for how long the setpoint change amplification will linger.
* `multiplier` defines
* `half-life` defines for how long the setpoint change amplification will linger. Value of `PT0S` disables sensitivity altogether.
* `multiplier` defines the amplitude of the kick the control pipeline receives when the setpoint is changed.

This section is optional with system defined defaults (logged at startup at `DEBUG` level), but feel free to tinker with it if you think that the system is too sluggish or too trigger-happy reacting to setpoint changes.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,16 @@ public void parse(Set<ZoneConfig> source) {

private Map.Entry<String, Zone> createZone(ZoneConfig cf) {

var ts = createThermostat(cf.name(), cf.settings().setpoint(), cf.settings().setpointRange(), cf.controller());
var ts = createThermostat(
cf.name(),
cf.settings().setpoint(), cf.settings().setpointRange(),
cf.controller(),
Optional.ofNullable(cf.sensitivity()).orElseGet(() -> {
var s = new HalfLifeConfig(Duration.ofSeconds(10), 1d);
logger.debug("{}: using default sensitivity configuration of (half-life={}, multiplier={})", cf.name(), s.halfLife(), s.multiplier());
return s;

}));
var eco = createEconomizer(cf.name(), cf.economizer());
var ecoSettings = Optional.ofNullable(eco).map(v -> v.config.settings).orElse(null);
var zone = new Zone(ts, map(cf.settings(), ecoSettings), eco);
Expand All @@ -61,7 +70,7 @@ private EconomizerContext createEconomizer(String zoneName, net.sf.dz3r.runtime.
var hvacDevice = HCCObjects.requireNonNull(getHvacDevice(cf.hvacDevice()), "can't resolve hvac-device=" + cf.hvacDevice());
var timeout = Optional.ofNullable(cf.timeout()).orElseGet(() -> {
var t = Duration.ofSeconds(90);
logger.info("{}: using default stale timeout of {} for the economizer", zoneName, t);
logger.debug("{}: using default stale timeout of {} for the economizer", zoneName, t);
return t;
});

Expand Down Expand Up @@ -97,10 +106,14 @@ private EconomizerContext createEconomizer(String zoneName, net.sf.dz3r.runtime.
timeout);
}

private Thermostat createThermostat(String name, Double setpoint, RangeConfig rangeConfig, PidControllerConfig cf) {
private Thermostat createThermostat(String name, Double setpoint, RangeConfig rangeConfig, PidControllerConfig cf, HalfLifeConfig sensitivity) {

var range = map(Optional.ofNullable(rangeConfig).orElse(new RangeConfig(10.0, 40.0)));
return new Thermostat(Clock.systemUTC(), name, range, setpoint, cf.p(), cf.i(), cf.d(), cf.limit());
var range = map(Optional.ofNullable(rangeConfig).orElseGet(() -> {
var rc = new RangeConfig(10.0, 40.0);
logger.debug("{}: using default setpoint range of ({}..{})", name, rc.min(), rc.max());
return rc;
}));
return new Thermostat(Clock.systemUTC(), name, range, setpoint, cf.p(), cf.i(), cf.d(), cf.limit(), sensitivity.halfLife(), sensitivity.multiplier());
}

private ZoneSettings map(ZoneSettingsConfig source, EconomizerSettings economizerSettings) {
Expand Down
33 changes: 30 additions & 3 deletions modules/hcc-model/src/main/java/net/sf/dz3r/model/Thermostat.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.sf.dz3r.model;

import net.sf.dz3r.common.HCCObjects;
import net.sf.dz3r.controller.HalfLifeController;
import net.sf.dz3r.controller.HysteresisController;
import net.sf.dz3r.controller.ProcessController.Status;
import net.sf.dz3r.controller.pid.AbstractPidController;
Expand All @@ -15,6 +16,7 @@
import reactor.core.publisher.Sinks;

import java.time.Clock;
import java.time.Duration;

/**
* Thermostat.
Expand Down Expand Up @@ -58,6 +60,16 @@ public class Thermostat implements Addressable<String> {
*/
private final AbstractPidController<Void> controller;

/**
* Controller defining how trigger happy the thermostat is.
*/
private final HalfLifeController<Void> sensitivityController;

/**
* Multiplier for {@link #sensitivityController}.
*/
private final double sensitivityMultiplier;

/**
* Controller defining this thermostat's output signal.
*/
Expand All @@ -67,11 +79,11 @@ public class Thermostat implements Addressable<String> {
private final Flux<Signal<Double, Status<Double>>> stateFlux = stateSink.asFlux();

/**
* Create a thermostat with a default 10C..40C setpoint range and specified setpoint and PID values.
* Create a thermostat with a default 10C..40C setpoint range, specified setpoint and PID values, and no sensitivity adjustment.
*
*/
public Thermostat(String name, Double setpoint, double p, double i, double d, double limit) {
this(Clock.systemUTC(), name, new Range<>(10d, 40d), setpoint, p, i, d, limit);
this(Clock.systemUTC(), name, new Range<>(10d, 40d), setpoint, p, i, d, limit, Duration.ZERO, 0);
}

/**
Expand All @@ -86,16 +98,31 @@ public Thermostat(String name, Double setpoint, double p, double i, double d, do
* @param d PID controller derivative weight.
* @param limit PID controller saturation limit.
*/
public Thermostat(Clock clock, String name, Range<Double> setpointRange, Double setpoint, double p, double i, double d, double limit) {
public Thermostat(Clock clock, String name, Range<Double> setpointRange, Double setpoint, double p, double i, double d, double limit, Duration halfLife, double multiplier) {

this.clock = HCCObjects.requireNonNull(clock, "clock can't be null");
this.name = name;
this.setpointRange = setpointRange;
this.sensitivityMultiplier = checkSensitivity(halfLife, multiplier);

controller = new SimplePidController<>("(controller) " + name, setpoint, p, i, d, limit);
sensitivityController = new HalfLifeController<>("(sensitivity) " + name, halfLife);
signalRenderer = new HysteresisController<>("(signalRenderer) " + name, 0, HYSTERESIS);
}

private double checkSensitivity(Duration halfLife, double multiplier) {

if (multiplier < 0) {
throw new IllegalArgumentException("multiplier cannot be negative");
}

if (multiplier == 0 && !halfLife.isZero()) {
throw new IllegalArgumentException("zero multiplier with non-zero half life will slow the system down, specify zero half life if you want to disable the sensitivity controller");
}

return multiplier;
}

/**
* Get the human readable thermostat name.
*
Expand Down

0 comments on commit b02a3b5

Please sign in to comment.