org.openhab.binding.fht.internal.FHTBinding.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.fht.internal.FHTBinding.java

Source

/**
 * Copyright (c) 2010-2015, openHAB.org and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.openhab.binding.fht.internal;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.lang.StringUtils;
import org.openhab.binding.fht.FHTBindingConfig;
import org.openhab.binding.fht.FHTBindingConfig.Datapoint;
import org.openhab.binding.fht.FHTBindingProvider;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.io.transport.cul.CULCommunicationException;
import org.openhab.io.transport.cul.CULDeviceException;
import org.openhab.io.transport.cul.CULHandler;
import org.openhab.io.transport.cul.CULListener;
import org.openhab.io.transport.cul.CULManager;
import org.openhab.io.transport.cul.CULMode;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implements the connection to the FHT devices via CUL. Some commands aren't
 * send immediatley, but are queued and send in execute(). For every FHT-80b
 * there can be only one command in the queue, so we don't overuse the RF band
 * and flood the send buffer of CUL devices.
 * 
 * @author Till Klocke
 * @since 1.4.0
 */
public class FHTBinding extends AbstractActiveBinding<FHTBindingProvider> implements ManagedService, CULListener {

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

    private final static SimpleDateFormat configDateFormat = new SimpleDateFormat("mm:HH:dd:MM:yy");

    /**
     * Config key for the device address. i.e. serial:/dev/ttyACM0
     */
    private final static String KEY_DEVICE = "device";
    /**
     * Our housecode we need to simulate a central device.
     */
    private final static String KEY_HOUSECODE = "housecode";
    /**
     * Do we want to update the time and date of our FHTs?
     */
    private final static String KEY_UPDATE_TIME = "time.update";
    /**
     * Cron expression for Quartz to schedule the time update.
     */
    private final static String KEY_UPDATE_CRON = "time.update.cron";
    /**
     * Do we want to actively requests reports from FHT-80b?
     */
    private final static String KEY_REPORTS = "reports";
    /**
     * Cron expression for Quartz to schedule the request of reports.
     */
    private final static String KEY_REPORTS_CRON = "reports.cron";

    private String deviceName;
    private String housecode;
    private boolean doTimeUpdate = false;
    private String timeUpdatecronExpression;
    private String reportsCronExpression;
    private boolean requestReports;

    private CULHandler cul;

    private JobKey updateTimeJobKey;
    private JobKey requestReportJobKey;

    /**
     * the refresh interval which is used to poll values from the FHT server
     * (optional, defaults to 60000ms)
     */
    private long refreshInterval = 60000;

    private Map<String, FHTDesiredTemperatureCommand> temperatureCommandQueue = new HashMap<String, FHTDesiredTemperatureCommand>();
    private HashMap<String, Integer> valueCache = new HashMap<String, Integer>();

    public FHTBinding() {
    }

    public void activate() {
        bindCULHandler();
    }

    public void deactivate() {
        if (cul != null) {
            CULManager.close(cul);
        }
        unscheduleJob(requestReportJobKey);
        unscheduleJob(updateTimeJobKey);
    }

    private void bindCULHandler() {
        if (!StringUtils.isEmpty(deviceName)) {
            try {
                cul = CULManager.getOpenCULHandler(deviceName, CULMode.SLOW_RF);
                cul.registerListener(this);
                cul.send("T01" + housecode);
            } catch (CULDeviceException e) {
                logger.error("Can't open CUL", e);
            } catch (CULCommunicationException e) {
                logger.error("Can't set our own housecode", e);
            }
        }
    }

    private boolean checkCULDevice() {
        if (cul == null) {
            logger.error("CUL device is not accessible");
            return false;
        }
        return true;
    }

    private void setNewDeviceName(String newDeviceName) {
        if (!StringUtils.isEmpty(newDeviceName)) {
            if (cul != null) {
                cul.unregisterListener(this);
                CULManager.close(cul);
            }
            deviceName = newDeviceName;
            bindCULHandler();
        }
    }

    /**
     * @{inheritDoc
     */
    @Override
    protected long getRefreshInterval() {
        return refreshInterval;
    }

    /**
     * @{inheritDoc
     */
    @Override
    protected String getName() {
        return "FHT Refresh Service";
    }

