/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* 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.heatmiser.internal.thermostat;
import java.util.Calendar;
import org.openhab.core.items.Item;
import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for the Heatmiser thermostats.
* This provides the core functionality - other thermostat classes
* extend this to provide or update the specific functionality of that thermostat.
*
* @author Chris Jackson
* @since 1.4.0
*
*/
public class HeatmiserThermostat {
private static Logger logger = LoggerFactory.getLogger(HeatmiserThermostat.class);
private byte address;
private int frameLength;
private byte function;
protected byte data[];
private int dcbStart;
private Models dcbModel;
protected byte dcbState;
protected byte dcbHeatState;
protected byte dcbWaterState;
protected double dcbRoomTemperature;
protected double dcbFrostTemperature;
protected double dcbFloorTemperature;
protected double dcbSetTemperature;
protected int dcbHolidayTime;
protected int dcbHoldTime;
public void setAddress(byte newAddress) {
address = newAddress;
}
public int getAddress() {
return address;
}
protected int getInt(int pos) {
int val;
val = (data[pos] & 0xFF) + ((data[pos + 1] & 0xFF) * 256);
return val;
}
protected double getTemp(int pos) {
double val;
val = (double) ((data[pos + 1] & 0xFF) + ((data[pos] & 0xFF) * 256)) / 10;
return val;
}
/*
* Sets the new packet data for the thermostat This function processes the
* basic data and checks the validity of the incoming data
*
* @param in Input data byte array
*/
public boolean setData(byte in[]) {
if (in.length < 9) {
return false;
}
data = in;
frameLength = getInt(1);
if (in.length != frameLength) {
return false;
}
int crc = getInt(frameLength - 2);
if (crc != checkCRC(data)) {
return false;
}
address = data[3];
function = data[4];
if (function == 1) {
return false;
}
// Check that the whole DCB is returned
// While this isn't 100% necessary, it's what the binding does to make
// things easier!
dcbStart = getInt(5);
if (dcbStart != 0) {
return false;
}
switch (data[13]) {
case 0:
dcbModel = Models.DT;
break;
case 1:
dcbModel = Models.DTE;
break;
case 2:
dcbModel = Models.PRT;
break;
case 3:
dcbModel = Models.PRTE;
break;
case 4:
dcbModel = Models.PRTHW;
break;
}
return true;
}
private int checkCRC(byte[] packet) {
int crc = 0xFFFF; // initial value
int polynomial = 0x1021; // 0001 0000 0010 0001 (0, 5, 12)
for (int cnt = 0; cnt < packet.length - 2; cnt++) {
byte b = packet[cnt];
for (int i = 0; i < 8; i++) {
boolean bit = ((b >> (7 - i) & 1) == 1);
boolean c15 = ((crc >> 15 & 1) == 1);
crc <<= 1;
if (c15 ^ bit) {
crc ^= polynomial;
}
}
}
crc &= 0xffff;
return crc;
}
protected byte[] makePacket(boolean write, int start, int length, byte[] data) {
byte[] outPacket;
if (write == false) {
outPacket = new byte[10];
} else {
outPacket = new byte[10 + length];
}
outPacket[0] = address;
if (write) {
outPacket[1] = (byte) (length + 10);
outPacket[3] = 1;
} else {
outPacket[1] = 10;
outPacket[3] = 0;
}
outPacket[2] = (byte) 0x81;
outPacket[4] = (byte) (start & 0xff);
outPacket[5] = (byte) ((start >> 8) & 0xff);
outPacket[6] = (byte) (length & 0xff);
outPacket[7] = (byte) ((length >> 8) & 0xff);
if (write == true) {
for (byte cnt = 0; cnt < length; cnt++) {
outPacket[8 + cnt] = data[cnt];
}
} else {
length = 0;
}
int crc = checkCRC(outPacket);
outPacket[length + 8] = (byte) (crc & 0xff);
outPacket[length + 9] = (byte) ((crc >> 8) & 0xff);
return outPacket;
}
/**
* Produces a packet to poll this thermostat
*
* @return byte array with the packet
*/
public byte[] pollThermostat() {
return makePacket(false, 0, 0xffff, null);
}
/**
* Formats a command to the thermostat
*
* @param function The command function
* @param command The openHAB command parameter
* @return byte array with the command packet
*/
public byte[] formatCommand(Functions function, Command command) {
switch (function) {
case SETTEMP:
return setRoomTemperature(command);
case ONOFF:
return setOnOff(command);
case RUNMODE:
return setRunMode(command);
case FROSTTEMP:
return setFrostTemperature(command);
case HOLIDAYSET:
return setHolidayTime(command);
default:
return null;
}
}
public boolean setTime() {
return true;
}
/**
* Command to set the room temperature
*
* @param command
* @return byte array with the command data
*/
public byte[] setRoomTemperature(Command command) {
byte[] cmdByte = new byte[1];
if (!(command instanceof DecimalType)) {
return null;
}
byte temperature = ((DecimalType) command).byteValue();
if (temperature < 5) {
return null;
}
if (temperature > 35) {
return null;
}
cmdByte[0] = temperature;
return makePacket(true, 18, 1, cmdByte);
}
/**
* Sets the frost temperature
*
* @param command
* @return byte array with the command packet
*/
public byte[] setFrostTemperature(Command command) {
byte[] cmdByte = new byte[1];
byte temperature = ((DecimalType) command).byteValue();
if (temperature < 5) {
temperature = 5;
}
if (temperature > 18) {
temperature = 18;
}
cmdByte[0] = temperature;
return makePacket(true, 17, 1, cmdByte);
}
/**
* Sets the holiday time
*
* @param command time to set holiday mode - specified in days
* @return command string to send to thermostat
*/
public byte[] setHolidayTime(Command command) {
byte[] cmdBytes = new byte[2];
// Convert the time into an integer - 99 days is the maximum
int time = Integer.parseInt(command.toString());
if (time < 0) {
time = 0;
}
if (time > 99) {
time = 0;
}
// Convert time to hours
time = time * 24;
// Subtract the hours gone today
Calendar now = Calendar.getInstance();
time -= now.get(Calendar.HOUR_OF_DAY);
// Sanity check
if (time < 0) {
time = 0;
}
if (time > (99 * 24)) {
time = 0;
}
logger.debug("Setting holiday time {} days = {} hours.", command.toString(), time);
cmdBytes[0] = (byte) (time & 0xff);
cmdBytes[1] = (byte) ((time >> 8) & 0xff);
return makePacket(true, 24, 2, cmdBytes);
}
/**
* Sets the current time for the thermostat
*
* @param command
* @return command string to send to thermostat
*/
public byte[] setTime(Command command) {
byte[] cmdBytes = new byte[4];
// Now.wday = Now.wday - 1
// if(Now.wday == 0) then
// Now.wday = Now.wday + 7
// end
// cmdFrame =
// Dec2Hex(Now.wday)..Dec2Hex(Now.hour)..Dec2Hex(Now.min)..Dec2Hex(Now.sec)
return makePacket(true, 43, 4, cmdBytes);
}
/**
* Enables or disables the thermostat
*
* @param command
* @return
*/
public byte[] setOnOff(Command command) {
byte[] cmdByte = new byte[1];
if (command.toString().contentEquals("ON")) {
cmdByte[0] = 1;
} else {
cmdByte[0] = 0;
}
return makePacket(true, 21, 1, cmdByte);
}
public byte[] setRunMode(Command command) {
byte[] cmdByte = new byte[1];
if (command.toString().contentEquals("ON")) {
cmdByte[0] = 1;
} else {
cmdByte[0] = 0;
}
return makePacket(true, 23, 1, cmdByte);
}
/**
* Returns the current room temperature
*
* @param itemType
* @return
*/
public State getTemperature(Class<? extends Item> itemType) {
if (itemType == StringItem.class) {
return StringType.valueOf(Double.toString(dcbRoomTemperature));
}
// Default to DecimalType
return DecimalType.valueOf(Double.toString(dcbRoomTemperature));
}
/**
* Returns the current frost temperature
*
* @param itemType
* @return
*/
public State getFrostTemperature(Class<? extends Item> itemType) {
if (itemType == StringItem.class) {
return StringType.valueOf(Double.toString(dcbFrostTemperature));
}
// Default to DecimalType
return DecimalType.valueOf(Double.toString(dcbFrostTemperature));
}
/**
* Returns the current floor temperature
*
* @param itemType
* @return
*/
public State getFloorTemperature(Class<? extends Item> itemType) {
if (itemType == StringItem.class) {
return StringType.valueOf(Double.toString(dcbFloorTemperature));
}
// Default to DecimalType
return DecimalType.valueOf(Double.toString(dcbFloorTemperature));
}
/**
* Returns the current state of the thermostat
* This is a consolidated status that brings together a number of
* status registers within the thermostat.
*
* @param itemType
* @return
*/
public State getState(Class<? extends Item> itemType) {
// If this is a switch, then just treat this like the getOnOffState() function
if (itemType == SwitchItem.class) {
return dcbState == 1 ? OnOffType.ON : OnOffType.OFF;
}
// Default to a string
if (dcbState == 0) {
return StringType.valueOf(States.OFF.toString());
}
if (dcbHolidayTime != 0) {
return StringType.valueOf(States.HOLIDAY.toString());
}
if (dcbHoldTime != 0) {
return StringType.valueOf(States.HOLD.toString());
}
return StringType.valueOf(States.ON.toString());
}
/**
* Returns the current heating state
*
* @param itemType
* @return
*/
public State getOnOffState(Class<? extends Item> itemType) {
if (itemType == StringItem.class) {
return dcbState == 1 ? StringType.valueOf("ON") : StringType.valueOf("OFF");
}
if (itemType == SwitchItem.class) {
return dcbState == 1 ? OnOffType.ON : OnOffType.OFF;
}
// Default to DecimalType
return DecimalType.valueOf(Integer.toString(dcbState));
}
public State getWaterState(Class<? extends Item> itemType) {
if (itemType == StringItem.class) {
return dcbWaterState == 1 ? StringType.valueOf("ON") : StringType.valueOf("OFF");
}
if (itemType == SwitchItem.class) {
return dcbWaterState == 1 ? OnOffType.ON : OnOffType.OFF;
}
// Default to DecimalType
return DecimalType.valueOf(Integer.toString(dcbWaterState));
}
public State getSetTemperature(Class<? extends Item> itemType) {
if (itemType == StringItem.class) {
return StringType.valueOf(Double.toString(dcbSetTemperature));
}
// Default to DecimalType
return DecimalType.valueOf(Double.toString(dcbSetTemperature));
}
public State getHeatState(Class<? extends Item> itemType) {
if (itemType == StringItem.class) {
return dcbHeatState == 1 ? StringType.valueOf("ON") : StringType.valueOf("OFF");
}
if (itemType == SwitchItem.class) {
return dcbHeatState == 1 ? OnOffType.ON : OnOffType.OFF;
}
// Default to DecimalType
return DecimalType.valueOf(Integer.toString(dcbHeatState));
}
public State getHolidayMode(Class<? extends Item> itemType) {
return dcbHolidayTime > 0 ? OnOffType.ON : OnOffType.OFF;
}
public State getHolidayTime(Class<? extends Item> itemType) {
if (itemType == SwitchItem.class) {
return dcbHolidayTime > 0 ? OnOffType.ON : OnOffType.OFF;
}
// Return a date with the end time
Calendar now = Calendar.getInstance();
now.set(Calendar.MINUTE, 0);
now.set(Calendar.SECOND, 0);
now.set(Calendar.MILLISECOND, 0);
now.add(Calendar.HOUR, dcbHolidayTime);
return new DateTimeType(now);
}
public State getHolidaySet(Class<? extends Item> itemType) {
if (itemType == SwitchItem.class) {
return dcbHolidayTime > 0 ? OnOffType.ON : OnOffType.OFF;
}
// Return the number of days (midnights) remaining
// ie midnight tonight == 1
int days = 0;
if (dcbHolidayTime > 0) {
days = dcbHolidayTime / 24 + 1;
}
return DecimalType.valueOf(Integer.toString(days));
}
public State getHoldMode(Class<? extends Item> itemType) {
return dcbHoldTime > 0 ? OnOffType.ON : OnOffType.OFF;
}
public State getHoldTime(Class<? extends Item> itemType) {
if (itemType == SwitchItem.class) {
return dcbHoldTime > 0 ? OnOffType.ON : OnOffType.OFF;
}
// Return a date with the end time
Calendar now = Calendar.getInstance();
now.add(Calendar.MINUTE, dcbHoldTime);
return new DateTimeType(now);
}
public Models getModel() {
return dcbModel;
}
public enum Functions {
UNKNOWN,
ROOMTEMP,
FLOORTEMP,
ONOFF,
RUNMODE,
SETTEMP,
FROSTTEMP,
HOLIDAYTIME,
HOLIDAYMODE,
HOLIDAYSET,
HEATSTATE,
WATERSTATE,
HOLDTIME,
HOLDMODE,
STATE;
}
public enum States {
OFF,
ON,
HOLD,
HOLIDAY;
}
public enum Models {
PRT,
PRTHW,
DT,
DTE,
PRTE
}
}