diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/CommunicationController.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/CommunicationController.java new file mode 100644 index 0000000000000..6d0873762a1d4 --- /dev/null +++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/CommunicationController.java @@ -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; +} diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnit.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnit.java index 1fbb4c770c1d6..0e3646281ed17 100644 --- a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnit.java +++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnit.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; -import java.net.InetAddress; import java.nio.charset.StandardCharsets; import java.time.DateTimeException; import java.time.ZoneId; @@ -43,30 +42,22 @@ * * @author Ralf Duckstein - Initial contribution * @author Robert Bach - heavy refactorings + * @author Jacob Laursen - Refactoring, bugfixes and enhancements */ -@SuppressWarnings("SameParameterValue") @NonNullByDefault public class DanfossAirUnit { - private final DanfossAirUnitCommunicationController communicationController; + private final CommunicationController communicationController; - public DanfossAirUnit(InetAddress inetAddr, int port) { - this.communicationController = new DanfossAirUnitCommunicationController(inetAddr, port); - } - - public void cleanUp() { - this.communicationController.disconnect(); + public DanfossAirUnit(CommunicationController communicationController) { + this.communicationController = communicationController; } private boolean getBoolean(byte[] operation, byte[] register) throws IOException { 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 { byte[] resultBytes = communicationController.sendRobustRequest(operation, register); return (short) ((resultBytes[0] << 8) | (resultBytes[1] & 0xFF)); @@ -87,14 +78,6 @@ private void set(byte[] operation, byte[] register, byte value) throws IOExcepti 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 { byte[] result = communicationController.sendRobustRequest(operation, register); return (short) ((result[0] << 8) + (result[1] & 0xff)); @@ -141,14 +124,6 @@ private static float asPercentByte(byte b) { 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 { return getString(REGISTER_1_READ, UNIT_NAME); } @@ -161,8 +136,12 @@ public StringType getMode() throws IOException { return new StringType(Mode.values()[getByte(REGISTER_1_READ, MODE)].name()); } - public PercentType getManualFanStep() throws IOException { - return new PercentType(BigDecimal.valueOf(getByte(REGISTER_1_READ, MANUAL_FAN_SPEED_STEP) * 10)); + public PercentType getManualFanStep() throws IOException, UnexpectedResponseValueException { + 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 { diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitBindingConstants.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitBindingConstants.java index c49ed3ce37345..5b95646a888fa 100644 --- a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitBindingConstants.java +++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitBindingConstants.java @@ -30,12 +30,6 @@ public class DanfossAirUnitBindingConstants { 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 public static ThingTypeUID THING_TYPE_AIRUNIT = new ThingTypeUID(BINDING_ID, "airunit"); diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitCommunicationController.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitCommunicationController.java index f607e7f83b014..c897163705604 100644 --- a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitCommunicationController.java +++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitCommunicationController.java @@ -30,10 +30,13 @@ * The {@link DanfossAirUnitCommunicationController} class does the actual network communication with the air unit. * * @author Robert Bach - initial contribution + * @author Jacob Laursen - Refactoring, bugfixes and enhancements */ @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); @@ -41,8 +44,8 @@ public class DanfossAirUnitCommunicationController { private final int port; private boolean connected = false; private @Nullable Socket socket; - private @Nullable OutputStream oStream; - private @Nullable InputStream iStream; + private @Nullable OutputStream outputStream; + private @Nullable InputStream inputStream; public DanfossAirUnitCommunicationController(InetAddress inetAddr, int port) { this.inetAddr = inetAddr; @@ -53,9 +56,11 @@ public synchronized void connect() throws IOException { if (connected) { return; } - socket = new Socket(inetAddr, port); - oStream = socket.getOutputStream(); - iStream = socket.getInputStream(); + Socket localSocket = new Socket(inetAddr, port); + localSocket.setSoTimeout(SOCKET_TIMEOUT_MILLISECONDS); + this.outputStream = localSocket.getOutputStream(); + this.inputStream = localSocket.getInputStream(); + this.socket = localSocket; connected = true; } @@ -64,15 +69,16 @@ public synchronized void disconnect() { return; } try { - if (socket != null) { - socket.close(); + Socket localSocket = this.socket; + if (localSocket != null) { + localSocket.close(); } } catch (IOException ioe) { logger.debug("Connection to air unit could not be closed gracefully. {}", ioe.getMessage()); } finally { - socket = null; - iStream = null; - oStream = null; + this.socket = null; + this.inputStream = null; + this.outputStream = null; } connected = false; } @@ -98,21 +104,27 @@ public synchronized byte[] sendRobustRequest(byte[] operation, byte[] register, } private synchronized byte[] sendRequestInternal(byte[] request) throws IOException { + OutputStream localOutputStream = this.outputStream; - if (oStream == null) { + if (localOutputStream == null) { throw new IOException( String.format("Output stream is null while sending request: %s", Arrays.toString(request))); } - oStream.write(request); - oStream.flush(); + localOutputStream.write(request); + localOutputStream.flush(); byte[] result = new byte[63]; - if (iStream == null) { + InputStream localInputStream = this.inputStream; + if (localInputStream == null) { throw new IOException( 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; } diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/ValueCache.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/ValueCache.java index 569741ff80232..e4bed6eb1873c 100644 --- a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/ValueCache.java +++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/ValueCache.java @@ -57,7 +57,6 @@ public boolean updateValue(String channelId, State newState) { return writeToCache; } - @NonNullByDefault private static class StateWithTimestamp { State state; long timestamp; diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/discovery/DanfossAirUnitDiscoveryService.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/discovery/DanfossAirUnitDiscoveryService.java index 2570d63444ff8..df98da487953e 100644 --- a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/discovery/DanfossAirUnitDiscoveryService.java +++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/discovery/DanfossAirUnitDiscoveryService.java @@ -51,11 +51,12 @@ public class DanfossAirUnitDiscoveryService extends AbstractDiscoveryService { 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_RECEIVE = { 0x0d, 0x00, 0x07, 0x00, 0x02, 0x02, 0x00 }; + private static final int TIMEOUT_IN_SECONDS = 15; private final Logger logger = LoggerFactory.getLogger(DanfossAirUnitDiscoveryService.class); public DanfossAirUnitDiscoveryService() { - super(SUPPORTED_THING_TYPES_UIDS, 15, true); + super(SUPPORTED_THING_TYPES_UIDS, TIMEOUT_IN_SECONDS, true); } @Override diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitHandler.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/handler/DanfossAirUnitHandler.java similarity index 54% rename from bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitHandler.java rename to bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/handler/DanfossAirUnitHandler.java index 52bdadd1edcd4..0bf43f57662fe 100644 --- a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitHandler.java +++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/handler/DanfossAirUnitHandler.java @@ -40,15 +40,19 @@ * * @author Ralf Duckstein - Initial contribution * @author Robert Bach - heavy refactorings + * @author Jacob Laursen - Refactoring, bugfixes and enhancements */ @NonNullByDefault 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 @NonNullByDefault({}) DanfossAirUnitConfiguration config; private @Nullable ValueCache valueCache; private @Nullable ScheduledFuture pollingJob; - private @Nullable DanfossAirUnit hrv; + private @Nullable DanfossAirUnitCommunicationController communicationController; + private @Nullable DanfossAirUnit airUnit; public DanfossAirUnitHandler(Thing thing) { super(thing); @@ -60,12 +64,12 @@ public void handleCommand(ChannelUID channelUID, Command command) { updateAllChannels(); } else { try { - DanfossAirUnit danfossAirUnit = hrv; - if (danfossAirUnit != null) { + DanfossAirUnit localAirUnit = this.airUnit; + if (localAirUnit != null) { Channel channel = Channel.getByName(channelUID.getIdWithoutGroup()); DanfossAirUnitWriteAccessor writeAccessor = channel.getWriteAccessor(); if (writeAccessor != null) { - updateState(channelUID, writeAccessor.access(danfossAirUnit, command)); + updateState(channelUID, writeAccessor.access(localAirUnit, command)); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE, @@ -86,14 +90,16 @@ public void initialize() { config = getConfigAs(DanfossAirUnitConfiguration.class); valueCache = new ValueCache(config.updateUnchangedValuesEveryMillis); try { - hrv = new DanfossAirUnit(InetAddress.getByName(config.host), 30046); - DanfossAirUnit danfossAirUnit = hrv; + var localCommunicationController = new DanfossAirUnitCommunicationController( + InetAddress.getByName(config.host), TCP_PORT); + this.communicationController = localCommunicationController; + var localAirUnit = new DanfossAirUnit(localCommunicationController); + this.airUnit = localAirUnit; scheduler.execute(() -> { try { - thing.setProperty(PROPERTY_UNIT_NAME, danfossAirUnit.getUnitName()); - thing.setProperty(PROPERTY_SERIAL, danfossAirUnit.getUnitSerialNumber()); - pollingJob = scheduler.scheduleWithFixedDelay(this::updateAllChannels, 5, config.refreshInterval, - TimeUnit.SECONDS); + thing.setProperty(PROPERTY_UNIT_NAME, localAirUnit.getUnitName()); + thing.setProperty(PROPERTY_SERIAL, localAirUnit.getUnitSerialNumber()); + startPolling(); updateStatus(ThingStatus.ONLINE); } catch (IOException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); @@ -107,33 +113,37 @@ public void initialize() { } private void updateAllChannels() { - DanfossAirUnit danfossAirUnit = hrv; - if (danfossAirUnit != null) { - logger.debug("Updating DanfossHRV data '{}'", getThing().getUID()); + DanfossAirUnit localAirUnit = this.airUnit; + if (localAirUnit == null) { + return; + } - for (Channel channel : Channel.values()) { - if (Thread.interrupted()) { - logger.debug("Polling thread interrupted..."); - return; + logger.debug("Updating DanfossHRV data '{}'", getThing().getUID()); + + for (Channel channel : Channel.values()) { + if (Thread.interrupted()) { + logger.debug("Polling thread interrupted..."); + return; + } + try { + updateState(channel.getGroup().getGroupName(), channel.getChannelName(), + channel.getReadAccessor().access(localAirUnit)); + if (getThing().getStatus() == ThingStatus.OFFLINE) { + updateStatus(ThingStatus.ONLINE); } - try { - updateState(channel.getGroup().getGroupName(), channel.getChannelName(), - channel.getReadAccessor().access(danfossAirUnit)); - } catch (UnexpectedResponseValueException e) { - updateState(channel.getGroup().getGroupName(), channel.getChannelName(), UnDefType.UNDEF); - logger.debug( - "Cannot update channel {}: an unexpected or invalid response has been received from the air unit: {}", - channel.getChannelName(), e.getMessage()); - } catch (IOException e) { - updateState(channel.getGroup().getGroupName(), channel.getChannelName(), UnDefType.UNDEF); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); - logger.debug("Cannot update channel {}: an error occurred retrieving the value: {}", - channel.getChannelName(), e.getMessage()); + } catch (UnexpectedResponseValueException e) { + updateState(channel.getGroup().getGroupName(), channel.getChannelName(), UnDefType.UNDEF); + logger.debug( + "Cannot update channel {}: an unexpected or invalid response has been received from the air unit: {}", + channel.getChannelName(), e.getMessage()); + if (getThing().getStatus() == ThingStatus.OFFLINE) { + updateStatus(ThingStatus.ONLINE); } - } - - if (getThing().getStatus() == ThingStatus.OFFLINE) { - updateStatus(ThingStatus.ONLINE); + } catch (IOException e) { + updateState(channel.getGroup().getGroupName(), channel.getChannelName(), UnDefType.UNDEF); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage()); + logger.debug("Cannot update channel {}: an error occurred retrieving the value: {}", + channel.getChannelName(), e.getMessage()); } } } @@ -142,19 +152,37 @@ private void updateAllChannels() { public void dispose() { logger.debug("Disposing Danfoss HRV handler '{}'", getThing().getUID()); - if (pollingJob != null) { - pollingJob.cancel(true); - pollingJob = null; + stopPolling(); + + this.airUnit = null; + + DanfossAirUnitCommunicationController localCommunicationController = this.communicationController; + if (localCommunicationController != null) { + localCommunicationController.disconnect(); } + this.communicationController = null; + } - if (hrv != null) { - hrv.cleanUp(); - hrv = null; + private synchronized void startPolling() { + this.pollingJob = scheduler.scheduleWithFixedDelay(this::updateAllChannels, POLLING_INTERVAL_SECONDS, + 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) { - 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); } } diff --git a/bundles/org.openhab.binding.danfossairunit/src/test/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitTest.java b/bundles/org.openhab.binding.danfossairunit/src/test/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitTest.java new file mode 100644 index 0000000000000..f515f9d323a3f --- /dev/null +++ b/bundles/org.openhab.binding.danfossairunit/src/test/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitTest.java @@ -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()); + } +}