    /**
     * Here we send waiting commands to the FHT-80b. Since we can only send
     * every 2 minutes a limited amount of commads, we collect commands and
     * discard older commands in favor of newer ones, so we send as less packets
     * as possible.
     */
    @Override
    protected void execute() {
        if (!checkCULDevice()) {
            return;
        }
        logger.debug("Processing " + temperatureCommandQueue.size() + " waiting FHT temperature commands");
        Map<String, FHTDesiredTemperatureCommand> copyMap = new HashMap<String, FHTDesiredTemperatureCommand>(
                temperatureCommandQueue);
        for (Entry<String, FHTDesiredTemperatureCommand> entry : copyMap.entrySet()) {
            FHTDesiredTemperatureCommand waitingCommand = entry.getValue();
            String commandString = "T" + waitingCommand.getAddress() + waitingCommand.getCommand();
            try {
                cul.send(commandString);
                temperatureCommandQueue.remove(entry.getKey());
            } catch (CULCommunicationException e) {
                logger.error("Can't send desired temperature via CUL", e);
            }
        }
    }

    /**
     * @{inheritDoc
     */
    @Override
    protected void internalReceiveCommand(String itemName, Command command) {
        if (!checkCULDevice()) {
            return;
        }
        logger.debug("internalReceiveCommand() is called!");
        FHTBindingConfig config = null;
        for (FHTBindingProvider provider : providers) {
            config = provider.getConfigByItemName(itemName);
            if (config != null) {
                break;
            }
        }
        if (config != null) {
            if (Datapoint.DESIRED_TEMP == config.getDatapoint() && command instanceof DecimalType) {
                setDesiredTemperature(config, (DecimalType) command);
            } else {
                logger.error(
                        "You can only manipulate the desired temperature via commands, all other data points are read only");
            }
        }
    }

    private void setDesiredTemperature(FHTBindingConfig config, DecimalType command) {
        double temperature = command.doubleValue();
        if ((temperature >= 5.5) && (temperature <= 30.5)) {
            int temp = (int) (temperature * 2.0);

            FHTDesiredTemperatureCommand commandItem = new FHTDesiredTemperatureCommand(config.getFullAddress(),
                    "41" + Integer.toHexString(temp));
            logger.debug("Queuing new desired temperature");
            temperatureCommandQueue.put(config.getFullAddress(), commandItem);
        } else {
            logger.error("The desired temperature is outside of the valid range");
        }
    }

    /**
     * @{inheritDoc
     */
    @Override
    public void updated(Dictionary<String, ?> config) throws ConfigurationException {
        if (config != null) {

            // to override the default refresh interval one has to add a
            // parameter to openhab.cfg like
            // <bindingName>:refresh=<intervalInMs>
            String refreshIntervalString = (String) config.get("refresh");
            if (StringUtils.isNotBlank(refreshIntervalString)) {
                refreshInterval = Long.parseLong(refreshIntervalString);
            }

            housecode = parseMandatoryValue(KEY_HOUSECODE, config);
            doTimeUpdate = Boolean.parseBoolean((String) config.get(KEY_UPDATE_TIME));
            if (doTimeUpdate) {
                timeUpdatecronExpression = (String) config.get(KEY_UPDATE_CRON);
                if (StringUtils.isEmpty(timeUpdatecronExpression)) {
                    setProperlyConfigured(false);
                    throw new ConfigurationException(KEY_UPDATE_CRON,
                            "Time update was configured but no cron expression");
                }
                updateTimeJobKey = scheduleJob(UpdateFHTTimeJob.class, timeUpdatecronExpression);
            } else {
                unscheduleJob(updateTimeJobKey);
            }

            requestReports = Boolean.parseBoolean((String) config.get(KEY_REPORTS));
            if (requestReports) {
                reportsCronExpression = (String) config.get(KEY_REPORTS_CRON);
                if (StringUtils.isEmpty(reportsCronExpression)) {
                    setProperlyConfigured(false);
                    throw new ConfigurationException(KEY_REPORTS_CRON,
                            "Reports are requested, bu no cron expression is supplied");
                }
                requestReportJobKey = scheduleJob(RequestReportsJob.class, reportsCronExpression);
            } else {
                unscheduleJob(requestReportJobKey);
            }

            // At last the device, after we received all other config values
            String deviceName = parseMandatoryValue(KEY_DEVICE, config);
            setNewDeviceName(deviceName);
            setProperlyConfigured(true);
        }
    }

