--- /dev/null
+/**
+ * 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;
+}
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;
*
* @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));
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));
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);
}
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 {
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");
* 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);
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;
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;
}
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;
}
}
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;
}
+++ /dev/null
-/**
- * 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.openhab.binding.danfossairunit.internal.DanfossAirUnitBindingConstants.*;
-
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.ThingStatus;
-import org.openhab.core.thing.ThingStatusDetail;
-import org.openhab.core.thing.binding.BaseThingHandler;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.openhab.core.types.State;
-import org.openhab.core.types.UnDefType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link DanfossAirUnitHandler} is responsible for handling commands, which are
- * sent to one of the channels.
- *
- * @author Ralf Duckstein - Initial contribution
- * @author Robert Bach - heavy refactorings
- */
-@NonNullByDefault
-public class DanfossAirUnitHandler extends BaseThingHandler {
-
- private final Logger logger = LoggerFactory.getLogger(DanfossAirUnitHandler.class);
- private @NonNullByDefault({}) DanfossAirUnitConfiguration config;
- private @Nullable ValueCache valueCache;
- private @Nullable ScheduledFuture<?> pollingJob;
- private @Nullable DanfossAirUnit hrv;
-
- public DanfossAirUnitHandler(Thing thing) {
- super(thing);
- }
-
- @Override
- public void handleCommand(ChannelUID channelUID, Command command) {
- if (command instanceof RefreshType) {
- updateAllChannels();
- } else {
- try {
- DanfossAirUnit danfossAirUnit = hrv;
- if (danfossAirUnit != null) {
- Channel channel = Channel.getByName(channelUID.getIdWithoutGroup());
- DanfossAirUnitWriteAccessor writeAccessor = channel.getWriteAccessor();
- if (writeAccessor != null) {
- updateState(channelUID, writeAccessor.access(danfossAirUnit, command));
- }
- } else {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE,
- "Air unit connection not initialized.");
- return;
- }
- } catch (IllegalArgumentException e) {
- logger.debug("Ignoring unknown channel id: {}", channelUID.getIdWithoutGroup(), e);
- } catch (IOException ioe) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, ioe.getMessage());
- }
- }
- }
-
- @Override
- public void initialize() {
- updateStatus(ThingStatus.UNKNOWN);
- config = getConfigAs(DanfossAirUnitConfiguration.class);
- valueCache = new ValueCache(config.updateUnchangedValuesEveryMillis);
- try {
- hrv = new DanfossAirUnit(InetAddress.getByName(config.host), 30046);
- DanfossAirUnit danfossAirUnit = hrv;
- 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);
- updateStatus(ThingStatus.ONLINE);
- } catch (IOException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
- }
- });
- } catch (UnknownHostException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
- "Unknown host: " + config.host);
- return;
- }
- }
-
- private void updateAllChannels() {
- DanfossAirUnit danfossAirUnit = hrv;
- if (danfossAirUnit != null) {
- 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(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());
- }
- }
-
- if (getThing().getStatus() == ThingStatus.OFFLINE) {
- updateStatus(ThingStatus.ONLINE);
- }
- }
- }
-
- @Override
- public void dispose() {
- logger.debug("Disposing Danfoss HRV handler '{}'", getThing().getUID());
-
- if (pollingJob != null) {
- pollingJob.cancel(true);
- pollingJob = null;
- }
-
- if (hrv != null) {
- hrv.cleanUp();
- hrv = null;
- }
- }
-
- private void updateState(String groupId, String channelId, State state) {
- if (valueCache.updateValue(channelId, state)) {
- updateState(new ChannelUID(thing.getUID(), groupId, channelId), state);
- }
- }
-}
return writeToCache;
}
- @NonNullByDefault
private static class StateWithTimestamp {
State state;
long timestamp;
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
--- /dev/null
+/**
+ * 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.openhab.binding.danfossairunit.internal.DanfossAirUnitBindingConstants.*;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link DanfossAirUnitHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @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 DanfossAirUnitCommunicationController communicationController;
+ private @Nullable DanfossAirUnit airUnit;
+
+ public DanfossAirUnitHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ updateAllChannels();
+ } else {
+ try {
+ DanfossAirUnit localAirUnit = this.airUnit;
+ if (localAirUnit != null) {
+ Channel channel = Channel.getByName(channelUID.getIdWithoutGroup());
+ DanfossAirUnitWriteAccessor writeAccessor = channel.getWriteAccessor();
+ if (writeAccessor != null) {
+ updateState(channelUID, writeAccessor.access(localAirUnit, command));
+ }
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE,
+ "Air unit connection not initialized.");
+ return;
+ }
+ } catch (IllegalArgumentException e) {
+ logger.debug("Ignoring unknown channel id: {}", channelUID.getIdWithoutGroup(), e);
+ } catch (IOException ioe) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, ioe.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void initialize() {
+ updateStatus(ThingStatus.UNKNOWN);
+ config = getConfigAs(DanfossAirUnitConfiguration.class);
+ valueCache = new ValueCache(config.updateUnchangedValuesEveryMillis);
+ try {
+ 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, localAirUnit.getUnitName());
+ thing.setProperty(PROPERTY_SERIAL, localAirUnit.getUnitSerialNumber());
+ startPolling();
+ updateStatus(ThingStatus.ONLINE);
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
+ }
+ });
+ } catch (UnknownHostException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+ "Unknown host: " + config.host);
+ return;
+ }
+ }
+
+ private void updateAllChannels() {
+ DanfossAirUnit localAirUnit = this.airUnit;
+ if (localAirUnit == null) {
+ 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);
+ }
+ } 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);
+ }
+ } 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());
+ }
+ }
+ }
+
+ @Override
+ public void dispose() {
+ logger.debug("Disposing Danfoss HRV handler '{}'", getThing().getUID());
+
+ stopPolling();
+
+ this.airUnit = null;
+
+ DanfossAirUnitCommunicationController localCommunicationController = this.communicationController;
+ if (localCommunicationController != null) {
+ localCommunicationController.disconnect();
+ }
+ this.communicationController = 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) {
+ ValueCache cache = valueCache;
+ if (cache == null) {
+ return;
+ }
+
+ if (cache.updateValue(channelId, state)) {
+ updateState(new ChannelUID(thing.getUID(), groupId, channelId), state);
+ }
+ }
+}
--- /dev/null
+/**
+ * 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());
+ }
+}