From: jlaur Date: Mon, 27 Sep 2021 05:58:10 +0000 (+0200) Subject: [danfossairunit] Fix network reliability issues and setting of all channel values... X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=8255f29320666ef5162a4dcb681f43445d5412ac;p=openhab-addons.git [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 --- 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 0000000000..6d0873762a --- /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 1fbb4c770c..0e3646281e 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 static org.openhab.binding.danfossairunit.internal.Commands.*; 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 @@ import org.openhab.core.types.Command; * * @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 @@ public class DanfossAirUnit { 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 @@ public class DanfossAirUnit { 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 class DanfossAirUnit { 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 c49ed3ce37..5b95646a88 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 f607e7f83b..c897163705 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 @@ import org.slf4j.LoggerFactory; * 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 class DanfossAirUnitCommunicationController { 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 class DanfossAirUnitCommunicationController { 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 class DanfossAirUnitCommunicationController { } 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/DanfossAirUnitHandler.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitHandler.java deleted file mode 100644 index 52bdadd1ed..0000000000 --- a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitHandler.java +++ /dev/null @@ -1,161 +0,0 @@ -/** - * 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); - } - } -} 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 569741ff80..e4bed6eb18 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 class ValueCache { 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 2570d63444..df98da4879 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/handler/DanfossAirUnitHandler.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/handler/DanfossAirUnitHandler.java new file mode 100644 index 0000000000..0bf43f5766 --- /dev/null +++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/handler/DanfossAirUnitHandler.java @@ -0,0 +1,189 @@ +/** + * 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); + } + } +} 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 0000000000..f515f9d323 --- /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()); + } +}