mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
Compare commits
5 Commits
dd296fe466
...
3e71bc603f
Author | SHA1 | Date | |
---|---|---|---|
|
3e71bc603f | ||
|
adacdebb9f | ||
|
d36b2a8d82 | ||
|
5ac2780749 | ||
|
288d64be54 |
@ -51,7 +51,6 @@ public class DigiplexBindingConstants {
|
|||||||
public static final String BRIDGE_MESSAGES_SENT = "statistics#messages_sent";
|
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_RESPONSES_RECEIVED = "statistics#responses_received";
|
||||||
public static final String BRIDGE_EVENTS_RECEIVED = "statistics#events_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_TLM_TROUBLE = "troubles#tlm_trouble";
|
||||||
public static final String BRIDGE_AC_FAILURE = "troubles#ac_failure";
|
public static final String BRIDGE_AC_FAILURE = "troubles#ac_failure";
|
||||||
public static final String BRIDGE_BATTERY_FAILURE = "troubles#battery_failure";
|
public static final String BRIDGE_BATTERY_FAILURE = "troubles#battery_failure";
|
||||||
|
@ -52,6 +52,9 @@ public interface DigiplexMessageHandler {
|
|||||||
default void handleUnknownResponse(UnknownResponse response) {
|
default void handleUnknownResponse(UnknownResponse response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default void handleErroneousResponse(ErroneousResponse response) {
|
||||||
|
}
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
default void handleZoneEvent(ZoneEvent event) {
|
default void handleZoneEvent(ZoneEvent event) {
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
package org.openhab.binding.digiplex.internal.communication;
|
package org.openhab.binding.digiplex.internal.communication;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
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.AreaEvent;
|
||||||
import org.openhab.binding.digiplex.internal.communication.events.AreaEventType;
|
import org.openhab.binding.digiplex.internal.communication.events.AreaEventType;
|
||||||
import org.openhab.binding.digiplex.internal.communication.events.GenericEvent;
|
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
|
* Resolves serial messages to appropriate classes
|
||||||
*
|
*
|
||||||
* @author Robert Michalak - Initial contribution
|
* @author Robert Michalak - Initial contribution
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class DigiplexResponseResolver {
|
public class DigiplexResponseResolver {
|
||||||
|
|
||||||
private static final String OK = "&ok";
|
private static final String OK = "&ok";
|
||||||
// TODO: handle failures
|
|
||||||
private static final String FAIL = "&fail";
|
private static final String FAIL = "&fail";
|
||||||
|
|
||||||
public static DigiplexResponse resolveResponse(String message) {
|
public static DigiplexResponse resolveResponse(String message) {
|
||||||
if (message.length() < 4) { // sanity check: try to filter out malformed responses
|
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);
|
String commandType = message.substring(0, 2);
|
||||||
switch (commandType) {
|
switch (commandType) {
|
||||||
case "CO": // communication status
|
case "CO": // communication status
|
||||||
@ -53,24 +52,36 @@ public class DigiplexResponseResolver {
|
|||||||
return CommunicationStatus.OK;
|
return CommunicationStatus.OK;
|
||||||
}
|
}
|
||||||
case "ZL": // zone label
|
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)) {
|
if (message.contains(FAIL)) {
|
||||||
return ZoneLabelResponse.failure(zoneNo);
|
return ZoneLabelResponse.failure(zoneNo);
|
||||||
} else {
|
} else {
|
||||||
return ZoneLabelResponse.success(zoneNo, message.substring(5).trim());
|
return ZoneLabelResponse.success(zoneNo, message.substring(5).trim());
|
||||||
}
|
}
|
||||||
case "AL": // area label
|
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)) {
|
if (message.contains(FAIL)) {
|
||||||
return AreaLabelResponse.failure(areaNo);
|
return AreaLabelResponse.failure(areaNo);
|
||||||
} else {
|
} else {
|
||||||
return AreaLabelResponse.success(areaNo, message.substring(5).trim());
|
return AreaLabelResponse.success(areaNo, message.substring(5).trim());
|
||||||
}
|
}
|
||||||
case "RZ": // zone status
|
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)) {
|
if (message.contains(FAIL)) {
|
||||||
return ZoneStatusResponse.failure(zoneNo);
|
return ZoneStatusResponse.failure(zoneNo);
|
||||||
} else {
|
} else {
|
||||||
|
if (message.length() < 10) {
|
||||||
|
return new ErroneousResponse(message);
|
||||||
|
}
|
||||||
return ZoneStatusResponse.success(zoneNo, // zone number
|
return ZoneStatusResponse.success(zoneNo, // zone number
|
||||||
ZoneStatus.fromMessage(message.charAt(5)), // status
|
ZoneStatus.fromMessage(message.charAt(5)), // status
|
||||||
toBoolean(message.charAt(6)), // alarm
|
toBoolean(message.charAt(6)), // alarm
|
||||||
@ -79,10 +90,16 @@ public class DigiplexResponseResolver {
|
|||||||
toBoolean(message.charAt(9))); // battery low
|
toBoolean(message.charAt(9))); // battery low
|
||||||
}
|
}
|
||||||
case "RA": // area status
|
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)) {
|
if (message.contains(FAIL)) {
|
||||||
return AreaStatusResponse.failure(areaNo);
|
return AreaStatusResponse.failure(areaNo);
|
||||||
} else {
|
} else {
|
||||||
|
if (message.length() < 12) {
|
||||||
|
return new ErroneousResponse(message);
|
||||||
|
}
|
||||||
return AreaStatusResponse.success(areaNo, // zone number
|
return AreaStatusResponse.success(areaNo, // zone number
|
||||||
AreaStatus.fromMessage(message.charAt(5)), // status
|
AreaStatus.fromMessage(message.charAt(5)), // status
|
||||||
toBoolean(message.charAt(6)), // zone in memory
|
toBoolean(message.charAt(6)), // zone in memory
|
||||||
@ -95,7 +112,10 @@ public class DigiplexResponseResolver {
|
|||||||
case "AA": // area arm
|
case "AA": // area arm
|
||||||
case "AQ": // area quick arm
|
case "AQ": // area quick arm
|
||||||
case "AD": // area disarm
|
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)) {
|
if (message.contains(FAIL)) {
|
||||||
return AreaArmDisarmResponse.failure(areaNo, ArmDisarmType.fromMessage(commandType));
|
return AreaArmDisarmResponse.failure(areaNo, ArmDisarmType.fromMessage(commandType));
|
||||||
} else {
|
} else {
|
||||||
@ -105,21 +125,41 @@ public class DigiplexResponseResolver {
|
|||||||
case "PG": // PGM events
|
case "PG": // PGM events
|
||||||
default:
|
default:
|
||||||
if (message.startsWith("G")) {
|
if (message.startsWith("G")) {
|
||||||
return resolveSystemEvent(message);
|
if (message.length() >= 12) {
|
||||||
|
return resolveSystemEvent(message);
|
||||||
|
} else {
|
||||||
|
return new ErroneousResponse(message);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return new UnknownResponse(message);
|
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) {
|
private static boolean toBoolean(char value) {
|
||||||
return value != 'O';
|
return value != 'O';
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DigiplexResponse resolveSystemEvent(String message) {
|
private static DigiplexResponse resolveSystemEvent(String message) {
|
||||||
int eventGroup = Integer.parseInt(message.substring(1, 4));
|
int eventGroup, eventNumber, areaNumber;
|
||||||
int eventNumber = Integer.parseInt(message.substring(5, 8));
|
try {
|
||||||
int areaNumber = Integer.parseInt(message.substring(9, 12));
|
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) {
|
switch (eventGroup) {
|
||||||
case 0:
|
case 0:
|
||||||
return new ZoneStatusEvent(eventNumber, ZoneStatus.CLOSED, areaNumber);
|
return new ZoneStatusEvent(eventNumber, ZoneStatus.CLOSED, areaNumber);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,9 @@ package org.openhab.binding.digiplex.internal.communication;
|
|||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
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
|
* @author Robert Michalak - Initial contribution
|
||||||
*
|
*
|
||||||
|
@ -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.DigiplexRequest;
|
||||||
import org.openhab.binding.digiplex.internal.communication.DigiplexResponse;
|
import org.openhab.binding.digiplex.internal.communication.DigiplexResponse;
|
||||||
import org.openhab.binding.digiplex.internal.communication.DigiplexResponseResolver;
|
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.AbstractEvent;
|
||||||
import org.openhab.binding.digiplex.internal.communication.events.TroubleEvent;
|
import org.openhab.binding.digiplex.internal.communication.events.TroubleEvent;
|
||||||
import org.openhab.binding.digiplex.internal.communication.events.TroubleStatus;
|
import org.openhab.binding.digiplex.internal.communication.events.TroubleStatus;
|
||||||
@ -295,6 +296,12 @@ public class DigiplexBridgeHandler extends BaseBridgeHandler implements SerialPo
|
|||||||
updateState(channel, state);
|
updateState(channel, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleErroneousResponse(ErroneousResponse response) {
|
||||||
|
logger.debug("Erroneous response: {}", response.message);
|
||||||
|
handleCommunicationError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DigiplexReceiverThread extends Thread {
|
private class DigiplexReceiverThread extends Thread {
|
||||||
|
@ -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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -88,6 +88,7 @@ public enum DSCAlarmCode {
|
|||||||
ZoneRestored("610", "Zone Restored", "610: General status of the zone - restored."),
|
ZoneRestored("610", "Zone Restored", "610: General status of the zone - restored."),
|
||||||
EnvisalinkZoneTimerDump("615", "Envisalink Zone Timer Dump",
|
EnvisalinkZoneTimerDump("615", "Envisalink Zone Timer Dump",
|
||||||
"615: The raw zone timers used inside the Envisalink."),
|
"615: The raw zone timers used inside the Envisalink."),
|
||||||
|
BypassedZonesBitfield("616", "Bypassed Zones Bitfield", "616: Bypassed zones bitfield."),
|
||||||
DuressAlarm("620", "Duress Alarm", "620: A duress code has been entered on a system keypad."),
|
DuressAlarm("620", "Duress Alarm", "620: A duress code has been entered on a system keypad."),
|
||||||
FireKeyAlarm("621", "Fire Key Alarm", "621: A Fire key alarm has been activated."),
|
FireKeyAlarm("621", "Fire Key Alarm", "621: A Fire key alarm has been activated."),
|
||||||
FireKeyRestored("622", "Fire Key Alarm Restore", "622: A Fire key alarm has been restored."),
|
FireKeyRestored("622", "Fire Key Alarm Restore", "622: A Fire key alarm has been restored."),
|
||||||
|
@ -79,187 +79,180 @@ public class DSCAlarmMessage {
|
|||||||
* Processes the incoming DSC Alarm message and extracts the information.
|
* Processes the incoming DSC Alarm message and extracts the information.
|
||||||
*/
|
*/
|
||||||
private void processDSCAlarmMessage() {
|
private void processDSCAlarmMessage() {
|
||||||
DSCAlarmCode dscAlarmCode;
|
if (message.length() <= 3) {
|
||||||
|
|
||||||
if (message.length() > 3) {
|
|
||||||
try {
|
|
||||||
if (message.length() >= 8 && message.charAt(2) == ':' && message.charAt(5) == ':') {
|
|
||||||
timeStamp = message.substring(0, 8);
|
|
||||||
message = message.substring(9, message.length() - 2);
|
|
||||||
} else {
|
|
||||||
message = message.substring(0, message.length() - 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
codeReceived = message.substring(0, 3);
|
|
||||||
|
|
||||||
if (message.length() >= 4) {
|
|
||||||
data = message.substring(3);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("processDSCAlarmMessage(): Error processing message: ({}) ", message, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dscAlarmCode = DSCAlarmCode.getDSCAlarmCodeValue(codeReceived);
|
|
||||||
|
|
||||||
if (dscAlarmCode != null) {
|
|
||||||
name = dscAlarmCode.getName();
|
|
||||||
description = dscAlarmCode.getDescription();
|
|
||||||
|
|
||||||
MessageParameters messageParms = DSCALARM_MESSAGE_PARAMETERS.get(dscAlarmCode);
|
|
||||||
|
|
||||||
if (messageParms != null) {
|
|
||||||
boolean hasPartition = messageParms.hasPartition();
|
|
||||||
boolean hasZone = messageParms.hasZone();
|
|
||||||
|
|
||||||
if (hasPartition) {
|
|
||||||
partition = message.substring(3, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasZone) {
|
|
||||||
if (hasPartition) {
|
|
||||||
zone = message.substring(4);
|
|
||||||
} else {
|
|
||||||
zone = message.substring(3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messageType = messageParms.getType();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (dscAlarmCode) {
|
|
||||||
case SystemError: /* 502 */
|
|
||||||
int systemErrorCode = 0;
|
|
||||||
systemErrorCode = Integer.parseInt(data);
|
|
||||||
switch (systemErrorCode) {
|
|
||||||
case 1:
|
|
||||||
error = "Receive Buffer Overrun";
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
error = "Receive Buffer Overflow";
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
error = "Transmit Buffer Overflow";
|
|
||||||
break;
|
|
||||||
case 10:
|
|
||||||
error = "Keybus Transmit Buffer Overrun";
|
|
||||||
break;
|
|
||||||
case 11:
|
|
||||||
error = "Keybus Transmit Time Timeout";
|
|
||||||
break;
|
|
||||||
case 12:
|
|
||||||
error = "Keybus Transmit Mode Timeout";
|
|
||||||
break;
|
|
||||||
case 13:
|
|
||||||
error = "Keybus Transmit Keystring Timeout";
|
|
||||||
break;
|
|
||||||
case 14:
|
|
||||||
error = "Keybus Interface Not Functioning";
|
|
||||||
break;
|
|
||||||
case 15:
|
|
||||||
error = "Keybus Busy - Attempting to Disarm or Arm with user code";
|
|
||||||
break;
|
|
||||||
case 16:
|
|
||||||
error = "Keybus Busy – Lockout";
|
|
||||||
break;
|
|
||||||
case 17:
|
|
||||||
error = "Keybus Busy – Installers Mode";
|
|
||||||
break;
|
|
||||||
case 18:
|
|
||||||
error = "Keybus Busy - General Busy";
|
|
||||||
break;
|
|
||||||
case 20:
|
|
||||||
error = "API Command Syntax Error";
|
|
||||||
break;
|
|
||||||
case 21:
|
|
||||||
error = "API Command Partition Error - Requested Partition is out of bounds";
|
|
||||||
break;
|
|
||||||
case 22:
|
|
||||||
error = "API Command Not Supported";
|
|
||||||
break;
|
|
||||||
case 23:
|
|
||||||
error = "API System Not Armed - Sent in response to a disarm command";
|
|
||||||
break;
|
|
||||||
case 24:
|
|
||||||
error = "API System Not Ready to Arm - System is either not-secure, in exit-delay, or already armed";
|
|
||||||
break;
|
|
||||||
case 25:
|
|
||||||
error = "API Command Invalid Length";
|
|
||||||
break;
|
|
||||||
case 26:
|
|
||||||
error = "API User Code not Required";
|
|
||||||
break;
|
|
||||||
case 27:
|
|
||||||
error = "API Invalid Characters in Command - No alpha characters are allowed except for checksum";
|
|
||||||
break;
|
|
||||||
case 28:
|
|
||||||
error = "API Virtual Keypad is Disabled";
|
|
||||||
break;
|
|
||||||
case 29:
|
|
||||||
error = "API Not Valid Parameter";
|
|
||||||
break;
|
|
||||||
case 30:
|
|
||||||
error = "API Keypad Does Not Come Out of Blank Mode";
|
|
||||||
break;
|
|
||||||
case 31:
|
|
||||||
error = "API IT-100 is Already in Thermostat Menu";
|
|
||||||
break;
|
|
||||||
case 32:
|
|
||||||
error = "API IT-100 is NOT in Thermostat Menu";
|
|
||||||
break;
|
|
||||||
case 33:
|
|
||||||
error = "API No Response From Thermostat or Escort Module";
|
|
||||||
break;
|
|
||||||
case 0:
|
|
||||||
default:
|
|
||||||
error = "No Error";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PartitionArmed: /* 652 */
|
|
||||||
mode = message.substring(4);
|
|
||||||
if ("0".equals(mode)) {
|
|
||||||
name += " (Away)";
|
|
||||||
} else if ("1".equals(mode)) {
|
|
||||||
name += " (Stay)";
|
|
||||||
} else if ("2".equals(mode)) {
|
|
||||||
name += " (ZEA)";
|
|
||||||
} else if ("3".equals(mode)) {
|
|
||||||
name += " (ZES)";
|
|
||||||
}
|
|
||||||
messageType = DSCAlarmMessageType.PARTITION_EVENT;
|
|
||||||
break;
|
|
||||||
case UserClosing: /* 700 */
|
|
||||||
user = message.substring(4);
|
|
||||||
name = name.concat(": " + user);
|
|
||||||
description = codeReceived + ": Partition " + partition + " has been armed by user " + user
|
|
||||||
+ ".";
|
|
||||||
messageType = DSCAlarmMessageType.PARTITION_EVENT;
|
|
||||||
break;
|
|
||||||
case UserOpening: /* 750 */
|
|
||||||
user = message.substring(4);
|
|
||||||
name = name.concat(": " + user);
|
|
||||||
description = codeReceived + ": Partition " + partition + " has been disarmed by user " + user
|
|
||||||
+ ".";
|
|
||||||
messageType = DSCAlarmMessageType.PARTITION_EVENT;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
"parseAPIMessage(): Message Received ({}) - Code: {}, Name: {}, Description: {}, Data: {}\r\n",
|
|
||||||
message, codeReceived, name, description, data);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
codeReceived = "-1";
|
codeReceived = "-1";
|
||||||
data = "";
|
data = "";
|
||||||
dscAlarmCode = DSCAlarmCode.getDSCAlarmCodeValue(codeReceived);
|
DSCAlarmCode dscAlarmCode = DSCAlarmCode.UnknownCode;
|
||||||
name = dscAlarmCode.getName();
|
name = dscAlarmCode.getName();
|
||||||
description = dscAlarmCode.getDescription();
|
description = dscAlarmCode.getDescription();
|
||||||
logger.debug("parseAPIMessage(): Invalid Message Received");
|
logger.debug("parseAPIMessage(): Invalid Message Received");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (message.length() >= 8 && message.charAt(2) == ':' && message.charAt(5) == ':') {
|
||||||
|
timeStamp = message.substring(0, 8);
|
||||||
|
message = message.substring(9, message.length() - 2);
|
||||||
|
} else {
|
||||||
|
message = message.substring(0, message.length() - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
codeReceived = message.substring(0, 3);
|
||||||
|
|
||||||
|
if (message.length() >= 4) {
|
||||||
|
data = message.substring(3);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("processDSCAlarmMessage(): Error processing message: ({}) ", message, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DSCAlarmCode dscAlarmCode = DSCAlarmCode.getDSCAlarmCodeValue(codeReceived);
|
||||||
|
|
||||||
|
if (dscAlarmCode != null) {
|
||||||
|
name = dscAlarmCode.getName();
|
||||||
|
description = dscAlarmCode.getDescription();
|
||||||
|
|
||||||
|
MessageParameters messageParams = DSCALARM_MESSAGE_PARAMETERS.get(dscAlarmCode);
|
||||||
|
|
||||||
|
if (messageParams != null) {
|
||||||
|
boolean hasPartition = messageParams.hasPartition();
|
||||||
|
boolean hasZone = messageParams.hasZone();
|
||||||
|
|
||||||
|
if (hasPartition) {
|
||||||
|
partition = message.substring(3, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasZone) {
|
||||||
|
int zoneIndex = hasPartition ? 4 : 3;
|
||||||
|
zone = message.substring(zoneIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
messageType = messageParams.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (dscAlarmCode) {
|
||||||
|
case SystemError: /* 502 */
|
||||||
|
int systemErrorCode = 0;
|
||||||
|
systemErrorCode = Integer.parseInt(data);
|
||||||
|
switch (systemErrorCode) {
|
||||||
|
case 1:
|
||||||
|
error = "Receive Buffer Overrun";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
error = "Receive Buffer Overflow";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
error = "Transmit Buffer Overflow";
|
||||||
|
break;
|
||||||
|
case 10:
|
||||||
|
error = "Keybus Transmit Buffer Overrun";
|
||||||
|
break;
|
||||||
|
case 11:
|
||||||
|
error = "Keybus Transmit Time Timeout";
|
||||||
|
break;
|
||||||
|
case 12:
|
||||||
|
error = "Keybus Transmit Mode Timeout";
|
||||||
|
break;
|
||||||
|
case 13:
|
||||||
|
error = "Keybus Transmit Keystring Timeout";
|
||||||
|
break;
|
||||||
|
case 14:
|
||||||
|
error = "Keybus Interface Not Functioning";
|
||||||
|
break;
|
||||||
|
case 15:
|
||||||
|
error = "Keybus Busy - Attempting to Disarm or Arm with user code";
|
||||||
|
break;
|
||||||
|
case 16:
|
||||||
|
error = "Keybus Busy – Lockout";
|
||||||
|
break;
|
||||||
|
case 17:
|
||||||
|
error = "Keybus Busy – Installers Mode";
|
||||||
|
break;
|
||||||
|
case 18:
|
||||||
|
error = "Keybus Busy - General Busy";
|
||||||
|
break;
|
||||||
|
case 20:
|
||||||
|
error = "API Command Syntax Error";
|
||||||
|
break;
|
||||||
|
case 21:
|
||||||
|
error = "API Command Partition Error - Requested Partition is out of bounds";
|
||||||
|
break;
|
||||||
|
case 22:
|
||||||
|
error = "API Command Not Supported";
|
||||||
|
break;
|
||||||
|
case 23:
|
||||||
|
error = "API System Not Armed - Sent in response to a disarm command";
|
||||||
|
break;
|
||||||
|
case 24:
|
||||||
|
error = "API System Not Ready to Arm - System is either not-secure, in exit-delay, or already armed";
|
||||||
|
break;
|
||||||
|
case 25:
|
||||||
|
error = "API Command Invalid Length";
|
||||||
|
break;
|
||||||
|
case 26:
|
||||||
|
error = "API User Code not Required";
|
||||||
|
break;
|
||||||
|
case 27:
|
||||||
|
error = "API Invalid Characters in Command - No alpha characters are allowed except for checksum";
|
||||||
|
break;
|
||||||
|
case 28:
|
||||||
|
error = "API Virtual Keypad is Disabled";
|
||||||
|
break;
|
||||||
|
case 29:
|
||||||
|
error = "API Not Valid Parameter";
|
||||||
|
break;
|
||||||
|
case 30:
|
||||||
|
error = "API Keypad Does Not Come Out of Blank Mode";
|
||||||
|
break;
|
||||||
|
case 31:
|
||||||
|
error = "API IT-100 is Already in Thermostat Menu";
|
||||||
|
break;
|
||||||
|
case 32:
|
||||||
|
error = "API IT-100 is NOT in Thermostat Menu";
|
||||||
|
break;
|
||||||
|
case 33:
|
||||||
|
error = "API No Response From Thermostat or Escort Module";
|
||||||
|
break;
|
||||||
|
case 0:
|
||||||
|
default:
|
||||||
|
error = "No Error";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PartitionArmed: /* 652 */
|
||||||
|
mode = message.substring(4);
|
||||||
|
if ("0".equals(mode)) {
|
||||||
|
name += " (Away)";
|
||||||
|
} else if ("1".equals(mode)) {
|
||||||
|
name += " (Stay)";
|
||||||
|
} else if ("2".equals(mode)) {
|
||||||
|
name += " (ZEA)";
|
||||||
|
} else if ("3".equals(mode)) {
|
||||||
|
name += " (ZES)";
|
||||||
|
}
|
||||||
|
messageType = DSCAlarmMessageType.PARTITION_EVENT;
|
||||||
|
break;
|
||||||
|
case UserClosing: /* 700 */
|
||||||
|
user = message.substring(4);
|
||||||
|
name = name.concat(": " + user);
|
||||||
|
description = codeReceived + ": Partition " + partition + " has been armed by user " + user + ".";
|
||||||
|
messageType = DSCAlarmMessageType.PARTITION_EVENT;
|
||||||
|
break;
|
||||||
|
case UserOpening: /* 750 */
|
||||||
|
user = message.substring(4);
|
||||||
|
name = name.concat(": " + user);
|
||||||
|
description = codeReceived + ": Partition " + partition + " has been disarmed by user " + user
|
||||||
|
+ ".";
|
||||||
|
messageType = DSCAlarmMessageType.PARTITION_EVENT;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("parseAPIMessage(): Message Received ({}) - Code: {}, Name: {}, Description: {}, Data: {}\r\n",
|
||||||
|
message, codeReceived, name, description, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,6 +444,8 @@ public class DSCAlarmMessage {
|
|||||||
new MessageParameters(DSCAlarmMessageType.ZONE_EVENT, false, true));
|
new MessageParameters(DSCAlarmMessageType.ZONE_EVENT, false, true));
|
||||||
DSCALARM_MESSAGE_PARAMETERS.put(DSCAlarmCode.EnvisalinkZoneTimerDump,
|
DSCALARM_MESSAGE_PARAMETERS.put(DSCAlarmCode.EnvisalinkZoneTimerDump,
|
||||||
new MessageParameters(DSCAlarmMessageType.PANEL_EVENT, false, false));
|
new MessageParameters(DSCAlarmMessageType.PANEL_EVENT, false, false));
|
||||||
|
DSCALARM_MESSAGE_PARAMETERS.put(DSCAlarmCode.BypassedZonesBitfield,
|
||||||
|
new MessageParameters(DSCAlarmMessageType.ZONE_EVENT, false, false));
|
||||||
DSCALARM_MESSAGE_PARAMETERS.put(DSCAlarmCode.DuressAlarm,
|
DSCALARM_MESSAGE_PARAMETERS.put(DSCAlarmCode.DuressAlarm,
|
||||||
new MessageParameters(DSCAlarmMessageType.PANEL_EVENT, false, false));
|
new MessageParameters(DSCAlarmMessageType.PANEL_EVENT, false, false));
|
||||||
DSCALARM_MESSAGE_PARAMETERS.put(DSCAlarmCode.FireKeyAlarm,
|
DSCALARM_MESSAGE_PARAMETERS.put(DSCAlarmCode.FireKeyAlarm,
|
||||||
|
@ -37,6 +37,7 @@ import org.openhab.core.thing.ChannelUID;
|
|||||||
import org.openhab.core.thing.Thing;
|
import org.openhab.core.thing.Thing;
|
||||||
import org.openhab.core.thing.ThingStatus;
|
import org.openhab.core.thing.ThingStatus;
|
||||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||||
|
import org.openhab.core.thing.binding.ThingHandler;
|
||||||
import org.openhab.core.types.Command;
|
import org.openhab.core.types.Command;
|
||||||
import org.openhab.core.types.RefreshType;
|
import org.openhab.core.types.RefreshType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -449,94 +450,123 @@ public abstract class DSCAlarmBaseBridgeHandler extends BaseBridgeHandler {
|
|||||||
return thing;
|
return thing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Thing> findAllZoneThings() {
|
||||||
|
List<Thing> things = getThing().getThings();
|
||||||
|
return things.stream().filter(this::isZoneThing).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isZoneThing(Thing thing) {
|
||||||
|
ThingHandler thingHandler = thing.getHandler();
|
||||||
|
if (thingHandler == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DSCAlarmBaseThingHandler handler = (DSCAlarmBaseThingHandler) thingHandler;
|
||||||
|
return DSCAlarmThingType.ZONE.equals(handler.getDSCAlarmThingType());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles an incoming message from the DSC Alarm System.
|
* Handles an incoming message from the DSC Alarm System.
|
||||||
*
|
*
|
||||||
* @param incomingMessage
|
* @param incomingMessage
|
||||||
*/
|
*/
|
||||||
public synchronized void handleIncomingMessage(String incomingMessage) {
|
public synchronized void handleIncomingMessage(String incomingMessage) {
|
||||||
if (incomingMessage != null && !incomingMessage.isEmpty()) {
|
if (incomingMessage == null || incomingMessage.isEmpty()) {
|
||||||
DSCAlarmMessage dscAlarmMessage = new DSCAlarmMessage(incomingMessage);
|
logger.debug("handleIncomingMessage(): No Message Received!");
|
||||||
DSCAlarmMessageType dscAlarmMessageType = dscAlarmMessage.getDSCAlarmMessageType();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug("handleIncomingMessage(): Message received: {} - {}", incomingMessage,
|
DSCAlarmMessage dscAlarmMessage = new DSCAlarmMessage(incomingMessage);
|
||||||
dscAlarmMessage.toString());
|
DSCAlarmMessageType dscAlarmMessageType = dscAlarmMessage.getDSCAlarmMessageType();
|
||||||
|
|
||||||
DSCAlarmEvent event = new DSCAlarmEvent(this);
|
logger.debug("handleIncomingMessage(): Message received: {} - {}", incomingMessage, dscAlarmMessage);
|
||||||
event.dscAlarmEventMessage(dscAlarmMessage);
|
|
||||||
DSCAlarmThingType dscAlarmThingType = null;
|
|
||||||
int partitionId = 0;
|
|
||||||
int zoneId = 0;
|
|
||||||
|
|
||||||
DSCAlarmCode dscAlarmCode = DSCAlarmCode
|
DSCAlarmCode dscAlarmCode = DSCAlarmCode
|
||||||
.getDSCAlarmCodeValue(dscAlarmMessage.getMessageInfo(DSCAlarmMessageInfoType.CODE));
|
.getDSCAlarmCodeValue(dscAlarmMessage.getMessageInfo(DSCAlarmMessageInfoType.CODE));
|
||||||
|
|
||||||
if (panelThingHandler != null) {
|
if (panelThingHandler != null) {
|
||||||
panelThingHandler.setPanelMessage(dscAlarmMessage);
|
panelThingHandler.setPanelMessage(dscAlarmMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dscAlarmCode == DSCAlarmCode.LoginResponse) {
|
||||||
|
String dscAlarmMessageData = dscAlarmMessage.getMessageInfo(DSCAlarmMessageInfoType.DATA);
|
||||||
|
if ("3".equals(dscAlarmMessageData)) {
|
||||||
|
sendCommand(DSCAlarmCode.NetworkLogin);
|
||||||
|
// onConnected();
|
||||||
|
} else if ("1".equals(dscAlarmMessageData)) {
|
||||||
|
onConnected();
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
if (dscAlarmCode == DSCAlarmCode.LoginResponse) {
|
} else if (dscAlarmCode == DSCAlarmCode.CommandAcknowledge) {
|
||||||
String dscAlarmMessageData = dscAlarmMessage.getMessageInfo(DSCAlarmMessageInfoType.DATA);
|
String dscAlarmMessageData = dscAlarmMessage.getMessageInfo(DSCAlarmMessageInfoType.DATA);
|
||||||
if ("3".equals(dscAlarmMessageData)) {
|
if ("000".equals(dscAlarmMessageData)) {
|
||||||
sendCommand(DSCAlarmCode.NetworkLogin);
|
setBridgeStatus(true);
|
||||||
// onConnected();
|
|
||||||
} else if ("1".equals(dscAlarmMessageData)) {
|
|
||||||
onConnected();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (dscAlarmCode == DSCAlarmCode.CommandAcknowledge) {
|
|
||||||
String dscAlarmMessageData = dscAlarmMessage.getMessageInfo(DSCAlarmMessageInfoType.DATA);
|
|
||||||
if ("000".equals(dscAlarmMessageData)) {
|
|
||||||
setBridgeStatus(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (dscAlarmMessageType) {
|
DSCAlarmEvent event = new DSCAlarmEvent(this);
|
||||||
case PANEL_EVENT:
|
event.dscAlarmEventMessage(dscAlarmMessage);
|
||||||
dscAlarmThingType = DSCAlarmThingType.PANEL;
|
|
||||||
break;
|
DSCAlarmThingType dscAlarmThingType = null;
|
||||||
case PARTITION_EVENT:
|
int partitionId = 0;
|
||||||
dscAlarmThingType = DSCAlarmThingType.PARTITION;
|
int zoneId = 0;
|
||||||
partitionId = Integer
|
|
||||||
.parseInt(event.getDSCAlarmMessage().getMessageInfo(DSCAlarmMessageInfoType.PARTITION));
|
switch (dscAlarmMessageType) {
|
||||||
break;
|
case PANEL_EVENT:
|
||||||
case ZONE_EVENT:
|
dscAlarmThingType = DSCAlarmThingType.PANEL;
|
||||||
dscAlarmThingType = DSCAlarmThingType.ZONE;
|
break;
|
||||||
zoneId = Integer.parseInt(event.getDSCAlarmMessage().getMessageInfo(DSCAlarmMessageInfoType.ZONE));
|
case PARTITION_EVENT:
|
||||||
break;
|
dscAlarmThingType = DSCAlarmThingType.PARTITION;
|
||||||
case KEYPAD_EVENT:
|
partitionId = Integer
|
||||||
dscAlarmThingType = DSCAlarmThingType.KEYPAD;
|
.parseInt(event.getDSCAlarmMessage().getMessageInfo(DSCAlarmMessageInfoType.PARTITION));
|
||||||
break;
|
break;
|
||||||
default:
|
case ZONE_EVENT:
|
||||||
break;
|
dscAlarmThingType = DSCAlarmThingType.ZONE;
|
||||||
|
zoneId = Integer.parseInt(event.getDSCAlarmMessage().getMessageInfo(DSCAlarmMessageInfoType.ZONE));
|
||||||
|
break;
|
||||||
|
case KEYPAD_EVENT:
|
||||||
|
dscAlarmThingType = DSCAlarmThingType.KEYPAD;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dscAlarmThingType == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DSCAlarmCode.BypassedZonesBitfield.equals(dscAlarmCode)) {
|
||||||
|
List<Thing> allZoneThings = findAllZoneThings();
|
||||||
|
for (Thing zone : allZoneThings) {
|
||||||
|
handleIncomingMessage(event, zone, dscAlarmThingType);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Thing thing = findThing(dscAlarmThingType, partitionId, zoneId);
|
||||||
|
handleIncomingMessage(event, thing, dscAlarmThingType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (dscAlarmThingType != null) {
|
private void handleIncomingMessage(DSCAlarmEvent event, Thing thing, DSCAlarmThingType dscAlarmThingType) {
|
||||||
Thing thing = findThing(dscAlarmThingType, partitionId, zoneId);
|
|
||||||
|
|
||||||
logger.debug("handleIncomingMessage(): Thing Search - '{}'", thing);
|
logger.debug("handleIncomingMessage(): Thing Search - '{}'", thing);
|
||||||
|
|
||||||
if (thing != null) {
|
if (thing != null) {
|
||||||
DSCAlarmBaseThingHandler thingHandler = (DSCAlarmBaseThingHandler) thing.getHandler();
|
DSCAlarmBaseThingHandler thingHandler = (DSCAlarmBaseThingHandler) thing.getHandler();
|
||||||
|
|
||||||
if (thingHandler != null) {
|
if (thingHandler != null) {
|
||||||
if (thingHandler.isThingHandlerInitialized() && thing.getStatus() == ThingStatus.ONLINE) {
|
if (thingHandler.isThingHandlerInitialized() && thing.getStatus() == ThingStatus.ONLINE) {
|
||||||
thingHandler.dscAlarmEventReceived(event, thing);
|
thingHandler.dscAlarmEventReceived(event, thing);
|
||||||
|
|
||||||
} else {
|
|
||||||
logger.debug("handleIncomingMessage(): Thing '{}' Not Refreshed!", thing.getUID());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.debug("handleIncomingMessage(): Thing Not Found! Send to Discovery Service!");
|
logger.debug("handleIncomingMessage(): Thing '{}' Not Refreshed!", thing.getUID());
|
||||||
|
|
||||||
if (dscAlarmDiscoveryService != null) {
|
|
||||||
dscAlarmDiscoveryService.addThing(getThing(), dscAlarmThingType, event);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.debug("handleIncomingMessage(): No Message Received!");
|
logger.debug("handleIncomingMessage(): Thing Not Found! Send to Discovery Service!");
|
||||||
|
|
||||||
|
if (dscAlarmDiscoveryService != null) {
|
||||||
|
dscAlarmDiscoveryService.addThing(getThing(), dscAlarmThingType, event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,9 @@ package org.openhab.binding.dscalarm.internal.handler;
|
|||||||
|
|
||||||
import static org.openhab.binding.dscalarm.internal.DSCAlarmBindingConstants.*;
|
import static org.openhab.binding.dscalarm.internal.DSCAlarmBindingConstants.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.EventObject;
|
import java.util.EventObject;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.openhab.binding.dscalarm.internal.DSCAlarmCode;
|
import org.openhab.binding.dscalarm.internal.DSCAlarmCode;
|
||||||
import org.openhab.binding.dscalarm.internal.DSCAlarmEvent;
|
import org.openhab.binding.dscalarm.internal.DSCAlarmEvent;
|
||||||
@ -129,11 +131,11 @@ public class ZoneThingHandler extends DSCAlarmBaseThingHandler {
|
|||||||
DSCAlarmEvent dscAlarmEvent = (DSCAlarmEvent) event;
|
DSCAlarmEvent dscAlarmEvent = (DSCAlarmEvent) event;
|
||||||
DSCAlarmMessage dscAlarmMessage = dscAlarmEvent.getDSCAlarmMessage();
|
DSCAlarmMessage dscAlarmMessage = dscAlarmEvent.getDSCAlarmMessage();
|
||||||
|
|
||||||
ChannelUID channelUID = null;
|
|
||||||
DSCAlarmCode dscAlarmCode = DSCAlarmCode
|
DSCAlarmCode dscAlarmCode = DSCAlarmCode
|
||||||
.getDSCAlarmCodeValue(dscAlarmMessage.getMessageInfo(DSCAlarmMessageInfoType.CODE));
|
.getDSCAlarmCodeValue(dscAlarmMessage.getMessageInfo(DSCAlarmMessageInfoType.CODE));
|
||||||
logger.debug("dscAlarmEventRecieved(): Thing - {} Command - {}", thing.getUID(), dscAlarmCode);
|
logger.debug("dscAlarmEventRecieved(): Thing - {} Command - {}", thing.getUID(), dscAlarmCode);
|
||||||
|
|
||||||
|
ChannelUID channelUID;
|
||||||
int state = 0;
|
int state = 0;
|
||||||
String status = "";
|
String status = "";
|
||||||
|
|
||||||
@ -173,10 +175,45 @@ public class ZoneThingHandler extends DSCAlarmBaseThingHandler {
|
|||||||
updateChannel(channelUID, state, "");
|
updateChannel(channelUID, state, "");
|
||||||
zoneMessage(status);
|
zoneMessage(status);
|
||||||
break;
|
break;
|
||||||
|
case BypassedZonesBitfield:
|
||||||
|
state = isZoneByPassed(dscAlarmMessage) ? 1 : 0;
|
||||||
|
channelUID = new ChannelUID(getThing().getUID(), ZONE_BYPASS_MODE);
|
||||||
|
updateChannel(channelUID, state, "");
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isZoneByPassed(DSCAlarmMessage dscAlarmMessage) {
|
||||||
|
String data = dscAlarmMessage.getMessageInfo(DSCAlarmMessageInfoType.DATA);
|
||||||
|
List<Integer> bypassedZones = parseZoneIdsFromHex(data);
|
||||||
|
return bypassedZones.contains(getZoneNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Integer> parseZoneIdsFromHex(String data) {
|
||||||
|
// List to store bypassed zones
|
||||||
|
List<Integer> bypassedZones = new ArrayList<>();
|
||||||
|
|
||||||
|
// Process each byte in the HEX string
|
||||||
|
for (int byteIndex = 0; byteIndex < data.length() / 2; byteIndex++) {
|
||||||
|
// Get two characters representing the byte (2 HEX characters = 1 byte)
|
||||||
|
String byteHex = data.substring(byteIndex * 2, byteIndex * 2 + 2);
|
||||||
|
|
||||||
|
// Convert the HEX string to an integer
|
||||||
|
int byteValue = Integer.parseInt(byteHex, 16);
|
||||||
|
|
||||||
|
// Process each bit in the byte (8 bits per byte)
|
||||||
|
for (int bit = 0; bit < 8; bit++) {
|
||||||
|
if ((byteValue & (1 << bit)) != 0) {
|
||||||
|
// Calculate the zone number (1-based)
|
||||||
|
int zoneNumber = byteIndex * 8 + bit + 1;
|
||||||
|
bypassedZones.add(zoneNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bypassedZones;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -365,13 +365,19 @@ public class SpeedtestHandler extends BaseThingHandler {
|
|||||||
isp = tmpCont.getIsp();
|
isp = tmpCont.getIsp();
|
||||||
interfaceInternalIp = tmpCont.getInterface().getInternalIp();
|
interfaceInternalIp = tmpCont.getInterface().getInternalIp();
|
||||||
interfaceExternalIp = tmpCont.getInterface().getExternalIp();
|
interfaceExternalIp = tmpCont.getInterface().getExternalIp();
|
||||||
resultUrl = tmpCont.getResult().getUrl();
|
if (tmpCont.getResult().isPersisted()) {
|
||||||
String url = String.valueOf(resultUrl) + ".png";
|
resultUrl = tmpCont.getResult().getUrl();
|
||||||
logger.debug("Downloading result image from: {}", url);
|
String url = String.valueOf(resultUrl) + ".png";
|
||||||
RawType image = HttpUtil.downloadImage(url);
|
logger.debug("Downloading result image from: {}", url);
|
||||||
if (image != null) {
|
RawType image = HttpUtil.downloadImage(url);
|
||||||
resultImage = image;
|
if (image != null) {
|
||||||
|
resultImage = image;
|
||||||
|
} else {
|
||||||
|
resultImage = UnDefType.NULL;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
logger.debug("Result image not persisted");
|
||||||
|
resultUrl = "";
|
||||||
resultImage = UnDefType.NULL;
|
resultImage = UnDefType.NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,6 +263,9 @@ public class ResultContainer {
|
|||||||
@SerializedName("url")
|
@SerializedName("url")
|
||||||
@Expose
|
@Expose
|
||||||
private String url;
|
private String url;
|
||||||
|
@SerializedName("persisted")
|
||||||
|
@Expose
|
||||||
|
private boolean persisted;
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
@ -279,6 +282,14 @@ public class ResultContainer {
|
|||||||
public void setUrl(String url) {
|
public void setUrl(String url) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPersisted() {
|
||||||
|
return persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPersisted(boolean persisted) {
|
||||||
|
this.persisted = persisted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Server {
|
public class Server {
|
||||||
|
@ -1,6 +1,18 @@
|
|||||||
# Basic Profiles
|
# Basic Profiles
|
||||||
|
|
||||||
This bundle provides a list of useful Profiles.
|
This bundle provides a list of useful Profiles:
|
||||||
|
|
||||||
|
| Profile | Description |
|
||||||
|
| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||||
|
| [Generic Command Profile](#generic-command-profile) | Sends a Command towards the Item when an event is triggered |
|
||||||
|
| [Generic Toggle Switch Profile](#generic-toggle-switch-profile) | Toggles a Switch Item when an event is triggered |
|
||||||
|
| [Debounce (Counting) Profile](#debounce-counting-profile) | Counts and skip a number of State changes |
|
||||||
|
| [Debounce (Time) Profile](#debounce-time-profile) | Reduces the frequency of commands/state updates |
|
||||||
|
| [Invert / Negate Profile](#invert--negate-profile) | Inverts or negate a Command / State |
|
||||||
|
| [Round Profile](#round-profile) | Reduces the number of decimal places from input data |
|
||||||
|
| [Threshold Profile](#threshold-profile) | Translates numeric input data to `ON` or `OFF` based on a threshold value |
|
||||||
|
| [Time Range Command Profile](#time-range-command-profile) | An enhanced implementation of a follow profile which converts `OnOffType` to a `PercentType` |
|
||||||
|
| [State Filter Profile](#state-filter-profile) | Filters input data using arithmetic comparison conditions |
|
||||||
|
|
||||||
## Generic Command Profile
|
## Generic Command Profile
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user