public static RunConfig getInstance() {
	if (runConfig == null) {
		throw new IllegalStateException("Initialize first");
	}
	return runConfig;
} public static void initialize(String localIp) {
	setProperty("calimero.knxnetip.tunneling.resyncSkippedRcvSeq", "true");

	runConfig = new RunConfig(localIp);
	runConfig.initialize();
} public static GroupAddress createGroupAddress(String groupAddress) {
	try {
		return new GroupAddress(groupAddress);
	} catch (KNXException knxException) {
		logger.error("Could not create group address from " + groupAddress, knxException);
		throw new RuntimeException(knxException);
	}
} public class AdcController {

	private static final Logger logger = LoggerFactory.getLogger(AdcController.class);

	private static final ExecutorService adcPool = newFixedThreadPool(4);

	public void run() throws Exception {
		ObjectStatusUdpSender sender = new ObjectStatusUdpSender(getInstance().getLoxoneIp(), getInstance().getLoxonePort());
		logger.debug("ADC controller starting");

		AdcControllerConfiguration adcControllerConfiguration = new AdcControllerConfiguration();

		int i = 0;
		while (true) {
			List<CompletableFuture<List<Pair<AdcChannel, ObjectStatusType>>>> futures = new ArrayList();

			for (AdcBoard adcBoard : adcControllerConfiguration.getAdcBoards(getInstance().getBus())) {
				futures.add(supplyAsync(() -> adcBoard.read(new ObjectStatusReader()), adcPool));
			}

			List<Pair<AdcChannel, ObjectStatusType>> results = allOf(futures.toArray(new CompletableFuture[0])).thenApply(v -> futures.stream().flatMap(f -> {
				try {
					return f.get().stream();
				} catch (Exception e) {
					logger.error("AdcController could not complete future", e);
					throw new RuntimeException();
				}
			})).get().collect(toList());

			sleep(1000);

			//Send internally
			getInstance().postAdcEvent(results.stream().map(p -> of(p.getLeft().getId(), p.getRight())).collect(toList()));

			//Only send every 2 seconds via UDP
			i++;
			if (i == 2) {
				sender.send(results);
				i = 0;
			}
		}
	}
} public class Dimmer extends Thread {

	private static final Logger logger = LoggerFactory.getLogger(Dimmer.class);
	private static final BigDecimal ZERO = new BigDecimal("0.0");

	private long turnOffDelay = 2000;
	private long delayBeforeIncreasingDimValue = 300;
	private int stepDelay = 30;

	private List<GroupAddress> feedbackGroupAddresses = new ArrayList();
	private List<GroupAddress> switchLedControlGroupAddresses = new ArrayList();
	private List<GroupAddress> switchGroupAddresses = new ArrayList();
	private List<GroupAddress> outputSwitchUpdateGroupAddresses = new ArrayList<>();

	private BigDecimal curVal = ZERO;
	private BigDecimal sentVal = ZERO;

	private BigDecimal minDimValue = new BigDecimal("1.0");
	private DimDirection lastDimDirection;

	private final AtomicBoolean interupt = new AtomicBoolean(false);
	private final BlockingQueue<DimmerCommand> commandQueue = new LinkedBlockingDeque();

	private final LocationId dimmerName;
	private final DimmerBackend dimmerBackend;
	private final int boardAddress;
	private final int channel;

	private KnxDimmerProcessListener knxDimmerProcessListener;

	private Optional<DimmerCommand> lastDimCommand = empty();
	private Optional<DimmerCommand> activeScene = empty();

	public Dimmer(LocationId dimmerName, DimmerBackend dimmerBackend, int boardAddress, int channel, List<GroupAddress> switchGroupAddresses,
			List<GroupAddress> feedbackGroupAddresses, List<GroupAddress> switchLedControlGroupAddresses, List<GroupAddress> outputSwitchUpdateGroupAddresses)
			throws IOException {

		this.dimmerName = dimmerName;
		this.dimmerBackend = dimmerBackend;
		this.boardAddress = boardAddress;
		this.channel = channel;
		this.switchGroupAddresses = switchGroupAddresses;
		this.feedbackGroupAddresses = feedbackGroupAddresses;
		this.switchLedControlGroupAddresses = switchLedControlGroupAddresses;
		this.outputSwitchUpdateGroupAddresses = outputSwitchUpdateGroupAddresses;
	}

	public void run() {
		while (true) {
			try {
				DimmerCommand command = commandQueue.take();

				KnxConnectionFactory.getInstance().runWithProcessCommunicator(pc -> {
					DimmerCommand dimmerCommand = command;
					interupt.set(false);
					boolean feedbackSend = false;

					if (dimmerCommand.getSceneContext().isPresent()) {
						if (dimmerCommand.getSceneContext().get().isSceneActive()) {
							lastDimCommand = empty();
							activeScene = of(dimmerCommand);
							activateSceneLeds(pc);
						} else {
							activeScene = empty();
							dimmerCommand = dimmerCommand.isUseThisDimCommandOnSceneDeativate() ? dimmerCommand : lastDimCommand.orElse(dimmerCommand);
						}
					} else {
						lastDimCommand = of(dimmerCommand);
						if ((dimmerCommand.getTargetVal().compareTo(ZERO) == 0)) {
							if ((activeScene.isPresent())) {
								activateSceneLeds(pc);
								dimmerCommand = activeScene.get();
								sendDimmerFeedback(pc, dimmerCommand.getTargetVal().intValue(), true);
								feedbackSend = true;
							}
						} else {
							sendStatusFeedback(true, !command.isActicatedByPrecense(), pc);
							sleep(delayBeforeIncreasingDimValue);
						}
					}

					if (dimmerCommand.getTargetVal().compareTo(curVal) > 0) {
						lastDimDirection = UP;
					} else if (dimmerCommand.getTargetVal().compareTo(curVal) < 0) {
						lastDimDirection = DOWN;
					} else {
						lastDimDirection = (lastDimDirection == UP ? DOWN : UP);
					}

					while (curVal.compareTo(dimmerCommand.getTargetVal()) != 0) {
						processCommand(dimmerCommand);
						if (commandQueue.peek() != null) {
							break;
						}
						if (interupt.get()) {
							break;
						}
						sleep(stepDelay);
						if (interupt.get()) {
							break;
						}
					}

					if (!feedbackSend && !getInstance().getLoxoneIa().equals(dimmerCommand.getOrigin())) {
						sendDimmerFeedback(pc, getCurVal().intValue(), false);
					}

					if (curVal.compareTo(ZERO) == 0) {
						for (int i = 0; i < turnOffDelay; i++) {
							sleep(1);
							if (interupt.get()) {
								break;
							}
						}

						if (!interupt.get()) {
							sendStatusFeedback(false, !dimmerCommand.isActicatedByPrecense(), pc);
						}
					}
				});
			} catch (InterruptedException e) {
				logger.error("Dimmer " + dimmerName + " got interrupt", e);
				interrupt();
				//Do nothing
			} catch (Exception e) {
				logger.error("Dimmer " + dimmerName + " got exception", e);
				interrupt();
			}
		}
	}

	public synchronized void putCommand(DimmerCommand dimmerCommand) throws Exception {
		commandQueue.clear();
		commandQueue.put(dimmerCommand);
	}

	public void interrupt() {
		interupt.set(true);
	}

	private void sendStatusFeedback(boolean status, boolean sendLedFeedback, ProcessCommunicator pc) throws Exception {
		Collection<GroupAddress> collection = union(switchGroupAddresses, outputSwitchUpdateGroupAddresses);

		if (sendLedFeedback) {
			collection = union(collection, switchLedControlGroupAddresses);
		}

		for (GroupAddress groupAddress : collection) {
			pc.write(groupAddress, status);
		}
	}

	private void activateSceneLeds(ProcessCommunicator pc) throws Exception {
		sendStatusFeedback(false, true, pc);

		for (GroupAddress groupAddress : activeScene.get().getSceneContext().get().getSceneParticipants()) {
			pc.write(groupAddress, true);
		}
	}

	private void sendDimmerFeedback(ProcessCommunicator pc, int val, boolean refresh) throws Exception {
		for (GroupAddress groupAddress : feedbackGroupAddresses) {

			DPTXlator8BitUnsigned dDPTXlator8BitUnsigned = new DPTXlator8BitUnsigned(DPT_PERCENT_U8);
			if (refresh) {
				dDPTXlator8BitUnsigned.setValue(0);
				pc.write(groupAddress, dDPTXlator8BitUnsigned);
			}

			dDPTXlator8BitUnsigned.setValue(val);
			pc.write(groupAddress, dDPTXlator8BitUnsigned);
		}
	}

	private void processCommand(DimmerCommand dimmerCommand) throws IOException {
		if (curVal.compareTo(dimmerCommand.getTargetVal()) < 0) {
			curVal = curVal.add(new BigDecimal("1.0"));
		} else {
			curVal = curVal.subtract(new BigDecimal("1.0"));
		}

		//If curval is below  mindimval we sent mindimval instead. This ensures that the driver does not turn itself of if the dim value gets too low. The driver
		// should be turned off via it's relay (~KNX actor) instead and not by the dimmer input. Curval remains the internal state as it is also used to determine when
		// the relais is to be switched off (namely when curval reaches ZERO). The value actually sent to the DAC is then reflected as sentval (mostly for debug
		// purposes)
		sentVal = curVal.compareTo(minDimValue) < 0 ? minDimValue : curVal;

		dim(sentVal);
	}

	private void dim(BigDecimal targetValue) throws IOException {
		if (dimmerBackend == I2C) {
			byte[] b = convertPercentageToDacBytes(targetValue);
			getInstance().getI2CCommunicator().write(boardAddress, channel, b);
		} else {
			getInstance().doWithLucidControl(boardAddress, (lucidControlAO4) -> {
				try {
					lucidControlAO4.setIo(channel, new ValueVOS2(convertPercentageTo10Volt(targetValue)));
				} catch (IOException e) {
					throw new UncheckedIOException(e);
				}
			});
		}
	}

	public BigDecimal getCurVal() {
		return curVal;
	}

	public DimDirection getLastDimDirection() {
		return lastDimDirection;
	}

	public Optional<DimmerCommand> getLastDimCommand() {
		return lastDimCommand;
	}

	void setTurnOffDelay(final long turnOffDelay) {
		this.turnOffDelay = turnOffDelay;
	}

	void setDelayBeforeIncreasingDimValue(final long delayBeforeIncreasingDimValue) {
		this.delayBeforeIncreasingDimValue = delayBeforeIncreasingDimValue;
	}

	void setStepDelay(final int stepDelay) {
		this.stepDelay = stepDelay;
	}

	public KnxDimmerProcessListener getKnxDimmerProcessListener() {
		return knxDimmerProcessListener;
	}

	public void setKnxDimmerProcessListener(final KnxDimmerProcessListener knxDimmerProcessListener) {
		this.knxDimmerProcessListener = knxDimmerProcessListener;
	}
} public class DimmerVoordeur implements DimmerConfig {

	@Override
	public Dimmer start() throws Exception {
		return new DimmerBuilder() {
			{
				name(VOORDEUR);
				boardAddress(0x5B);
				boardChannel(1);
				delayBeforeIncreasingDimValue(0);

				outputGroupAddressesForActorSwitchingOnAndOff("1/1/0");
				outputGroupAddressesForVisualisationStatusFeedback("15/0/17");
				//outputGroupAddressesForSwitchLedControl();  not needed as no physical button
				//outputGroupAddressesSwitchUpdate();		not needed as no physical button

				precenseOperated("1/1/6");

				inputGroupAddressForAbsoluteDimValue("14/0/8");
				inputGroupAddressForAbsoluteDimValueOverride("14/0/21");
			}
		}.build();
	}
} public class DimmerBerging implements DimmerConfig {

	@Override
	public Dimmer start() throws Exception {
		return new DimmerBuilder() {
			{
				name(BERGING);
				lucidControl();
				boardAddress(0);
				boardChannel(1);
				delayBeforeIncreasingDimValue(15);

				outputGroupAddressesForVisualisationStatusFeedback("15/0/11");
				outputGroupAddressesForActorSwitchingOnAndOff("5/6/1");
				outputGroupAddressesSwitchUpdate("5/6/5");

				precenseOperated("5/6/6");

				inputGroupAddressForOnAndOff("5/6/0");
				inputGroupAddressForOnOffOverride("5/6/4");
				inputGroupAddressForAbsoluteDimValue("14/0/11");
				inputGroupAddressForDimStartAndStop("5/6/3");
			}
		}.build();
	}
} public class DimmerEethoek implements DimmerConfig {

	@Override
	public Dimmer start() throws Exception {
		return new DimmerBuilder() {
			{
				name(EETHOEK);
				boardAddress(0x5B);
				boardChannel(0);
				delayBeforeIncreasingDimValue(15);

				outputGroupAddressesForActorSwitchingOnAndOff("5/1/8", "5/1/7");
				outputGroupAddressesForVisualisationStatusFeedback("15/0/6");
				outputGroupAddressesForSwitchLedControl("5/1/4");
				outputGroupAddressesSwitchUpdate("5/1/6");

				inputGroupAddressForOnAndOff("5/1/6");
				inputGroupAddressForDimStartAndStop("5/1/5");
				inputGroupAddressForAbsoluteDimValue("14/0/6");
			}
		}.build();
	}
} public class DimmerGang implements DimmerConfig {

	@Override
	public Dimmer start() throws Exception {
		return new DimmerBuilder() {
			{
				name(GANG);
				lucidControl();
				boardAddress(0);
				boardChannel(3);
				delayBeforeIncreasingDimValue(15);

				outputGroupAddressesForVisualisationStatusFeedback("15/0/9");
				outputGroupAddressesForActorSwitchingOnAndOff("5/4/0");
				inputGroupAddressForAbsoluteDimValue("14/0/9");
			}
		}.build();
	}
} public class DimmerGarage implements DimmerConfig {

	@Override
	public Dimmer start() throws Exception {
		return new DimmerBuilder() {
			{
				name(GARAGE);
				boardAddress(0x58);
				boardChannel(0);
				delayBeforeIncreasingDimValue(15);

				outputGroupAddressesForActorSwitchingOnAndOff("5/7/5");
				outputGroupAddressesForVisualisationStatusFeedback("15/0/12");
				outputGroupAddressesSwitchUpdate("5/7/1");

				precenseOperated("5/7/6");

				inputGroupAddressForOnAndOff("5/7/0");
				inputGroupAddressForOnOffOverride("5/7/2");
				inputGroupAddressForDimStartAndStop("5/7/3");
				inputGroupAddressForAbsoluteDimValue("14/0/12");
			}
		}.build();
	}
} public class DimmerInkomhal implements DimmerConfig {

	@Override
	public Dimmer start() throws Exception {
		return new DimmerBuilder() {
			{
				name(INKOMHAL);
				lucidControl();
				boardAddress(0);
				boardChannel(0);
				delayBeforeIncreasingDimValue(15);

				outputGroupAddressesForVisualisationStatusFeedback("15/0/7");
				outputGroupAddressesForActorSwitchingOnAndOff("5/5/1");
				inputGroupAddressForAbsoluteDimValue("14/0/7");
			}
		}.build();
	}
} public class DimmerKeuken implements DimmerConfig {

	@Override
	public Dimmer start() throws Exception {
		return new DimmerBuilder() {
			{
				name(KEUKEN);
				boardAddress(0x5B);
				boardChannel(2);
				delayBeforeIncreasingDimValue(15);

				outputGroupAddressesForActorSwitchingOnAndOff("5/0/6", "5/0/7");
				outputGroupAddressesForVisualisationStatusFeedback("15/0/5");
				outputGroupAddressesForSwitchLedControl("5/0/3");
				outputGroupAddressesSwitchUpdate("5/0/5");
				inputGroupAddressForOnAndOff("5/0/5");
				inputGroupAddressForDimStartAndStop("5/0/4");
				inputGroupAddressForAbsoluteDimValue("14/0/5");
			}
		}.build();
	}
} public class DimmerWc implements DimmerConfig {

	@Override
	public Dimmer start() throws Exception {
		return new DimmerBuilder() {
			{
				name(WC);
				lucidControl();
				boardAddress(0);
				boardChannel(2);
				inputGroupAddressForOnAndOff("5/2/0");
				outputGroupAddressesForVisualisationStatusFeedback("15/0/10");
				outputGroupAddressesForActorSwitchingOnAndOff("5/2/1");
				inputGroupAddressForAbsoluteDimValue("14/0/10");
			}
		}.build();
	}
} public class DimmerZitHoek implements DimmerConfig {

	@Override
	public Dimmer start() throws Exception {
		return new DimmerBuilder() {
			{
				name(ZITHOEK);
				boardAddress(0x5B);
				boardChannel(3);
				delayBeforeIncreasingDimValue(15);

				outputGroupAddressesForActorSwitchingOnAndOff("5/3/8", "5/3/9");
				outputGroupAddressesForVisualisationStatusFeedback("15/0/0");
				outputGroupAddressesForSwitchLedControl("5/3/4");
				outputGroupAddressesSwitchUpdate("5/3/6");
				inputGroupAddressForOnAndOff("5/3/6");
				inputGroupAddressForDimStartAndStop("5/3/5");
				inputGroupAddressForAbsoluteDimValue("14/0/0");
			}
		}.build();
	}
} public class Gang implements Runnable {

	private static final Logger logger = LoggerFactory.getLogger("gang");

	private Dimmer dimmerInkomhal;
	private Dimmer dimmerGang;

	private boolean motionVg;
	private Optional<Boolean> armed = empty();

	private ObjectStatusType voordeur;

	public Gang(final Dimmer dimmerInkomhal, final Dimmer dimmerGang) {
		this.dimmerInkomhal = dimmerInkomhal;
		this.dimmerGang = dimmerGang;
		getInstance().registerAdcEventListener(this);
	}

	@Override
	public void run() {
		ProcessListenerAdapter processListenerAdapter = new ProcessListenerAdapter() {
			@Override
			public void groupWrite(final ProcessEvent e) {
				try {
					if (e.getDestination().equals(new GroupAddress("1/0/7"))) {
						motionVg = asBool(e);
						logger.debug("Light channel VG:" + motionVg + "" + armed());
						if (!motionVg && (voordeur == null || voordeur == CLOSED)) {
							armed = empty();
							logger.debug("Light channel VG was false and voordeur closed, disarming" + armed());
						}
					} else if (e.getDestination().equals(new GroupAddress("2/0/0")) || e.getDestination().equals(new GroupAddress("2/1/0"))) {
						boolean motionLock = asBool(e);
						logger.debug("Lock channel VG:" + motionLock + "" + armed());
						if (motionLock && (voordeur == null || voordeur == CLOSED)) {
							motionVg = false;
							armed = empty();
							logger.debug("Lock channel VG was true and voordeur closed, disarming" + armed());
						}
					} else if (e.getDestination().equals(new GroupAddress("14/0/7"))) {
						int i = asUnsigned(e, SCALING);
						logger.debug("Inkomhal dimmer:" + i + "" + armed());
						if (i == 0) {
							logger.debug("Inkomhal dimmer was 0, turning of gang" + armed());
							dimmerGang.putCommand(new DimmerCommand(new BigDecimal(0), null));
						} else if (armed.isPresent() && armed.get()) {
							logger.debug("Inkomhal dimmer was <> 0 and gang is armed, setting gang to:" + i + "" + armed());
							dimmerGang.putCommand(new DimmerCommand(new BigDecimal(i), null));
						} else {
							logger.debug("Inkomhal dimmer was <> 0 but gang is not armed" + armed());
						}
					}
				} catch (Exception ex) {
					throw new RuntimeException(ex);
				}
			}
		};
		getInstance().getKnxConnectionFactory().addProcessListener(processListenerAdapter);
	}

	@Subscribe
	public void objectStatusChanged(List<Pair<String, ObjectStatusType>> list) throws Exception {
		voordeur = list.stream().filter(p -> p.getLeft().equals("25")).findFirst().get().getRight();

		if (!armed.isPresent() && voordeur == OPEN) {
			logger.debug("Voordeur open" + armed());
			if (motionVg) {
				logger.debug("Voordeur open, Light channel VG active" + armed());
				armed = of(true);
				if (dimmerInkomhal.getCurVal().compareTo(ZERO) > 0 && dimmerGang.getCurVal().compareTo(ZERO) == 0) {
					logger.debug("Setting gang dimmer to:" + dimmerInkomhal.getLastDimCommand().get().getTargetVal() + "" + armed());
					dimmerGang.putCommand(new DimmerCommand(dimmerInkomhal.getLastDimCommand().get().getTargetVal(), null));
				}
			} else {
				logger.debug("Voordeur open, Light channel VG not active, disarming" + armed());
				armed = of(false);
			}
		}

		if (armed.isPresent() && motionVg == false && voordeur == CLOSED) {
			armed = empty();
			logger.debug("Armed was present, motion not active and voordeur closed, disarming" + armed());
		}
	}

	public String armed() {
		return " [armed:" + (armed.isPresent() ? armed.get() : "N/A") + "]" + " [motionVg:" + motionVg + "]";
	}
} public class GvComfort implements Runnable {

	private static final Logger logger = LoggerFactory.getLogger(GvComfort.class);

	private Dimmer eethoek;
	private Dimmer zithoek;
	private Dimmer keuken;

	private boolean active;

	public GvComfort(final Dimmer eethoek, final Dimmer zithoek, final Dimmer keuken) {
		this.eethoek = eethoek;
		this.zithoek = zithoek;
		this.keuken = keuken;
	}

	@Override
	public void run() {
		ProcessListenerAdapter processListenerAdapter = new ProcessListenerAdapter() {
			@Override
			public void groupWrite(final ProcessEvent e) {
				try {
					if (e.getDestination().equals(new GroupAddress("11/0/0"))) {
						active = !active;
						int i = asUnsigned(e, UNSCALED);
						if (i == 1) {
							sceneOne(active, e);
						}
					}
				} catch (Exception ex) {
					logger.error(GvComfort.class.getSimpleName() + " had exception in ProcessListenerAdapter", ex);
				}

				super.groupWrite(e);
			}
		};
		getInstance().getKnxConnectionFactory().addProcessListener(processListenerAdapter);
	}

	private void sceneOne(boolean on, ProcessEvent e) throws Exception {

		if (on) {
			getInstance().getKnxConnectionFactory().runWithProcessCommunicator(pc -> pc.write(new GroupAddress("11/0/1"), true));
		} else {
			getInstance().getKnxConnectionFactory().runWithProcessCommunicator(pc -> pc.write(new GroupAddress("11/0/1"), false));
		}

		eethoek.interrupt();
		zithoek.interrupt();
		keuken.interrupt();
		keuken.putCommand(new DimmerCommand(new BigDecimal(on ? 1 : 0), e.getSourceAddr(), on ? active(new GroupAddress("5/0/2")) : inactive()));
		zithoek.putCommand(new DimmerCommand(new BigDecimal(on ? 1 : 0), e.getSourceAddr(), on ? active(new GroupAddress("5/3/1")) : inactive()));
		eethoek.putCommand(new DimmerCommand(new BigDecimal(on ? 1 : 0), e.getSourceAddr(), on ? active(new GroupAddress("5/1/0")) : inactive()));
	}
} public interface EbusCommand<R> {

	String[] getEbusCommands();

	R convertResult(List<String> results);

	default boolean withResult() {
		return false;
	}
} public class GetDepartWaterTemperatureGv implements EbusCommand<String> {

	@Override
	public String[] getEbusCommands() {
		return new String[] { "15b509030d2c01" };
	}

	public String convertResult(List<String> result) {
		String hex = result.get(0).substring(4, 6) + result.get(0).substring(2, 4);
		try {
			return decodeDATA2c(decodeHex(hex.toCharArray())).toString();
		} catch (DecoderException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public boolean withResult() {
		return true;
	}
} public class GetHeatingCircuitEnabled implements EbusCommand<Boolean> {

	@Override
	public String[] getEbusCommands() {
		return new String[] { "15b50903290300" };
	}

	public Boolean convertResult(List<String> result) {
		return result.get(0).charAt(7) == '1';
	}

	@Override
	public boolean withResult() {
		return true;
	}
} public class GetHeatingCircuitHeatingDemandGv implements EbusCommand<Boolean> {

	@Override
	public String[] getEbusCommands() {
		return new String[] { "15b509030d2c01" };
	}

	public Boolean convertResult(List<String> result) {
		return !"0000".equals(result.get(0).substring(2, 6));
	}

	@Override
	public boolean withResult() {
		return true;
	}
} public class GetOutsideTemperature implements EbusCommand<BigDecimal> {

	@Override
	public String[] getEbusCommands() {
		return new String[] { "15b50903293c00" };
	}

	public BigDecimal convertResult(List<String> result) {
		String hex = result.get(0).substring(8, 10) + result.get(0).substring(6, 8);
		try {
			return decodeDATA2c(decodeHex(hex.toCharArray()));
		} catch (DecoderException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public boolean withResult() {
		return true;
	}
} public class HeatingController extends Thread {

	private static final Logger logger = LoggerFactory.getLogger("heating");

	private final EbusDeviceAddress ebusDeviceAddress;
	private final HeatingInfoPollerJobSchedulerFactory heatingInfoPollerJobSchedulerFactory;

	private final BlockingQueue<RoomTemperature> commandQueue = new LinkedBlockingDeque();
	private final ControlValueCalculator controlValueCalculator = new ControlValueCalculator();
	private final List<RoomTemperature> roomTemperatureList = new ArrayList<>();
	private final List<RoomValveController> roomValveControllers = new ArrayList<>();

	private Optional<RoomTemperature> lastSendRoomTemperature = empty();
	private Optional<Boolean> heatingEnabled = empty();
	private Optional<Boolean> lastSendHeatingEnabled = empty();

	private boolean heatingEnabledBecauseOfFrostProtection;

	public HeatingController(final EbusDeviceAddress ebusDeviceAddress, final HeatingInfoPollerJobSchedulerFactory heatingInfoPollerJobSchedulerFactory) {
		this.ebusDeviceAddress = ebusDeviceAddress;
		this.heatingInfoPollerJobSchedulerFactory = heatingInfoPollerJobSchedulerFactory;
	}

	@Override
	public void run() {
		getInstance().addUdpChannelCallback(new UdpChannelCallback() {
			@Override
			public UdpChannelCommand command() {
				return HEATING_ENABLED;
			}

			@Override
			public void callBack(final String enabled) throws Exception {
				logger.debug("Heating enabled status callback received:" + enabled + " [heatingEnabledBecauseOfFrostProtection=" + heatingEnabledBecauseOfFrostProtection
						+ "]");
				if (enabled.equals("1")) {
					logger.debug("Setting heating enabled status to true");
					heatingEnabledBecauseOfFrostProtection = false;
					enableOrDisableHeatingIfNeeded(true);
				} else {
					if (!heatingEnabledBecauseOfFrostProtection) {
						logger.debug("Setting heating enabled status to false");
						enableOrDisableHeatingIfNeeded(false);
					} else {
						logger.debug("Not changing heating enabled status (" + heatingEnabled.get() + ") as heatingEnabledBecauseOfFrostProtection is " + "true");
					}
				}
			}
		});

		while (true) {
			try {
				if (!heatingEnabled.isPresent()) {
					logger.debug("Heating enabled status not yet received. Waiting 20 seconds. Heating controll is offline during this time");
					sleep(20000);
					continue;
				}
				logger.debug("Heating enabled: " + heatingEnabled.get());

				RoomTemperature roomTemperature = commandQueue.take();
				logger.debug("Heating controller received temperature from room " + roomTemperature.getRoomId() + " Details:" + roomTemperature);

				updateList(roomTemperature);

				if (roomTemperatureList.size() != roomValveControllers.size()) {
					logger.debug(
							"Not all temperatures from registered rooms are received. Not controlling heater at this time. Received temperatures:" + roomTemperatureList
									.toString());
					continue;
				}

				control();
				logger.debug("Check if valve update need " + roomTemperature.getRoomId());
				updateValveIfNeeded(roomTemperature);
			} catch (Exception e) {
				logger.error(HeatingController.class.getName() + " had exception while processing. Restarting.", e);
			}
		}
	}

	private void control() throws Exception {
		RoomTemperature sorted = new RoomTemperatureDeltaSorter().sortRoomTemperatureInfos(roomTemperatureList).get(0);
		RoomTemperature previous = lastSendRoomTemperature.orElse(sorted);
		logger.debug("Sorted: " + sorted + " Last send:" + (lastSendRoomTemperature.isPresent() ? lastSendRoomTemperature.get() : null) + " Previous:" + previous);

		process(sorted, previous);

		/**
		 * If heating is disabled, it is expected that the room controllers go into frost protection mode. Ie. they will continue sending the current temperature, but
		 * the desired temperature will be a low temperature (eg. 10°C). If one of the rooms would get colder than this temperature, we enable heating again. From the
		 * moment minimum temperature is reached, heating is turned off again (unless it was re-enabled in the meantime)
		 */
		if (!heatingEnabled.get() || heatingEnabledBecauseOfFrostProtection) {
			RoomTemperature coldest = roomTemperatureList.stream().sorted(comparing(RoomTemperature::getCurrentTemp)).findFirst().get();
			if (coldest.getHeatingDemand()) {
				enableOrDisableHeatingIfNeeded(true);
				heatingEnabledBecauseOfFrostProtection = true;
			} else {
				enableOrDisableHeatingIfNeeded(false);
				heatingEnabledBecauseOfFrostProtection = false;
			}
		}
	}

	private void enableOrDisableHeatingIfNeeded(boolean enabled) throws Exception {
		if (!lastSendHeatingEnabled.isPresent() || lastSendHeatingEnabled.get() != enabled) {
			logger.debug("Setting heating circuit enabled status to " + enabled);
			new EbusdTcpCommunicatorImpl(ebusDeviceAddress).send(new SetHeatCircruitEnabled(enabled));
			lastSendHeatingEnabled = of(enabled);
		} else {
			logger.debug("Not sending heating circuit enabled status. lastSendHeatingEnabled=" + lastSendHeatingEnabled.orElseGet(null) + " value:" + enabled);
		}
		logger.debug("Scheduling job for obtaining heating circuit enabled status");
		heatingInfoPollerJobSchedulerFactory.triggerNow();
		heatingEnabled = of(enabled);
	}

	private void process(RoomTemperature sorted, RoomTemperature previous) throws Exception {
		//Whenever we switch room, the heating controller needs to be 'reset' to reflect the current heating demand of that room
		if (previous.getRoomId() != sorted.getRoomId()) {
			logger.debug("Sending RESET desired temp " + sorted.getDesiredTemp().toString() + " to ebusd for room " + sorted.getRoomId());
			new EbusdTcpCommunicatorImpl(ebusDeviceAddress).send(new SetDesiredRoomTemperature(sorted.getDesiredTemp(), ebusDeviceAddress));

			BigDecimal resetControlTemp = controlValueCalculator.getResetControlValue(sorted.getHeatingDemand(), sorted.getDesiredTemp());
			logger.debug(
					"Sending RESET current temp " + sorted.getCurrentTemp().toString() + " (control temp:" + resetControlTemp.toString() + ") to ebusd for room " + sorted
							.getRoomId());
			new EbusdTcpCommunicatorImpl(ebusDeviceAddress).send(new SetCurrentRoomTemperature(resetControlTemp, sorted.getCurrentTemp()));
		}

		BigDecimal currentTemp = sorted.getCurrentTemp();
		BigDecimal desiredTemp = sorted.getDesiredTemp();

		logger.debug("Sending desired temp " + desiredTemp.toString() + " to ebusd for room " + sorted.getRoomId());
		new EbusdTcpCommunicatorImpl(ebusDeviceAddress).send(new SetDesiredRoomTemperature(desiredTemp, ebusDeviceAddress));

		BigDecimal controlTemp = controlValueCalculator.getControlValue(currentTemp, desiredTemp);
		logger.debug("Sending current temp " + currentTemp.toString() + "(control temp:" + controlTemp.toString() + " to ebusd for room " + sorted.getRoomId());
		new EbusdTcpCommunicatorImpl(ebusDeviceAddress).send(new SetCurrentRoomTemperature(controlTemp, currentTemp));

		lastSendRoomTemperature = of(sorted);

		logger.debug("Scheduling job for obtaining HC status");
		heatingInfoPollerJobSchedulerFactory.triggerNow();
	}

	private void updateValveIfNeeded(RoomTemperature roomTemperature) {
		RoomValveController roomValveController = extractSingleton(
				roomValveControllers.stream().filter(r -> r.getLocationId() == roomTemperature.getRoomId()).collect(toList()));
		roomValveController.updateIfNeeded(roomTemperature.getHeatingDemand());
	}

	public HeatingController registerRoom(LocationId locationId, GroupAddress currentTemperatureGa, GroupAddress... valves) {
		roomValveControllers.add(new RoomValveController(locationId, asList(valves)));
		new RoomTemperatureCollector(locationId, this, currentTemperatureGa).start();
		return this;
	}

	private void updateList(RoomTemperature roomTemperature) {
		Optional<RoomTemperature> optional = this.roomTemperatureList.stream().filter(k -> k.getRoomId() == roomTemperature.getRoomId()).findFirst();

		if (optional.isPresent()) {
			optional.get().update(roomTemperature);
		} else {
			roomTemperatureList.add(roomTemperature);
		}
	}

	void send(RoomTemperature roomTemperature) {
		try {
			commandQueue.put(roomTemperature);
		} catch (Exception e) {
			throw new RuntimeException("Command " + roomTemperature + " not processed", e);
		}
	}
} public class HeatingInfoPollerJobSchedulerFactory {

	private static final Logger logger = LoggerFactory.getLogger("heating");

	private final EbusDeviceAddress ebusDeviceAddress;
	private final Map<EbusCommand<?>, GroupAddress> map;

	public HeatingInfoPollerJobSchedulerFactory(EbusDeviceAddress ebusDeviceAddress, Map<EbusCommand<?>, GroupAddress> map) throws Exception {
		this.map = map;
		this.ebusDeviceAddress = ebusDeviceAddress;

		logger.debug("Scheduling HeatingInfoPollerJob");
		getInstance().getScheduler().scheduleJob(getJob(HeatingInfoPollerJob.class, ebusDeviceAddress, map),
				newTrigger().withSchedule(cronSchedule(new CronExpression("0 0/2 * * * ?"))).startNow().build());
	}

	public void triggerNow() throws SchedulerException {
		getInstance().getScheduler().scheduleJob(getJob(HeatingInfoPollerJob.class, ebusDeviceAddress, map),
				newTrigger().startAt(addSeconds(new Date(), 5)).withSchedule(simpleSchedule().withRepeatCount(0)).build());
	}

	private JobDetail getJob(Class<? extends Job> job, EbusDeviceAddress ebusDeviceAddress, Map<EbusCommand<?>, GroupAddress> map) {
		JobDetail jobDetail = newJob(job).build();
		JobConfig jobConfig = new JobConfig(ebusDeviceAddress, map);
		jobDetail.getJobDataMap().put(JOB_CONFIG_KEY, jobConfig);
		return jobDetail;
	}
} public class SonosController {

	public void start() throws MalformedURLException {
		Sonos sonos = new Sonos();
		KnxSonosProcessListener knxSonosProcessListener = new KnxSonosProcessListener(sonos, createGroupAddress("15/1/0"), createGroupAddress("15/1/1"),
				asList(createGroupAddress("15/1/2"), createGroupAddress("5/1/5"), createGroupAddress("5/1/6")));
		sonos.setKnxDimmerProcessListener(knxSonosProcessListener);
		RunConfig.getInstance().getKnxConnectionFactory().addProcessListener(knxSonosProcessListener);
		sonos.start();
	}
}
import static be.error.rpi.config.RunConfig.getInstance;
import static be.error.rpi.config.RunConfig.initialize;
import static be.error.rpi.heating.EbusDeviceAddress.GROUND_FLOOR;
import static be.error.rpi.knx.Support.createGroupAddress;
import static be.error.types.LocationId.GELIJKVLOERS;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.GroupAddress;
import be.error.rpi.adc.AdcController;
import be.error.rpi.dac.dimmer.builder.Dimmer;
import be.error.rpi.dac.dimmer.config.dimmers.buiten.DimmerVoordeur;
import be.error.rpi.dac.dimmer.config.dimmers.gv.DimmerBerging;
import be.error.rpi.dac.dimmer.config.dimmers.gv.DimmerEethoek;
import be.error.rpi.dac.dimmer.config.dimmers.gv.DimmerGang;
import be.error.rpi.dac.dimmer.config.dimmers.gv.DimmerGarage;
import be.error.rpi.dac.dimmer.config.dimmers.gv.DimmerInkomhal;
import be.error.rpi.dac.dimmer.config.dimmers.gv.DimmerKeuken;
import be.error.rpi.dac.dimmer.config.dimmers.gv.DimmerWc;
import be.error.rpi.dac.dimmer.config.dimmers.gv.DimmerZitHoek;
import be.error.rpi.dac.dimmer.config.scenes.Gang;
import be.error.rpi.dac.dimmer.config.scenes.GvComfort;
import be.error.rpi.ebus.EbusCommand;
import be.error.rpi.ebus.commands.GetDepartWaterTemperatureGv;
import be.error.rpi.ebus.commands.GetHeatingCircuitEnabled;
import be.error.rpi.ebus.commands.GetHeatingCircuitHeatingDemandGv;
import be.error.rpi.ebus.commands.GetOutsideTemperature;
import be.error.rpi.heating.HeatingController;
import be.error.rpi.heating.HeatingInfoPollerJobSchedulerFactory;
import be.error.rpi.sonos.SonosController;
/*-
 * #%L
 * Home Automation
 * %%
 * Copyright (C) 2016 - 2017 Koen Serneels
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package be.error.rpi;






/**
 * @author Koen Serneels
 */
public class StartRpiGv {

	private static final Logger logger = LoggerFactory.getLogger(StartRpiGv.class);

	private static final String RPI_LAN_IP = "192.168.0.10";

	public static void main(String[] args) throws Exception {
		initialize(RPI_LAN_IP);
		getInstance().registerLucidControlAO4(0, "/dev/lucidAO0");

		Map<EbusCommand<?>, GroupAddress> config = new HashMap<>();
		config.put(new GetHeatingCircuitHeatingDemandGv(), createGroupAddress("10/2/1"));