    private String parseMandatoryValue(String key, Dictionary<String, ?> config) throws ConfigurationException {
        String value = (String) config.get(key);
        if (StringUtils.isEmpty(value)) {
            setProperlyConfigured(false);
            throw new ConfigurationException(key, "Configuration option " + key + " is mandatory");
        }
        return value;
    }

    @Override
    public void dataReceived(String data) {
        if (data != null && data.startsWith("T")) {
            handleFHTMessage(data);
        }

    }

    private void handleFHTMessage(String data) {
        logger.debug("Received FHT message");
        if (data.length() >= 13) {
            logger.debug("Received FHT report");
            String device = data.substring(1, 5); // dev
            String command = data.substring(5, 7); // cde
            FHTCommand cde = FHTCommand.getEventById(Integer.parseInt(command, 16));
            String origin = data.substring(7, 9); // ??
            String argument = data.substring(9, 11); // val
            if (cde != null) {
                switch (cde) {
                case FHT_DESIRED_TEMP:
                    double desiredTemperature = ((double) Integer.parseInt(argument, 16)) / 2.0;
                    receivedNewDesiredTemperature(device, desiredTemperature);
                    break;

                case FHT_MEASURED_TEMP_LOW:
                    valueCache.put(device + "lowtemp", new Integer(Integer.parseInt(argument, 16)));
                    break;
                case FHT_MEASURED_TEMP_HIGH:
                    Integer lowtemp = valueCache.get(device + "lowtemp");

                    if (lowtemp != null) {
                        double temperature = (double) lowtemp + ((double) Integer.parseInt(argument, 16)) * 256.0;
                        temperature /= 10.0;
                        receivedNewMeasuredTemperature(device, temperature);
                    }

                    break;
                case FHT_STATE:
                    receivedFHTState(device, argument);
                    break;
                case FHT_ACTUATOR_0:
                case FHT_ACTUATOR_1:
                case FHT_ACTUATOR_2:
                case FHT_ACTUATOR_3:
                case FHT_ACTUATOR_4:
                case FHT_ACTUATOR_5:
                case FHT_ACTUATOR_6:
                case FHT_ACTUATOR_7:
                case FHT_ACTUATOR_8:
                    double valve = (((double) Integer.parseInt(argument, 16)) / 255.0) * 100.0;
                    receivedNewValveOpening(device, cde.getId(), valve);
                    break;
                default:
                    logger.warn("Unknown message: FHT " + device + ": " + command + "=" + argument + "\r\n");

                }
            } else {
                logger.warn("Received unkown FHT command: ", command);
            }
        } else if (data.length() == 11) {
            // is FHT8b frame
            logger.debug("We received probably a FHT 8b frame");
            String device = data.substring(1, 7);
            String argument = data.substring(7, 9);
            FHTState state = null;

            if ((argument.startsWith("1")) || (argument.startsWith("9"))) {
                state = FHTState.BATTERY_LOW;
            }

            if (argument.substring(1).equals("1")) {
                state = FHTState.WINDOW_OPEN;
            }

            if (argument.substring(1).equals("2")) {
                state = FHTState.WINDOW_CLOSED;
            }

            if (state != null) {
                receivedNewFHT8bState(device, state);
            } else {
                logger.warn("Received unknown state (" + argument + ") from device " + device);
            }
        } else {
            logger.warn("Received unparseable message");
        }
    }

    private void receivedFHTState(String device, String state) {
        logger.debug("Received state " + state + " for FHT device " + device);
        int stateValue = Integer.parseInt(state, 16);
        FHTBindingConfig config = getConfig(device, Datapoint.BATTERY);
        OnOffType batteryAlarm = null;
        if (stateValue % 2 == 0) {
            batteryAlarm = OnOffType.OFF;
        } else {
            stateValue = stateValue - 1;
            batteryAlarm = OnOffType.ON;
        }
        if (config != null) {
            logger.debug("Updating item " + config.getItem().getName() + " with battery state");
            eventPublisher.postUpdate(config.getItem().getName(), batteryAlarm);
        }

        OpenClosedType windowState = null;
        if (stateValue == 0) {
            windowState = OpenClosedType.CLOSED;
        } else {
            windowState = OpenClosedType.OPEN;
        }
        config = getConfig(device, Datapoint.WINDOW);
        if (config != null) {
            logger.debug("Updating item " + config.getItem().getName() + " with window state");
            eventPublisher.postUpdate(config.getItem().getName(), windowState);
        } else {
            logger.debug("Received FHT state from unknown device " + device);
        }
    }

