/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package de.root1.kad.logic.lib.blindscontrol; import de.root1.kad.knxservice.KnxServiceException; import de.root1.kad.logicplugin.Logic; import de.root1.kad.logicplugin.LogicKnxEventListener; import de.root1.sunposition.SunPosition; import java.text.DecimalFormat; /** * Sonnenstandsgeführte Steuerung von Lamellen-Jalousien und Rolläden * * @author achristian */ public class BlindsControl implements LogicKnxEventListener { private static final DecimalFormat df = new DecimalFormat("##0.00"); private static final String MOVE_UP = "0"; private static final String MOVE_DOWN = "1"; private enum STATE { } /** * Reference to controlling logic */ private final Logic logic; /** * Name or if of this blinds control. only used for logging! */ private final String identification; /** * Sun position calculator */ private final SunPosition sunPosition; // Eingänge Beschattungssteuerung /** * Enables the control-output */ private boolean enabled; /** * Azimuth window FROM */ private double azimuthFrom; /** * Azimuth window TO */ private double azimuthTo; /** * Altitude window FROM */ private int altitudeFrom; /** * Altitude window TO */ private int altitudeTo; /** * Current azimuth value */ private double currentAzimuth; /** * current altitude value */ private double currentAltitude; /** * Flag telling the control if it is warm or sunny (additional flag that is * required to finally control the blinds). if <code>!warmOrSunny</code>, * then control is not doing anything. */ private boolean warmOrSunny; /** * Set when shading is required, unset when sun leaves window * (currentAzimuth > azimuthTo) */ private boolean shadingCycle = false; public void setSlatsMinimumChange(int slatsMinimumChange) { this.slatsMinimumChange = slatsMinimumChange; } public void setSlatsHorizontalPosition(int slatsHorizontalPosition) { this.slatsHorizontalPosition = slatsHorizontalPosition; } public void setSlatsVerticalPosition(int slatsVerticalPosition) { this.slatsVerticalPosition = slatsVerticalPosition; } public void setBlindsVerticalPositionTolerance(int blindsVerticalPositionTolerance) { this.blindsVerticalPositionTolerance = blindsVerticalPositionTolerance; } public void setSlatsStandbyPosition(int slatsStandbyPosition) { this.slatsStandbyPosition = slatsStandbyPosition; } public void setSlatsPositionOffset(int slatsPositionOffset) { this.slatsPositionOffset = slatsPositionOffset; } public void setBlindsSunPosition(int blindsSunPosition) { this.blindsSunPosition = blindsSunPosition; } /** * Group address for slat control */ private String gaBlindsSlat; /** * Group address for position control */ private String gaBlindsPosition; /** * Group address for current blinds position */ private String gaCurrentBlindsPosition; // Eingänge Lamellen-Nachführung // ------------------------------------- /** * Mindeständerung in % für eine Lamellenänderung (typisch 2%) */ int slatsMinimumChange = 2; /** * Waagerechte Position der Lamellen in Prozent (typisch 50%) */ int slatsHorizontalPosition = 50; /** * Senkrechte Position der Lamellen in Prozent (typisch 100%) */ int slatsVerticalPosition = 100; /** * Toleranzfenster für Behang-Position in absoluten Prozent (typisch 5%) */ int blindsVerticalPositionTolerance = 5; /** * Standby-Wert Lamellenposition (typisch 50%) Wird angewendet, wenn Sonne * noch im Zyklus des Fensters, aber keine Verschattung notwendig */ int slatsStandbyPosition = 50; /** * Lamellenposition Offset +/- in absoluten Prozent (typisch 0) Wird auf die * errechnete Lamellenposition aufaddiert. */ int slatsPositionOffset = 0; /** * Behang % bei Verschattung (typisch 100) */ int blindsSunPosition = 100; /** * Behang Ist (in Prozent). Wird beim Start aktiv abgefragt und wird am Bus * überwacht. */ private int currentBlindsPosition; /** * Letzter gesendeter Lamellenwinkel. Dient zum Vergleichen mit letzten * Winkel für <code>slatsMinimumChange</code> */ private double lastSlatsPosition = -1; /** * Creates a new blinds control object. * * @param logic reference to logic to have KNX access * @param identification an identifier for logging status etc. * @param sunPosition the sun position calculator */ public BlindsControl(Logic logic, String identification, SunPosition sunPosition) { if (logic == null) { throw new IllegalArgumentException("Referenc to logic required!"); } if (sunPosition == null) { throw new IllegalArgumentException("Reference to sun position required!"); } if (identification == null || identification.isEmpty()) { throw new IllegalArgumentException("identification required. At least one character!"); } this.logic = logic; this.identification = identification; this.sunPosition = sunPosition; } /** * Begin controlling the blinds */ public void start() { // register listeners for inputs logic.listenTo(gaCurrentBlindsPosition); logic.addListener(this); try { String value = logic.read(gaCurrentBlindsPosition); currentBlindsPosition = Integer.parseInt(value); } catch (KnxServiceException ex) { ex.printStackTrace(); } logic.log.info(id("Starting with current blinds position: {}%"), currentBlindsPosition); } /** * True if control is enabled. * * @return */ public boolean isEnabled() { return enabled; } /** * Enable of disable blinds control */ public void setEnabled(boolean enabled) { this.enabled = enabled; } /** * Sets the altitude window: from..to * * @param from * @param to */ public void setAltitudeWindow(int from, int to) { this.altitudeFrom = from; this.altitudeTo = to; } /** * Sets the azimuth window: from..to * * @param from * @param to */ public void setAzimuthWindow(int from, int to) { this.azimuthFrom = from; this.azimuthTo = to; } /** * Returns true if current known sun position is in window. * * Pseudocode: * <pre> * azimuthFrom <= currentAzimuth <= azimuthTo * AND * altitudeFrom <= currentAltitude <= altitudeTo * </pre> * * @return */ public boolean isInWindow() { return currentAzimuth >= azimuthFrom && currentAzimuth <= azimuthTo && currentAltitude >= altitudeFrom && currentAltitude <= altitudeTo; } /** * Check whether shading is required or not. Pseudocode: * <pre>isInWindow AND isWarmOrSunny</pre> * * @return */ public boolean isShadingRequired() { return isInWindow() && warmOrSunny; } /** * Sets the group address for controlling the slats. DPT5.001 * * @param ga */ public void setGaBlindsSlat(String ga) { this.gaBlindsSlat = ga; } /** * Sets the group address for controlling the absolute blinds position. * DPT5.001 * * @param ga */ public void setGaBlindsPosition(String ga) { this.gaBlindsPosition = ga; } /** * Sets the group address used to listen/query for blinds position. * * @param ga */ public void setGaCurrentBlindsPosition(String ga) { this.gaCurrentBlindsPosition = ga; } /** * Flag that can be used to enable/disable shading, based on sunny/warm * state. Flag can f.i. be triggered by lux level state and/or room * temperature state. If shading should happen in every case, just set to * <code>true</code> * * @param b if true, shading algorithm is applied. if false, shading is not * happening */ public void setWarmOrSunny(boolean b) { this.warmOrSunny = b; } public boolean isWarmOrSunny() { return warmOrSunny; } /** * Update sun position calculation and control blinds and slats if required */ public void update() { SunPosition.Sun calcGMT = sunPosition.calcGMT(); this.currentAzimuth = calcGMT.getAzimuth(); this.currentAltitude = calcGMT.getAltitude(); logic.log.info(id("Sun position: azimuth={}° altitude={}°"), df.format(currentAzimuth), df.format(currentAltitude)); if (enabled) { logic.log.info(id("Blinds control is enabled")); /** * calculate shading cycle if no shading cycle yet + shading * required --> switch to shadingCycle=true if shading cycle and sun * moved out of "window angle" --> switch to shadingCycle=false */ if (!shadingCycle && isShadingRequired()) { shadingCycle = true; } else if (shadingCycle && currentAzimuth > azimuthTo) { shadingCycle = false; } logic.log.info(id("Shading cycle: {}"), shadingCycle); if (isInWindow()) { logic.log.info(id("Sun in window!")); // Prüfen ob Behang in Sonnenposition ist boolean inVerticalPosition = false; logic.log.info(id("Current blinds position: {}%"), currentBlindsPosition); if (this.slatsVerticalPosition - currentBlindsPosition <= blindsVerticalPositionTolerance) { inVerticalPosition = true; } logic.log.info(id("Blinds in shade position?: {}"), inVerticalPosition); if (isShadingRequired()) { logic.log.info(id("Shading required")); // Lamellenwinkel berechnen // 100 = 100% double slatsPercentage = (90d - currentAltitude) * ((slatsVerticalPosition - slatsHorizontalPosition) / 90d) + slatsHorizontalPosition + slatsPositionOffset; //winkel begrenzen auf senkrechten Wert if (slatsPercentage > slatsVerticalPosition) { logic.log.info(id("Limit slats to vertical value")); slatsPercentage = slatsVerticalPosition; } logic.log.info(id("Calculated slats: {}%"), slatsPercentage); // Behang abfahren, wenn Zyklus und noch nicht unten if (shadingCycle && !inVerticalPosition) { logic.log.info(id("Move blinds to sun position: {}%"), blindsSunPosition); try { logic.write(gaBlindsPosition, String.valueOf(blindsSunPosition)); } catch (KnxServiceException ex) { ex.printStackTrace(); } } // Behang auffahren, wenn !Zyklus -> 0 if (!shadingCycle && inVerticalPosition) { logic.log.info(id("Move blinds to 0%")); try { logic.write(gaBlindsPosition, String.valueOf(0)); } catch (KnxServiceException ex) { ex.printStackTrace(); } } // Lamelle stellen wenn Behang in Beschattungsposition gegangen ist if (inVerticalPosition) { // only change slats if there is an GA AND if minimum change is reached if (gaBlindsSlat!=null && (lastSlatsPosition == -1 || Math.abs(lastSlatsPosition - slatsPercentage) >= slatsMinimumChange)) { logic.log.info(id("Setting slats to: {}%"), df.format((int)slatsPercentage)); try { logic.write(gaBlindsSlat, String.valueOf(slatsPercentage)); lastSlatsPosition = slatsPercentage; } catch (KnxServiceException ex) { ex.printStackTrace(); } } else { logic.log.info(id("Slats change too small: {} vs. required +-{}. No change at all."), lastSlatsPosition - slatsPercentage, slatsMinimumChange); } } // Lamelle stellen, wenn Inkl.Tel UND SonnenPos UND Beschattung UND Änderung >= Mindest // Lamelle öffnen, wenn Beschattungsposition UND ! -> 0 // Behang auffahren wenn Steuereingang = 1 und B! -> 0 } else { // Sonne im Fenster, aber keine Beschattung notwendig. Fahre Lamellen auf Standby Position wenn noch im Zyklus String msg = "Beschattung nicht notwendig. "; String pos = ""; if (shadingCycle) { msg += "Noch im Zyklus, Standby anfahren."; // standby position anfahren pos = String.valueOf(slatsStandbyPosition); } logic.log.info(id(msg)); setSlats(pos); } } } } private void setSlats(String pos) { if (gaBlindsSlat != null) { try { logic.write(gaBlindsSlat, pos); } catch (KnxServiceException ex) { ex.printStackTrace(); } } else { // skip setting slats, as no address is avilable. Maybe its a blind only?! } } @Override public void onData(String ga, String value, Logic.TYPE type) throws KnxServiceException { if (type == Logic.TYPE.WRITE && ga.equals(gaCurrentBlindsPosition)) { // blinds position received, update slate angle if required currentBlindsPosition = Integer.parseInt(value); logic.log.info(id("Got event for ga=[{}] blinds-position={}%"), ga, value); update(); } } private String id(String string) { return "[" + identification + "] " + string; } }