/*
* 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;
}
}