Skip to content

Commit e079b06

Browse files
committed
Introduce synchronized websocket interaction
1 parent a2276e2 commit e079b06

3 files changed

Lines changed: 115 additions & 20 deletions

File tree

src/main/java/cz/smarteon/loxone/LoxoneWebSocket.java

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ public class LoxoneWebSocket {
6969
private CountDownLatch authSeqLatch;
7070
private CountDownLatch visuLatch;
7171

72+
private SyncCommandGuard<?> syncCommandGuard;
73+
7274
private int authTimeoutSeconds = 3;
7375
private int visuTimeoutSeconds = 3;
7476
private int retries = 5;
@@ -115,7 +117,7 @@ public void registerListener(@NotNull final LoxoneEventListener listener) {
115117
eventListeners.add(listener);
116118
}
117119

118-
public void sendCommand(@NotNull final Command<?> command) {
120+
public synchronized void sendCommand(@NotNull final Command<?> command) {
119121
requireNonNull(command, "command can't be null");
120122
if (command.isWsSupported()) {
121123
sendWithRetry(command, retries);
@@ -124,10 +126,25 @@ public void sendCommand(@NotNull final Command<?> command) {
124126
}
125127
}
126128

127-
public void sendSecureCommand(@NotNull final ControlCommand<?> command) {
129+
public synchronized void sendSecureCommand(@NotNull final ControlCommand<?> command) {
128130
sendSecureWithRetry(command, retries);
129131
}
130132

133+
public synchronized <T> T commandRequest(@NotNull final Command<T> command) {
134+
requireNonNull(command, "command can't be null");
135+
if (command.isWsSupported()) {
136+
try {
137+
syncCommandGuard = new SyncCommandGuard<>(command);
138+
sendWithRetry(command, retries);
139+
return (T) syncCommandGuard.waitForResponse(retries * authTimeoutSeconds);
140+
} finally {
141+
syncCommandGuard = null;
142+
}
143+
} else {
144+
throw new IllegalArgumentException("Only websocket commands are supported");
145+
}
146+
}
147+
131148
public void close() {
132149
scheduler.shutdownNow();
133150
closeWebSocket();
@@ -328,7 +345,7 @@ void sendInternal(final Command<?> command) {
328345
LOG.debug("Sending websocket message: " + command.getCommand());
329346
webSocketClient.send(command.getCommand());
330347
// KEEP_ALIVE command has no response at all
331-
if (!KEEP_ALIVE.getCommand().equals(command.getCommand())) {
348+
if (!KEEP_ALIVE.getCommand().equals(command.getCommand()) && syncCommandGuard == null) {
332349
commands.add(command);
333350
}
334351
}
@@ -339,7 +356,12 @@ void sendInternal(final Command<?> command) {
339356
*/
340357
void processMessage(final String message) {
341358
try {
342-
final Command<?> command = commands.remove();
359+
Command<?> command;
360+
if (syncCommandGuard != null) {
361+
command = syncCommandGuard.getCommand();
362+
} else {
363+
command= commands.remove();
364+
}
343365
if (!Void.class.equals(command.getResponseType())) {
344366
final Object parsedMessage = Codec.readMessage(message, command.getResponseType());
345367
if (parsedMessage instanceof LoxoneMessage) {
@@ -467,26 +489,29 @@ private boolean checkLoxoneMessage(final Command<?> command, final LoxoneMessage
467489

468490
@SuppressWarnings("unchecked")
469491
private void processCommand(final Command<?> command, final Object message, final boolean isError) {
470-
CommandResponseListener.State commandState = CommandResponseListener.State.IGNORED;
471-
final Iterator<CommandResponseListener<?>> listeners = commandResponseListeners.iterator();
472-
while (listeners.hasNext() && commandState != CommandResponseListener.State.CONSUMED) {
473-
@SuppressWarnings("rawtypes")
474-
final CommandResponseListener next = listeners.next();
475-
if (isError && next instanceof LoxoneMessageCommandResponseListener) {
476-
if (((LoxoneMessageCommandResponseListener) next).acceptsErrorResponses()) {
492+
if (syncCommandGuard != null) {
493+
syncCommandGuard.receive(message);
494+
} else {
495+
CommandResponseListener.State commandState = CommandResponseListener.State.IGNORED;
496+
final Iterator<CommandResponseListener<?>> listeners = commandResponseListeners.iterator();
497+
while (listeners.hasNext() && commandState != CommandResponseListener.State.CONSUMED) {
498+
@SuppressWarnings("rawtypes") final CommandResponseListener next = listeners.next();
499+
if (isError && next instanceof LoxoneMessageCommandResponseListener) {
500+
if (((LoxoneMessageCommandResponseListener) next).acceptsErrorResponses()) {
501+
commandState = commandState.fold(next.onCommand(command, message));
502+
}
503+
} else if (next.accepts(message.getClass())) {
477504
commandState = commandState.fold(next.onCommand(command, message));
478505
}
479-
} else if (next.accepts(message.getClass())) {
480-
commandState = commandState.fold(next.onCommand(command, message));
481506
}
482-
}
483507

484-
if (commandState == CommandResponseListener.State.IGNORED) {
485-
LOG.warn("No command listener registered, ignoring command=" + command);
486-
}
508+
if (commandState == CommandResponseListener.State.IGNORED) {
509+
LOG.warn("No command listener registered, ignoring command=" + command);
510+
}
487511

488-
if (command != null && command.getCommand().startsWith(C_SYS_ENC)) {
489-
LOG.warn("Encrypted message receive is not supported");
512+
if (command != null && command.getCommand().startsWith(C_SYS_ENC)) {
513+
LOG.warn("Encrypted message receive is not supported");
514+
}
490515
}
491516
}
492517

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package cz.smarteon.loxone;
2+
3+
import cz.smarteon.loxone.message.LoxoneMessage;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
7+
import java.util.concurrent.CountDownLatch;
8+
import java.util.concurrent.TimeUnit;
9+
10+
class SyncCommandGuard<T> {
11+
12+
private static final Logger LOG = LoggerFactory.getLogger(SyncCommandGuard.class);
13+
14+
private CountDownLatch latch;
15+
16+
private Command<T> command;
17+
18+
private Object response;
19+
20+
SyncCommandGuard(final Command<T> command) {
21+
this.command = command;
22+
latch = new CountDownLatch(1);
23+
}
24+
25+
T waitForResponse(int seconds) {
26+
try {
27+
if (latch.await(seconds, TimeUnit.SECONDS)) {
28+
try {
29+
return (T) response;
30+
} catch (ClassCastException cce) {
31+
if (response instanceof LoxoneMessage<?>) {
32+
LoxoneMessage error = (LoxoneMessage<?>) response;
33+
throw new LoxoneException("Error received of " + error.getControl() + " code " + error.getCode());
34+
} else {
35+
throw new LoxoneException("Unrecognizable error received to " + command.getCommand());
36+
}
37+
}
38+
} else {
39+
throw new LoxoneException("Timeout waiting for sync command response " + command.getCommand());
40+
}
41+
} catch (InterruptedException e) {
42+
LOG.error("Interrupted while waiting for sync command request completion", e);
43+
throw new LoxoneException("Interrupted while waiting for sync command request completion");
44+
}
45+
}
46+
47+
void receive(final Object response) {
48+
this.response = response;
49+
latch.countDown();
50+
}
51+
52+
Command<T> getCommand() {
53+
return command;
54+
}
55+
}

src/test/kotlin/LoxoneAT.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cz.smarteon.loxone
22

33
import cz.smarteon.loxone.app.SwitchControl
4+
import cz.smarteon.loxone.message.ControlCommand
45
import cz.smarteon.loxone.message.JsonValue
56
import cz.smarteon.loxone.message.LoxoneMessage
67
import io.mockk.every
@@ -101,6 +102,20 @@ class LoxoneAT {
101102

102103
@Test
103104
@Order(4)
105+
fun `should pulse on switch sync`() {
106+
val response = device?.let { device ->
107+
loxone.webSocket().commandRequest(ControlCommand.genericControlCommand(device.uuid.toString(), "Pulse"))
108+
}
109+
110+
expectThat(response){
111+
isA<LoxoneMessage<*>>()
112+
.get { value }.isA<JsonValue>()
113+
.get { jsonNode.textValue() }.isEqualTo("1")
114+
}
115+
}
116+
117+
@Test
118+
@Order(5)
104119
fun `should pulse on secured switch`() {
105120
val latch = commands.expectCommand(".*${secDevice?.uuid}/Pulse")
106121
secDevice?.let {secDevice -> loxone.sendControlPulse(secDevice) }
@@ -119,7 +134,7 @@ class LoxoneAT {
119134
}
120135

121136
@Test
122-
@Order(5)
137+
@Order(6)
123138
fun `should refresh token`() {
124139
val evaluator = mockk<TokenStateEvaluator> {
125140
every { evaluate(any()) } answers { mockk {

0 commit comments

Comments
 (0)