[innogysmarthome] NullPointerException fixed which could occur when no device state is available (#9660) (#9677)

* [innogysmarthome] Bug-fix - NPE solved which could occur when devices without a corresponding device state were returned by the API. That happened with the virtual device "NotificationSender" which is unimportant for the binding.

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] JavaDoc fixed

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code optimizations (warnings fixed)

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code refactoring (InnogyClient class split to make it smaller)

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code optimization

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code optimization

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code optimization

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code optimization

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code optimization ; Potential bug with the reachable attribute solved (it was set multiple times within a loop)

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code optimization

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code optimization

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code optimization

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code optimization

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code optimization

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code optimization

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Tests added

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Tests added

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code changed to not overwrite the reachable state provided by the API (when no corresponding message is available)

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Copyright notice updated

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

* [innogysmarthome] Code and performance optimization

Signed-off-by: Sven Strohschein <sven.strohschein@gmail.com>

Co-authored-by: Sven Strohschein <sven.strohschein@gmail.com>
This commit is contained in:
Sven Strohschein 2021-01-08 23:07:38 +01:00 committed by GitHub
parent fcb774ca35
commit 3557094c13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 496 additions and 368 deletions

View File

@ -12,19 +12,15 @@
*/
package org.openhab.binding.innogysmarthome.internal.client;
import static org.openhab.binding.innogysmarthome.internal.InnogyBindingConstants.*;
import static org.openhab.binding.innogysmarthome.internal.client.Constants.*;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -36,7 +32,6 @@ import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.innogysmarthome.internal.InnogyBindingConstants;
import org.openhab.binding.innogysmarthome.internal.client.entity.StatusResponse;
import org.openhab.binding.innogysmarthome.internal.client.entity.action.Action;
import org.openhab.binding.innogysmarthome.internal.client.entity.action.ShutterAction;
@ -48,7 +43,6 @@ import org.openhab.binding.innogysmarthome.internal.client.entity.device.DeviceS
import org.openhab.binding.innogysmarthome.internal.client.entity.device.Gateway;
import org.openhab.binding.innogysmarthome.internal.client.entity.device.State;
import org.openhab.binding.innogysmarthome.internal.client.entity.error.ErrorResponse;
import org.openhab.binding.innogysmarthome.internal.client.entity.link.Link;
import org.openhab.binding.innogysmarthome.internal.client.entity.location.Location;
import org.openhab.binding.innogysmarthome.internal.client.entity.message.Message;
import org.openhab.binding.innogysmarthome.internal.client.exception.ApiException;
@ -103,13 +97,6 @@ public class InnogyClient {
this.httpClient = httpClient;
}
/**
* @return the bridgeInfo
*/
public @Nullable Gateway getBridgeDetails() {
return bridgeDetails;
}
/**
* Gets the status
*
@ -117,8 +104,6 @@ public class InnogyClient {
* the {@link #configVersion} is set.
*
* @throws SessionExistsException thrown, if a session already exists
* @throws IOException
* @throws ApiException
*/
public void refreshStatus() throws IOException, ApiException, AuthenticationException {
logger.debug("Get innogy SmartHome status...");
@ -133,12 +118,9 @@ public class InnogyClient {
/**
* Executes a HTTP GET request with default headers and returns data as object of type T.
*
* @param url
* @param url request URL
* @param clazz type of data to return
* @return
* @throws IOException
* @throws AuthenticationException
* @throws ApiException
* @return response content
*/
private <T> T executeGet(final String url, final Class<T> clazz)
throws IOException, AuthenticationException, ApiException {
@ -150,11 +132,9 @@ public class InnogyClient {
/**
* Executes a HTTP GET request with default headers and returns data as List of type T.
*
* @param url
* @param url request URL
* @param clazz array type of data to return as list
* @throws IOException
* @throws AuthenticationException
* @throws ApiException
* @return response content (as a List)
*/
private <T> List<T> executeGetList(final String url, final Class<T[]> clazz)
throws IOException, AuthenticationException, ApiException {
@ -164,19 +144,15 @@ public class InnogyClient {
/**
* Executes a HTTP POST request with the given {@link Action} as content.
*
* @param url
* @param action
* @return
* @throws IOException
* @throws AuthenticationException
* @throws ApiException
* @param url request URL
* @param action action to execute
*/
private ContentResponse executePost(final String url, final Action action)
private void executePost(final String url, final Action action)
throws IOException, AuthenticationException, ApiException {
final String json = gson.toJson(action);
logger.debug("Action {} JSON: {}", action.getType(), json);
return request(httpClient.newRequest(url).method(HttpMethod.POST)
request(httpClient.newRequest(url).method(HttpMethod.POST)
.content(new StringContentProvider(json), CONTENT_TYPE).accept(CONTENT_TYPE));
}
@ -197,6 +173,7 @@ public class InnogyClient {
}
public AccessTokenResponse getAccessTokenResponse() throws AuthenticationException, IOException {
@Nullable
final AccessTokenResponse accessTokenResponse;
try {
accessTokenResponse = oAuthService.getAccessTokenResponse();
@ -212,17 +189,11 @@ public class InnogyClient {
/**
* Handles errors from the {@link ContentResponse} and throws the following errors:
*
* @param response
* @param response response
* @param uri uri of api call made
* @throws SessionExistsException
* @throws SessionNotFoundException
* @throws ControllerOfflineException thrown, if the innogy SmartHome controller (SHC) is offline.
* @throws IOException
* @throws ApiException
* @throws AuthenticationException
*/
private void handleResponseErrors(final ContentResponse response, final URI uri)
throws IOException, ApiException, AuthenticationException {
private void handleResponseErrors(final ContentResponse response, final URI uri) throws IOException, ApiException {
String content = "";
switch (response.getStatus()) {
@ -275,11 +246,6 @@ public class InnogyClient {
/**
* Sets a new state of a SwitchActuator.
*
* @param capabilityId
* @param state
* @throws IOException
* @throws ApiException
*/
public void setSwitchActuatorState(final String capabilityId, final boolean state)
throws IOException, ApiException, AuthenticationException {
@ -288,11 +254,6 @@ public class InnogyClient {
/**
* Sets the dimmer level of a DimmerActuator.
*
* @param capabilityId
* @param dimLevel
* @throws IOException
* @throws ApiException
*/
public void setDimmerActuatorState(final String capabilityId, final int dimLevel)
throws IOException, ApiException, AuthenticationException {
@ -301,12 +262,6 @@ public class InnogyClient {
/**
* Sets the roller shutter level of a RollerShutterActuator.
*
* @param capabilityId
* @param rollerShutterLevel
* @throws IOException
* @throws ApiException
* @throws AuthenticationException
*/
public void setRollerShutterActuatorState(final String capabilityId, final int rollerShutterLevel)
throws IOException, ApiException, AuthenticationException {
@ -316,12 +271,6 @@ public class InnogyClient {
/**
* Starts or stops moving a RollerShutterActuator
*
* @param capabilityId
* @param rollerShutterAction
* @throws IOException
* @throws ApiException
* @throws AuthenticationException
*/
public void setRollerShutterAction(final String capabilityId,
final ShutterAction.ShutterActions rollerShutterAction)
@ -331,11 +280,6 @@ public class InnogyClient {
/**
* Sets a new state of a VariableActuator.
*
* @param capabilityId
* @param state
* @throws IOException
* @throws ApiException
*/
public void setVariableActuatorState(final String capabilityId, final boolean state)
throws IOException, ApiException, AuthenticationException {
@ -344,11 +288,6 @@ public class InnogyClient {
/**
* Sets the point temperature.
*
* @param capabilityId
* @param pointTemperature
* @throws IOException
* @throws ApiException
*/
public void setPointTemperatureState(final String capabilityId, final double pointTemperature)
throws IOException, ApiException, AuthenticationException {
@ -358,11 +297,6 @@ public class InnogyClient {
/**
* Sets the operation mode to "Auto" or "Manu".
*
* @param capabilityId
* @param autoMode
* @throws IOException
* @throws ApiException
*/
public void setOperationMode(final String capabilityId, final boolean autoMode)
throws IOException, ApiException, AuthenticationException {
@ -374,11 +308,6 @@ public class InnogyClient {
/**
* Sets the alarm state.
*
* @param capabilityId
* @param alarmState
* @throws IOException
* @throws ApiException
*/
public void setAlarmActuatorState(final String capabilityId, final boolean alarmState)
throws IOException, ApiException, AuthenticationException {
@ -388,222 +317,26 @@ public class InnogyClient {
/**
* Load the device and returns a {@link List} of {@link Device}s..
*
* @param deviceIds Ids of the devices to return
* @return List of Devices
* @throws IOException
* @throws ApiException
*/
public List<Device> getDevices() throws IOException, ApiException, AuthenticationException {
public List<Device> getDevices(Collection<String> deviceIds)
throws IOException, ApiException, AuthenticationException {
logger.debug("Loading innogy devices...");
return executeGetList(API_URL_DEVICE, Device[].class);
List<Device> devices = executeGetList(API_URL_DEVICE, Device[].class);
return devices.stream().filter(d -> deviceIds.contains(d.getId())).collect(Collectors.toList());
}
/**
* Loads the {@link Device} with the given deviceId.
*
* @param deviceId
* @return
* @throws IOException
* @throws ApiException
*/
public Device getDeviceById(final String deviceId) throws IOException, ApiException, AuthenticationException {
logger.debug("Loading device with id {}...", deviceId);
return executeGet(API_URL_DEVICE_ID.replace("{id}", deviceId), Device.class);
}
/**
* Returns a {@link List} of all {@link Device}s with the full configuration details, {@link Capability}s and
* states. Calling this may take a while...
*
* @return
* @throws IOException
* @throws ApiException
*/
public List<Device> getFullDevices() throws IOException, ApiException, AuthenticationException {
// LOCATIONS
final List<Location> locationList = getLocations();
final Map<String, Location> locationMap = new HashMap<>();
for (final Location l : locationList) {
locationMap.put(l.getId(), l);
}
// CAPABILITIES
final List<Capability> capabilityList = getCapabilities();
final Map<String, Capability> capabilityMap = new HashMap<>();
for (final Capability c : capabilityList) {
capabilityMap.put(c.getId(), c);
}
// CAPABILITY STATES
final List<CapabilityState> capabilityStateList = getCapabilityStates();
final Map<String, CapabilityState> capabilityStateMap = new HashMap<>();
for (final CapabilityState cs : capabilityStateList) {
capabilityStateMap.put(cs.getId(), cs);
}
// DEVICE STATES
final List<DeviceState> deviceStateList = getDeviceStates();
final Map<String, DeviceState> deviceStateMap = new HashMap<>();
for (final DeviceState es : deviceStateList) {
deviceStateMap.put(es.getId(), es);
}
// MESSAGES
final List<Message> messageList = getMessages();
final Map<String, List<Message>> deviceMessageMap = new HashMap<>();
for (final Message m : messageList) {
if (m.getDevices() != null && !m.getDevices().isEmpty()) {
final String deviceId = m.getDevices().get(0).replace("/device/", "");
List<Message> ml;
if (deviceMessageMap.containsKey(deviceId)) {
ml = deviceMessageMap.get(deviceId);
} else {
ml = new ArrayList<>();
}
ml.add(m);
deviceMessageMap.put(deviceId, ml);
}
}
// DEVICES
final List<Device> deviceList = getDevices();
for (final Device d : deviceList) {
if (InnogyBindingConstants.BATTERY_POWERED_DEVICES.contains(d.getType())) {
d.setIsBatteryPowered(true);
}
// location
d.setLocation(locationMap.get(d.getLocationId()));
final HashMap<String, Capability> deviceCapabilityMap = new HashMap<>();
// capabilities and their states
for (final String cl : d.getCapabilityLinkList()) {
final Capability c = capabilityMap.get(Link.getId(cl));
final String capabilityId = c.getId();
final CapabilityState capabilityState = capabilityStateMap.get(capabilityId);
c.setCapabilityState(capabilityState);
deviceCapabilityMap.put(capabilityId, c);
}
d.setCapabilityMap(deviceCapabilityMap);
// device states
d.setDeviceState(deviceStateMap.get(d.getId()));
// messages
if (deviceMessageMap.containsKey(d.getId())) {
d.setMessageList(deviceMessageMap.get(d.getId()));
for (final Message m : d.getMessageList()) {
switch (m.getType()) {
case Message.TYPE_DEVICE_LOW_BATTERY:
d.setLowBattery(true);
d.setLowBatteryMessageId(m.getId());
break;
}
}
}
}
return deviceList;
}
/**
* Returns the {@link Device} with the given deviceId with full configuration details, {@link Capability}s and
* states. Calling this may take a little bit longer...
*
* @param deviceId
* @return
* @throws IOException
* @throws ApiException
*/
public Device getFullDeviceById(final String deviceId) throws IOException, ApiException, AuthenticationException {
// LOCATIONS
final List<Location> locationList = getLocations();
final Map<String, Location> locationMap = new HashMap<>();
for (final Location l : locationList) {
locationMap.put(l.getId(), l);
}
// CAPABILITIES FOR DEVICE
final List<Capability> capabilityList = getCapabilitiesForDevice(deviceId);
final Map<String, Capability> capabilityMap = new HashMap<>();
for (final Capability c : capabilityList) {
capabilityMap.put(c.getId(), c);
}
// CAPABILITY STATES
final List<CapabilityState> capabilityStateList = getCapabilityStates();
final Map<String, CapabilityState> capabilityStateMap = new HashMap<>();
for (final CapabilityState cs : capabilityStateList) {
capabilityStateMap.put(cs.getId(), cs);
}
// DEVICE STATE
final State state = getDeviceStateByDeviceId(deviceId);
final DeviceState deviceState = new DeviceState();
deviceState.setId(deviceId);
deviceState.setState(state);
// MESSAGES
final List<Message> messageList = getMessages();
final List<Message> ml = new ArrayList<>();
final String deviceIdPath = "/device/" + deviceId;
for (final Message m : messageList) {
logger.trace("Message Type {} with ID {}", m.getType(), m.getId());
if (m.getDevices() != null && !m.getDevices().isEmpty()) {
for (final String li : m.getDevices()) {
if (deviceIdPath.equals(li)) {
ml.add(m);
}
}
}
}
// DEVICE
final Device d = getDeviceById(deviceId);
if (BATTERY_POWERED_DEVICES.contains(d.getType())) {
d.setIsBatteryPowered(true);
d.setLowBattery(false);
}
// location
d.setLocation(locationMap.get(d.getLocationId()));
// capabilities and their states
final HashMap<String, Capability> deviceCapabilityMap = new HashMap<>();
for (final String cl : d.getCapabilityLinkList()) {
final Capability c = capabilityMap.get(Link.getId(cl));
c.setCapabilityState(capabilityStateMap.get(c.getId()));
deviceCapabilityMap.put(c.getId(), c);
}
d.setCapabilityMap(deviceCapabilityMap);
// device states
d.setDeviceState(deviceState);
// messages
if (!ml.isEmpty()) {
d.setMessageList(ml);
for (final Message m : d.getMessageList()) {
switch (m.getType()) {
case Message.TYPE_DEVICE_LOW_BATTERY:
d.setLowBattery(true);
d.setLowBatteryMessageId(m.getId());
break;
}
}
}
return d;
}
/**
* Loads the states for all {@link Device}s.
*
* @return
* @throws IOException
* @throws ApiException
*/
public List<DeviceState> getDeviceStates() throws IOException, ApiException, AuthenticationException {
logger.debug("Loading device states...");
@ -612,11 +345,6 @@ public class InnogyClient {
/**
* Loads the device state for the given deviceId.
*
* @param deviceId
* @return
* @throws IOException
* @throws ApiException
*/
public State getDeviceStateByDeviceId(final String deviceId)
throws IOException, ApiException, AuthenticationException {
@ -628,8 +356,6 @@ public class InnogyClient {
* Loads the locations and returns a {@link List} of {@link Location}s.
*
* @return a List of Devices
* @throws IOException
* @throws ApiException
*/
public List<Location> getLocations() throws IOException, ApiException, AuthenticationException {
logger.debug("Loading locations...");
@ -640,9 +366,7 @@ public class InnogyClient {
* Loads and returns a {@link List} of {@link Capability}s for the given deviceId.
*
* @param deviceId the id of the {@link Device}
* @return
* @throws IOException
* @throws ApiException
* @return capabilities of the device
*/
public List<Capability> getCapabilitiesForDevice(final String deviceId)
throws IOException, ApiException, AuthenticationException {
@ -652,10 +376,6 @@ public class InnogyClient {
/**
* Loads and returns a {@link List} of all {@link Capability}s.
*
* @return
* @throws IOException
* @throws ApiException
*/
public List<Capability> getCapabilities() throws IOException, ApiException, AuthenticationException {
logger.debug("Loading capabilities...");
@ -664,10 +384,6 @@ public class InnogyClient {
/**
* Loads and returns a {@link List} of all {@link Capability}States.
*
* @return
* @throws IOException
* @throws ApiException
*/
public List<CapabilityState> getCapabilityStates() throws IOException, ApiException, AuthenticationException {
logger.debug("Loading capability states...");
@ -676,10 +392,6 @@ public class InnogyClient {
/**
* Returns a {@link List} of all {@link Message}s.
*
* @return
* @throws IOException
* @throws ApiException
*/
public List<Message> getMessages() throws IOException, ApiException, AuthenticationException {
logger.debug("Loading messages...");
@ -692,11 +404,4 @@ public class InnogyClient {
public String getConfigVersion() {
return configVersion;
}
/**
* @param configVersion the configVersion to set
*/
public void setConfigVersion(final String configVersion) {
this.configVersion = configVersion;
}
}

View File

@ -14,9 +14,7 @@ package org.openhab.binding.innogysmarthome.internal.client.entity.device;
import static org.openhab.binding.innogysmarthome.internal.InnogyBindingConstants.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.*;
import org.openhab.binding.innogysmarthome.internal.client.entity.capability.Capability;
import org.openhab.binding.innogysmarthome.internal.client.entity.location.Location;
@ -38,8 +36,6 @@ public class Device {
protected static final String PROTOCOL_ID_VIRTUAL = "Virtual";
protected static final String PROTOCOL_ID_WMBUS = "wMBus";
public static final List<String> EMPTY_CAPABILITY_LINK_LIST = new ArrayList<>();
/**
* Unique id for the device, always available in model.
*/
@ -82,15 +78,9 @@ public class Device {
private DeviceConfig config;
/**
* Contains a list of the device capabilities.
*
* Optional.
*/
@SerializedName("capabilities")
private List<String> capabilityLinkList;
private List<String> capabilities;
private HashMap<String, Capability> capabilityMap;
private Map<String, Capability> capabilityMap;
private DeviceState deviceState;
@ -115,12 +105,6 @@ public class Device {
private List<Message> messageList;
private boolean lowBattery;
/**
* Stores the message id, that contains the low battery state. This is needed to identify the device, when the
* message
* with that id is deleted (thus low battery state is false again).
*/
private String lowBatteryMessageId;
/**
* Stores, if the {@link Device} is battery powered.
@ -263,32 +247,28 @@ public class Device {
/**
* @return the capabilityList
*/
public List<String> getCapabilityLinkList() {
if (capabilityLinkList != null) {
return capabilityLinkList;
} else {
return EMPTY_CAPABILITY_LINK_LIST;
}
public List<String> getCapabilities() {
return Objects.requireNonNullElse(capabilities, Collections.emptyList());
}
/**
* @param capabilityList the capabilityList to set
*/
public void setCapabilityList(List<String> capabilityList) {
this.capabilityLinkList = capabilityList;
public void setCapabilities(List<String> capabilityList) {
this.capabilities = capabilityList;
}
/**
* @param capabilityMap the capabilityMap to set
*/
public void setCapabilityMap(HashMap<String, Capability> capabilityMap) {
public void setCapabilityMap(Map<String, Capability> capabilityMap) {
this.capabilityMap = capabilityMap;
}
/**
* @return the capabilityMap
*/
public HashMap<String, Capability> getCapabilityMap() {
public Map<String, Capability> getCapabilityMap() {
return this.capabilityMap;
}
@ -310,7 +290,7 @@ public class Device {
}
/**
* @param locationList the locationList to set
* @param locationLink the locationList to set
*/
public void setLocation(String locationLink) {
this.locationLink = locationLink;
@ -366,10 +346,31 @@ public class Device {
*/
public void setMessageList(List<Message> messageList) {
this.messageList = messageList;
applyMessageList(messageList);
}
for (final Message m : messageList) {
setLowBattery(Message.TYPE_DEVICE_LOW_BATTERY.equals(m.getType()));
setReachable(!Message.TYPE_DEVICE_UNREACHABLE.equals(m.getType()));
private void applyMessageList(List<Message> messageList) {
if (messageList != null && !messageList.isEmpty()) {
boolean isUnreachableMessageFound = false;
boolean isLowBatteryMessageFound = false;
for (final Message message : messageList) {
switch (message.getType()) {
case Message.TYPE_DEVICE_UNREACHABLE:
isUnreachableMessageFound = true;
break;
case Message.TYPE_DEVICE_LOW_BATTERY:
isLowBatteryMessageFound = true;
break;
}
}
if (isUnreachableMessageFound) {
setReachable(false); // overwrite only when there is a corresponding message (to keep the state of the
// API in other cases)
}
if (isLowBatteryMessageFound) {
setLowBattery(true); // overwrite only when there is a corresponding message (to keep the state of the
// API in other cases)
}
}
}
@ -378,7 +379,7 @@ public class Device {
*
* @param isReachable
*/
public void setReachable(boolean isReachable) {
private void setReachable(boolean isReachable) {
if (getDeviceState().hasIsReachableState()) {
getDeviceState().setReachable(isReachable);
}
@ -398,7 +399,7 @@ public class Device {
*
* @param hasLowBattery
*/
public void setLowBattery(boolean hasLowBattery) {
private void setLowBattery(boolean hasLowBattery) {
this.lowBattery = hasLowBattery;
}
@ -411,14 +412,6 @@ public class Device {
return lowBattery;
}
public String getLowBatteryMessageId() {
return this.lowBatteryMessageId;
}
public void setLowBatteryMessageId(String messageId) {
this.lowBatteryMessageId = messageId;
}
/**
* Returns true, if the {@link Device} is battery powered.
*

View File

@ -49,6 +49,7 @@ import org.openhab.binding.innogysmarthome.internal.discovery.InnogyDeviceDiscov
import org.openhab.binding.innogysmarthome.internal.listener.DeviceStatusListener;
import org.openhab.binding.innogysmarthome.internal.listener.EventListener;
import org.openhab.binding.innogysmarthome.internal.manager.DeviceStructureManager;
import org.openhab.binding.innogysmarthome.internal.manager.FullDeviceManager;
import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
@ -167,7 +168,7 @@ public class InnogyBridgeHandler extends BaseBridgeHandler
if (checkOnAuthCode()) {
final InnogyClient localClient = createInnogyClient(oAuthService, httpClient);
client = localClient;
deviceStructMan = new DeviceStructureManager(localClient);
deviceStructMan = new DeviceStructureManager(createFullDeviceManager(localClient));
oAuthService.addAccessTokenRefreshListener(this);
registerDeviceStatusListener(InnogyBridgeHandler.this);
scheduleRestartClient(false);
@ -892,6 +893,10 @@ public class InnogyBridgeHandler extends BaseBridgeHandler
return scheduler;
}
FullDeviceManager createFullDeviceManager(InnogyClient client) {
return new FullDeviceManager(client);
}
InnogyClient createInnogyClient(final OAuthClientService oAuthService, final HttpClient httpClient) {
return new InnogyClient(oAuthService, httpClient);
}

View File

@ -16,7 +16,6 @@ import static org.openhab.binding.innogysmarthome.internal.InnogyBindingConstant
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@ -708,7 +707,7 @@ public class InnogyDeviceHandler extends BaseThingHandler implements DeviceStatu
boolean deviceChanged = false;
final String linkedCapabilityId = event.getSourceId();
HashMap<String, Capability> capabilityMap = device.getCapabilityMap();
Map<String, Capability> capabilityMap = device.getCapabilityMap();
Capability capability = capabilityMap.get(linkedCapabilityId);
logger.trace("Loaded Capability {}, {} with id {}, device {} from device id {}", capability.getType(),
capability.getName(), capability.getId(), capability.getDeviceLink(), device.getId());

View File

@ -46,7 +46,7 @@ public class DeviceStructureManager {
private final Logger logger = LoggerFactory.getLogger(DeviceStructureManager.class);
private final InnogyClient client;
private final FullDeviceManager deviceManager;
private final Map<String, Device> deviceMap;
private final Map<String, Device> capabilityIdToDeviceMap;
private String bridgeDeviceId = "";
@ -54,10 +54,10 @@ public class DeviceStructureManager {
/**
* Constructs the {@link DeviceStructureManager}.
*
* @param client the {@link InnogyClient}
* @param deviceManager the {@link FullDeviceManager}
*/
public DeviceStructureManager(InnogyClient client) {
this.client = client;
public DeviceStructureManager(FullDeviceManager deviceManager) {
this.deviceManager = deviceManager;
deviceMap = Collections.synchronizedMap(new HashMap<>());
capabilityIdToDeviceMap = new ConcurrentHashMap<>();
}
@ -82,7 +82,7 @@ public class DeviceStructureManager {
public void refreshDevices() throws IOException, ApiException, AuthenticationException {
deviceMap.clear();
capabilityIdToDeviceMap.clear();
List<Device> devices = client.getFullDevices();
List<Device> devices = deviceManager.getFullDevices();
for (Device d : devices) {
handleRefreshedDevice(d);
}
@ -98,7 +98,7 @@ public class DeviceStructureManager {
*/
public void refreshDevice(String deviceId) throws IOException, ApiException, AuthenticationException {
logger.trace("Refreshing Device with id '{}'", deviceId);
Device d = client.getFullDeviceById(deviceId);
Device d = deviceManager.getFullDeviceById(deviceId);
handleRefreshedDevice(d);
}
@ -155,7 +155,7 @@ public class DeviceStructureManager {
getDeviceMap().put(device.getId(), device);
}
for (String cl : device.getCapabilityLinkList()) {
for (String cl : device.getCapabilities()) {
capabilityIdToDeviceMap.put(Link.getId(cl), device);
}
}

View File

@ -0,0 +1,223 @@
/**
* Copyright (c) 2010-2021 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.innogysmarthome.internal.manager;
import static org.openhab.binding.innogysmarthome.internal.InnogyBindingConstants.BATTERY_POWERED_DEVICES;
import java.io.IOException;
import java.util.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.innogysmarthome.internal.client.InnogyClient;
import org.openhab.binding.innogysmarthome.internal.client.entity.capability.Capability;
import org.openhab.binding.innogysmarthome.internal.client.entity.capability.CapabilityState;
import org.openhab.binding.innogysmarthome.internal.client.entity.device.Device;
import org.openhab.binding.innogysmarthome.internal.client.entity.device.DeviceState;
import org.openhab.binding.innogysmarthome.internal.client.entity.link.Link;
import org.openhab.binding.innogysmarthome.internal.client.entity.location.Location;
import org.openhab.binding.innogysmarthome.internal.client.entity.message.Message;
import org.openhab.binding.innogysmarthome.internal.client.exception.ApiException;
import org.openhab.binding.innogysmarthome.internal.client.exception.AuthenticationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Sven Strohschein - Initial contribution (but only created by refactoring the InnogyClient class)
*/
@NonNullByDefault
public class FullDeviceManager {
private final Logger logger = LoggerFactory.getLogger(FullDeviceManager.class);
private final InnogyClient client;
public FullDeviceManager(InnogyClient client) {
this.client = client;
}
/**
* Returns a {@link List} of all {@link Device}s with the full configuration details, {@link Capability}s and
* states. Calling this may take a while...
*/
public List<Device> getFullDevices() throws IOException, ApiException, AuthenticationException {
final Map<String, Location> locationMap = createLocationMap(client);
final Map<String, Capability> capabilityMap = createCapabilityMap(client);
final Map<String, DeviceState> deviceStateMap = createDeviceStateMap(client);
final Map<String, List<Message>> messageMap = createMessageMap(client);
final List<Device> deviceList = client.getDevices(deviceStateMap.keySet());
for (final Device device : deviceList) {
final String deviceId = device.getId();
initializeDevice(device, deviceStateMap.get(deviceId), locationMap, capabilityMap,
getMessageList(device, messageMap));
}
return deviceList;
}
/**
* Returns the {@link Device} with the given deviceId with full configuration details, {@link Capability}s and
* states. Calling this may take a little bit longer...
*/
public Device getFullDeviceById(final String deviceId) throws IOException, ApiException, AuthenticationException {
final Map<String, Location> locationMap = createLocationMap(client);
final Map<String, Capability> capabilityMap = createCapabilityMap(deviceId, client);
final List<Message> messageMap = createMessageMap(deviceId, client);
final DeviceState deviceState = new DeviceState();
deviceState.setId(deviceId);
deviceState.setState(client.getDeviceStateByDeviceId(deviceId));
final Device device = client.getDeviceById(deviceId);
initializeDevice(device, deviceState, locationMap, capabilityMap, messageMap);
return device;
}
private void initializeDevice(Device device, @Nullable DeviceState deviceState, Map<String, Location> locationMap,
Map<String, Capability> capabilityMap, List<Message> messageList) {
device.setDeviceState(deviceState);
if (isBatteryPowered(device)) {
device.setIsBatteryPowered(true);
}
device.setLocation(locationMap.get(device.getLocationId()));
device.setCapabilityMap(createDeviceCapabilityMap(device, capabilityMap));
device.setMessageList(messageList);
}
private static boolean isBatteryPowered(Device device) {
return BATTERY_POWERED_DEVICES.contains(device.getType());
}
private List<Message> getMessageList(Device device, Map<String, List<Message>> messageMap) {
return Objects.requireNonNullElse(messageMap.get(device.getId()), Collections.emptyList());
}
private static Map<String, Location> createLocationMap(InnogyClient client)
throws IOException, ApiException, AuthenticationException {
final List<Location> locationList = client.getLocations();
final Map<String, Location> locationMap = new HashMap<>(locationList.size());
for (final Location location : locationList) {
locationMap.put(location.getId(), location);
}
return locationMap;
}
private static Map<String, CapabilityState> createCapabilityStateMap(InnogyClient client)
throws IOException, ApiException, AuthenticationException {
final List<CapabilityState> capabilityStateList = client.getCapabilityStates();
final Map<String, CapabilityState> capabilityStateMap = new HashMap<>(capabilityStateList.size());
for (final CapabilityState capabilityState : capabilityStateList) {
capabilityStateMap.put(capabilityState.getId(), capabilityState);
}
return capabilityStateMap;
}
private static Map<String, Capability> createCapabilityMap(InnogyClient client)
throws IOException, ApiException, AuthenticationException {
final Map<String, CapabilityState> capabilityStateMap = createCapabilityStateMap(client);
final List<Capability> capabilityList = client.getCapabilities();
return initializeCapabilities(capabilityStateMap, capabilityList);
}
private static Map<String, Capability> createCapabilityMap(String deviceId, InnogyClient client)
throws IOException, ApiException, AuthenticationException {
final Map<String, CapabilityState> capabilityStateMap = createCapabilityStateMap(client);
final List<Capability> capabilityList = client.getCapabilitiesForDevice(deviceId);
return initializeCapabilities(capabilityStateMap, capabilityList);
}
private static Map<String, Capability> initializeCapabilities(Map<String, CapabilityState> capabilityStateMap,
List<Capability> capabilityList) {
final Map<String, Capability> capabilityMap = new HashMap<>(capabilityList.size());
for (final Capability capability : capabilityList) {
String capabilityId = capability.getId();
CapabilityState capabilityState = capabilityStateMap.get(capabilityId);
capability.setCapabilityState(capabilityState);
capabilityMap.put(capabilityId, capability);
}
return capabilityMap;
}
private static Map<String, Capability> createDeviceCapabilityMap(Device device,
Map<String, Capability> capabilityMap) {
final HashMap<String, Capability> deviceCapabilityMap = new HashMap<>();
for (final String capabilityValue : device.getCapabilities()) {
final Capability capability = capabilityMap.get(Link.getId(capabilityValue));
final String capabilityId = capability.getId();
deviceCapabilityMap.put(capabilityId, capability);
}
return deviceCapabilityMap;
}
private static Map<String, DeviceState> createDeviceStateMap(InnogyClient client)
throws IOException, ApiException, AuthenticationException {
final List<DeviceState> deviceStateList = client.getDeviceStates();
final Map<String, DeviceState> deviceStateMap = new HashMap<>(deviceStateList.size());
for (final DeviceState deviceState : deviceStateList) {
deviceStateMap.put(deviceState.getId(), deviceState);
}
return deviceStateMap;
}
private List<Message> createMessageMap(String deviceId, InnogyClient client)
throws IOException, ApiException, AuthenticationException {
final List<Message> messages = client.getMessages();
final List<Message> messageList = new ArrayList<>();
final String deviceIdPath = "/device/" + deviceId;
for (final Message message : messages) {
logger.trace("Message Type {} with ID {}", message.getType(), message.getId());
if (message.getDevices() != null && !message.getDevices().isEmpty()) {
for (final String li : message.getDevices()) {
if (deviceIdPath.equals(li)) {
messageList.add(message);
}
}
}
}
return messageList;
}
private static Map<String, List<Message>> createMessageMap(InnogyClient client)
throws IOException, ApiException, AuthenticationException {
final List<Message> messageList = client.getMessages();
final Map<String, List<Message>> deviceMessageMap = new HashMap<>();
for (final Message message : messageList) {
if (message.getDevices() != null && !message.getDevices().isEmpty()) {
final String deviceId = message.getDevices().get(0).replace("/device/", "");
List<Message> ml;
if (deviceMessageMap.containsKey(deviceId)) {
ml = deviceMessageMap.get(deviceId);
} else {
ml = new ArrayList<>();
}
ml.add(message);
deviceMessageMap.put(deviceId, ml);
}
}
return deviceMessageMap;
}
}

View File

@ -0,0 +1,194 @@
/**
* Copyright (c) 2010-2021 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.innogysmarthome.internal.client.entity.device;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.openhab.binding.innogysmarthome.internal.client.entity.message.Message;
import org.openhab.binding.innogysmarthome.internal.client.entity.state.BooleanState;
/**
* @author Sven Strohschein - Initial contribution
*/
public class DeviceTest {
@Test
public void testSetMessageListLowBatteryMessage() {
Device device = createDevice();
assertTrue(device.isReachable());
assertFalse(device.hasLowBattery());
device.setMessageList(Collections.singletonList(createMessage(Message.TYPE_DEVICE_LOW_BATTERY)));
assertTrue(device.isReachable());
assertTrue(device.hasLowBattery());
}
@Test
public void testSetMessageListUnreachableMessage() {
Device device = createDevice();
assertTrue(device.isReachable());
assertFalse(device.hasLowBattery());
device.setMessageList(Collections.singletonList(createMessage(Message.TYPE_DEVICE_UNREACHABLE)));
assertFalse(device.isReachable());
assertFalse(device.hasLowBattery());
}
@Test
public void testSetMessageListResetByEmpty() {
Device device = createDevice();
assertNull(device.getMessageList());
assertTrue(device.isReachable());
assertFalse(device.hasLowBattery());
List<Message> messages = Arrays.asList(createMessage(Message.TYPE_DEVICE_LOW_BATTERY),
createMessage(Message.TYPE_DEVICE_UNREACHABLE));
device.setMessageList(messages);
assertEquals(messages, device.getMessageList());
assertFalse(device.isReachable());
assertTrue(device.hasLowBattery());
device.setMessageList(Collections.emptyList());
// Nothing should get changed.
// New messages are only set in real-life when the device is refreshed with new data of the API.
// Therefore the data of the API should be kept / not overwritten when no corresponding messages are available.
assertEquals(Collections.emptyList(), device.getMessageList());
assertFalse(device.isReachable());
assertTrue(device.hasLowBattery());
}
@Test
public void testSetMessageListResetByNULL() {
Device device = createDevice();
assertNull(device.getMessageList());
assertTrue(device.isReachable());
assertFalse(device.hasLowBattery());
List<Message> messages = Arrays.asList(createMessage(Message.TYPE_DEVICE_LOW_BATTERY),
createMessage(Message.TYPE_DEVICE_UNREACHABLE));
device.setMessageList(messages);
assertEquals(messages, device.getMessageList());
assertFalse(device.isReachable());
assertTrue(device.hasLowBattery());
device.setMessageList(null);
// Nothing should get changed.
// New messages are only set in real-life when the device is refreshed with new data of the API.
// Therefore the data of the API should be kept / not overwritten when no corresponding messages are available.
assertNull(device.getMessageList());
assertFalse(device.isReachable());
assertTrue(device.hasLowBattery());
}
@Test
public void testSetMessageListResetByUnimportantMessage() {
Device device = createDevice();
assertNull(device.getMessageList());
assertTrue(device.isReachable());
assertFalse(device.hasLowBattery());
List<Message> messages = Arrays.asList(createMessage(Message.TYPE_DEVICE_LOW_BATTERY),
createMessage(Message.TYPE_DEVICE_UNREACHABLE));
device.setMessageList(messages);
assertEquals(messages, device.getMessageList());
assertFalse(device.isReachable());
assertTrue(device.hasLowBattery());
messages = Collections.singletonList(createMessage("UNKNOWN"));
device.setMessageList(messages);
// Nothing should get changed.
// New messages are only set in real-life when the device is refreshed with new data of the API.
// Therefore the data of the API should be kept / not overwritten when no corresponding messages are available.
assertEquals(messages, device.getMessageList());
assertFalse(device.isReachable());
assertTrue(device.hasLowBattery());
}
@Test
public void testSetMessageListUnimportantMessage() {
Device device = createDevice();
assertTrue(device.isReachable());
assertFalse(device.hasLowBattery());
device.setMessageList(Collections.singletonList(createMessage("UNKNOWN")));
assertTrue(device.isReachable());
assertFalse(device.hasLowBattery());
}
private Message createMessage(String messageType) {
Message message = new Message();
message.setType(messageType);
return message;
}
@Test
public void testSetMessageListNULL() {
Device device = createDevice();
assertTrue(device.isReachable());
assertFalse(device.hasLowBattery());
device.setMessageList(null);
assertTrue(device.isReachable());
assertFalse(device.hasLowBattery());
}
@Test
public void testSetMessageListEmpty() {
Device device = createDevice();
assertTrue(device.isReachable());
assertFalse(device.hasLowBattery());
device.setMessageList(Collections.emptyList());
assertTrue(device.isReachable());
assertFalse(device.hasLowBattery());
}
private static Device createDevice() {
BooleanState isReachableState = new BooleanState();
isReachableState.setValue(true);
State state = new State();
state.setIsReachable(isReachableState);
DeviceState deviceState = new DeviceState();
deviceState.setState(state);
Device device = new Device();
device.setDeviceState(deviceState);
return device;
}
}

View File

@ -30,6 +30,7 @@ import org.openhab.binding.innogysmarthome.internal.InnogyWebSocket;
import org.openhab.binding.innogysmarthome.internal.client.InnogyClient;
import org.openhab.binding.innogysmarthome.internal.client.entity.device.Device;
import org.openhab.binding.innogysmarthome.internal.client.entity.device.DeviceConfig;
import org.openhab.binding.innogysmarthome.internal.manager.FullDeviceManager;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.config.core.Configuration;
@ -174,6 +175,7 @@ public class InnogyBridgeHandlerTest {
private class InnogyBridgeHandlerAccessible extends InnogyBridgeHandler {
private final InnogyClient innogyClientMock;
private final FullDeviceManager fullDeviceManagerMock;
private final ScheduledExecutorService schedulerMock;
private int executionCount;
private int directExecutionCount;
@ -188,7 +190,8 @@ public class InnogyBridgeHandlerTest {
bridgeDevice.setConfig(new DeviceConfig());
innogyClientMock = mock(InnogyClient.class);
when(innogyClientMock.getFullDevices()).thenReturn(Collections.singletonList(bridgeDevice));
fullDeviceManagerMock = mock(FullDeviceManager.class);
when(fullDeviceManagerMock.getFullDevices()).thenReturn(Collections.singletonList(bridgeDevice));
schedulerMock = mock(ScheduledExecutorService.class);
@ -218,6 +221,12 @@ public class InnogyBridgeHandlerTest {
return directExecutionCount;
}
@Override
@NonNull
FullDeviceManager createFullDeviceManager(@NonNull InnogyClient client) {
return fullDeviceManagerMock;
}
@Override
@NonNull
InnogyClient createInnogyClient(@NonNull OAuthClientService oAuthService, @NonNull HttpClient httpClient) {