[danfossairunit] Fix network reliability issues and setting of all channel values to zero (#11172)
* Fix Potential null pointer accesses * Added constants for TCP port and poll interval in seconds. * Extract interface from DanfossAirUnitCommunicationController. * Remove unused constants which seems to be left-overs from skeleton. * Added constant for discovery timeout value for readability. * Created handler subfolder for DanfossAirUnitHandler (like discovery) for consistency with other bindings. * Handle lost connection gracefully without updating all channels to zero. * Fix infinitly blocking network calls preventing proper error handling. * Fix thing status being reset to ONLINE after failing to update all channels. * Fix error handling when receiving invalid manual fan step. Fixes #11167 Fixes #11188 Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
parent
2f4a27217f
commit
8255f29320
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.danfossairunit.internal;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface defines a communication controller that can be used to send requests to the Danfoss Air Unit.
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Refactoring, bugfixes and enhancements
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public interface CommunicationController {
|
||||||
|
void connect() throws IOException;
|
||||||
|
|
||||||
|
void disconnect();
|
||||||
|
|
||||||
|
byte[] sendRobustRequest(byte[] operation, byte[] register) throws IOException;
|
||||||
|
|
||||||
|
byte[] sendRobustRequest(byte[] operation, byte[] register, byte[] value) throws IOException;
|
||||||
|
}
|
|
@ -17,7 +17,6 @@ import static org.openhab.binding.danfossairunit.internal.Commands.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.DateTimeException;
|
import java.time.DateTimeException;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
|
@ -43,30 +42,22 @@ import org.openhab.core.types.Command;
|
||||||
*
|
*
|
||||||
* @author Ralf Duckstein - Initial contribution
|
* @author Ralf Duckstein - Initial contribution
|
||||||
* @author Robert Bach - heavy refactorings
|
* @author Robert Bach - heavy refactorings
|
||||||
|
* @author Jacob Laursen - Refactoring, bugfixes and enhancements
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@SuppressWarnings("SameParameterValue")
|
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class DanfossAirUnit {
|
public class DanfossAirUnit {
|
||||||
|
|
||||||
private final DanfossAirUnitCommunicationController communicationController;
|
private final CommunicationController communicationController;
|
||||||
|
|
||||||
public DanfossAirUnit(InetAddress inetAddr, int port) {
|
public DanfossAirUnit(CommunicationController communicationController) {
|
||||||
this.communicationController = new DanfossAirUnitCommunicationController(inetAddr, port);
|
this.communicationController = communicationController;
|
||||||
}
|
|
||||||
|
|
||||||
public void cleanUp() {
|
|
||||||
this.communicationController.disconnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean getBoolean(byte[] operation, byte[] register) throws IOException {
|
private boolean getBoolean(byte[] operation, byte[] register) throws IOException {
|
||||||
return communicationController.sendRobustRequest(operation, register)[0] != 0;
|
return communicationController.sendRobustRequest(operation, register)[0] != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setSetting(byte[] register, boolean value) throws IOException {
|
|
||||||
setSetting(register, value ? (byte) 1 : (byte) 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private short getWord(byte[] operation, byte[] register) throws IOException {
|
private short getWord(byte[] operation, byte[] register) throws IOException {
|
||||||
byte[] resultBytes = communicationController.sendRobustRequest(operation, register);
|
byte[] resultBytes = communicationController.sendRobustRequest(operation, register);
|
||||||
return (short) ((resultBytes[0] << 8) | (resultBytes[1] & 0xFF));
|
return (short) ((resultBytes[0] << 8) | (resultBytes[1] & 0xFF));
|
||||||
|
@ -87,14 +78,6 @@ public class DanfossAirUnit {
|
||||||
communicationController.sendRobustRequest(operation, register, valueArray);
|
communicationController.sendRobustRequest(operation, register, valueArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void set(byte[] operation, byte[] register, short value) throws IOException {
|
|
||||||
communicationController.sendRobustRequest(operation, register, shortToBytes(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] shortToBytes(short s) {
|
|
||||||
return new byte[] { (byte) ((s & 0xFF00) >> 8), (byte) (s & 0x00FF) };
|
|
||||||
}
|
|
||||||
|
|
||||||
private short getShort(byte[] operation, byte[] register) throws IOException {
|
private short getShort(byte[] operation, byte[] register) throws IOException {
|
||||||
byte[] result = communicationController.sendRobustRequest(operation, register);
|
byte[] result = communicationController.sendRobustRequest(operation, register);
|
||||||
return (short) ((result[0] << 8) + (result[1] & 0xff));
|
return (short) ((result[0] << 8) + (result[1] & 0xff));
|
||||||
|
@ -141,14 +124,6 @@ public class DanfossAirUnit {
|
||||||
return f * 100 / 255;
|
return f * 100 / 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setSetting(byte[] register, short value) throws IOException {
|
|
||||||
byte[] valueArray = new byte[2];
|
|
||||||
valueArray[0] = (byte) (value >> 8);
|
|
||||||
valueArray[1] = (byte) value;
|
|
||||||
|
|
||||||
communicationController.sendRobustRequest(REGISTER_1_WRITE, register, valueArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUnitName() throws IOException {
|
public String getUnitName() throws IOException {
|
||||||
return getString(REGISTER_1_READ, UNIT_NAME);
|
return getString(REGISTER_1_READ, UNIT_NAME);
|
||||||
}
|
}
|
||||||
|
@ -161,8 +136,12 @@ public class DanfossAirUnit {
|
||||||
return new StringType(Mode.values()[getByte(REGISTER_1_READ, MODE)].name());
|
return new StringType(Mode.values()[getByte(REGISTER_1_READ, MODE)].name());
|
||||||
}
|
}
|
||||||
|
|
||||||
public PercentType getManualFanStep() throws IOException {
|
public PercentType getManualFanStep() throws IOException, UnexpectedResponseValueException {
|
||||||
return new PercentType(BigDecimal.valueOf(getByte(REGISTER_1_READ, MANUAL_FAN_SPEED_STEP) * 10));
|
byte value = getByte(REGISTER_1_READ, MANUAL_FAN_SPEED_STEP);
|
||||||
|
if (value < 0 || value > 10) {
|
||||||
|
throw new UnexpectedResponseValueException(String.format("Invalid fan step: %d", value));
|
||||||
|
}
|
||||||
|
return new PercentType(BigDecimal.valueOf(value * 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
public DecimalType getSupplyFanSpeed() throws IOException {
|
public DecimalType getSupplyFanSpeed() throws IOException {
|
||||||
|
|
|
@ -30,12 +30,6 @@ public class DanfossAirUnitBindingConstants {
|
||||||
|
|
||||||
public static String BINDING_ID = "danfossairunit";
|
public static String BINDING_ID = "danfossairunit";
|
||||||
|
|
||||||
// List of all Thing Type UIDs
|
|
||||||
public static ThingTypeUID THING_TYPE_SAMPLE = new ThingTypeUID(BINDING_ID, "sample");
|
|
||||||
|
|
||||||
// List of all Channel ids
|
|
||||||
public static String CHANNEL_1 = "channel1";
|
|
||||||
|
|
||||||
// The only thing type UIDs
|
// The only thing type UIDs
|
||||||
public static ThingTypeUID THING_TYPE_AIRUNIT = new ThingTypeUID(BINDING_ID, "airunit");
|
public static ThingTypeUID THING_TYPE_AIRUNIT = new ThingTypeUID(BINDING_ID, "airunit");
|
||||||
|
|
||||||
|
|
|
@ -30,10 +30,13 @@ import org.slf4j.LoggerFactory;
|
||||||
* The {@link DanfossAirUnitCommunicationController} class does the actual network communication with the air unit.
|
* The {@link DanfossAirUnitCommunicationController} class does the actual network communication with the air unit.
|
||||||
*
|
*
|
||||||
* @author Robert Bach - initial contribution
|
* @author Robert Bach - initial contribution
|
||||||
|
* @author Jacob Laursen - Refactoring, bugfixes and enhancements
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class DanfossAirUnitCommunicationController {
|
public class DanfossAirUnitCommunicationController implements CommunicationController {
|
||||||
|
|
||||||
|
private static final int SOCKET_TIMEOUT_MILLISECONDS = 5_000;
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(DanfossAirUnitCommunicationController.class);
|
private final Logger logger = LoggerFactory.getLogger(DanfossAirUnitCommunicationController.class);
|
||||||
|
|
||||||
|
@ -41,8 +44,8 @@ public class DanfossAirUnitCommunicationController {
|
||||||
private final int port;
|
private final int port;
|
||||||
private boolean connected = false;
|
private boolean connected = false;
|
||||||
private @Nullable Socket socket;
|
private @Nullable Socket socket;
|
||||||
private @Nullable OutputStream oStream;
|
private @Nullable OutputStream outputStream;
|
||||||
private @Nullable InputStream iStream;
|
private @Nullable InputStream inputStream;
|
||||||
|
|
||||||
public DanfossAirUnitCommunicationController(InetAddress inetAddr, int port) {
|
public DanfossAirUnitCommunicationController(InetAddress inetAddr, int port) {
|
||||||
this.inetAddr = inetAddr;
|
this.inetAddr = inetAddr;
|
||||||
|
@ -53,9 +56,11 @@ public class DanfossAirUnitCommunicationController {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
socket = new Socket(inetAddr, port);
|
Socket localSocket = new Socket(inetAddr, port);
|
||||||
oStream = socket.getOutputStream();
|
localSocket.setSoTimeout(SOCKET_TIMEOUT_MILLISECONDS);
|
||||||
iStream = socket.getInputStream();
|
this.outputStream = localSocket.getOutputStream();
|
||||||
|
this.inputStream = localSocket.getInputStream();
|
||||||
|
this.socket = localSocket;
|
||||||
connected = true;
|
connected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,15 +69,16 @@ public class DanfossAirUnitCommunicationController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (socket != null) {
|
Socket localSocket = this.socket;
|
||||||
socket.close();
|
if (localSocket != null) {
|
||||||
|
localSocket.close();
|
||||||
}
|
}
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
logger.debug("Connection to air unit could not be closed gracefully. {}", ioe.getMessage());
|
logger.debug("Connection to air unit could not be closed gracefully. {}", ioe.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
socket = null;
|
this.socket = null;
|
||||||
iStream = null;
|
this.inputStream = null;
|
||||||
oStream = null;
|
this.outputStream = null;
|
||||||
}
|
}
|
||||||
connected = false;
|
connected = false;
|
||||||
}
|
}
|
||||||
|
@ -98,21 +104,27 @@ public class DanfossAirUnitCommunicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized byte[] sendRequestInternal(byte[] request) throws IOException {
|
private synchronized byte[] sendRequestInternal(byte[] request) throws IOException {
|
||||||
|
OutputStream localOutputStream = this.outputStream;
|
||||||
|
|
||||||
if (oStream == null) {
|
if (localOutputStream == null) {
|
||||||
throw new IOException(
|
throw new IOException(
|
||||||
String.format("Output stream is null while sending request: %s", Arrays.toString(request)));
|
String.format("Output stream is null while sending request: %s", Arrays.toString(request)));
|
||||||
}
|
}
|
||||||
oStream.write(request);
|
localOutputStream.write(request);
|
||||||
oStream.flush();
|
localOutputStream.flush();
|
||||||
|
|
||||||
byte[] result = new byte[63];
|
byte[] result = new byte[63];
|
||||||
if (iStream == null) {
|
InputStream localInputStream = this.inputStream;
|
||||||
|
if (localInputStream == null) {
|
||||||
throw new IOException(
|
throw new IOException(
|
||||||
String.format("Input stream is null while sending request: %s", Arrays.toString(request)));
|
String.format("Input stream is null while sending request: %s", Arrays.toString(request)));
|
||||||
}
|
}
|
||||||
// noinspection ResultOfMethodCallIgnored
|
|
||||||
iStream.read(result, 0, 63);
|
int bytesRead = localInputStream.read(result, 0, 63);
|
||||||
|
if (bytesRead < 63) {
|
||||||
|
throw new IOException(String.format(
|
||||||
|
"Error reading from stream, read returned %d as number of bytes read into the buffer", bytesRead));
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,6 @@ public class ValueCache {
|
||||||
return writeToCache;
|
return writeToCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNullByDefault
|
|
||||||
private static class StateWithTimestamp {
|
private static class StateWithTimestamp {
|
||||||
State state;
|
State state;
|
||||||
long timestamp;
|
long timestamp;
|
||||||
|
|
|
@ -51,11 +51,12 @@ public class DanfossAirUnitDiscoveryService extends AbstractDiscoveryService {
|
||||||
private static final int BROADCAST_PORT = 30045;
|
private static final int BROADCAST_PORT = 30045;
|
||||||
private static final byte[] DISCOVER_SEND = { 0x0c, 0x00, 0x30, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13 };
|
private static final byte[] DISCOVER_SEND = { 0x0c, 0x00, 0x30, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13 };
|
||||||
private static final byte[] DISCOVER_RECEIVE = { 0x0d, 0x00, 0x07, 0x00, 0x02, 0x02, 0x00 };
|
private static final byte[] DISCOVER_RECEIVE = { 0x0d, 0x00, 0x07, 0x00, 0x02, 0x02, 0x00 };
|
||||||
|
private static final int TIMEOUT_IN_SECONDS = 15;
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(DanfossAirUnitDiscoveryService.class);
|
private final Logger logger = LoggerFactory.getLogger(DanfossAirUnitDiscoveryService.class);
|
||||||
|
|
||||||
public DanfossAirUnitDiscoveryService() {
|
public DanfossAirUnitDiscoveryService() {
|
||||||
super(SUPPORTED_THING_TYPES_UIDS, 15, true);
|
super(SUPPORTED_THING_TYPES_UIDS, TIMEOUT_IN_SECONDS, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -40,15 +40,19 @@ import org.slf4j.LoggerFactory;
|
||||||
*
|
*
|
||||||
* @author Ralf Duckstein - Initial contribution
|
* @author Ralf Duckstein - Initial contribution
|
||||||
* @author Robert Bach - heavy refactorings
|
* @author Robert Bach - heavy refactorings
|
||||||
|
* @author Jacob Laursen - Refactoring, bugfixes and enhancements
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class DanfossAirUnitHandler extends BaseThingHandler {
|
public class DanfossAirUnitHandler extends BaseThingHandler {
|
||||||
|
|
||||||
|
private static final int TCP_PORT = 30046;
|
||||||
|
private static final int POLLING_INTERVAL_SECONDS = 5;
|
||||||
private final Logger logger = LoggerFactory.getLogger(DanfossAirUnitHandler.class);
|
private final Logger logger = LoggerFactory.getLogger(DanfossAirUnitHandler.class);
|
||||||
private @NonNullByDefault({}) DanfossAirUnitConfiguration config;
|
private @NonNullByDefault({}) DanfossAirUnitConfiguration config;
|
||||||
private @Nullable ValueCache valueCache;
|
private @Nullable ValueCache valueCache;
|
||||||
private @Nullable ScheduledFuture<?> pollingJob;
|
private @Nullable ScheduledFuture<?> pollingJob;
|
||||||
private @Nullable DanfossAirUnit hrv;
|
private @Nullable DanfossAirUnitCommunicationController communicationController;
|
||||||
|
private @Nullable DanfossAirUnit airUnit;
|
||||||
|
|
||||||
public DanfossAirUnitHandler(Thing thing) {
|
public DanfossAirUnitHandler(Thing thing) {
|
||||||
super(thing);
|
super(thing);
|
||||||
|
@ -60,12 +64,12 @@ public class DanfossAirUnitHandler extends BaseThingHandler {
|
||||||
updateAllChannels();
|
updateAllChannels();
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
DanfossAirUnit danfossAirUnit = hrv;
|
DanfossAirUnit localAirUnit = this.airUnit;
|
||||||
if (danfossAirUnit != null) {
|
if (localAirUnit != null) {
|
||||||
Channel channel = Channel.getByName(channelUID.getIdWithoutGroup());
|
Channel channel = Channel.getByName(channelUID.getIdWithoutGroup());
|
||||||
DanfossAirUnitWriteAccessor writeAccessor = channel.getWriteAccessor();
|
DanfossAirUnitWriteAccessor writeAccessor = channel.getWriteAccessor();
|
||||||
if (writeAccessor != null) {
|
if (writeAccessor != null) {
|
||||||
updateState(channelUID, writeAccessor.access(danfossAirUnit, command));
|
updateState(channelUID, writeAccessor.access(localAirUnit, command));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE,
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE,
|
||||||
|
@ -86,14 +90,16 @@ public class DanfossAirUnitHandler extends BaseThingHandler {
|
||||||
config = getConfigAs(DanfossAirUnitConfiguration.class);
|
config = getConfigAs(DanfossAirUnitConfiguration.class);
|
||||||
valueCache = new ValueCache(config.updateUnchangedValuesEveryMillis);
|
valueCache = new ValueCache(config.updateUnchangedValuesEveryMillis);
|
||||||
try {
|
try {
|
||||||
hrv = new DanfossAirUnit(InetAddress.getByName(config.host), 30046);
|
var localCommunicationController = new DanfossAirUnitCommunicationController(
|
||||||
DanfossAirUnit danfossAirUnit = hrv;
|
InetAddress.getByName(config.host), TCP_PORT);
|
||||||
|
this.communicationController = localCommunicationController;
|
||||||
|
var localAirUnit = new DanfossAirUnit(localCommunicationController);
|
||||||
|
this.airUnit = localAirUnit;
|
||||||
scheduler.execute(() -> {
|
scheduler.execute(() -> {
|
||||||
try {
|
try {
|
||||||
thing.setProperty(PROPERTY_UNIT_NAME, danfossAirUnit.getUnitName());
|
thing.setProperty(PROPERTY_UNIT_NAME, localAirUnit.getUnitName());
|
||||||
thing.setProperty(PROPERTY_SERIAL, danfossAirUnit.getUnitSerialNumber());
|
thing.setProperty(PROPERTY_SERIAL, localAirUnit.getUnitSerialNumber());
|
||||||
pollingJob = scheduler.scheduleWithFixedDelay(this::updateAllChannels, 5, config.refreshInterval,
|
startPolling();
|
||||||
TimeUnit.SECONDS);
|
|
||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.ONLINE);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
|
||||||
|
@ -107,8 +113,11 @@ public class DanfossAirUnitHandler extends BaseThingHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAllChannels() {
|
private void updateAllChannels() {
|
||||||
DanfossAirUnit danfossAirUnit = hrv;
|
DanfossAirUnit localAirUnit = this.airUnit;
|
||||||
if (danfossAirUnit != null) {
|
if (localAirUnit == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug("Updating DanfossHRV data '{}'", getThing().getUID());
|
logger.debug("Updating DanfossHRV data '{}'", getThing().getUID());
|
||||||
|
|
||||||
for (Channel channel : Channel.values()) {
|
for (Channel channel : Channel.values()) {
|
||||||
|
@ -118,12 +127,18 @@ public class DanfossAirUnitHandler extends BaseThingHandler {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
updateState(channel.getGroup().getGroupName(), channel.getChannelName(),
|
updateState(channel.getGroup().getGroupName(), channel.getChannelName(),
|
||||||
channel.getReadAccessor().access(danfossAirUnit));
|
channel.getReadAccessor().access(localAirUnit));
|
||||||
|
if (getThing().getStatus() == ThingStatus.OFFLINE) {
|
||||||
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
}
|
||||||
} catch (UnexpectedResponseValueException e) {
|
} catch (UnexpectedResponseValueException e) {
|
||||||
updateState(channel.getGroup().getGroupName(), channel.getChannelName(), UnDefType.UNDEF);
|
updateState(channel.getGroup().getGroupName(), channel.getChannelName(), UnDefType.UNDEF);
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Cannot update channel {}: an unexpected or invalid response has been received from the air unit: {}",
|
"Cannot update channel {}: an unexpected or invalid response has been received from the air unit: {}",
|
||||||
channel.getChannelName(), e.getMessage());
|
channel.getChannelName(), e.getMessage());
|
||||||
|
if (getThing().getStatus() == ThingStatus.OFFLINE) {
|
||||||
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
updateState(channel.getGroup().getGroupName(), channel.getChannelName(), UnDefType.UNDEF);
|
updateState(channel.getGroup().getGroupName(), channel.getChannelName(), UnDefType.UNDEF);
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
|
||||||
|
@ -131,30 +146,43 @@ public class DanfossAirUnitHandler extends BaseThingHandler {
|
||||||
channel.getChannelName(), e.getMessage());
|
channel.getChannelName(), e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getThing().getStatus() == ThingStatus.OFFLINE) {
|
|
||||||
updateStatus(ThingStatus.ONLINE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
logger.debug("Disposing Danfoss HRV handler '{}'", getThing().getUID());
|
logger.debug("Disposing Danfoss HRV handler '{}'", getThing().getUID());
|
||||||
|
|
||||||
if (pollingJob != null) {
|
stopPolling();
|
||||||
pollingJob.cancel(true);
|
|
||||||
pollingJob = null;
|
this.airUnit = null;
|
||||||
|
|
||||||
|
DanfossAirUnitCommunicationController localCommunicationController = this.communicationController;
|
||||||
|
if (localCommunicationController != null) {
|
||||||
|
localCommunicationController.disconnect();
|
||||||
|
}
|
||||||
|
this.communicationController = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hrv != null) {
|
private synchronized void startPolling() {
|
||||||
hrv.cleanUp();
|
this.pollingJob = scheduler.scheduleWithFixedDelay(this::updateAllChannels, POLLING_INTERVAL_SECONDS,
|
||||||
hrv = null;
|
config.refreshInterval, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private synchronized void stopPolling() {
|
||||||
|
ScheduledFuture<?> localPollingJob = this.pollingJob;
|
||||||
|
if (localPollingJob != null) {
|
||||||
|
localPollingJob.cancel(true);
|
||||||
|
}
|
||||||
|
this.pollingJob = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateState(String groupId, String channelId, State state) {
|
private void updateState(String groupId, String channelId, State state) {
|
||||||
if (valueCache.updateValue(channelId, state)) {
|
ValueCache cache = valueCache;
|
||||||
|
if (cache == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache.updateValue(channelId, state)) {
|
||||||
updateState(new ChannelUID(thing.getUID(), groupId, channelId), state);
|
updateState(new ChannelUID(thing.getUID(), groupId, channelId), state);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.danfossairunit.internal;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.openhab.binding.danfossairunit.internal.Commands.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openhab.core.library.types.DateTimeType;
|
||||||
|
import org.openhab.core.library.types.OnOffType;
|
||||||
|
import org.openhab.core.library.types.PercentType;
|
||||||
|
import org.openhab.core.library.types.QuantityType;
|
||||||
|
import org.openhab.core.test.java.JavaTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides test cases for {@link DanfossAirUnit}
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Refactoring, bugfixes and enhancements
|
||||||
|
*/
|
||||||
|
public class DanfossAirUnitTest extends JavaTest {
|
||||||
|
|
||||||
|
private CommunicationController communicationController;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
private void setUp() {
|
||||||
|
this.communicationController = mock(CommunicationController.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getUnitNameIsReturned() throws IOException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x05, (byte) 'w', (byte) '2', (byte) '/', (byte) 'a', (byte) '2' };
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_1_READ, UNIT_NAME)).thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertEquals("w2/a2", airUnit.getUnitName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getHumidityWhenNearestNeighborIsBelowRoundsDown() throws IOException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x64 };
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_1_READ, HUMIDITY)).thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertEquals(new QuantityType<>("39.2 %"), airUnit.getHumidity());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getHumidityWhenNearestNeighborIsAboveRoundsUp() throws IOException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x67 };
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_1_READ, HUMIDITY)).thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertEquals(new QuantityType<>("40.4 %"), airUnit.getHumidity());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getSupplyTemperatureWhenNearestNeighborIsBelowRoundsDown()
|
||||||
|
throws IOException, UnexpectedResponseValueException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x09, (byte) 0xf0 }; // 0x09f0 = 2544 => 25.44
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_4_READ, SUPPLY_TEMPERATURE)).thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertEquals(new QuantityType<>("25.4 °C"), airUnit.getSupplyTemperature());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getSupplyTemperatureWhenBothNeighborsAreEquidistantRoundsUp()
|
||||||
|
throws IOException, UnexpectedResponseValueException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x09, (byte) 0xf1 }; // 0x09f1 = 2545 => 25.45
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_4_READ, SUPPLY_TEMPERATURE)).thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertEquals(new QuantityType<>("25.5 °C"), airUnit.getSupplyTemperature());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getSupplyTemperatureWhenBelowValidRangeThrows() throws IOException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x94, (byte) 0xf8 }; // 0x94f8 = -27400 => -274
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_4_READ, SUPPLY_TEMPERATURE)).thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertThrows(UnexpectedResponseValueException.class, () -> airUnit.getSupplyTemperature());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getSupplyTemperatureWhenAboveValidRangeThrows() throws IOException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x27, (byte) 0x11 }; // 0x2711 = 10001 => 100,01
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_4_READ, SUPPLY_TEMPERATURE)).thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertThrows(UnexpectedResponseValueException.class, () -> airUnit.getSupplyTemperature());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getCurrentTimeWhenWellFormattedIsParsed() throws IOException, UnexpectedResponseValueException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x03, (byte) 0x02, (byte) 0x0f, (byte) 0x1d, (byte) 0x08, (byte) 0x15 }; // 29.08.21
|
||||||
|
// 15:02:03
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_1_READ, CURRENT_TIME)).thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertEquals(new DateTimeType(ZonedDateTime.of(2021, 8, 29, 15, 2, 3, 0, ZoneId.systemDefault())),
|
||||||
|
airUnit.getCurrentTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getCurrentTimeWhenInvalidDateThrows() throws IOException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x03, (byte) 0x02, (byte) 0x0f, (byte) 0x20, (byte) 0x08, (byte) 0x15 }; // 32.08.21
|
||||||
|
// 15:02:03
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_1_READ, CURRENT_TIME)).thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertThrows(UnexpectedResponseValueException.class, () -> airUnit.getCurrentTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getBoostWhenZeroIsOff() throws IOException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x00 };
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_1_READ, BOOST)).thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertEquals(OnOffType.OFF, airUnit.getBoost());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getBoostWhenNonZeroIsOn() throws IOException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x66 };
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_1_READ, BOOST)).thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertEquals(OnOffType.ON, airUnit.getBoost());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getManualFanStepWhenWithinValidRangeIsConvertedIntoPercent()
|
||||||
|
throws IOException, UnexpectedResponseValueException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x05 };
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_1_READ, MANUAL_FAN_SPEED_STEP))
|
||||||
|
.thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertEquals(new PercentType(50), airUnit.getManualFanStep());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getManualFanStepWhenOutOfRangeThrows() throws IOException {
|
||||||
|
byte[] response = new byte[] { (byte) 0x0b };
|
||||||
|
when(this.communicationController.sendRobustRequest(REGISTER_1_READ, MANUAL_FAN_SPEED_STEP))
|
||||||
|
.thenReturn(response);
|
||||||
|
var airUnit = new DanfossAirUnit(communicationController);
|
||||||
|
assertThrows(UnexpectedResponseValueException.class, () -> airUnit.getManualFanStep());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue