/**
* 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.plugwise.internal;
import org.joda.time.DateTime;
import org.joda.time.LocalTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.openhab.binding.plugwise.PlugwiseCommandType;
import org.openhab.binding.plugwise.protocol.AcknowledgeMessage;
import org.openhab.binding.plugwise.protocol.CalibrationRequestMessage;
import org.openhab.binding.plugwise.protocol.CalibrationResponseMessage;
import org.openhab.binding.plugwise.protocol.ClockGetRequestMessage;
import org.openhab.binding.plugwise.protocol.ClockGetResponseMessage;
import org.openhab.binding.plugwise.protocol.InformationRequestMessage;
import org.openhab.binding.plugwise.protocol.InformationResponseMessage;
import org.openhab.binding.plugwise.protocol.Message;
import org.openhab.binding.plugwise.protocol.PowerBufferRequestMessage;
import org.openhab.binding.plugwise.protocol.PowerBufferResponseMessage;
import org.openhab.binding.plugwise.protocol.PowerChangeRequestMessage;
import org.openhab.binding.plugwise.protocol.PowerInformationRequestMessage;
import org.openhab.binding.plugwise.protocol.PowerInformationResponseMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A class that represents a Plugwise Circle device
*
* Circles maintain current energy usage by counting 'pulses' in a one or eight-second interval. Furthermore, they
* store hourly energy usage as well in a buffer. Each entry in the buffer contains usage for the last 4 full hours
* of consumption. In order to convert pulses to power (Watt) or KWh you need to apply a formula that uses some
* calibration information.
*
* @author Karel Goderis
* @since 1.1.0
*/
public class Circle extends PlugwiseDevice {
private static Logger logger = LoggerFactory.getLogger(Circle.class);
private static final double PULSES_PER_KW_SECOND = 468.9385193;
private static final double PULSES_PER_W_SECOND = (PULSES_PER_KW_SECOND / 1000);
private static final int POWER_STATE_RETRIES = 3;
private class PendingPowerStateChange {
final boolean state;
int retries;
PendingPowerStateChange(boolean state) {
this.state = state;
}
public String getStateAsString() {
return state ? "ON" : "OFF";
}
}
protected Stick stick;
// Calibration data, required to calculate energy consumption
protected boolean calibrated;
protected double gainA;
protected double gainB;
protected double offsetTotal;
protected double offsetNoise;
// System variables as kept/maintained by the Circle hardware
protected DateTime stamp;
protected LocalTime systemClock;
protected int recentLogAddress;
protected int previousLogAddress = 0;
protected boolean powerState;
protected int hertz;
protected String hardwareVersion;
protected DateTime firmwareVersion;
protected Energy one;
protected Energy lastHour;
// Pending power state changes are tracked for retries and temporarily
// ignoring an outdated result of an InformationJob
protected PendingPowerStateChange pendingPowerStateChange;
public Circle(String mac, Stick stick, String friendly) {
super(mac, DeviceType.Circle, friendly);
this.stick = stick;
}
public boolean getPowerState() {
return powerState;
}
public boolean setPowerState(String state) {
if (state != null) {
if (state.equals("ON") || state.equals("OPEN") || state.equals("UP")) {
pendingPowerStateChange = new PendingPowerStateChange(true);
return setPowerState(true);
} else if (state.equals("OFF") || state.equals("CLOSED") || state.equals("DOWN")) {
pendingPowerStateChange = new PendingPowerStateChange(false);
return setPowerState(false);
}
}
return true;
}
public boolean setPowerState(boolean state) {
stick.sendPriorityMessage(new PowerChangeRequestMessage(MAC, state));
stick.sendPriorityMessage(new InformationRequestMessage(MAC));
return true;
}
public LocalTime getSystemClock() {
if (systemClock != null) {
return systemClock;
} else {
updateSystemClock();
return null;
}
}
public void updateSystemClock() {
ClockGetRequestMessage message = new ClockGetRequestMessage(MAC);
stick.sendMessage(message);
}
public void updateInformation() {
InformationRequestMessage message = new InformationRequestMessage(MAC);
stick.sendMessage(message);
}
public Stick getStick() {
return stick;
}
public void calibrate() {
CalibrationRequestMessage message = new CalibrationRequestMessage(MAC, "");
stick.sendMessage(message);
}
public void updateCurrentEnergy() {
PowerInformationRequestMessage message = new PowerInformationRequestMessage(MAC);
stick.sendMessage(message);
}
public void updateEnergy(boolean completeHistory) {
if (!completeHistory) {
// fetch only the last available buffer
previousLogAddress = recentLogAddress - 1;
} else {
previousLogAddress = 0;
}
while (previousLogAddress <= recentLogAddress) {
PowerBufferRequestMessage message = new PowerBufferRequestMessage(MAC, previousLogAddress);
previousLogAddress = previousLogAddress + 1;
stick.sendMessage(message);
}
}
public double getCurrentWatt() {
if (one != null) {
return pulseToWatt(one);
} else {
return 0;
}
}
private double correctPulses(double pulses) {
double correctedPulses = Math.pow(pulses + offsetNoise, 2) * gainB + (pulses + offsetNoise) * gainA
+ offsetTotal;
if ((pulses > 0 && correctedPulses < 0) || (pulses < 0 && correctedPulses > 0)) {
return 0;
}
return correctedPulses;
}
private double pulseToWatt(Energy energy) {
double averagePulses = energy.getPulses() / energy.getInterval();
return correctPulses(averagePulses) / PULSES_PER_W_SECOND;
}
private double pulseTokWh(Energy energy) {
return pulseToWatt(energy) * energy.getInterval() / (3600 * 1000);
}
@Override
public boolean processMessage(Message message) {
if (message != null) {
switch (message.getType()) {
case CLOCK_GET_RESPONSE:
systemClock = ((ClockGetResponseMessage) message).getTime();
DateTimeFormatter sc = DateTimeFormat.forPattern("HH:mm:ss");
postUpdate(MAC, PlugwiseCommandType.CURRENTCLOCK, sc.print(systemClock));
return true;
case DEVICE_CALIBRATION_RESPONSE:
gainA = ((CalibrationResponseMessage) message).getGainA();
gainB = ((CalibrationResponseMessage) message).getGainB();
offsetTotal = ((CalibrationResponseMessage) message).getOffsetTotal();
offsetNoise = ((CalibrationResponseMessage) message).getOffsetNoise();
calibrated = true;
return true;
case DEVICE_INFORMATION_RESPONSE:
stamp = new DateTime(((InformationResponseMessage) message).getYear(),
((InformationResponseMessage) message).getMonth(), 1, 0, 0)
.plusMinutes(((InformationResponseMessage) message).getMinutes());
recentLogAddress = ((InformationResponseMessage) message).getLogAddress();
powerState = ((InformationResponseMessage) message).getPowerState();
hertz = ((InformationResponseMessage) message).getHertz();
hardwareVersion = ((InformationResponseMessage) message).getHardwareVersion();
if (pendingPowerStateChange != null) {
if (powerState == pendingPowerStateChange.state) {
pendingPowerStateChange = null;
} else {
// power state change message may be lost or an InformationJob may have queried the power
// state just before the power state change message arrived
if (pendingPowerStateChange.retries < POWER_STATE_RETRIES) {
pendingPowerStateChange.retries++;
logger.warn("Retrying to switch {} {} {} (retry #{})", type.name().toString(),
this.getName(), pendingPowerStateChange.getStateAsString(),
pendingPowerStateChange.retries);
setPowerState(pendingPowerStateChange.state);
} else {
logger.warn("Failed to switch {} {} {} after {} retries", type.name().toString(),
this.getName(), pendingPowerStateChange.getStateAsString(),
pendingPowerStateChange.retries);
pendingPowerStateChange = null;
}
}
}
if (pendingPowerStateChange == null) {
postUpdate(MAC, PlugwiseCommandType.CURRENTSTATE, powerState);
}
if (lastHour == null) {
updateEnergy(false);
}
return true;
case POWER_INFORMATION_RESPONSE:
if (!calibrated) {
logger.debug(
"{} with name: {} and MAC address: {} received power information without "
+ "being calibrated, calibrating and skipping response",
getType().name(), name, MAC);
calibrate();
return true;
}
one = ((PowerInformationResponseMessage) message).getOneSecond();
double watt = pulseToWatt(one);
if (watt > 10000) {
logger.debug("{} with name: {} and MAC address: {} is in a kind of error state, "
+ "skipping power information response", type.name(), name, MAC);
return true;
}
postUpdate(MAC, PlugwiseCommandType.CURRENTPOWER, watt);
postUpdate(MAC, PlugwiseCommandType.CURRENTPOWERSTAMP, one.getTime());
return true;
case POWER_BUFFER_RESPONSE:
if (!calibrated) {
return true;
}
// get the most recent energy consumption
Energy mostRecentEnergy = null;
for (int i = 0; i < 4; i++) {
Energy energy = ((PowerBufferResponseMessage) message).getEnergy(i);
if (energy != null) {
mostRecentEnergy = energy;
}
}
if (mostRecentEnergy != null) {
// when the current time is '11:44:55.888', the last hour energy has time '10:00:00.000'
boolean isLastHour = mostRecentEnergy.getTime().isAfter(DateTime.now().minusHours(2));
if (isLastHour) {
lastHour = mostRecentEnergy;
postUpdate(MAC, PlugwiseCommandType.LASTHOURCONSUMPTION, pulseTokWh(lastHour));
postUpdate(MAC, PlugwiseCommandType.LASTHOURCONSUMPTIONSTAMP, lastHour.getTime());
}
}
return true;
case ACKNOWLEDGEMENT:
if (((AcknowledgeMessage) message).isExtended()) {
switch (((AcknowledgeMessage) message).getExtensionCode()) {
case ON:
postUpdate(MAC, PlugwiseCommandType.CURRENTSTATE,
((AcknowledgeMessage) message).isOn());
break;
case OFF:
postUpdate(MAC, PlugwiseCommandType.CURRENTSTATE,
((AcknowledgeMessage) message).isOff());
break;
default:
return stick.processMessage(message);
}
}
return true;
default:
// Let's have the Stick a go at this message
return stick.processMessage(message);
}
} else {
return false;
}
}
@Override
public boolean postUpdate(String MAC, PlugwiseCommandType type, Object value) {
if (MAC != null && type != null && value != null) {
stick.postUpdate(MAC, type, value);
return true;
} else {
return false;
}
}
}