diff --git a/dz3r-influxdb/src/main/java/net/sf/dz3r/view/influxdb/v3/InfluxDbLogger.java b/dz3r-influxdb/src/main/java/net/sf/dz3r/view/influxdb/v3/InfluxDbLogger.java index 6417e1e62..e074a3795 100644 --- a/dz3r-influxdb/src/main/java/net/sf/dz3r/view/influxdb/v3/InfluxDbLogger.java +++ b/dz3r-influxdb/src/main/java/net/sf/dz3r/view/influxdb/v3/InfluxDbLogger.java @@ -127,7 +127,7 @@ public void connect(UnitDirector.Feed feed) { var zoneStatusFeed = new ZoneMetricsConverter(config.instance, feed.unit).compute(feed.aggregateZoneFlux); var zoneControllerFeed = new ZoneControllerMetricsConverter(config.instance, feed.unit).compute(feed.zoneControllerFlux); var unitControllerFeed = new UnitControllerMetricsConverter(config.instance, feed.unit).compute(feed.unitControllerFlux); - var hvacDeficeFeed = new HvacDeviceMetricsConverter(config.instance, feed.unit).compute(feed.hvacDeviceFlux); + var hvacDeviceFeed = new HvacDeviceMetricsConverter(config.instance, feed.unit).compute(feed.hvacDeviceFlux); var all = Flux.merge( sensorFeeds, @@ -135,7 +135,7 @@ public void connect(UnitDirector.Feed feed) { zoneStatusFeed, zoneControllerFeed, unitControllerFeed, - hvacDeficeFeed); + hvacDeviceFeed); all.publishOn(Schedulers.boundedElastic()).subscribe(this); } diff --git a/dz3r-model/src/main/java/net/sf/dz3r/model/SingleStageUnitController.java b/dz3r-model/src/main/java/net/sf/dz3r/model/SingleStageUnitController.java index 3b94aa2d4..7792009c2 100644 --- a/dz3r-model/src/main/java/net/sf/dz3r/model/SingleStageUnitController.java +++ b/dz3r-model/src/main/java/net/sf/dz3r/model/SingleStageUnitController.java @@ -29,8 +29,6 @@ protected SingleStageUnitController(String name) { @Override public Flux> compute(Flux> in) { - logger.debug("compute()"); - return in .filter(Signal::isOK) .map(s -> { diff --git a/dz3r-model/src/main/java/net/sf/dz3r/model/Thermostat.java b/dz3r-model/src/main/java/net/sf/dz3r/model/Thermostat.java index a697046f9..8d40a05a3 100644 --- a/dz3r-model/src/main/java/net/sf/dz3r/model/Thermostat.java +++ b/dz3r-model/src/main/java/net/sf/dz3r/model/Thermostat.java @@ -149,8 +149,6 @@ public double getError() { @Override public Flux, Void>> compute(Flux> pv) { - logger.debug("compute()"); - // Compute the control signal to feed to the renderer. // Might want to make this available to outside consumers for instrumentation. var stage1 = controller diff --git a/dz3r-model/src/main/java/net/sf/dz3r/model/Zone.java b/dz3r-model/src/main/java/net/sf/dz3r/model/Zone.java index 1f6b9ad28..0c4ee3c0b 100644 --- a/dz3r-model/src/main/java/net/sf/dz3r/model/Zone.java +++ b/dz3r-model/src/main/java/net/sf/dz3r/model/Zone.java @@ -92,8 +92,6 @@ public ZoneSettings getSettings() { @Override public Flux> compute(Flux> in) { - logger.debug("compute()"); - var source = Optional.ofNullable(economizer) .map(eco -> eco.compute(in)) .orElse(in); @@ -106,16 +104,16 @@ public Flux> compute(Flux> in) // regardless of whether the zone is enabled var stage1 = ts .compute(stage0) - .doOnNext(e -> logger.trace("ts/{}: {}", getAddress(), e)); + .doOnNext(e -> logger.trace("compute {}/ts: {}", getAddress(), e)); // Now, need to translate into a form that is easier manipulated var stage2 = stage1.map(this::translate) - .doOnNext(e -> logger.debug("translated/{}: {}", getAddress(), e)); + .doOnNext(e -> logger.debug("compute {}/translated: {}", getAddress(), e)); // Now, dampen the signal if the zone is disabled var stage3 = stage2 .map(this::suppressIfNotEnabled) - .doOnNext(e -> logger.debug("isOn/{}: {} {}", getAddress(), settings.enabled ? "enabled" : "DISABLED", e)); + .doOnNext(e -> logger.debug("compute {}/isOn: {} {}", getAddress(), settings.enabled ? "enabled" : "DISABLED", e)); // And finally, suppress if the economizer says so return stage3.map(this::suppressEconomizer); diff --git a/dz3r-model/src/main/java/net/sf/dz3r/model/ZoneController.java b/dz3r-model/src/main/java/net/sf/dz3r/model/ZoneController.java index 04970bffe..b2f291efa 100644 --- a/dz3r-model/src/main/java/net/sf/dz3r/model/ZoneController.java +++ b/dz3r-model/src/main/java/net/sf/dz3r/model/ZoneController.java @@ -69,8 +69,6 @@ public ZoneController(Collection zones) { @Override public Flux> compute(Flux> in) { - logger.debug("compute()"); - return in .filter(this::isOurs) .doOnNext(this::capture) diff --git a/dz3r-model/src/main/java/net/sf/dz3r/signal/Signal.java b/dz3r-model/src/main/java/net/sf/dz3r/signal/Signal.java index b2cc49d82..f807f632b 100644 --- a/dz3r-model/src/main/java/net/sf/dz3r/signal/Signal.java +++ b/dz3r-model/src/main/java/net/sf/dz3r/signal/Signal.java @@ -131,15 +131,15 @@ public Throwable getError() { @Override public String toString() { - var result = "@" + timestamp + ", value={" + value + "}, " + printPayload() + var result = "{@" + timestamp + ", value={" + value + "}, " + printPayload() + "status=" + status + ", isOK=" + isOK() + ", isError=" + isError() ; if (isOK()) { - return result; + return result + "}"; } - return result + ", error=" + error.getClass().getName() + "(" + error.getMessage() + ")"; + return result + ", error=" + error.getClass().getName() + "(" + error.getMessage() + ")}"; } private String printPayload() { diff --git a/dz3r-model/src/main/java/net/sf/dz3r/signal/hvac/CallingStatus.java b/dz3r-model/src/main/java/net/sf/dz3r/signal/hvac/CallingStatus.java index 31b661606..3d5df1f0e 100644 --- a/dz3r-model/src/main/java/net/sf/dz3r/signal/hvac/CallingStatus.java +++ b/dz3r-model/src/main/java/net/sf/dz3r/signal/hvac/CallingStatus.java @@ -25,6 +25,6 @@ public CallingStatus(Double sample, double demand, boolean calling) { @Override public String toString() { - return "{sample=" + sample + ",demand=" + demand + ", calling=" + calling + "}"; + return "{sample=" + sample + ", demand=" + demand + ", calling=" + calling + "}"; } } diff --git a/dz3r-model/src/test/java/net/sf/dz3r/device/actuator/economizer/AbstractEconomizerTest.java b/dz3r-model/src/test/java/net/sf/dz3r/device/actuator/economizer/AbstractEconomizerTest.java index d49eaa6b3..9a3d09ee8 100644 --- a/dz3r-model/src/test/java/net/sf/dz3r/device/actuator/economizer/AbstractEconomizerTest.java +++ b/dz3r-model/src/test/java/net/sf/dz3r/device/actuator/economizer/AbstractEconomizerTest.java @@ -15,9 +15,12 @@ class AbstractEconomizerTest { + /** + * Make sure that control signal is computed properly as the indoor temperature is approaching the {@link EconomizerSettings#targetTemperature}. + */ @ParameterizedTest @MethodSource("targetAdjustmentProvider") - void targetAdjustmentTest(TestData source) { + void targetAdjustmentTest(TargetAdjustmentTestData source) { var settings = new EconomizerSettings( source.mode, @@ -41,8 +44,6 @@ private class TestEconomizer extends AbstractEconomizer { *

* Note that only the {@code ambientFlux} argument is present, indoor flux is provided to {@link #compute(Flux)}. * - * @param name - * @param settings * @param ambientFlux Flux from the ambient temperature sensor. * @param targetDevice Switch to control the economizer actuator. */ @@ -62,7 +63,7 @@ public double computeCombined(Double indoorTemperature, Double ambientTemperatur return super.computeCombined(indoorTemperature, ambientTemperature); } } - private static final class TestData { + private static final class TargetAdjustmentTestData { public final HvacMode mode; public final double changeoverDelta; @@ -71,7 +72,7 @@ private static final class TestData { public final double ambientTemperature; public final double expectedSignal; - private TestData(HvacMode mode, double changeoverDelta, double targetTemperature, double indoorTemperature, double ambientTemperature, double expectedSignal) { + private TargetAdjustmentTestData(HvacMode mode, double changeoverDelta, double targetTemperature, double indoorTemperature, double ambientTemperature, double expectedSignal) { this.mode = mode; this.changeoverDelta = changeoverDelta; this.targetTemperature = targetTemperature; @@ -82,16 +83,16 @@ private TestData(HvacMode mode, double changeoverDelta, double targetTemperature } /** - * @return Stream of {@link TestData} for {@link #targetAdjustmentTest(TestData)}. + * @return Stream of {@link TargetAdjustmentTestData} for {@link #targetAdjustmentTest(TargetAdjustmentTestData)}. */ - private static Stream targetAdjustmentProvider() { + private static Stream targetAdjustmentProvider() { return Stream.of( - new TestData(HvacMode.COOLING, 1.0, 22.0, 25.0, 10.0, 14.0), - new TestData(HvacMode.COOLING, 1.0, 22.0, 23.0, 10.0, 12.0), - new TestData(HvacMode.COOLING, 1.0, 22.0, 22.5, 10.0, 5.75), - new TestData(HvacMode.COOLING, 1.0, 22.0, 22.0, 10.0, 0.0), - new TestData(HvacMode.COOLING, 1.0, 22.0, 21.0, 10.0, -10.0) + new TargetAdjustmentTestData(HvacMode.COOLING, 1.0, 22.0, 25.0, 10.0, 14.0), + new TargetAdjustmentTestData(HvacMode.COOLING, 1.0, 22.0, 23.0, 10.0, 12.0), + new TargetAdjustmentTestData(HvacMode.COOLING, 1.0, 22.0, 22.5, 10.0, 5.75), + new TargetAdjustmentTestData(HvacMode.COOLING, 1.0, 22.0, 22.0, 10.0, 0.0), + new TargetAdjustmentTestData(HvacMode.COOLING, 1.0, 22.0, 21.0, 10.0, -10.0) ); } diff --git a/dz3r-swing/src/main/java/net/sf/dz3r/common/DataSet.java b/dz3r-swing/src/main/java/net/sf/dz3r/common/DataSet.java index 77f8ee8d1..67ae39658 100644 --- a/dz3r-swing/src/main/java/net/sf/dz3r/common/DataSet.java +++ b/dz3r-swing/src/main/java/net/sf/dz3r/common/DataSet.java @@ -15,7 +15,7 @@ public class DataSet { /** * The data set. The key is sampling time, the value is sample value. */ - private final LinkedHashMap samples = new LinkedHashMap<>(); + private final Map samples = new LinkedHashMap<>(); /** * The expiration interval. Values older than the last key by this many diff --git a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/AbstractChart.java b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/AbstractChart.java index 6a0c8b63c..044237d58 100644 --- a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/AbstractChart.java +++ b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/AbstractChart.java @@ -64,16 +64,14 @@ public abstract class AbstractChart extends SwingSink { /** * Horizontal grid spacing. * - * Vertical grid lines will be painted every timeSpacing - * milliseconds. Default is 30 minutes. + * Vertical grid lines will be painted every {@code timeSpacing} milliseconds. Default is 30 minutes. */ protected static final Duration SPACING_TIME = Duration.of(30, ChronoUnit.MINUTES); /** * Vertical grid spacing. * - * Horizontal grid lines will be painted every valueSpacing - * units. Default is 1.0. + * Horizontal grid lines will be painted every {@code valueSpacing} units. Default is 1.0. */ protected static final double SPACING_VALUE = 1.0; @@ -85,23 +83,22 @@ public abstract class AbstractChart extends SwingSink { /** * Maximum known data value. */ - protected Double dataMax = null; + protected Double dataMax; /** * Minimum known data value. */ - protected Double dataMin = null; + protected Double dataMin ; /** * Timestamp on {@link #dataMin} or {@link #dataMax}, whichever is younger. * * @see #adjustVerticalLimits(long, double, double) */ - private Long minmaxTime = null; + private Long minmaxTime ; /** - * Amount of extra time to wait before {@link #recalculateVerticalLimits() - * recalculating} the limits. + * Amount of extra time to wait before {@link #recalculateVerticalLimits() recalculating} the limits. * * Chances are, new min/max values will be pretty close to old, so unless * this value is used, recalculation will be happening more often than @@ -169,7 +166,6 @@ public synchronized void paintComponent(Graphics g) { try { - // Draw background super.paintComponent(g); @@ -197,10 +193,10 @@ public synchronized void paintComponent(Graphics g) { paintValueGrid(g2d, boundary, insets, yScale, yOffset); paintCharts(g2d, boundary, insets, now, xScale, xOffset, yScale, yOffset); - logger.debug("Painted in {}ms", (clock.instant().toEpochMilli() - startTime)); + logger.debug("Painted in {}ms", clock.instant().toEpochMilli() - startTime); } catch (Exception ex) { - logger.warn("Painted in {}ms (FAIL)", (clock.instant().toEpochMilli() - startTime)); + logger.warn("Painted in {}ms (FAIL)", clock.instant().toEpochMilli() - startTime); logger.warn("Unexpected exception, ignored", ex); } finally { ThreadContext.pop(); @@ -239,7 +235,7 @@ private void paintTimeGrid(Graphics2D g2d, Dimension boundary, Insets insets, lo private void paintTimeGrid(Graphics2D g2d, double top, double bottom, Insets insets, long now, double xScale, long xOffset, Duration spacingTime) { - var originalStroke = (BasicStroke) g2d.getStroke(); + var originalStroke = g2d.getStroke(); var gridStroke = setGridStroke(g2d); g2d.setStroke(gridStroke); @@ -279,7 +275,7 @@ private void paintValueGrid( // VT: NOTE: squid:S107 - following this rule will hurt performance, so no. - var originalStroke = (BasicStroke) g2d.getStroke(); + var originalStroke = g2d.getStroke(); var gridStroke = setGridStroke(g2d); // The zero line gets painted with the default stroke @@ -288,7 +284,7 @@ private void paintValueGrid( var gridY = yOffset * yScale + insets.top; - Line2D gridLine = new Line2D.Double( + var gridLine = new Line2D.Double( insets.left, gridY, (double)boundary.width - insets.right - 1, @@ -300,7 +296,7 @@ private void paintValueGrid( g2d.setStroke(gridStroke); - var halfWidth = (boundary.width - insets.right - 1) / 2d; + var halfWidth = (boundary.width - insets.right - 1) / 2.0; for (var valueOffset = SPACING_VALUE; valueOffset < dataMax + PADDING; valueOffset += SPACING_VALUE) { @@ -355,9 +351,9 @@ protected final void drawGradientLine( // VT: NOTE: squid:S107 - following this rule will hurt performance, so no. var gp = new GradientPaint( - (int) x0, (int) y0, startColor, - (int) x1, (int) y1, endColor); - Line2D line = new Line2D.Double(x0, y0, x1, y1); + (float) x0, (float) y0, startColor, + (float) x1, (float) y1, endColor); + var line = new Line2D.Double(x0, y0, x1, y1); g2d.setPaint(gp); g2d.setStroke(emphasize ? strokeDouble : strokeSingle); @@ -370,13 +366,24 @@ protected final void drawGradientLine( * @param timestamp Value timestamp. * @param value Incoming data element. * @param setpoint Incoming setpoint. + * @param ambient Ambient temperature, {@code null} if unavailable. + * @param target Incoming economizer target temperature, {@code null} if unavailable. * * @see #dataMax * @see #dataMin */ - protected final void adjustVerticalLimits(long timestamp, double value, double setpoint) { + protected final void adjustVerticalLimits(long timestamp, double value, double setpoint, Double ambient, Double target) { + adjustVerticalLimits(timestamp, value); adjustVerticalLimitsExt(timestamp, setpoint); + + if (ambient != null) { + adjustVerticalLimits(timestamp, ambient); + } + + if (target != null) { + adjustVerticalLimitsExt(timestamp, target); + } } protected final void adjustVerticalLimits(long timestamp, double value) { diff --git a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/AbstractZoneChart.java b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/AbstractZoneChart.java index 2b57ba2f8..5fd4d9948 100644 --- a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/AbstractZoneChart.java +++ b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/AbstractZoneChart.java @@ -3,6 +3,7 @@ import net.sf.dz3r.common.DataSet; import net.sf.dz3r.signal.Signal; import net.sf.dz3r.view.swing.AbstractChart; +import net.sf.dz3r.view.swing.ColorScheme; import java.awt.Color; import java.awt.Dimension; @@ -11,8 +12,6 @@ import java.awt.RenderingHints; import java.time.Clock; import java.time.Duration; -import java.util.Iterator; -import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -23,15 +22,39 @@ * * @author Copyright © Vadim Tkachenko 2001-2021 */ -public abstract class AbstractZoneChart extends AbstractChart { +public abstract class AbstractZoneChart extends AbstractChart { - protected final transient DataSet dsValues = new DataSet<>(chartLengthMillis); + /** + * Thermostat output signals. + */ + protected final transient DataSet dsValues = new DataSet<>(chartLengthMillis); + + /** + * Thermostat setpoints. + */ protected final transient DataSet dsSetpoints = new DataSet<>(chartLengthMillis); - protected final transient ReadWriteLock lockValues = new ReentrantReadWriteLock(); + /** + * Economizer status signals. + */ + protected final transient DataSet dsEconomizer = new DataSet<>(chartLengthMillis); + + /** + * Economizer target temperatures. + */ + protected final transient DataSet dsTargets = new DataSet<>(chartLengthMillis); + + /** + * Lock common for all the data sets. Suboptimal, but not a bottleneck. + */ + protected final transient ReadWriteLock lock = new ReentrantReadWriteLock(); protected static final Color SIGNAL_COLOR_LOW = Color.GREEN; protected static final Color SIGNAL_COLOR_HIGH = Color.RED; + protected static final Color ECO_COLOR_LOW = ColorScheme.coolingMap.setpoint.darker(); + protected static final Color ECO_COLOR_HIGH = ColorScheme.heatingMap.setpoint.darker(); + protected static final Color TARGET_COLOR = Color.GREEN.darker(); + protected static final Color SETPOINT_COLOR = Color.YELLOW; protected AbstractZoneChart(Clock clock, long chartLengthMillis, boolean needFahrenheit) { @@ -49,126 +72,19 @@ protected final void paintCharts( double xScale, long xOffset, double yScale, double yOffset) { g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - paintChart(g2d, boundary, insets, now, xScale, xOffset, yScale, yOffset, dsValues, lockValues, dsSetpoints); + paintChart(g2d, boundary, insets, now, xScale, xOffset, yScale, yOffset, dsValues, dsEconomizer, lock, dsTargets, dsSetpoints); } @SuppressWarnings("squid:S107") protected abstract void paintChart( Graphics2D g2d, Dimension boundary, Insets insets, long now, double xScale, long xOffset, double yScale, double yOffset, - DataSet dsValues, ReadWriteLock lockValues, + DataSet dsValues, + DataSet dsEconomizer, + ReadWriteLock lock, + DataSet dsTargets, DataSet dsSetpoints); - private static final Color[] signalCache = new Color[256]; - - /** - * Convert signal from -1 to +1 to color from low color to high color. - * - * @param signal Signal to convert to color. - * @param low Color corresponding to -1 signal value. - * @param high Color corresponding to +1 signal value. - * - * @return Color resolved from the incoming signal. - */ - protected final Color signal2color(double signal, Color low, Color high) { - - signal = signal > 1 ? 1: signal; - signal = signal < -1 ? -1 : signal; - signal = (signal + 1) / 2; - - int index = (int) (signal * 255); - - synchronized (signalCache) { - - Color result = signalCache[index]; - - if ( result == null) { - - float[] hsbLow = resolve(low); - float[] hsbHigh = resolve(high); - - float h = transform(signal, hsbLow[0], hsbHigh[0]); - float s = transform(signal, hsbLow[1], hsbHigh[1]); - float b = transform(signal, hsbLow[2], hsbHigh[2]); - - result = new Color(Color.HSBtoRGB(h, s, b)); - signalCache[index] = result; - } - - return result; - } - } - - private static class RGB2HSB { - - public final int rgb; - public final float[] hsb; - - public RGB2HSB(int rgb, float[] hsb) { - - this.rgb = rgb; - this.hsb = hsb; - } - } - - /** - * Cache medium for {@link #resolve(Color)}. - * - * According to "worse is better" rule, there's no error checking against - * the array size - too expensive. In all likelihood, this won't grow beyond 2 entries. - */ - private static final RGB2HSB[] rgb2hsb = new RGB2HSB[16]; - - /** - * Resolve a possibly cached {@link Color#RGBtoHSB(int, int, int, float[])} result, - * or compute it and store it for later retrieval if it hasn't been done. - * - * @param color Color to transform. - * @return Transformation result. - */ - private float[] resolve(Color color) { - - var rgb = color.getRGB(); - var offset = 0; - - for (; offset < rgb2hsb.length && rgb2hsb[offset] != null; offset++) { - - if (rgb == rgb2hsb[offset].rgb) { - - return rgb2hsb[offset].hsb; - } - } - - synchronized (rgb2hsb) { - - // VT: NOTE: Not the cleanest solution. It is possible that someone has just tried to do the same thing - // and we'll end up writing the same value twice, but oh well, it's the same value in an array of size 16 - - rgb2hsb[offset] = new RGB2HSB(rgb, Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null)); - } - - logger.info("RGB2HSB offset={}", offset ); - - return rgb2hsb[offset].hsb; - } - - /** - * Get the point between the start and end values corresponding to the value of the signal. - * - * @param signal Signal value, from -1 to +1. - * @param start Start point. - * @param end End point. - * - * @return Desired position between the start and end points. - */ - private float transform(double signal, float start, float end) { - - assert(signal <= 1); - assert(signal >= -1); - - return (float) (start + signal * (end - start)); - } - /** * Calculate {@link #dataMin} and {@link #dataMax} based on all values available in {@link #dsValues}. */ @@ -181,9 +97,9 @@ protected Limits recalculateVerticalLimits() { Double max = null; Long minmaxTime = null; - for (Iterator> i2 = dsValues.entryIterator(); i2.hasNext(); ) { + for (var i = dsValues.entryIterator(); i.hasNext(); ) { - var entry = i2.next(); + var entry = i.next(); var timestamp = entry.getKey(); var tv = entry.getValue(); @@ -198,6 +114,27 @@ protected Limits recalculateVerticalLimits() { } } + logger.info("minmax/thermostat set to {}/{}", min, max); + + for (var i = dsEconomizer.entryIterator(); i.hasNext(); ) { + + var entry = i.next(); + var timestamp = entry.getKey(); + var tv = entry.getValue(); + + if (max == null || tv.ambient > max) { + max = tv.ambient; + minmaxTime = timestamp; + } + + if (min == null || tv.ambient < min) { + min = tv.ambient; + minmaxTime = timestamp; + } + } + + logger.info("minmax/eco adjusted to {}/{}", min, max); + var result = new Limits(min, max, minmaxTime); logger.info("Recalculated in {}ms", (clock.instant().toEpochMilli() - startTime)); @@ -209,7 +146,7 @@ protected Limits recalculateVerticalLimits() { /** * Averaging tool. */ - protected class Averager { + protected abstract class Averager { /** * The expiration interval. Values older than the last key by this many @@ -220,14 +157,11 @@ protected class Averager { /** * The timestamp of the oldest recorded sample. */ - private Long timestamp; + protected Long oldestTimestamp; private int count; - private double valueAccumulator = 0; - private double tintAccumulator = 0; - private double emphasizeAccumulator = 0; - public Averager(long expirationInterval) { + protected Averager(long expirationInterval) { this.expirationInterval = expirationInterval; } @@ -239,33 +173,91 @@ public Averager(long expirationInterval) { * @return The average of all data stored in the buffer if this sample is more than {@link #expirationInterval} * away from the first sample stored, {@code null} otherwise. */ - public TintedValue append(Signal signal) { + public final O append(Signal signal) { - if (timestamp == null) { - timestamp = signal.timestamp.toEpochMilli(); + if (oldestTimestamp == null) { + oldestTimestamp = signal.timestamp.toEpochMilli(); } - var age = signal.timestamp.toEpochMilli() - timestamp; + var age = signal.timestamp.toEpochMilli() - oldestTimestamp; - if ( age < expirationInterval) { + if (age < expirationInterval) { count++; - valueAccumulator += signal.getValue().value; - tintAccumulator += signal.getValue().tint; - emphasizeAccumulator += signal.getValue().emphasize ? 1 : 0; + + accumulate(signal.getValue()); return null; } - logger.debug("RingBuffer: flushing at {}", () -> Duration.ofMillis(age)); + logger.trace("RingBuffer: flushing {} elements at {}", () -> count, () -> Duration.ofMillis(age)); - var result = new TintedValue(valueAccumulator / count, tintAccumulator / count, emphasizeAccumulator > 0); + var result = complete(signal.getValue(), count); count = 1; - valueAccumulator = signal.getValue().value; - tintAccumulator = signal.getValue().tint; + oldestTimestamp = signal.timestamp.toEpochMilli(); + + return result; + } + + protected abstract void accumulate(I value); + protected abstract O complete(I value, int count); + } + + protected class ThermostatAverager extends Averager { + + private double valueAccumulator = 0; + private double tintAccumulator = 0; + private double emphasizeAccumulator = 0; + + public ThermostatAverager(long expirationInterval) { + super(expirationInterval); + } + + @Override + protected void accumulate(ZoneChartDataPoint value) { + + valueAccumulator += value.tintedValue.value; + tintAccumulator += value.tintedValue.tint; + emphasizeAccumulator += value.tintedValue.emphasize ? 1.0 : 0.0; + } + + @Override + protected ThermostatTintedValue complete(ZoneChartDataPoint value, int count) { + + var result = new ThermostatTintedValue(valueAccumulator / count, tintAccumulator / count, emphasizeAccumulator > 0); + + valueAccumulator = value.tintedValue.value; + tintAccumulator = value.tintedValue.tint; emphasizeAccumulator = 0; - timestamp = signal.timestamp.toEpochMilli(); + + return result; + } + } + + protected class EconomizerAverager extends Averager { + + private double ambientAccumulator = 0; + private double signalAccumulator = 0; + + public EconomizerAverager(long expirationInterval) { + super(expirationInterval); + } + + @Override + protected void accumulate(ZoneChartDataPoint value) { + + ambientAccumulator += value.economizerStatus.ambient.getValue(); + signalAccumulator += value.economizerStatus.callingStatus.sample; + } + + @Override + protected EconomizerTintedValue complete(ZoneChartDataPoint value, int count) { + + var result = new EconomizerTintedValue(ambientAccumulator / count, signalAccumulator / count); + + ambientAccumulator = value.economizerStatus.ambient.getValue(); + signalAccumulator = value.economizerStatus.callingStatus.sample; return result; } diff --git a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/BackgroundRenderer.java b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/BackgroundRenderer.java index ef98d48cb..d97f74c89 100644 --- a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/BackgroundRenderer.java +++ b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/BackgroundRenderer.java @@ -71,7 +71,7 @@ public static void drawBottom(Zone.State state, HvacMode mode, Double signal, Gr g2d.setPaint(bgColor); - Rectangle2D.Double background = new Rectangle2D.Double(boundary.x, boundary.y, boundary.width, boundary.height); + var background = new Rectangle2D.Double(boundary.x, boundary.y, boundary.width, boundary.height); g2d.fill(background); return; @@ -91,19 +91,19 @@ public static void drawBottom(Zone.State state, HvacMode mode, Double signal, Gr scale /= 2; } - int startHeight = (int)(boundary.height * scale); + var startHeight = (int)(boundary.height * scale); startHeight = startHeight > 0 ? startHeight : 1; - Color startColor = getBottomColor(state, mode); - Color endColor = ColorScheme.getScheme(mode).background; + var startColor = getBottomColor(state, mode); + var endColor = ColorScheme.getScheme(mode).background; - GradientPaint gp = new GradientPaint( + var gp = new GradientPaint( boundary.x, startHeight, endColor, boundary.x, boundary.height, startColor); g2d.setPaint(gp); - Rectangle2D.Double gradient = new Rectangle2D.Double( + var gradient = new Rectangle2D.Double( boundary.x, boundary.y, boundary.width, boundary.height); diff --git a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/EconomizerTintedValue.java b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/EconomizerTintedValue.java new file mode 100644 index 000000000..277f6f7e7 --- /dev/null +++ b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/EconomizerTintedValue.java @@ -0,0 +1,33 @@ +package net.sf.dz3r.view.swing.zone; + +/** + * Intended to hold an economizer data sample for the {@link AbstractZoneChart variable color chart}. + * + * @author Copyright © Vadim Tkachenko 2001-2023 + * + * @see ThermostatTintedValue + */ +public class EconomizerTintedValue { + + /** + * Ambient temperature. + */ + public final double ambient; + + /** + * Control signal. + */ + public final double signal; + + public EconomizerTintedValue(double ambient, double signal) { + + this.ambient = ambient; + this.signal = signal; + } + + @Override + public String toString() { + + return "{ambient=" + ambient + ", signal=" + signal + "}"; + } +} diff --git a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/SignalColorCache.java b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/SignalColorCache.java new file mode 100644 index 000000000..e623a61ac --- /dev/null +++ b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/SignalColorCache.java @@ -0,0 +1,151 @@ +package net.sf.dz3r.view.swing.zone; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.Color; + +public class SignalColorCache { + + private final Logger logger = LogManager.getLogger(); + + /** + * Color corresponding to {@code -1} signal value. + */ + private final Color low; + + /** + * Color corresponding to {@code 1} signal value. + */ + private final Color high; + private final Color[] signalCache = new Color[256]; + + /** + * Cache medium for {@link #resolve(Color)}. + * + * According to "worse is better" rule, there's no error checking against + * the array size - too expensive. In all likelihood, this won't grow beyond 2 entries. + */ + private final RGB2HSB[] rgb2hsb = new RGB2HSB[16]; + + public SignalColorCache(Color low, Color high) { + this.low = low; + this.high = high; + } + + /** + * Convert signal from -1 to +1 to color from low color to high color. + * + * @param signal Signal to convert to color. + * + * @return Color resolved from the incoming signal. + */ + public Color signal2color(double signal) { + return signal2color(signal, 0xFF); + } + + /** + * Convert signal from -1 to +1 to color from low color to high color with a given alpha channel value. + * + * @param signal Signal to convert to color. + * @param alpha Alpha channel value to apply. + * + * @return Color resolved from the incoming signal and alpha channel. + */ + public Color signal2color(double signal, int alpha) { + + var limited = signal > 1 ? 1: signal; + limited = limited < -1 ? -1 : limited; + var centered = (limited + 1) / 2; + + int index = (int) (centered * 255); + + synchronized (signalCache) { + + Color result = signalCache[index]; + + if ( result == null) { + + float[] hsbLow = resolve(low); + float[] hsbHigh = resolve(high); + + float h = transform(centered, hsbLow[0], hsbHigh[0]); + float s = transform(centered, hsbLow[1], hsbHigh[1]); + float b = transform(centered, hsbLow[2], hsbHigh[2]); + + result = new Color(((alpha & 0xFF) << 24) | Color.HSBtoRGB(h, s, b)); + signalCache[index] = result; + + logger.debug("signal2color({}, {}): {} -> {}", + Integer.toHexString(low.getRGB()), + Integer.toHexString(high.getRGB()), + signal, Integer.toHexString(result.getRGB())); + } + + return result; + } + } + + /** + * Resolve a possibly cached {@link Color#RGBtoHSB(int, int, int, float[])} result, + * or compute it and store it for later retrieval if it hasn't been done. + * + * @param color Color to transform. + * @return Transformation result. + */ + private float[] resolve(Color color) { + + var rgb = color.getRGB(); + var offset = 0; + + for (; offset < rgb2hsb.length && rgb2hsb[offset] != null; offset++) { + + if (rgb == rgb2hsb[offset].rgb) { + return rgb2hsb[offset].hsb; + } + } + + synchronized (rgb2hsb) { + + // VT: NOTE: Not the cleanest solution. It is possible that someone has just tried to do the same thing + // and we'll end up writing the same value twice, but oh well, it's the same value in an array of size 16 + + rgb2hsb[offset] = new RGB2HSB(rgb, Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null)); + } + + logger.info("RGB2HSB offset={}", offset ); + + return rgb2hsb[offset].hsb; + } + + /** + * Get the point between the start and end values corresponding to the value of the signal. + * + * @param signal Signal value, from -1 to +1. + * @param start Start point. + * @param end End point. + * + * @return Desired position between the start and end points. + */ + private float transform(double signal, float start, float end) { + + if (signal < -1.0 || signal > 1.0) { + throw new IllegalArgumentException("signal (" + signal + ") is outside of -1...1 range "); + } + + return (float) (start + signal * (end - start)); + } + + private static class RGB2HSB { + + public final int rgb; + public final float[] hsb; + + public RGB2HSB(int rgb, float[] hsb) { + + this.rgb = rgb; + this.hsb = hsb; + } + } + +} diff --git a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/TintedValue.java b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ThermostatTintedValue.java similarity index 61% rename from dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/TintedValue.java rename to dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ThermostatTintedValue.java index 0338aa1b6..6232b2564 100644 --- a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/TintedValue.java +++ b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ThermostatTintedValue.java @@ -1,11 +1,13 @@ package net.sf.dz3r.view.swing.zone; /** - * Intended to hold a data sample for the {@link AbstractZoneChart variable color chart}. + * Intended to hold a thermostat data sample for the {@link AbstractZoneChart variable color chart}. * - * @author Copyright © Vadim Tkachenko 2001-2020 + * @author Copyright © Vadim Tkachenko 2001-2023 + * + * @see EconomizerTintedValue */ -public class TintedValue { +public class ThermostatTintedValue { /** * Value Y coordinate on the chart. @@ -25,10 +27,16 @@ public class TintedValue { */ public final boolean emphasize; - public TintedValue(double value, double tint, boolean emphasize) { + public ThermostatTintedValue(double value, double tint, boolean emphasize) { this.value = value; this.tint = tint; this.emphasize = emphasize; } + + @Override + public String toString() { + + return "{value=" + value + ", tint=" + tint + ", emphasize=" + emphasize + "}"; + } } diff --git a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/TintedValueAndSetpoint.java b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/TintedValueAndSetpoint.java deleted file mode 100644 index c20a4742c..000000000 --- a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/TintedValueAndSetpoint.java +++ /dev/null @@ -1,34 +0,0 @@ -package net.sf.dz3r.view.swing.zone; - -/** - * Intended to hold a data sample for the {@link AbstractZoneChart variable color chart}. - * - * @author Copyright © Vadim Tkachenko 2001-2020 - */ -public class TintedValueAndSetpoint extends TintedValue { - - /** - * Setpoint Y coordinate on the chart. - * - * VT: FIXME: Makes little sense in the context of a single value, a prime candidate for optimization. - */ - public final double setpoint; - - public TintedValueAndSetpoint(double value, double tint, boolean emphasize, double setpoint) { - - super(value, tint, emphasize); - - this.setpoint = setpoint; - } - - @Override - public String toString() { - - StringBuilder sb = new StringBuilder(); - - sb.append("TintedValue(").append(value).append(", ").append(tint).append(", ").append(emphasize).append(", ").append(setpoint); - sb.append(")"); - - return sb.toString(); - } -} diff --git a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ZoneChart2021.java b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ZoneChart2021.java index ee6679553..d6fa48587 100644 --- a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ZoneChart2021.java +++ b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ZoneChart2021.java @@ -9,13 +9,24 @@ import java.awt.Insets; import java.time.Clock; import java.time.Instant; -import java.util.Iterator; -import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; public class ZoneChart2021 extends AbstractZoneChart { - private transient Averager averager; + private transient ThermostatAverager thermostatAverager; + private transient EconomizerAverager economizerAverager; + + /** + * Thermostat signal to color cache. + */ + private final transient SignalColorCache thermostatSignalCache = new SignalColorCache(SIGNAL_COLOR_LOW, SIGNAL_COLOR_HIGH); + + /** + * Economizer signal to color cache. Note that low and high colors are reversed due to economizer logic + */ + private final transient SignalColorCache economizerSignalCache = new SignalColorCache(ECO_COLOR_HIGH, ECO_COLOR_LOW); + + private final static int ECO_ALPHA = 0x80; protected ZoneChart2021(Clock clock, long chartLengthMillis, boolean needFahrenheit) { super(clock, chartLengthMillis, needFahrenheit); @@ -29,9 +40,35 @@ protected void update() { } } - private boolean append(Signal signal) { + private boolean append(Signal signal) { + + // Economizer signal may be unavailable, either yet, or at all + + Double ambient; + Double target; + + if (signal.getValue().economizerStatus == null) { + + ambient = null; + target = null; - adjustVerticalLimits(signal.timestamp.toEpochMilli(), signal.getValue().value, signal.getValue().setpoint); + } else { + + logger.trace("eco: {}", signal.getValue().economizerStatus); + + ambient = signal.getValue().economizerStatus.ambient == null + ? null + : signal.getValue().economizerStatus.ambient.getValue(); + + target = signal.getValue().economizerStatus.settings.targetTemperature; + } + + adjustVerticalLimits( + signal.timestamp.toEpochMilli(), + signal.getValue().tintedValue.value, + signal.getValue().setpoint, + ambient, + target); synchronized (AbstractZoneChart.class) { @@ -45,7 +82,8 @@ private boolean append(Signal signal) { logger.info("new width {}, {}ms per pixel", localWidth, step); // We lose one sample this way, might want to improve it later, for now, no big deal - averager = new Averager(step); + thermostatAverager = new ThermostatAverager(step); + economizerAverager = new EconomizerAverager(step); return true; } @@ -56,30 +94,66 @@ private boolean append(Signal signal) { // There's nothing we can do before the width is set. // It's not even worth it to record the value. - logger.info("please repaint"); + // VT: NOTE: This used to be a pretty often encountered race condition. Nowadays, it's a sign of a programming error. + + logger.info("please repaint (is everything all right?)"); return true; } - var tintedValue = averager.append(signal); + // VT: NOTE: Imperative code sucks here, it would be nice to rewrite the averagers as reactive + // and flatmap the hell out of null values - if (tintedValue == null) { - // The average is still being calculated, nothing to do - return false; - } + // These two may be non-null at different times, must analyze them separately + var thermostatTintedValue = thermostatAverager.append(signal); + var economizerTintedValue = (signal.getValue().economizerStatus == null || signal.getValue().economizerStatus.ambient == null) + ? null + : economizerAverager.append(signal); + logger.trace("thermostatTintedValue={}", thermostatTintedValue); + logger.trace("economizerTintedValue={}", economizerTintedValue); + + var timestamp = signal.timestamp.toEpochMilli(); + + // VT: NOTE: Write lock is acquired once per all sets, it's a short operation var lockNow = Instant.now().toEpochMilli(); - lockValues.writeLock().lock(); + lock.writeLock().lock(); try { logger.debug("write lock acquired in {}ms", Instant.now().toEpochMilli() - lockNow); - dsValues.append(signal.timestamp.toEpochMilli(), tintedValue, true); + + var thermostatChange = capture(timestamp, thermostatTintedValue, signal.getValue().setpoint); + var economizerChange =capture(timestamp, economizerTintedValue, signal.getValue().economizerStatus.settings.targetTemperature); + + return thermostatChange || economizerChange; } finally { - lockValues.writeLock().unlock(); + lock.writeLock().unlock(); } + } + + private boolean capture(long timestamp, ThermostatTintedValue thermostatTintedValue, double setpoint) { - dsSetpoints.append(signal.timestamp.toEpochMilli(), signal.getValue().setpoint, true); + if (thermostatTintedValue == null) { + // The average is still being calculated, nothing to do + return false; + } + + dsValues.append(timestamp, thermostatTintedValue, true); + dsSetpoints.append(timestamp, setpoint, true); + + return true; + } + + private boolean capture(long timestamp, EconomizerTintedValue economizerTintedValue, double target) { + + if (economizerTintedValue == null) { + // The average is still being calculated, nothing to do + return false; + } + + dsEconomizer.append(timestamp, economizerTintedValue, true); + dsTargets.append(timestamp, target, true); return true; } @@ -87,29 +161,108 @@ private boolean append(Signal signal) { @Override protected void paintChart(Graphics2D g2d, Dimension boundary, Insets insets, long now, double xScale, long xOffset, double yScale, double yOffset, - DataSet dsValues, ReadWriteLock lockValues, DataSet dsSetpoints) { + DataSet dsValues, DataSet dsEconomizer, ReadWriteLock lock, + DataSet dsTargets, DataSet dsSetpoints) { + + // Layer order: economizer, thermostat, economizer target, setpoint + + paintEconomizerValues(g2d, insets, now, xScale, xOffset, yScale, yOffset, dsEconomizer, lock); + paintThermostatValues(g2d, insets, now, xScale, xOffset, yScale, yOffset, dsValues, lock); - // Setpoint history is rendered over the value history - paintValues(g2d, insets, now, xScale, xOffset, yScale, yOffset, dsValues, lockValues); + paintTargets(g2d, insets, xScale, xOffset, yScale, yOffset, dsTargets); paintSetpoints(g2d, insets, xScale, xOffset, yScale, yOffset, dsSetpoints); } @SuppressWarnings("squid:S107") - private void paintValues(Graphics2D g2d, Insets insets, - long now, double xScale, long xOffset, double yScale, double yOffset, - DataSet ds, ReadWriteLock lockValues) { + private void paintEconomizerValues(Graphics2D g2d, Insets insets, + long now, double xScale, long xOffset, double yScale, double yOffset, + DataSet ds, ReadWriteLock lock) { + + var lockNow = Instant.now().toEpochMilli(); + + lock.readLock().lock(); + try { + + logger.debug("read/eco lock acquired in {}ms", Instant.now().toEpochMilli() - lockNow); + + Long timeTrailer = null; + EconomizerTintedValue trailer = null; + + for (var di = ds.entryIterator(); di.hasNext(); ) { + + var entry = di.next(); + var timeNow = entry.getKey(); + var cursor = entry.getValue(); + + if (timeTrailer != null) { + + var x0 = (timeTrailer - xOffset) * xScale + insets.left; + var y0 = (yOffset - trailer.ambient) * yScale + insets.top; + + var x1 = (timeNow - xOffset) * xScale + insets.left; + var y1 = (yOffset - cursor.ambient) * yScale + insets.top; + + // Decide whether the line is alive or dead + + if (timeNow - timeTrailer > DEAD_TIMEOUT.toMillis()) { + + // It's dead, all right + // Paint the horizontal line in dead color and skew the x0 so the next part will be painted vertical + var startColor = economizerSignalCache.signal2color(trailer.signal - 1, ECO_ALPHA); + + // End color differs from the start in alpha, not hue - this plays nicer with backgrounds + // Even though this is a memory allocation, it won't affect performance since [hopefully] + // there'll be just a few dead drops + var endColor = new Color(startColor.getRed(), startColor.getGreen(), startColor.getBlue(), 0x40); + + drawGradientLine(g2d, x0, y0, x1, y0, startColor, endColor, false); + + x0 = x1; + } + + var startColor = economizerSignalCache.signal2color(trailer.signal - 1, ECO_ALPHA); + var endColor = economizerSignalCache.signal2color(cursor.signal - 1, ECO_ALPHA); + + drawGradientLine(g2d, x0, y0, x1, y1, startColor, endColor, false); + } + + timeTrailer = timeNow; + trailer = cursor; + } + + if (timeTrailer != null && now - timeTrailer > DEAD_TIMEOUT.toMillis()) { + + // There's a gap on the right, let's fill it + + var x0 = (timeTrailer - xOffset) * xScale + insets.left; + var x1 = (now - xOffset) * xScale + insets.left; + var y = (yOffset - trailer.ambient) * yScale + insets.top; + + var startColor = economizerSignalCache.signal2color(trailer.signal - 1, ECO_ALPHA); + var endColor = getBackground(); + + drawGradientLine(g2d, x0, y, x1, y, startColor, endColor, false); + } + } finally { + lock.readLock().unlock(); + } + } + + private void paintThermostatValues(Graphics2D g2d, Insets insets, + long now, double xScale, long xOffset, double yScale, double yOffset, + DataSet ds, ReadWriteLock lock) { var lockNow = Instant.now().toEpochMilli(); - lockValues.readLock().lock(); + lock.readLock().lock(); try { - logger.debug("read lock acquired in {}ms", Instant.now().toEpochMilli() - lockNow); + logger.debug("read/values lock acquired in {}ms", Instant.now().toEpochMilli() - lockNow); Long timeTrailer = null; - TintedValue trailer = null; + ThermostatTintedValue trailer = null; - for (Iterator> di = ds.entryIterator(); di.hasNext(); ) { + for (var di = ds.entryIterator(); di.hasNext(); ) { var entry = di.next(); var timeNow = entry.getKey(); @@ -129,20 +282,20 @@ private void paintValues(Graphics2D g2d, Insets insets, // It's dead, all right // Paint the horizontal line in dead color and skew the x0 so the next part will be painted vertical - var startColor = signal2color(trailer.tint - 1, SIGNAL_COLOR_LOW, SIGNAL_COLOR_HIGH); + var startColor = thermostatSignalCache.signal2color(trailer.tint - 1); // End color differs from the start in alpha, not hue - this plays nicer with backgrounds // Even though this is a memory allocation, it won't affect performance since [hopefully] // there'll be just a few dead drops - var endColor = new Color(startColor.getRed(), startColor.getGreen(), startColor.getBlue(), 64); + var endColor = new Color(startColor.getRed(), startColor.getGreen(), startColor.getBlue(), 0x40); drawGradientLine(g2d, x0, y0, x1, y0, startColor, endColor, cursor.emphasize); x0 = x1; } - var startColor = signal2color(trailer.tint - 1, SIGNAL_COLOR_LOW, SIGNAL_COLOR_HIGH); - var endColor = signal2color(cursor.tint - 1, SIGNAL_COLOR_LOW, SIGNAL_COLOR_HIGH); + var startColor = thermostatSignalCache.signal2color(trailer.tint - 1); + var endColor = thermostatSignalCache.signal2color(cursor.tint - 1); drawGradientLine(g2d, x0, y0, x1, y1, startColor, endColor, cursor.emphasize); } @@ -159,13 +312,13 @@ private void paintValues(Graphics2D g2d, Insets insets, var x1 = (now - xOffset) * xScale + insets.left; var y = (yOffset - trailer.value) * yScale + insets.top; - var startColor = signal2color(trailer.tint - 1, SIGNAL_COLOR_LOW, SIGNAL_COLOR_HIGH); + var startColor = thermostatSignalCache.signal2color(trailer.tint - 1); var endColor = getBackground(); drawGradientLine(g2d, x0, y, x1, y, startColor, endColor, false); } } finally { - lockValues.readLock().unlock(); + lock.readLock().unlock(); } } @@ -173,12 +326,27 @@ private void paintSetpoints(Graphics2D g2d, Insets insets, double xScale, long xOffset, double yScale, double yOffset, DataSet ds) { - var startColor = new Color(SETPOINT_COLOR.getRed(), SETPOINT_COLOR.getGreen(), SETPOINT_COLOR.getBlue(), 64); - var endColor = SETPOINT_COLOR; // NOSONAR Retained for clarity + paintSetpointLines(g2d, insets, xScale, xOffset, yScale, yOffset, ds, SETPOINT_COLOR); + } + private void paintTargets(Graphics2D g2d, Insets insets, + double xScale, long xOffset, double yScale, double yOffset, + DataSet ds) { + + paintSetpointLines(g2d, insets, xScale, xOffset, yScale, yOffset, ds, TARGET_COLOR); + } + private void paintSetpointLines(Graphics2D g2d, Insets insets, + double xScale, long xOffset, double yScale, double yOffset, + DataSet ds, + Color baseColor) { + + var startColor = new Color(baseColor.getRed(), baseColor.getGreen(), baseColor.getBlue(), 64); Long timeTrailer = null; - for (Iterator> di = ds.entryIterator(); di.hasNext();) { + // VT: NOTE: This iterator is not protected by the read lock, the probability of concurrent access is much lower. + // If it starts blowing up, though... + + for (var di = ds.entryIterator(); di.hasNext();) { var entry = di.next(); var timeNow = entry.getKey(); @@ -197,7 +365,7 @@ private void paintSetpoints(Graphics2D g2d, Insets insets, x1 = (timeNow - xOffset) * xScale + insets.left; } - drawGradientLine(g2d, x0, y, x1, y, startColor, endColor, false); + drawGradientLine(g2d, x0, y, x1, y, startColor, baseColor, false); timeTrailer = timeNow; } diff --git a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ZoneChartDataPoint.java b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ZoneChartDataPoint.java new file mode 100644 index 000000000..219f6986d --- /dev/null +++ b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ZoneChartDataPoint.java @@ -0,0 +1,38 @@ +package net.sf.dz3r.view.swing.zone; + +import net.sf.dz3r.signal.hvac.EconomizerStatus; + +/** + * {@link AbstractZoneChart} data point wrapper. + * + * @author Copyright © Vadim Tkachenko 2001-2023 + */ +public class ZoneChartDataPoint { + + public final ThermostatTintedValue tintedValue; + + /** + * Setpoint Y coordinate on the chart. + * + * VT: FIXME: Makes little sense in the context of a single value, a prime candidate for optimization. + */ + public final double setpoint; + + public final EconomizerStatus economizerStatus; + + public ZoneChartDataPoint( + ThermostatTintedValue tintedValue, + double setpoint, + EconomizerStatus economizerStatus) { + + this.tintedValue = tintedValue; + this.setpoint = setpoint; + this.economizerStatus = economizerStatus; + } + + @Override + public String toString() { + + return "{tintedValue=" + tintedValue + ", setpoint=" + setpoint + ", economizer=" + economizerStatus + "}"; + } +} diff --git a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ZonePanel.java b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ZonePanel.java index 4d48712c0..4bc6d0683 100644 --- a/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ZonePanel.java +++ b/dz3r-swing/src/main/java/net/sf/dz3r/view/swing/zone/ZonePanel.java @@ -535,14 +535,16 @@ private void updateChart() { return; } - var tint = new TintedValueAndSetpoint( - sensorSignal.getValue(), - zoneStatus.callingStatus.demand * 2, - zoneStatus.callingStatus.calling, - zoneStatus.settings.setpoint); + var dataPoint = new ZoneChartDataPoint( + new ThermostatTintedValue( + sensorSignal.getValue(), + zoneStatus.callingStatus.demand * 2, + zoneStatus.callingStatus.calling), + zoneStatus.settings.setpoint, + zoneStatus.economizerStatus); // VT: FIXME: This must be driven via Flux - chart.consumeSignal(new Signal<>(getSignal().timestamp, tint)); + chart.consumeSignal(new Signal<>(getSignal().timestamp, dataPoint)); } private HvacMode getMode() {