be.error.rpi.heating.HeatingController.java Source code

Java tutorial

Introduction

Here is the source code for be.error.rpi.heating.HeatingController.java

Source

/*-
 * #%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.heating;

import static be.error.rpi.config.RunConfig.getInstance;
import static be.error.rpi.knx.UdpChannelCommand.HEATING_ENABLED;
import static java.util.Arrays.asList;
import static java.util.Comparator.comparing;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.collections4.CollectionUtils.extractSingleton;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import tuwien.auto.calimero.GroupAddress;

import be.error.rpi.ebus.EbusdTcpCommunicatorImpl;
import be.error.rpi.ebus.commands.SetCurrentRoomTemperature;
import be.error.rpi.ebus.commands.SetDesiredRoomTemperature;
import be.error.rpi.ebus.commands.SetHeatCircruitEnabled;
import be.error.rpi.knx.UdpChannel.UdpChannelCallback;
import be.error.rpi.knx.UdpChannelCommand;
import be.error.types.LocationId;

/**
 * Calculates for each room if heating is required. Heating is required when the delta between desired and actual temperature for that room exceeds a pre defined
 * threshhold. Each room is an active object responsible for gathering the heating parameters and passing it on to the controller using the
 * {@link #send(RoomTemperature)} method. The largest delta between desired and actual temperature (taken from the different rooms) is sent to the heater. Heater
 * communication is done via ebusd. The heater will thus show desired and requested temperature from the room with the largest delta. This is important as the heater
 * will use this information as a parameter (amongst outside temperature) for calculation of the heating curve which controls the water temperature. In other words,
 * the room with the largest delta will play a part in the selection of the heating curve.<p>
 * Besides sending the information to the heater, the controller will also invoke the valves of the registered rooms based on the heating demand
 */
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. 10C). 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);
        }
    }
}