mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
Compare commits
6 Commits
d7558091ec
...
10155a4115
Author | SHA1 | Date | |
---|---|---|---|
|
10155a4115 | ||
|
f6efa87fb2 | ||
|
98ff656400 | ||
|
adacdebb9f | ||
|
d36b2a8d82 | ||
|
5ac2780749 |
@ -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";
|
||||
|
@ -52,6 +52,9 @@ public interface DigiplexMessageHandler {
|
||||
default void handleUnknownResponse(UnknownResponse response) {
|
||||
}
|
||||
|
||||
default void handleErroneousResponse(ErroneousResponse response) {
|
||||
}
|
||||
|
||||
// Events
|
||||
default void handleZoneEvent(ZoneEvent event) {
|
||||
}
|
||||
|
@ -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")) {
|
||||
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);
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
* Unknown message from PRT3
|
||||
* Unknown message from PRT3.
|
||||
*
|
||||
* Message that is otherwise valid, but not handled in this binding.
|
||||
*
|
||||
* @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.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 {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -46,6 +46,8 @@ The following channels are available:
|
||||
| playMode | String | The current playback mode ie: stop, play, pause (ReadOnly). |
|
||||
| timeElapsed | Number:Time | The total number of seconds of playback time elapsed for the current playing title (ReadOnly). |
|
||||
| timeTotal | Number:Time | The total length of the current playing title in seconds (ReadOnly). This data is not provided by all streaming apps. |
|
||||
| endTime | DateTime | The date/time when the currently playing media will end (ReadOnly). N/A if timeTotal is not provided by the current streaming app. |
|
||||
| progress | Dimmer | The current progress [0-100%] of playing media (ReadOnly). N/A if timeTotal is not provided by the current streaming app. |
|
||||
| activeChannel | String | A dropdown containing a list of available TV channels on the Roku TV. The channel currently tuned is automatically selected. The list updates every 10 minutes. |
|
||||
| signalMode | String | The signal type of the current TV channel, ie: 1080i (ReadOnly). |
|
||||
| signalQuality | Number:Dimensionless | The signal quality of the current TV channel, 0-100% (ReadOnly). |
|
||||
@ -59,6 +61,7 @@ The following channels are available:
|
||||
Some Notes:
|
||||
|
||||
- The values for `activeApp`, `activeAppName`, `playMode`, `timeElapsed`, `timeTotal`, `activeChannel`, `signalMode`, `signalQuality`, `channelName`, `programTitle`, `programDescription`, `programRating`, `power` & `powerState` refresh automatically per the configured `refresh` interval.
|
||||
- The `endTime` and `progress` channels may not be accurate for some streaming apps especially 'live' streams where the `timeTotal` value constantly increases.
|
||||
|
||||
**List of available button commands for Roku streaming devices:**
|
||||
|
||||
@ -120,6 +123,8 @@ Player Player_Control "Control" { channel="roku:roku_
|
||||
String Player_PlayMode "Status: [%s]" { channel="roku:roku_player:myplayer1:playMode" }
|
||||
Number:Time Player_TimeElapsed "Elapsed Time: [%d %unit%]" { channel="roku:roku_player:myplayer1:timeElapsed" }
|
||||
Number:Time Player_TimeTotal "Total Time: [%d %unit%]" { channel="roku:roku_player:myplayer1:timeTotal" }
|
||||
DateTime Player_EndTime "End Time: [%1$tl:%1$tM %1$tp]" { channel="roku:roku_player:myplayer1:endTime" }
|
||||
Dimmer Player_Progress "Progress [%.0f%%]" { channel="roku:roku_player:myplayer1:progress" }
|
||||
|
||||
// Roku TV items:
|
||||
|
||||
@ -132,6 +137,8 @@ Player Player_Control "Control" { channel="roku:rok
|
||||
String Player_PlayMode "Status: [%s]" { channel="roku:roku_tv:mytv1:playMode" }
|
||||
Number:Time Player_TimeElapsed "Elapsed Time: [%d %unit%]" { channel="roku:roku_tv:mytv1:timeElapsed" }
|
||||
Number:Time Player_TimeTotal "Total Time: [%d %unit%]" { channel="roku:roku_tv:mytv1:timeTotal" }
|
||||
DateTime Player_EndTime "End Time: [%1$tl:%1$tM %1$tp]" { channel="roku:roku_tv:mytv1:endTime" }
|
||||
Dimmer Player_Progress "Progress [%.0f%%]" { channel="roku:roku_tv:mytv1:progress" }
|
||||
String Player_ActiveChannel "Current Channel: [%s]" { channel="roku:roku_tv:mytv1:activeChannel" }
|
||||
String Player_SignalMode "Signal Mode: [%s]" { channel="roku:roku_tv:mytv1:signalMode" }
|
||||
Number Player_SignalQuality "Signal Quality: [%d %%]" { channel="roku:roku_tv:mytv1:signalQuality" }
|
||||
@ -154,6 +161,8 @@ sitemap roku label="Roku" {
|
||||
Text item=Player_PlayMode
|
||||
Text item=Player_TimeElapsed icon="time"
|
||||
Text item=Player_TimeTotal icon="time"
|
||||
Text item=Player_EndTime icon="time"
|
||||
Slider item=Player_Progress icon="time"
|
||||
// The following items apply to Roku TVs only
|
||||
Switch item=Player_Power
|
||||
Text item=Player_PowerState
|
||||
|
@ -55,6 +55,8 @@ public class RokuBindingConstants {
|
||||
public static final String PLAY_MODE = "playMode";
|
||||
public static final String TIME_ELAPSED = "timeElapsed";
|
||||
public static final String TIME_TOTAL = "timeTotal";
|
||||
public static final String END_TIME = "endTime";
|
||||
public static final String PROGRESS = "progress";
|
||||
public static final String ACTIVE_CHANNEL = "activeChannel";
|
||||
public static final String SIGNAL_MODE = "signalMode";
|
||||
public static final String SIGNAL_QUALITY = "signalQuality";
|
||||
|
@ -14,6 +14,8 @@ package org.openhab.binding.roku.internal.handler;
|
||||
|
||||
import static org.openhab.binding.roku.internal.RokuBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -34,8 +36,10 @@ import org.openhab.binding.roku.internal.dto.DeviceInfo;
|
||||
import org.openhab.binding.roku.internal.dto.Player;
|
||||
import org.openhab.binding.roku.internal.dto.TvChannel;
|
||||
import org.openhab.binding.roku.internal.dto.TvChannels.Channel;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.NextPreviousType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.PlayPauseType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
@ -195,21 +199,32 @@ public class RokuHandler extends BaseThingHandler {
|
||||
PLAY.equalsIgnoreCase(playerInfo.getState()) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
|
||||
|
||||
// Remove non-numeric from string, ie: ' ms'
|
||||
String position = playerInfo.getPosition().replaceAll(NON_DIGIT_PATTERN, EMPTY);
|
||||
if (!EMPTY.equals(position)) {
|
||||
updateState(TIME_ELAPSED,
|
||||
new QuantityType<>(Integer.parseInt(position) / 1000, API_SECONDS_UNIT));
|
||||
final String positionStr = playerInfo.getPosition().replaceAll(NON_DIGIT_PATTERN, EMPTY);
|
||||
int position = -1;
|
||||
if (!EMPTY.equals(positionStr)) {
|
||||
position = Integer.parseInt(positionStr) / 1000;
|
||||
updateState(TIME_ELAPSED, new QuantityType<>(position, API_SECONDS_UNIT));
|
||||
} else {
|
||||
updateState(TIME_ELAPSED, UnDefType.UNDEF);
|
||||
}
|
||||
|
||||
String duration = playerInfo.getDuration().replaceAll(NON_DIGIT_PATTERN, EMPTY);
|
||||
if (!EMPTY.equals(duration)) {
|
||||
updateState(TIME_TOTAL,
|
||||
new QuantityType<>(Integer.parseInt(duration) / 1000, API_SECONDS_UNIT));
|
||||
final String durationStr = playerInfo.getDuration().replaceAll(NON_DIGIT_PATTERN, EMPTY);
|
||||
int duration = -1;
|
||||
if (!EMPTY.equals(durationStr)) {
|
||||
duration = Integer.parseInt(durationStr) / 1000;
|
||||
updateState(TIME_TOTAL, new QuantityType<>(duration, API_SECONDS_UNIT));
|
||||
} else {
|
||||
updateState(TIME_TOTAL, UnDefType.UNDEF);
|
||||
}
|
||||
|
||||
if (position >= 0 && duration > 0) {
|
||||
updateState(END_TIME, new DateTimeType(Instant.now().plusSeconds(duration - position)));
|
||||
updateState(PROGRESS,
|
||||
new PercentType(BigDecimal.valueOf(Math.round(position / (double) duration * 100.0))));
|
||||
} else {
|
||||
updateState(END_TIME, UnDefType.UNDEF);
|
||||
updateState(PROGRESS, UnDefType.UNDEF);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
logger.debug("Unable to parse playerInfo integer value. Exception: {}", e.getMessage());
|
||||
} catch (RokuLimitedModeException e) {
|
||||
@ -224,6 +239,8 @@ public class RokuHandler extends BaseThingHandler {
|
||||
updateState(PLAY_MODE, UnDefType.UNDEF);
|
||||
updateState(TIME_ELAPSED, UnDefType.UNDEF);
|
||||
updateState(TIME_TOTAL, UnDefType.UNDEF);
|
||||
updateState(END_TIME, UnDefType.UNDEF);
|
||||
updateState(PROGRESS, UnDefType.UNDEF);
|
||||
}
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_ROKU_TV) && tvActive) {
|
||||
|
@ -80,6 +80,8 @@ channel-type.roku.channelName.label = Channel Name
|
||||
channel-type.roku.channelName.description = The Name of the Channel Currently Selected
|
||||
channel-type.roku.control.label = Control
|
||||
channel-type.roku.control.description = Control playback e.g. Play/Pause/Next/Previous
|
||||
channel-type.roku.endTime.label = End Time
|
||||
channel-type.roku.endTime.description = The date/time when the currently playing media will end
|
||||
channel-type.roku.playMode.label = Play Mode
|
||||
channel-type.roku.playMode.description = The Current Playback Mode
|
||||
channel-type.roku.powerState.label = Power State
|
||||
@ -93,6 +95,8 @@ channel-type.roku.programRating.label = Program Rating
|
||||
channel-type.roku.programRating.description = The TV Parental Guideline Rating of the Current TV Program
|
||||
channel-type.roku.programTitle.label = Program Title
|
||||
channel-type.roku.programTitle.description = The Name of the Current TV Program
|
||||
channel-type.roku.progress.label = Media Progress
|
||||
channel-type.roku.progress.description = The current progress of playing media
|
||||
channel-type.roku.signalMode.label = Signal Mode
|
||||
channel-type.roku.signalMode.description = The Signal Type of the Current TV Channel, ie: 1080i
|
||||
channel-type.roku.signalQuality.label = Signal Quality
|
||||
|
@ -19,6 +19,8 @@
|
||||
<channel id="playMode" typeId="playMode"/>
|
||||
<channel id="timeElapsed" typeId="timeElapsed"/>
|
||||
<channel id="timeTotal" typeId="timeTotal"/>
|
||||
<channel id="endTime" typeId="endTime"/>
|
||||
<channel id="progress" typeId="progress"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
@ -28,7 +30,7 @@
|
||||
<property name="Serial Number">unknown</property>
|
||||
<property name="Device Id">unknown</property>
|
||||
<property name="Software Version">unknown</property>
|
||||
<property name="thingTypeVersion">1</property>
|
||||
<property name="thingTypeVersion">2</property>
|
||||
</properties>
|
||||
|
||||
<representation-property>uuid</representation-property>
|
||||
@ -52,6 +54,8 @@
|
||||
<channel id="playMode" typeId="playMode"/>
|
||||
<channel id="timeElapsed" typeId="timeElapsed"/>
|
||||
<channel id="timeTotal" typeId="timeTotal"/>
|
||||
<channel id="endTime" typeId="endTime"/>
|
||||
<channel id="progress" typeId="progress"/>
|
||||
<channel id="activeChannel" typeId="activeChannel"/>
|
||||
<channel id="signalMode" typeId="signalMode"/>
|
||||
<channel id="signalQuality" typeId="signalQuality"/>
|
||||
@ -69,7 +73,7 @@
|
||||
<property name="Serial Number">unknown</property>
|
||||
<property name="Device Id">unknown</property>
|
||||
<property name="Software Version">unknown</property>
|
||||
<property name="thingTypeVersion">1</property>
|
||||
<property name="thingTypeVersion">2</property>
|
||||
</properties>
|
||||
|
||||
<representation-property>uuid</representation-property>
|
||||
@ -185,6 +189,24 @@
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="endTime">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>End Time</label>
|
||||
<description>The date/time when the currently playing media will end</description>
|
||||
<category>Time</category>
|
||||
<tags>
|
||||
<tag>Status</tag>
|
||||
<tag>Timestamp</tag>
|
||||
</tags>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="progress">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Media Progress</label>
|
||||
<description>The current progress of playing media</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="activeChannel">
|
||||
<item-type>String</item-type>
|
||||
<label>Active Channel</label>
|
||||
|
@ -12,6 +12,15 @@
|
||||
<type>roku:control</type>
|
||||
</add-channel>
|
||||
</instruction-set>
|
||||
|
||||
<instruction-set targetVersion="2">
|
||||
<add-channel id="endTime">
|
||||
<type>roku:endTime</type>
|
||||
</add-channel>
|
||||
<add-channel id="progress">
|
||||
<type>roku:progress</type>
|
||||
</add-channel>
|
||||
</instruction-set>
|
||||
</thing-type>
|
||||
|
||||
<thing-type uid="roku:roku_tv">
|
||||
@ -29,6 +38,15 @@
|
||||
<type>roku:control</type>
|
||||
</add-channel>
|
||||
</instruction-set>
|
||||
|
||||
<instruction-set targetVersion="2">
|
||||
<add-channel id="endTime">
|
||||
<type>roku:endTime</type>
|
||||
</add-channel>
|
||||
<add-channel id="progress">
|
||||
<type>roku:progress</type>
|
||||
</add-channel>
|
||||
</instruction-set>
|
||||
</thing-type>
|
||||
|
||||
</update:update-descriptions>
|
||||
|
@ -365,6 +365,7 @@ public class SpeedtestHandler extends BaseThingHandler {
|
||||
isp = tmpCont.getIsp();
|
||||
interfaceInternalIp = tmpCont.getInterface().getInternalIp();
|
||||
interfaceExternalIp = tmpCont.getInterface().getExternalIp();
|
||||
if (tmpCont.getResult().isPersisted()) {
|
||||
resultUrl = tmpCont.getResult().getUrl();
|
||||
String url = String.valueOf(resultUrl) + ".png";
|
||||
logger.debug("Downloading result image from: {}", url);
|
||||
@ -374,6 +375,11 @@ public class SpeedtestHandler extends BaseThingHandler {
|
||||
} else {
|
||||
resultImage = UnDefType.NULL;
|
||||
}
|
||||
} else {
|
||||
logger.debug("Result image not persisted");
|
||||
resultUrl = "";
|
||||
resultImage = UnDefType.NULL;
|
||||
}
|
||||
|
||||
server = tmpCont.getServer().getName() + " (" + tmpCont.getServer().getId().toString() + ") "
|
||||
+ tmpCont.getServer().getLocation();
|
||||
|
@ -263,6 +263,9 @@ public class ResultContainer {
|
||||
@SerializedName("url")
|
||||
@Expose
|
||||
private String url;
|
||||
@SerializedName("persisted")
|
||||
@Expose
|
||||
private boolean persisted;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
@ -279,6 +282,14 @@ public class ResultContainer {
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public boolean isPersisted() {
|
||||
return persisted;
|
||||
}
|
||||
|
||||
public void setPersisted(boolean persisted) {
|
||||
this.persisted = persisted;
|
||||
}
|
||||
}
|
||||
|
||||
public class Server {
|
||||
|
@ -1,6 +1,18 @@
|
||||
# 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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user