Compare commits

...

6 Commits

Author SHA1 Message Date
Jeremy
fd48cc8d36
Merge 2a38b95c71 into 98ff656400 2025-01-08 17:58:19 -05:00
Jacob Laursen
98ff656400
Fix headers (#18070)
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
2025-01-08 23:25:39 +01:00
Robert Michalak
adacdebb9f
[digiplex] Handle erroneous responses and restart the bridge (#18035)
Signed-off-by: Robert Michalak <rbrt.michalak@gmail.com>
2025-01-08 22:21:07 +01:00
jimtng
d36b2a8d82
[basicprofiles] Add a table-of-contents at top of README.md (#18058)
Signed-off-by: Jimmy Tanagra <jcode@tanagra.id.au>
2025-01-08 20:15:25 +01:00
Mark Herwege
5ac2780749
fix offline when image not available (#18066)
Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
2025-01-08 20:03:21 +01:00
Jeremy Setton
2a38b95c71 [insteon] Add debug monitoring support for modem and x10 devices
Signed-off-by: Jeremy Setton <jeremy.setton@gmail.com>
2025-01-05 19:33:18 -05:00
11 changed files with 431 additions and 85 deletions

View File

@ -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";

View File

@ -52,6 +52,9 @@ public interface DigiplexMessageHandler {
default void handleUnknownResponse(UnknownResponse response) {
}
default void handleErroneousResponse(ErroneousResponse response) {
}
// Events
default void handleZoneEvent(ZoneEvent event) {
}

View File

@ -13,6 +13,7 @@
package org.openhab.binding.digiplex.internal.communication;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.digiplex.internal.communication.events.AreaEvent;
import org.openhab.binding.digiplex.internal.communication.events.AreaEventType;
import org.openhab.binding.digiplex.internal.communication.events.GenericEvent;
@ -29,21 +30,19 @@ import org.openhab.binding.digiplex.internal.communication.events.ZoneStatusEven
* Resolves serial messages to appropriate classes
*
* @author Robert Michalak - Initial contribution
*
*/
@NonNullByDefault
public class DigiplexResponseResolver {
private static final String OK = "&ok";
// TODO: handle failures
private static final String FAIL = "&fail";
public static DigiplexResponse resolveResponse(String message) {
if (message.length() < 4) { // sanity check: try to filter out malformed responses
return new UnknownResponse(message);
return new ErroneousResponse(message);
}
int zoneNo, areaNo;
Integer zoneNo, areaNo;
String commandType = message.substring(0, 2);
switch (commandType) {
case "CO": // communication status
@ -53,24 +52,36 @@ public class DigiplexResponseResolver {
return CommunicationStatus.OK;
}
case "ZL": // zone label
zoneNo = Integer.valueOf(message.substring(2, 5));
zoneNo = getZoneOrArea(message);
if (zoneNo == null) {
return new ErroneousResponse(message);
}
if (message.contains(FAIL)) {
return ZoneLabelResponse.failure(zoneNo);
} else {
return ZoneLabelResponse.success(zoneNo, message.substring(5).trim());
}
case "AL": // area label
areaNo = Integer.valueOf(message.substring(2, 5));
areaNo = getZoneOrArea(message);
if (areaNo == null) {
return new ErroneousResponse(message);
}
if (message.contains(FAIL)) {
return AreaLabelResponse.failure(areaNo);
} else {
return AreaLabelResponse.success(areaNo, message.substring(5).trim());
}
case "RZ": // zone status
zoneNo = Integer.valueOf(message.substring(2, 5));
zoneNo = getZoneOrArea(message);
if (zoneNo == null) {
return new ErroneousResponse(message);
}
if (message.contains(FAIL)) {
return ZoneStatusResponse.failure(zoneNo);
} else {
if (message.length() < 10) {
return new ErroneousResponse(message);
}
return ZoneStatusResponse.success(zoneNo, // zone number
ZoneStatus.fromMessage(message.charAt(5)), // status
toBoolean(message.charAt(6)), // alarm
@ -79,10 +90,16 @@ public class DigiplexResponseResolver {
toBoolean(message.charAt(9))); // battery low
}
case "RA": // area status
areaNo = Integer.valueOf(message.substring(2, 5));
areaNo = getZoneOrArea(message);
if (areaNo == null) {
return new ErroneousResponse(message);
}
if (message.contains(FAIL)) {
return AreaStatusResponse.failure(areaNo);
} else {
if (message.length() < 12) {
return new ErroneousResponse(message);
}
return AreaStatusResponse.success(areaNo, // zone number
AreaStatus.fromMessage(message.charAt(5)), // status
toBoolean(message.charAt(6)), // zone in memory
@ -95,7 +112,10 @@ public class DigiplexResponseResolver {
case "AA": // area arm
case "AQ": // area quick arm
case "AD": // area disarm
areaNo = Integer.valueOf(message.substring(2, 5));
areaNo = getZoneOrArea(message);
if (areaNo == null) {
return new ErroneousResponse(message);
}
if (message.contains(FAIL)) {
return AreaArmDisarmResponse.failure(areaNo, ArmDisarmType.fromMessage(commandType));
} else {
@ -105,21 +125,41 @@ public class DigiplexResponseResolver {
case "PG": // PGM events
default:
if (message.startsWith("G")) {
return resolveSystemEvent(message);
if (message.length() >= 12) {
return resolveSystemEvent(message);
} else {
return new ErroneousResponse(message);
}
} else {
return new UnknownResponse(message);
}
}
}
private static @Nullable Integer getZoneOrArea(String message) {
if (message.length() < 5) {
return null;
}
try {
return Integer.valueOf(message.substring(2, 5));
} catch (NumberFormatException e) {
return null;
}
}
private static boolean toBoolean(char value) {
return value != 'O';
}
private static DigiplexResponse resolveSystemEvent(String message) {
int eventGroup = Integer.parseInt(message.substring(1, 4));
int eventNumber = Integer.parseInt(message.substring(5, 8));
int areaNumber = Integer.parseInt(message.substring(9, 12));
int eventGroup, eventNumber, areaNumber;
try {
eventGroup = Integer.parseInt(message.substring(1, 4));
eventNumber = Integer.parseInt(message.substring(5, 8));
areaNumber = Integer.parseInt(message.substring(9, 12));
} catch (NumberFormatException e) {
return new ErroneousResponse(message);
}
switch (eventGroup) {
case 0:
return new ZoneStatusEvent(eventNumber, ZoneStatus.CLOSED, areaNumber);

View File

@ -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);
}
}

View File

@ -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
*

View File

@ -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 {

View File

@ -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));
}
}
}

View File

@ -27,6 +27,8 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.insteon.internal.device.DeviceAddress;
import org.openhab.binding.insteon.internal.device.InsteonAddress;
import org.openhab.binding.insteon.internal.device.InsteonScene;
import org.openhab.binding.insteon.internal.device.X10Address;
@ -82,7 +84,8 @@ public class DebugCommand extends InsteonCommand implements PortListener {
private boolean monitoring = false;
private boolean monitorAllDevices = false;
private Set<InsteonAddress> monitoredAddresses = new HashSet<>();
private Set<DeviceAddress> monitoredAddresses = new HashSet<>();
private @Nullable X10Address lastX10Address;
public DebugCommand(InsteonCommandExtension commandExtension) {
super(NAME, DESCRIPTION, commandExtension);
@ -90,11 +93,11 @@ public class DebugCommand extends InsteonCommand implements PortListener {
@Override
public List<String> getUsages() {
return List.of(buildCommandUsage(LIST_MONITORED, "list monitored device(s)"),
return List.of(buildCommandUsage(LIST_MONITORED, "list monitored Insteon/X10 device(s)"),
buildCommandUsage(START_MONITORING + " " + ALL_OPTION + "|<address>",
"start logging message events for device(s) in separate file(s)"),
"start logging message events for Insteon/X10 device(s) in separate file(s)"),
buildCommandUsage(STOP_MONITORING + " " + ALL_OPTION + "|<address>",
"stop logging message events for device(s) in separate file(s)"),
"stop logging message events for Insteon/X10 device(s) in separate file(s)"),
buildCommandUsage(SEND_BROADCAST_MESSAGE + " <group> <cmd1> <cmd2>",
"send an Insteon broadcast message to a group"),
buildCommandUsage(SEND_STANDARD_MESSAGE + " <address> <cmd1> <cmd2>",
@ -196,14 +199,15 @@ public class DebugCommand extends InsteonCommand implements PortListener {
case START_MONITORING:
strings = monitorAllDevices ? List.of()
: Stream.concat(Stream.of(ALL_OPTION),
getModem().getDB().getDevices().stream()
Stream.concat(Stream.of(getModem().getAddress()),
getModem().getDB().getDevices().stream())
.filter(address -> !monitoredAddresses.contains(address))
.map(InsteonAddress::toString))
.toList();
break;
case STOP_MONITORING:
strings = monitorAllDevices ? List.of(ALL_OPTION)
: monitoredAddresses.stream().map(InsteonAddress::toString).toList();
: monitoredAddresses.stream().map(DeviceAddress::toString).toList();
break;
case SEND_BROADCAST_MESSAGE:
strings = getModem().getDB().getBroadcastGroups().stream().map(String::valueOf).toList();
@ -235,54 +239,65 @@ public class DebugCommand extends InsteonCommand implements PortListener {
@Override
public void messageReceived(Msg msg) {
try {
InsteonAddress address = msg.getInsteonAddress(msg.isReply() ? "toAddress" : "fromAddress");
if (monitorAllDevices || monitoredAddresses.contains(address)) {
logMessageEvent(address, msg);
}
} catch (FieldException ignored) {
// ignore message with no address field
}
logMessageEvent(msg);
}
@Override
public void messageSent(Msg msg) {
try {
InsteonAddress address = msg.getInsteonAddress("toAddress");
if (monitorAllDevices || monitoredAddresses.contains(address)) {
logMessageEvent(address, msg);
logMessageEvent(msg);
}
private DeviceAddress getMsgEventAddress(Msg msg) throws FieldException {
if (msg.isX10()) {
X10Address address = msg.isX10Address() ? msg.getX10Address() : lastX10Address;
if (address == null) {
throw new FieldException("unknown x10 address");
}
} catch (FieldException ignored) {
// ignore message with no address field
lastX10Address = address;
return address;
} else if (msg.isInsteon()) {
return msg.isInbound() && !msg.isReply() ? msg.getInsteonAddress("fromAddress")
: !msg.isAllLinkBroadcast() ? msg.getInsteonAddress("toAddress") : getModem().getAddress();
} else {
return getModem().getAddress();
}
}
private Path getMsgEventsFilePath(String address) {
return getBindingDataFilePath(MSG_EVENTS_FILE_PREFIX + "-" + address.toLowerCase().replace(".", "") + ".log");
private Path getMsgEventsFilePath(DeviceAddress address) {
String name = address.toString().toLowerCase().replace(".", "");
if (address instanceof X10Address) {
name = "x10-" + name;
}
return getBindingDataFilePath(MSG_EVENTS_FILE_PREFIX + "-" + name + ".log");
}
private void clearMonitorFiles(String address) {
String prefix = ALL_OPTION.equals(address) ? MSG_EVENTS_FILE_PREFIX
private void clearMonitorFiles(@Nullable DeviceAddress address) {
String prefix = address == null ? MSG_EVENTS_FILE_PREFIX
: getMsgEventsFilePath(address).getFileName().toString();
getBindingDataFilePaths(prefix).map(Path::toFile).forEach(File::delete);
}
private void logMessageEvent(InsteonAddress address, Msg msg) {
String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date());
String line = timestamp + " " + msg + System.lineSeparator();
Path path = getMsgEventsFilePath(address.toString());
private void logMessageEvent(Msg msg) {
try {
Files.createDirectories(path.getParent());
Files.writeString(path, line, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
DeviceAddress address = getMsgEventAddress(msg);
if (monitorAllDevices || monitoredAddresses.contains(address)) {
String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date());
String line = timestamp + " " + msg + System.lineSeparator();
Path path = getMsgEventsFilePath(address);
Files.createDirectories(path.getParent());
Files.writeString(path, line, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
} catch (FieldException e) {
logger.warn("failed to parse message", e);
} catch (IOException e) {
logger.warn("failed to write to message event file", e);
}
}
private void listMonitoredDevices(Console console) {
String addresses = monitoredAddresses.stream().map(InsteonAddress::toString).collect(Collectors.joining(", "));
String addresses = monitoredAddresses.stream().map(DeviceAddress::toString).collect(Collectors.joining(", "));
if (!addresses.isEmpty()) {
console.println("The monitored device(s) are: " + addresses);
} else if (monitorAllDevices) {
@ -292,30 +307,27 @@ public class DebugCommand extends InsteonCommand implements PortListener {
}
}
private void startMonitoring(Console console, String address) {
if (ALL_OPTION.equals(address)) {
if (!monitorAllDevices) {
monitorAllDevices = true;
monitoredAddresses.clear();
console.println("Started monitoring all devices.");
console.println("Message events logged in " + getBindingDataDirPath());
clearMonitorFiles(address);
} else {
console.println("Already monitoring all devices.");
}
} else if (InsteonAddress.isValid(address)) {
if (monitorAllDevices) {
console.println("Already monitoring all devices.");
} else if (monitoredAddresses.add(new InsteonAddress(address))) {
private void startMonitoring(Console console, String arg) {
if (monitorAllDevices) {
console.println("Already monitoring all devices.");
} else if (ALL_OPTION.equals(arg)) {
monitorAllDevices = true;
monitoredAddresses.clear();
console.println("Started monitoring all devices.");
console.println("Message events logged in " + getBindingDataDirPath());
clearMonitorFiles(null);
} else {
DeviceAddress address = InsteonAddress.isValid(arg) ? new InsteonAddress(arg)
: X10Address.isValid(arg) ? new X10Address(arg) : null;
if (address == null) {
console.println("Invalid device address argument: " + arg);
} else if (monitoredAddresses.add(address)) {
console.println("Started monitoring the device " + address + ".");
console.println("Message events logged in " + getMsgEventsFilePath(address));
clearMonitorFiles(address);
} else {
console.println("Already monitoring the device " + address + ".");
}
} else {
console.println("Invalid device address" + address + ".");
return;
}
if (!monitoring) {
@ -324,31 +336,26 @@ public class DebugCommand extends InsteonCommand implements PortListener {
}
}
private void stopMonitoring(Console console, String address) {
if (!monitoring) {
console.println("Not monitoring any devices.");
return;
}
if (ALL_OPTION.equals(address)) {
private void stopMonitoring(Console console, String arg) {
if (ALL_OPTION.equals(arg)) {
if (monitorAllDevices) {
monitorAllDevices = false;
console.println("Stopped monitoring all devices.");
} else {
console.println("Not monitoring all devices.");
}
} else if (InsteonAddress.isValid(address)) {
} else {
DeviceAddress address = InsteonAddress.isValid(arg) ? new InsteonAddress(arg)
: X10Address.isValid(arg) ? new X10Address(arg) : null;
if (monitorAllDevices) {
console.println("Not monitoring individual devices.");
} else if (monitoredAddresses.remove(new InsteonAddress(address))) {
} else if (address == null) {
console.println("Invalid device address argument: " + arg);
} else if (monitoredAddresses.remove(address)) {
console.println("Stopped monitoring the device " + address + ".");
} else {
console.println("Not monitoring the device " + address + ".");
return;
}
} else {
console.println("Invalid address device address " + address + ".");
return;
}
if (!monitorAllDevices && monitoredAddresses.isEmpty()) {

View File

@ -365,13 +365,19 @@ public class SpeedtestHandler extends BaseThingHandler {
isp = tmpCont.getIsp();
interfaceInternalIp = tmpCont.getInterface().getInternalIp();
interfaceExternalIp = tmpCont.getInterface().getExternalIp();
resultUrl = tmpCont.getResult().getUrl();
String url = String.valueOf(resultUrl) + ".png";
logger.debug("Downloading result image from: {}", url);
RawType image = HttpUtil.downloadImage(url);
if (image != null) {
resultImage = image;
if (tmpCont.getResult().isPersisted()) {
resultUrl = tmpCont.getResult().getUrl();
String url = String.valueOf(resultUrl) + ".png";
logger.debug("Downloading result image from: {}", url);
RawType image = HttpUtil.downloadImage(url);
if (image != null) {
resultImage = image;
} else {
resultImage = UnDefType.NULL;
}
} else {
logger.debug("Result image not persisted");
resultUrl = "";
resultImage = UnDefType.NULL;
}

View File

@ -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 {

View File

@ -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