    private void receivedNewFHT8bState(String device, FHTState state) {
        FHTBindingConfig config = null;
        if (state == FHTState.BATTERY_LOW) {
            config = getConfig(device, Datapoint.BATTERY);
        } else {
            config = getConfig(device, Datapoint.WINDOW);
        }
        if (config != null) {
            logger.debug("Updating item " + config.getItem().getName() + " with new FHT state " + state.toString());
            State newState = null;
            if (state == FHTState.BATTERY_LOW) {
                // Battery alarm goes on
                newState = OnOffType.ON;
            } else if (state == FHTState.WINDOW_OPEN) {
                newState = OpenClosedType.OPEN;
            } else if (state == FHTState.WINDOW_CLOSED) {
                newState = OpenClosedType.CLOSED;
            }
            if (newState != null) {
                eventPublisher.postUpdate(config.getItem().getName(), newState);
            } else {
                logger.warn("Unknown FHT8b state, which is unmapped to openHAB state " + state.toString());
            }
        } else {
            logger.debug("Received FHT8b state for unknown device with address " + device);
        }
    }

    private void receivedNewValveOpening(String device, int actuatorNumber, double valve) {
        String fullAddress = device + "0" + actuatorNumber;
        FHTBindingConfig config = getConfig(fullAddress, Datapoint.VALVE);
        if (config != null) {
            logger.debug("Updating item " + config.getItem().getName() + " with new valve opening");
            DecimalType state = new DecimalType(valve);
            eventPublisher.postUpdate(config.getItem().getName(), state);
        } else {
            logger.debug("Received valve opening of unkown actuator with address " + fullAddress);
        }
    }

    private void receivedNewMeasuredTemperature(String deviceAddress, double temperature) {
        FHTBindingConfig config = getConfig(deviceAddress, Datapoint.MEASURED_TEMP);
        if (config != null) {
            logger.debug("Updating item " + config.getItem().getName() + " with new measured temperature "
                    + temperature);
            DecimalType state = new DecimalType(temperature);
            eventPublisher.postUpdate(config.getItem().getName(), state);
        } else {
            logger.debug("Received new measured temp for unkown device with address " + deviceAddress);
        }
    }

    private void receivedNewDesiredTemperature(String deviceAddress, double temperature) {
        FHTBindingConfig config = getConfig(deviceAddress, Datapoint.DESIRED_TEMP);
        if (config != null) {
            logger.debug(
                    "Updating item " + config.getItem().getName() + " with new desired temperature " + temperature);
            DecimalType state = new DecimalType(temperature);
            eventPublisher.postUpdate(config.getItem().getName(), state);
        } else {
            logger.debug(
                    "Received new desired temperature for currently unknown device with address " + deviceAddress);
        }
    }

    private FHTBindingConfig getConfig(String deviceAddress, Datapoint datapoint) {
        for (FHTBindingProvider provider : providers) {
            FHTBindingConfig config = provider.getConfigByFullAddress(deviceAddress, datapoint);
            if (config != null) {
                return config;
            }
        }
        return null;
    }

    @Override
    public void error(Exception e) {
        logger.error("Received error from CUL", e);

    }

