Skip to content

Commit db6d615

Browse files
authored
Merge pull request #227 from TCke83/feature/state-listener
Added listener capabilities to State objects
2 parents 80fa668 + 080f789 commit db6d615

13 files changed

+393
-88
lines changed

src/main/java/cz/smarteon/loxone/app/state/AnalogInfoControlState.java

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,7 @@
1010
/**
1111
* State class for keeping state of a <code>AnalogInfoControl</code>.
1212
*/
13-
public class AnalogInfoControlState extends ControlState<AnalogInfoControl> {
14-
15-
/**
16-
* Current value of the AnalogInfoControl.
17-
*/
18-
@Getter
19-
@Nullable
20-
private Double value;
21-
13+
public class AnalogInfoControlState extends ControlState<Double, AnalogInfoControl> {
2214

2315
public AnalogInfoControlState(Loxone loxone, AnalogInfoControl control) {
2416
super(loxone, control);
@@ -30,17 +22,8 @@ public AnalogInfoControlState(Loxone loxone, AnalogInfoControl control) {
3022
*/
3123
@Override
3224
void accept(@NotNull ValueEvent event) {
33-
super.accept(event);
34-
if (event.getUuid().equals(control.stateValue())) {
35-
processValueEvent(event);
25+
if (event.getUuid().equals(getControl().stateValue())) {
26+
setState(event.getValue());
3627
}
3728
}
38-
39-
/**
40-
* Process the ValueEvent as a value event message and update the state of the control accordingly.
41-
* @param event value event received
42-
*/
43-
private void processValueEvent(ValueEvent event) {
44-
value = event.getValue();
45-
}
4629
}

src/main/java/cz/smarteon/loxone/app/state/ControlState.java

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,95 @@
44
import cz.smarteon.loxone.app.Control;
55
import cz.smarteon.loxone.message.TextEvent;
66
import cz.smarteon.loxone.message.ValueEvent;
7+
import lombok.AccessLevel;
8+
import lombok.Getter;
79
import lombok.RequiredArgsConstructor;
10+
import lombok.Setter;
811
import org.jetbrains.annotations.NotNull;
912

13+
import java.util.Set;
14+
import java.util.concurrent.CopyOnWriteArraySet;
15+
1016
/**
1117
* Base class for all the controlStates in loxone application.
12-
* @param <T> The type of control this class keeps trakc of the state.
18+
* <p>
19+
* This class keeps track of the state of the control based on the events of the miniserver.
20+
* </p>
21+
* @param <S> The type of the state.
22+
* @param <T> The type of control this class keeps track of.
1323
*/
14-
@RequiredArgsConstructor
15-
public abstract class ControlState<T extends Control> {
24+
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
25+
public abstract class ControlState<S, T extends Control> {
1626

1727
/**
1828
* The webSocket connection to communicate with the loxone miniserver.
1929
*/
20-
protected final Loxone loxone;
30+
@Getter
31+
private final Loxone loxone;
2132

2233
/**
2334
* The control that this state refers to.
2435
*/
25-
protected final T control;
36+
@Getter
37+
private final T control;
38+
39+
/**
40+
* The current state of the control.
41+
*/
42+
@Getter
43+
private S state;
44+
45+
/**
46+
* The set of listeners registered for this control state.
47+
*/
48+
private final Set<ControlStateListener<S, T>> listeners = new CopyOnWriteArraySet<>();
49+
50+
/**
51+
* Registers a listener to be notified of state changes.
52+
* @param listener the listener to register (should not be null).
53+
*/
54+
public void registerListener(@NotNull ControlStateListener<S, T> listener) {
55+
listeners.add(listener);
56+
}
57+
58+
/**
59+
* Unregisters a previously registered listener.
60+
* @param listener the listener to unregister (should not be null).
61+
*/
62+
public void unregisterListener(@NotNull ControlStateListener<S, T> listener) {
63+
listeners.remove(listener);
64+
}
2665

2766
/**
2867
* Method that accepts ValueEvent from the miniserver to update the internal state.
29-
* @param event value event received (should not be null)
68+
* @param event value event received (should not be null).
3069
*/
3170
void accept(@NotNull ValueEvent event) {
3271
// default implementation
3372
}
3473

3574
/**
3675
* Method that accepts TextEvent from the miniserver to update the internal state.
37-
* @param event text event received (should not be null)
76+
* @param event text event received (should not be null).
3877
*/
3978
void accept(@NotNull TextEvent event) {
4079
// default implementation
4180
}
81+
82+
void setState(@NotNull S state) {
83+
boolean stateChanged = this.state == null || !this.state.equals(state);
84+
this.state = state;
85+
if (stateChanged) {
86+
notifyStateChanged();
87+
}
88+
}
89+
90+
/**
91+
* Notifies all registered listeners of a state change.
92+
*/
93+
void notifyStateChanged() {
94+
for (ControlStateListener<S, T> listener : listeners) {
95+
listener.onStateChange(this);
96+
}
97+
}
4298
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package cz.smarteon.loxone.app.state;
2+
3+
import cz.smarteon.loxone.app.Control;
4+
import org.jetbrains.annotations.NotNull;
5+
6+
/**
7+
* Allows reacting on control state changes.
8+
* @param <T> The type of control this listener is interested in.
9+
*/
10+
public interface ControlStateListener<S, T extends Control> {
11+
12+
/**
13+
* Called when an event is received and processed by the control state and the state of the control has changed.
14+
* @param controlState the control state that received the event (should not be null)
15+
*/
16+
void onStateChange(@NotNull ControlState<S, T> controlState);
17+
}

src/main/java/cz/smarteon/loxone/app/state/DigitalInfoControlState.java

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,7 @@
1010
/**
1111
* State class for keeping state of a <code>DigitalInfoControl</code>.
1212
*/
13-
public class DigitalInfoControlState extends ControlState<DigitalInfoControl> {
14-
15-
/**
16-
* Current value of the DigitalInfoControl.
17-
*/
18-
@Getter
19-
@Nullable
20-
private Boolean state;
21-
13+
public class DigitalInfoControlState extends ControlState<Boolean,DigitalInfoControl> {
2214

2315
public DigitalInfoControlState(Loxone loxone, DigitalInfoControl control) {
2416
super(loxone, control);
@@ -30,17 +22,8 @@ public DigitalInfoControlState(Loxone loxone, DigitalInfoControl control) {
3022
*/
3123
@Override
3224
void accept(@NotNull ValueEvent event) {
33-
super.accept(event);
34-
if (event.getUuid().equals(control.stateActive())) {
35-
processActiveEvent(event);
25+
if (event.getUuid().equals(getControl().stateActive())) {
26+
setState(event.getValue() == 1);
3627
}
3728
}
38-
39-
/**
40-
* Process the ValueEvent as an active state event message and update the state of the control accordingly.
41-
* @param event value event received
42-
*/
43-
private void processActiveEvent(ValueEvent event) {
44-
state = event.getValue() == 1;
45-
}
4629
}

src/main/java/cz/smarteon/loxone/app/state/LockableControlState.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import cz.smarteon.loxone.app.state.events.LockedEvent;
77
import cz.smarteon.loxone.message.TextEvent;
88
import lombok.Getter;
9+
import lombok.RequiredArgsConstructor;
910
import lombok.extern.slf4j.Slf4j;
1011
import org.jetbrains.annotations.NotNull;
1112
import org.jetbrains.annotations.Nullable;
@@ -14,20 +15,23 @@
1415

1516
/**
1617
* Base class for the controlStates in loxone application that supports locking.
17-
* @param <T> The type of control this class keeps trakc of the state.
18+
* <p>
19+
* This class keeps track of the state of the control based on the events of the miniserver.
20+
* </p>
21+
* @param <T> The type of control this class keeps track of.
1822
*/
1923
@Slf4j
20-
public abstract class LockableControlState<T extends Control> extends ControlState<T> {
24+
public abstract class LockableControlState<S, T extends Control> extends ControlState<S, T> {
2125

2226
/**
23-
* Current state of the lock of the control.
27+
* Current state of the lock.
2428
*/
2529
@Getter
2630
@Nullable
2731
private Locked locked;
2832

2933
/**
30-
* Extra free format reason in case of lock of the control.
34+
* Extra free format reason in case of lock.
3135
*/
3236
@Getter
3337
@Nullable
@@ -43,7 +47,7 @@ protected LockableControlState(Loxone loxone, T control) {
4347
*/
4448
@Override
4549
void accept(@NotNull TextEvent event) {
46-
if (event.getUuid().equals(control.stateLocked())) {
50+
if (event.getUuid().equals(getControl().stateLocked())) {
4751
processLockedEvent(event);
4852
}
4953
}
@@ -54,14 +58,20 @@ void accept(@NotNull TextEvent event) {
5458
*/
5559
private void processLockedEvent(TextEvent event) {
5660
try {
61+
final boolean isCurrentState;
5762
if (event.getText().isEmpty()) {
63+
isCurrentState = Locked.NO.equals(locked) && lockedReason == null;
5864
this.locked = Locked.NO;
5965
this.lockedReason = null;
60-
return;
66+
} else {
67+
final LockedEvent lockedEvent = Codec.readMessage(event.getText(), LockedEvent.class);
68+
isCurrentState = lockedEvent.getLocked().equals(locked) && lockedEvent.getReason().equals(lockedReason);
69+
this.locked = lockedEvent.getLocked();
70+
this.lockedReason = lockedEvent.getReason();
71+
}
72+
if (!isCurrentState) {
73+
notifyStateChanged();
6174
}
62-
final LockedEvent lockedEvent = Codec.readMessage(event.getText(), LockedEvent.class);
63-
this.locked = lockedEvent.getLocked();
64-
this.lockedReason = lockedEvent.getReason();
6575
} catch (IOException e) {
6676
log.info("Unable to parse locked event!", e);
6777
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package cz.smarteon.loxone.app.state;
2+
3+
import cz.smarteon.loxone.LoxoneException;
4+
5+
public class LoxoneLockedException extends LoxoneException {
6+
7+
public LoxoneLockedException(String message) {
8+
super(message);
9+
}
10+
}

src/main/java/cz/smarteon/loxone/app/state/LoxoneState.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@
2929
@Slf4j
3030
public class LoxoneState implements LoxoneAppListener, LoxoneEventListener {
3131

32-
private static final Map<Class<? extends Control>, Class<? extends ControlState<? extends Control>>> SUPPORTED_CONTROLS_STATE_MAP;
32+
private static final Map<
33+
Class<? extends Control>,
34+
Class<? extends ControlState<?, ? extends Control>>> SUPPORTED_CONTROLS_STATE_MAP;
3335

3436
private final Loxone loxone;
3537

3638
private LoxoneApp loxoneApp;
3739

38-
private Map<LoxoneUuid, ControlState<?>> controlStates;
40+
private Map<LoxoneUuid, ControlState<?, ?>> controlStates;
3941

4042
static {
4143
SUPPORTED_CONTROLS_STATE_MAP = new HashMap<>();
@@ -59,12 +61,12 @@ public LoxoneState(@NotNull Loxone loxone) {
5961
}
6062

6163
@TestOnly
62-
Map<Class<? extends Control>, Class<? extends ControlState<? extends Control>>> getSupportedControlsStateMap() {
64+
Map<Class<? extends Control>, Class<? extends ControlState<?, ? extends Control>>> getSupportedControlsStateMap() {
6365
return SUPPORTED_CONTROLS_STATE_MAP;
6466
}
6567

6668
@TestOnly
67-
Map<LoxoneUuid, ControlState<?>> getControlStates() {
69+
Map<LoxoneUuid, ControlState<?, ?>> getControlStates() {
6870
return controlStates;
6971
}
7072

@@ -103,7 +105,7 @@ private void initializeState() {
103105
Map.Entry::getKey,
104106
e -> {
105107
try {
106-
return (ControlState<?>) SUPPORTED_CONTROLS_STATE_MAP.get(e.getValue().getClass())
108+
return (ControlState<?, ?>) SUPPORTED_CONTROLS_STATE_MAP.get(e.getValue().getClass())
107109
.getConstructors()[0].newInstance(loxone, e.getValue());
108110
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
109111
| InvocationTargetException ex) {

src/main/java/cz/smarteon/loxone/app/state/SwitchControlState.java

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,7 @@
1212
/**
1313
* State class for keeping and managing state of a <code>SwitchControl</code>.
1414
*/
15-
public class SwitchControlState extends LockableControlState<SwitchControl> {
16-
17-
/**
18-
* Current state of the SwitchControl.
19-
*/
20-
@Getter
21-
@Nullable
22-
private Boolean state;
15+
public class SwitchControlState extends LockableControlState<Boolean, SwitchControl> {
2316

2417
public SwitchControlState(Loxone loxone, SwitchControl control) {
2518
super(loxone, control);
@@ -29,7 +22,7 @@ public SwitchControlState(Loxone loxone, SwitchControl control) {
2922
* Toggles state of SwitchControl. When current state is <code>SwitchState.UNINITIALIZED</code> it switches to On.
3023
*/
3124
public void toggleState() {
32-
if (Boolean.TRUE.equals(state)) {
25+
if (Boolean.TRUE.equals(getState())) {
3326
stateOff();
3427
} else {
3528
stateOn();
@@ -40,15 +33,21 @@ public void toggleState() {
4033
* Sets state of SwitchControl to On.
4134
*/
4235
public void stateOn() {
43-
loxone.sendControlCommand(control, switchControl -> genericControlCommand(switchControl.getUuid().toString(),
36+
if (getLocked() != null && !Locked.NO.equals(getLocked())) {
37+
throw new LoxoneLockedException("SwitchControl is locked, so no state change is possible");
38+
}
39+
getLoxone().sendControlCommand(getControl(), switchControl -> genericControlCommand(switchControl.getUuid().toString(),
4440
"On"));
4541
}
4642

4743
/**
4844
* Sets state of SwitchControl to Off.
4945
*/
5046
public void stateOff() {
51-
loxone.sendControlCommand(control, switchControl -> genericControlCommand(switchControl.getUuid().toString(),
47+
if (getLocked() != null && !Locked.NO.equals(getLocked())) {
48+
throw new LoxoneLockedException("SwitchControl is locked, so no state change is possible");
49+
}
50+
getLoxone().sendControlCommand(getControl(), switchControl -> genericControlCommand(switchControl.getUuid().toString(),
5251
"Off"));
5352
}
5453

@@ -58,17 +57,8 @@ public void stateOff() {
5857
*/
5958
@Override
6059
void accept(@NotNull ValueEvent event) {
61-
super.accept(event);
62-
if (event.getUuid().equals(control.stateActive())) {
63-
processActiveEvent(event);
60+
if (event.getUuid().equals(getControl().stateActive())) {
61+
setState(event.getValue() == 1);
6462
}
6563
}
66-
67-
/**
68-
* Process the ValueEvent as an active state event message and update the state of the control accordingly.
69-
* @param event value event received
70-
*/
71-
private void processActiveEvent(ValueEvent event) {
72-
state = event.getValue() == 1;
73-
}
7464
}

0 commit comments

Comments
 (0)