From adacdebb9fd4bc2ac5d867a211b8eac9ea9008d5 Mon Sep 17 00:00:00 2001 From: Robert Michalak Date: Wed, 8 Jan 2025 22:21:07 +0100 Subject: [PATCH] [digiplex] Handle erroneous responses and restart the bridge (#18035) Signed-off-by: Robert Michalak --- .../internal/DigiplexBindingConstants.java | 1 - .../communication/DigiplexMessageHandler.java | 3 + .../DigiplexResponseResolver.java | 66 ++++-- .../communication/ErroneousResponse.java | 38 +++ .../communication/UnknownResponse.java | 4 +- .../handler/DigiplexBridgeHandler.java | 7 + .../DigiplexResponseResolverTest.java | 221 ++++++++++++++++++ 7 files changed, 325 insertions(+), 15 deletions(-) create mode 100644 bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/ErroneousResponse.java create mode 100644 bundles/org.openhab.binding.digiplex/src/test/java/org/openhab/binding/digiplex/internal/communication/DigiplexResponseResolverTest.java diff --git a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/DigiplexBindingConstants.java b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/DigiplexBindingConstants.java index 2c00db4c3eb..94f72721919 100644 --- a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/DigiplexBindingConstants.java +++ b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/DigiplexBindingConstants.java @@ -51,7 +51,6 @@ public class DigiplexBindingConstants { public static final String BRIDGE_MESSAGES_SENT = "statistics#messages_sent"; public static final String BRIDGE_RESPONSES_RECEIVED = "statistics#responses_received"; public static final String BRIDGE_EVENTS_RECEIVED = "statistics#events_received"; - public static final String BRIDGE_TLM_TROUBLE = "troubles#tlm_trouble"; public static final String BRIDGE_AC_FAILURE = "troubles#ac_failure"; public static final String BRIDGE_BATTERY_FAILURE = "troubles#battery_failure"; diff --git a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/DigiplexMessageHandler.java b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/DigiplexMessageHandler.java index fa51e70d15b..c56657f551f 100644 --- a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/DigiplexMessageHandler.java +++ b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/DigiplexMessageHandler.java @@ -52,6 +52,9 @@ public interface DigiplexMessageHandler { default void handleUnknownResponse(UnknownResponse response) { } + default void handleErroneousResponse(ErroneousResponse response) { + } + // Events default void handleZoneEvent(ZoneEvent event) { } diff --git a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/DigiplexResponseResolver.java b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/DigiplexResponseResolver.java index 75f10bdb1d7..c7790c024d3 100644 --- a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/DigiplexResponseResolver.java +++ b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/DigiplexResponseResolver.java @@ -13,6 +13,7 @@ package org.openhab.binding.digiplex.internal.communication; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.digiplex.internal.communication.events.AreaEvent; import org.openhab.binding.digiplex.internal.communication.events.AreaEventType; import org.openhab.binding.digiplex.internal.communication.events.GenericEvent; @@ -29,21 +30,19 @@ import org.openhab.binding.digiplex.internal.communication.events.ZoneStatusEven * Resolves serial messages to appropriate classes * * @author Robert Michalak - Initial contribution - * */ @NonNullByDefault public class DigiplexResponseResolver { private static final String OK = "&ok"; - // TODO: handle failures private static final String FAIL = "&fail"; public static DigiplexResponse resolveResponse(String message) { if (message.length() < 4) { // sanity check: try to filter out malformed responses - return new UnknownResponse(message); + return new ErroneousResponse(message); } - int zoneNo, areaNo; + Integer zoneNo, areaNo; String commandType = message.substring(0, 2); switch (commandType) { case "CO": // communication status @@ -53,24 +52,36 @@ public class DigiplexResponseResolver { return CommunicationStatus.OK; } case "ZL": // zone label - zoneNo = Integer.valueOf(message.substring(2, 5)); + zoneNo = getZoneOrArea(message); + if (zoneNo == null) { + return new ErroneousResponse(message); + } if (message.contains(FAIL)) { return ZoneLabelResponse.failure(zoneNo); } else { return ZoneLabelResponse.success(zoneNo, message.substring(5).trim()); } case "AL": // area label - areaNo = Integer.valueOf(message.substring(2, 5)); + areaNo = getZoneOrArea(message); + if (areaNo == null) { + return new ErroneousResponse(message); + } if (message.contains(FAIL)) { return AreaLabelResponse.failure(areaNo); } else { return AreaLabelResponse.success(areaNo, message.substring(5).trim()); } case "RZ": // zone status - zoneNo = Integer.valueOf(message.substring(2, 5)); + zoneNo = getZoneOrArea(message); + if (zoneNo == null) { + return new ErroneousResponse(message); + } if (message.contains(FAIL)) { return ZoneStatusResponse.failure(zoneNo); } else { + if (message.length() < 10) { + return new ErroneousResponse(message); + } return ZoneStatusResponse.success(zoneNo, // zone number ZoneStatus.fromMessage(message.charAt(5)), // status toBoolean(message.charAt(6)), // alarm @@ -79,10 +90,16 @@ public class DigiplexResponseResolver { toBoolean(message.charAt(9))); // battery low } case "RA": // area status - areaNo = Integer.valueOf(message.substring(2, 5)); + areaNo = getZoneOrArea(message); + if (areaNo == null) { + return new ErroneousResponse(message); + } if (message.contains(FAIL)) { return AreaStatusResponse.failure(areaNo); } else { + if (message.length() < 12) { + return new ErroneousResponse(message); + } return AreaStatusResponse.success(areaNo, // zone number AreaStatus.fromMessage(message.charAt(5)), // status toBoolean(message.charAt(6)), // zone in memory @@ -95,7 +112,10 @@ public class DigiplexResponseResolver { case "AA": // area arm case "AQ": // area quick arm case "AD": // area disarm - areaNo = Integer.valueOf(message.substring(2, 5)); + areaNo = getZoneOrArea(message); + if (areaNo == null) { + return new ErroneousResponse(message); + } if (message.contains(FAIL)) { return AreaArmDisarmResponse.failure(areaNo, ArmDisarmType.fromMessage(commandType)); } else { @@ -105,21 +125,41 @@ public class DigiplexResponseResolver { case "PG": // PGM events default: if (message.startsWith("G")) { - return resolveSystemEvent(message); + if (message.length() >= 12) { + return resolveSystemEvent(message); + } else { + return new ErroneousResponse(message); + } } else { return new UnknownResponse(message); } } } + private static @Nullable Integer getZoneOrArea(String message) { + if (message.length() < 5) { + return null; + } + try { + return Integer.valueOf(message.substring(2, 5)); + } catch (NumberFormatException e) { + return null; + } + } + private static boolean toBoolean(char value) { return value != 'O'; } private static DigiplexResponse resolveSystemEvent(String message) { - int eventGroup = Integer.parseInt(message.substring(1, 4)); - int eventNumber = Integer.parseInt(message.substring(5, 8)); - int areaNumber = Integer.parseInt(message.substring(9, 12)); + int eventGroup, eventNumber, areaNumber; + try { + eventGroup = Integer.parseInt(message.substring(1, 4)); + eventNumber = Integer.parseInt(message.substring(5, 8)); + areaNumber = Integer.parseInt(message.substring(9, 12)); + } catch (NumberFormatException e) { + return new ErroneousResponse(message); + } switch (eventGroup) { case 0: return new ZoneStatusEvent(eventNumber, ZoneStatus.CLOSED, areaNumber); diff --git a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/ErroneousResponse.java b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/ErroneousResponse.java new file mode 100644 index 00000000000..e35b4429b7c --- /dev/null +++ b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/ErroneousResponse.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2025 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.digiplex.internal.communication; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Erroneous message from PRT3. + * + * Message that is invalid, which happens sometimes due to communication errors. + * + * @author Robert Michalak - Initial contribution + * + */ +@NonNullByDefault +public class ErroneousResponse implements DigiplexResponse { + + public final String message; + + public ErroneousResponse(String message) { + this.message = message; + } + + @Override + public void accept(DigiplexMessageHandler visitor) { + visitor.handleErroneousResponse(this); + } +} diff --git a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/UnknownResponse.java b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/UnknownResponse.java index d978885c1b2..6da35f68b6e 100644 --- a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/UnknownResponse.java +++ b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/communication/UnknownResponse.java @@ -15,7 +15,9 @@ package org.openhab.binding.digiplex.internal.communication; import org.eclipse.jdt.annotation.NonNullByDefault; /** - * Unknown message from PRT3 + * Unknown message from PRT3. + * + * Message that is otherwise valid, but not handled in this binding. * * @author Robert Michalak - Initial contribution * diff --git a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/handler/DigiplexBridgeHandler.java b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/handler/DigiplexBridgeHandler.java index 2eb3424781b..7379ba72165 100644 --- a/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/handler/DigiplexBridgeHandler.java +++ b/bundles/org.openhab.binding.digiplex/src/main/java/org/openhab/binding/digiplex/internal/handler/DigiplexBridgeHandler.java @@ -38,6 +38,7 @@ import org.openhab.binding.digiplex.internal.communication.DigiplexMessageHandle import org.openhab.binding.digiplex.internal.communication.DigiplexRequest; import org.openhab.binding.digiplex.internal.communication.DigiplexResponse; import org.openhab.binding.digiplex.internal.communication.DigiplexResponseResolver; +import org.openhab.binding.digiplex.internal.communication.ErroneousResponse; import org.openhab.binding.digiplex.internal.communication.events.AbstractEvent; import org.openhab.binding.digiplex.internal.communication.events.TroubleEvent; import org.openhab.binding.digiplex.internal.communication.events.TroubleStatus; @@ -295,6 +296,12 @@ public class DigiplexBridgeHandler extends BaseBridgeHandler implements SerialPo updateState(channel, state); } } + + @Override + public void handleErroneousResponse(ErroneousResponse response) { + logger.debug("Erroneous response: {}", response.message); + handleCommunicationError(); + } } private class DigiplexReceiverThread extends Thread { diff --git a/bundles/org.openhab.binding.digiplex/src/test/java/org/openhab/binding/digiplex/internal/communication/DigiplexResponseResolverTest.java b/bundles/org.openhab.binding.digiplex/src/test/java/org/openhab/binding/digiplex/internal/communication/DigiplexResponseResolverTest.java new file mode 100644 index 00000000000..61d729dafd2 --- /dev/null +++ b/bundles/org.openhab.binding.digiplex/src/test/java/org/openhab/binding/digiplex/internal/communication/DigiplexResponseResolverTest.java @@ -0,0 +1,221 @@ +/** + * Copyright (c) 2010-2025 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.digiplex.internal.communication; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.openhab.binding.digiplex.internal.communication.events.GenericEvent; + +/** + * Tests for {@link DigiplexResponseResolver} + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class DigiplexResponseResolverTest { + @ParameterizedTest + @MethodSource("provideTestCasesForResolveResponseReturnsErroneousResponseWhenMessageIsMalformed") + void resolveResponseReturnsErroneousResponseWhenMessageIsMalformed(String message) { + DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message); + assertThat(actual, is(instanceOf(ErroneousResponse.class))); + if (actual instanceof ErroneousResponse erroneousResponse) { + assertThat(erroneousResponse.message, is(equalTo(message))); + } + } + + private static Stream provideTestCasesForResolveResponseReturnsErroneousResponseWhenMessageIsMalformed() { + return Stream.of( // + Arguments.of("CO&"), Arguments.of("ZL&fail"), Arguments.of("ZL12"), Arguments.of("AL&fail"), + Arguments.of("AL12"), Arguments.of("RZZZ3COOOO&fail"), Arguments.of("RZ123C"), + Arguments.of("RZ123COOO"), Arguments.of("RA&fail"), Arguments.of("RA123DOOXOO"), + Arguments.of("AA&fail"), Arguments.of("GGGGGGGGGGGG"), Arguments.of("G1234567890")); + } + + @Test + void resolveResponseReturnsCommunicationStatusSuccessWhenWellformed() { + String message = "CO&ok"; + DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message); + assertThat(actual, is(instanceOf(CommunicationStatus.class))); + if (actual instanceof CommunicationStatus communicationStatus) { + assertThat(communicationStatus.success, is(true)); + } + } + + @Test + void resolveResponseReturnsCommunicationStatusFailureWhenMessageContainsFail() { + String message = "CO&fail"; + DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message); + assertThat(actual, is(instanceOf(CommunicationStatus.class))); + if (actual instanceof CommunicationStatus communicationStatus) { + assertThat(communicationStatus.success, is(false)); + } + } + + @ParameterizedTest + @MethodSource("provideTestCasesForResolveResponseReturnsZoneLabelResponse") + void resolveResponseReturnsZoneLabelResponse(String message, boolean expectedSuccess, int expectedZoneNo, + String expectedName) { + DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message); + assertThat(actual, is(instanceOf(ZoneLabelResponse.class))); + if (actual instanceof ZoneLabelResponse zoneLabelResponse) { + assertThat(zoneLabelResponse.success, is(expectedSuccess)); + assertThat(zoneLabelResponse.zoneNo, is(expectedZoneNo)); + assertThat(zoneLabelResponse.zoneName, is(expectedName)); + } + } + + private static Stream provideTestCasesForResolveResponseReturnsZoneLabelResponse() { + return Stream.of( // + Arguments.of("ZL123", true, 123, ""), Arguments.of("ZL123test ", true, 123, "test"), + Arguments.of("ZL123&fail", false, 123, null), Arguments.of("ZL123test&fail", false, 123, null)); + } + + @ParameterizedTest + @MethodSource("provideTestCasesForResolveResponseReturnsAreaLabelResponse") + void resolveResponseReturnsAreaLabelResponse(String message, boolean expectedSuccess, int expectedAreaNo, + String expectedName) { + DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message); + assertThat(actual, is(instanceOf(AreaLabelResponse.class))); + if (actual instanceof AreaLabelResponse areaLabelResponse) { + assertThat(areaLabelResponse.success, is(expectedSuccess)); + assertThat(areaLabelResponse.areaNo, is(expectedAreaNo)); + assertThat(areaLabelResponse.areaName, is(expectedName)); + } + } + + private static Stream provideTestCasesForResolveResponseReturnsAreaLabelResponse() { + return Stream.of( // + Arguments.of("AL123", true, 123, ""), Arguments.of("AL123test ", true, 123, "test"), + Arguments.of("AL123&fail", false, 123, null), Arguments.of("AL123test&fail", false, 123, null)); + } + + @ParameterizedTest + @MethodSource("provideTestCasesForResolveResponseReturnsZoneStatusResponse") + void resolveResponseReturnsZoneStatusResponse(String message, boolean expectedSuccess, int expectedZoneNo, + ZoneStatus expectedZoneStatus, boolean expectedAlarm, boolean expectedFireAlarm, + boolean expectedSupervisionLost, boolean expectedLowBattery) { + DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message); + assertThat(actual, is(instanceOf(ZoneStatusResponse.class))); + if (actual instanceof ZoneStatusResponse zoneStatusResponse) { + assertThat(zoneStatusResponse.success, is(expectedSuccess)); + assertThat(zoneStatusResponse.zoneNo, is(expectedZoneNo)); + assertThat(zoneStatusResponse.status, is(expectedZoneStatus)); + assertThat(zoneStatusResponse.alarm, is(expectedAlarm)); + assertThat(zoneStatusResponse.fireAlarm, is(expectedFireAlarm)); + assertThat(zoneStatusResponse.supervisionLost, is(expectedSupervisionLost)); + assertThat(zoneStatusResponse.lowBattery, is(expectedLowBattery)); + } + } + + private static Stream provideTestCasesForResolveResponseReturnsZoneStatusResponse() { + return Stream.of( // + Arguments.of("RZ123COOOO", true, 123, ZoneStatus.CLOSED, false, false, false, false), + Arguments.of("RZ123OOOOO", true, 123, ZoneStatus.OPEN, false, false, false, false), + Arguments.of("RZ123TOOOO", true, 123, ZoneStatus.TAMPERED, false, false, false, false), + Arguments.of("RZ123FOOOO", true, 123, ZoneStatus.FIRE_LOOP_TROUBLE, false, false, false, false), + Arguments.of("RZ123uOOOO", true, 123, ZoneStatus.UNKNOWN, false, false, false, false), + Arguments.of("RZ123cOOOO", true, 123, ZoneStatus.UNKNOWN, false, false, false, false), + Arguments.of("RZ123cXOOO", true, 123, ZoneStatus.UNKNOWN, true, false, false, false), + Arguments.of("RZ123cOXOO", true, 123, ZoneStatus.UNKNOWN, false, true, false, false), + Arguments.of("RZ123cOOXO", true, 123, ZoneStatus.UNKNOWN, false, false, true, false), + Arguments.of("RZ123cOOOX", true, 123, ZoneStatus.UNKNOWN, false, false, false, true), + Arguments.of("RZ123&fail", false, 123, null, false, false, false, false), + Arguments.of("RZ123COOOO&fail", false, 123, null, false, false, false, false)); + } + + @ParameterizedTest + @MethodSource("provideTestCasesForResolveResponseReturnsAreaStatusResponse") + void resolveResponseReturnsAreaStatusResponse(String message, boolean expectedSuccess, int expectedAreaNo, + AreaStatus expectedAreaStatus, boolean expectedZoneInMemory, boolean expectedTrouble, boolean expectedReady, + boolean expectedInProgramming, boolean expectedAlarm, boolean expectedStrobe) { + DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message); + assertThat(actual, is(instanceOf(AreaStatusResponse.class))); + if (actual instanceof AreaStatusResponse areaStatusResponse) { + assertThat(areaStatusResponse.success, is(expectedSuccess)); + assertThat(areaStatusResponse.areaNo, is(expectedAreaNo)); + assertThat(areaStatusResponse.status, is(expectedAreaStatus)); + assertThat(areaStatusResponse.zoneInMemory, is(expectedZoneInMemory)); + assertThat(areaStatusResponse.trouble, is(expectedTrouble)); + assertThat(areaStatusResponse.ready, is(expectedReady)); + assertThat(areaStatusResponse.inProgramming, is(expectedInProgramming)); + assertThat(areaStatusResponse.alarm, is(expectedAlarm)); + assertThat(areaStatusResponse.strobe, is(expectedStrobe)); + } + } + + private static Stream provideTestCasesForResolveResponseReturnsAreaStatusResponse() { + return Stream.of( // + Arguments.of("RA123DOOXOOO", true, 123, AreaStatus.DISARMED, false, false, false, false, false, false), + Arguments.of("RA123AOOXOOO", true, 123, AreaStatus.ARMED, false, false, false, false, false, false), + Arguments.of("RA123FOOXOOO", true, 123, AreaStatus.ARMED_FORCE, false, false, false, false, false, + false), + Arguments.of("RA123SOOXOOO", true, 123, AreaStatus.ARMED_STAY, false, false, false, false, false, + false), + Arguments.of("RA123IOOXOOO", true, 123, AreaStatus.ARMED_INSTANT, false, false, false, false, false, + false), + Arguments.of("RA123uOOXOOO", true, 123, AreaStatus.UNKNOWN, false, false, false, false, false, false), + Arguments.of("RA123dOOXOOO", true, 123, AreaStatus.UNKNOWN, false, false, false, false, false, false), + Arguments.of("RA123dXOXOOO", true, 123, AreaStatus.UNKNOWN, true, false, false, false, false, false), + Arguments.of("RA123dOXxOOO", true, 123, AreaStatus.UNKNOWN, false, true, false, false, false, false), + Arguments.of("RA123dOOOOOO", true, 123, AreaStatus.UNKNOWN, false, false, true, false, false, false), + Arguments.of("RA123dOOXXOO", true, 123, AreaStatus.UNKNOWN, false, false, false, true, false, false), + Arguments.of("RA123dOOXOXO", true, 123, AreaStatus.UNKNOWN, false, false, false, false, true, false), + Arguments.of("RA123dOOXOOX", true, 123, AreaStatus.UNKNOWN, false, false, false, false, false, true), + Arguments.of("RA123&fail", false, 123, null, false, false, false, false, false, false), + Arguments.of("RA123DOOXOOO&fail", false, 123, null, false, false, false, false, false, false)); + } + + @ParameterizedTest + @MethodSource("provideTestCasesForResolveResponseReturnsAreaArmDisarmResponse") + void resolveResponseReturnsAreaArmDisarmResponse(String message, boolean expectedSuccess, int expectedAreaNo, + ArmDisarmType expectedType) { + DigiplexResponse actual = DigiplexResponseResolver.resolveResponse(message); + assertThat(actual, is(instanceOf(AreaArmDisarmResponse.class))); + if (actual instanceof AreaArmDisarmResponse armDisarmResponse) { + assertThat(armDisarmResponse.success, is(expectedSuccess)); + assertThat(armDisarmResponse.areaNo, is(expectedAreaNo)); + assertThat(armDisarmResponse.type, is(expectedType)); + } + } + + private static Stream provideTestCasesForResolveResponseReturnsAreaArmDisarmResponse() { + return Stream.of( // + Arguments.of("AA123", true, 123, ArmDisarmType.ARM), + Arguments.of("AQ123", true, 123, ArmDisarmType.QUICK_ARM), + Arguments.of("AD123", true, 123, ArmDisarmType.DISARM), + Arguments.of("AA123&fail", false, 123, ArmDisarmType.ARM), + Arguments.of("AQ123&fail", false, 123, ArmDisarmType.QUICK_ARM), + Arguments.of("AD123&fail", false, 123, ArmDisarmType.DISARM)); + } + + @Test + void resolveResponseReturnsGenericEventWhenWellformed() { + DigiplexResponse actual = DigiplexResponseResolver.resolveResponse("G123 456 789"); + assertThat(actual, is(instanceOf(GenericEvent.class))); + if (actual instanceof GenericEvent genericEvent) { + assertThat(genericEvent.getEventGroup(), is(123)); + assertThat(genericEvent.getEventNumber(), is(456)); + assertThat(genericEvent.getAreaNo(), is(789)); + } + } +}