    /**
     * The user may configure this binding to update the internal clock of
     * FHT80b devices via rf command. The method takes care of scheduling this
     * job.
     */
    private JobKey scheduleJob(Class<? extends Job> jobClass, String cronExpression) {
        JobKey jobKey = null;
        try {
            Scheduler sched = StdSchedulerFactory.getDefaultScheduler();
            JobDetail detail = JobBuilder.newJob(jobClass).withIdentity("FHT " + jobClass.getSimpleName(), "cul")
                    .build();
            detail.getJobDataMap().put(FHTBinding.class.getName(), this);

            CronTrigger trigger = TriggerBuilder.newTrigger().forJob(detail)
                    .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)).build();
            jobKey = detail.getKey();
            sched.scheduleJob(detail, trigger);
        } catch (SchedulerException e) {
            logger.error("Can't schedule time update job", e);
        }
        return jobKey;
    }

    private void unscheduleJob(JobKey jobKey) {
        if (jobKey == null) {
            return;
        }
        try {
            Scheduler sched = StdSchedulerFactory.getDefaultScheduler();
            sched.deleteJob(jobKey);
        } catch (SchedulerException e) {
            logger.error("Error while unscheduling time update job", e);
        }

    }

    private void updateTime(FHTBindingConfig config) {
        Date date = new Date();
        String[] rawDateValues = configDateFormat.format(date).split(":");
        String device = config.getFullAddress();
        writeRegisters(device,
                new WriteRegisterCommand("64", Utils.convertDecimalStringToHexString(rawDateValues[0])),
                new WriteRegisterCommand("63", Utils.convertDecimalStringToHexString(rawDateValues[1])),
                new WriteRegisterCommand("62", Utils.convertDecimalStringToHexString(rawDateValues[2])),
                new WriteRegisterCommand("61", Utils.convertDecimalStringToHexString(rawDateValues[3])),
                new WriteRegisterCommand("60", Utils.convertDecimalStringToHexString(rawDateValues[4])));
    }

    private void writeRegister(String device, String register, String value) {
        StringBuffer sendBuffer = new StringBuffer(8);
        sendBuffer.append('F');
        sendBuffer.append(device);
        sendBuffer.append(register); // register to write
        sendBuffer.append(value);
        try {
            cul.send(sendBuffer.toString());
        } catch (CULCommunicationException e) {
            logger.error("Error while writing register " + register + " on device " + device);
        }
    }

    /**
     * It possible to chain up to 8 commands together to send to the CUL. Lists
     * with more than 8 commands will be discarded silently.
     * 
     * @param deviceAddress
     * @param commands
     */
    private void writeRegisters(String deviceAddress, WriteRegisterCommand... commands) {
        if (commands == null || commands.length == 0) {
            logger.warn("No commands to write to the CUL");
            return;
        }
        if (commands.length > 8) {
            logger.error("We can only send 8 commands at once to the CUL. Discarding all commands");
            return;
        }
        StringBuffer sendBuffer = new StringBuffer(8);
        sendBuffer.append('F');
        sendBuffer.append(deviceAddress);
        for (WriteRegisterCommand command : commands) {
            sendBuffer.append(command.register);
            sendBuffer.append(command.value);
        }
        try {
            cul.send(sendBuffer.toString());
        } catch (CULCommunicationException e) {
            logger.error("Error while writing multiple write register commands to the CUL", e);
        }
    }

    private void requestReport(FHTBindingConfig config) {
        writeRegister(config.getFullAddress(), "66", "FF");
    }

    public static class UpdateFHTTimeJob implements Job {

        private long updateInterval = 300000;

        @Override
        public void execute(JobExecutionContext arg0) throws JobExecutionException {
            FHTBinding binding = (FHTBinding) arg0.getJobDetail().getJobDataMap().get(FHTBinding.class.getName());
            List<FHTBindingConfig> configs = new ArrayList<FHTBindingConfig>();
            for (FHTBindingProvider provider : binding.providers) {
                configs.addAll(provider.getAllFHT80bBindingConfigs());
            }

            for (FHTBindingConfig config : configs) {
                binding.updateTime(config);
                try {
                    Thread.sleep(updateInterval);
                } catch (InterruptedException e) {
                    logger.error("Error while waiting between time updates", e);
                }
            }

        }

    }

    public static class RequestReportsJob implements Job {

        private long requestInterval = 120000;

        @Override
        public void execute(JobExecutionContext arg0) throws JobExecutionException {
            FHTBinding binding = (FHTBinding) arg0.getJobDetail().getJobDataMap().get(FHTBinding.class.getName());
            List<FHTBindingConfig> configs = new ArrayList<FHTBindingConfig>();
            for (FHTBindingProvider provider : binding.providers) {
                configs.addAll(provider.getAllFHT80bBindingConfigs());
            }

            for (FHTBindingConfig config : configs) {
                binding.requestReport(config);
                try {
                    Thread.sleep(requestInterval);
                } catch (InterruptedException e) {
                    logger.error("Error while waiting between report requests", e);
                }
            }

        }

    }

}