Skip to content

Commit

Permalink
[shelly] Support for Duo, EM3, DW, Smoke, Addon; new CoAP-based updat…
Browse files Browse the repository at this point in the history
…es; bug fixes (openhab#6985)

* Re-checkin based on latest PR review status

Signed-off-by: Markus Michels <markus7017@gmail.com>
  • Loading branch information
markus7017 committed Sep 18, 2020
1 parent 090a438 commit 66a11d6
Show file tree
Hide file tree
Showing 37 changed files with 4,553 additions and 2,596 deletions.
312 changes: 236 additions & 76 deletions bundles/org.openhab.binding.shelly/README.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,29 @@

import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import org.apache.commons.lang.Validate;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.eclipse.smarthome.core.i18n.LocaleProvider;
import org.eclipse.smarthome.core.i18n.TranslationProvider;
import org.eclipse.smarthome.core.net.HttpServiceUtil;
import org.eclipse.smarthome.core.net.NetworkAddressService;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
import org.eclipse.smarthome.io.net.http.HttpClientFactory;
import org.openhab.binding.shelly.internal.coap.ShellyCoapServer;
import org.openhab.binding.shelly.internal.config.ShellyBindingConfiguration;
import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
import org.openhab.binding.shelly.internal.handler.ShellyDeviceListener;
import org.openhab.binding.shelly.internal.handler.ShellyLightHandler;
import org.openhab.binding.shelly.internal.handler.ShellyProtectedHandler;
import org.openhab.binding.shelly.internal.handler.ShellyRelayHandler;
import org.openhab.binding.shelly.internal.util.ShellyTranslationProvider;
import org.openhab.binding.shelly.internal.util.ShellyUtils;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
Expand All @@ -53,9 +57,10 @@
@Component(service = { ThingHandlerFactory.class, ShellyHandlerFactory.class }, configurationPid = "binding.shelly")
public class ShellyHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(ShellyHandlerFactory.class);
private final HttpClient httpClient;
private final ShellyTranslationProvider messages;
private final ShellyCoapServer coapServer;
private final Set<ShellyDeviceListener> deviceListeners = new CopyOnWriteArraySet<>();

private final Set<ShellyBaseHandler> deviceListeners = new ConcurrentHashSet<>();
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = ShellyBindingConstants.SUPPORTED_THING_TYPES_UIDS;
private ShellyBindingConfiguration bindingConfig = new ShellyBindingConfiguration();
private String localIP = "";
Expand All @@ -70,24 +75,26 @@ public class ShellyHandlerFactory extends BaseThingHandlerFactory {
*/
@Activate
public ShellyHandlerFactory(@Reference NetworkAddressService networkAddressService,
ComponentContext componentContext, Map<String, @Nullable Object> configProperties) {
@Reference LocaleProvider localeProvider, @Reference TranslationProvider i18nProvider,
@Reference HttpClientFactory httpClientFactory, ComponentContext componentContext,
Map<String, Object> configProperties) {
logger.debug("Activate Shelly HandlerFactory");
super.activate(componentContext);

this.coapServer = new ShellyCoapServer();
Validate.notNull(coapServer, "coapServer creation failed!");
messages = new ShellyTranslationProvider(bundleContext.getBundle(), i18nProvider, localeProvider);
localIP = ShellyUtils.getString(networkAddressService.getPrimaryIpv4HostAddress().toString());

Validate.notNull(configProperties);
bindingConfig.updateFromProperties(configProperties);
this.httpClient = httpClientFactory.getCommonHttpClient();
httpPort = HttpServiceUtil.getHttpServicePort(componentContext.getBundleContext());
if (httpPort == -1) {
httpPort = 8080;
}
Validate.isTrue(httpPort > 0, "Unable to get OH HTTP port");
logger.debug("Using OH HTTP port {}", httpPort);

String lip = networkAddressService.getPrimaryIpv4HostAddress();
localIP = lip != null ? lip : "";
this.coapServer = new ShellyCoapServer();

// Save bindingConfig & pass it to all registered listeners
bindingConfig.updateFromProperties(configProperties);
}

@Override
Expand All @@ -102,16 +109,20 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
ShellyBaseHandler handler = null;

if (thingType.equals(THING_TYPE_SHELLYPROTECTED_STR)) {
logger.debug("Create new thing of type {} using ShellyRelayHandler", thingTypeUID.getId());
handler = new ShellyProtectedHandler(thing, bindingConfig, coapServer, localIP, httpPort);
} else if (thingType.equals(THING_TYPE_SHELLYBULB.getId())
logger.debug("{}: Create new thing of type {} using ShellyProtectedHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyProtectedHandler(thing, messages, bindingConfig, coapServer, localIP, httpPort,
httpClient);
} else if (thingType.equals(THING_TYPE_SHELLYBULB.getId()) || thingType.equals(THING_TYPE_SHELLYDUO.getId())
|| thingType.equals(THING_TYPE_SHELLYRGBW2_COLOR.getId())
|| thingType.equals(THING_TYPE_SHELLYRGBW2_WHITE.getId())) {
logger.debug("Create new thing of type {} using ShellyLightHandler", thingTypeUID.getId());
handler = new ShellyLightHandler(thing, bindingConfig, coapServer, localIP, httpPort);
logger.debug("{}: Create new thing of type {} using ShellyLightHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyLightHandler(thing, messages, bindingConfig, coapServer, localIP, httpPort, httpClient);
} else if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
logger.debug("Create new thing of type {} using ShellyRelayHandler", thingTypeUID.getId());
handler = new ShellyRelayHandler(thing, bindingConfig, coapServer, localIP, httpPort);
logger.debug("{}: Create new thing of type {} using ShellyRelayHandler", thing.getLabel(),
thingTypeUID.toString());
handler = new ShellyRelayHandler(thing, messages, bindingConfig, coapServer, localIP, httpPort, httpClient);
}

if (handler != null) {
Expand All @@ -126,7 +137,6 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
/**
* Remove handler of things.
*/
@SuppressWarnings("unlikely-arg-type")
@Override
protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) {
if (thingHandler instanceof ShellyBaseHandler) {
Expand All @@ -142,24 +152,17 @@ protected synchronized void removeHandler(@NonNull ThingHandler thingHandler) {
* @param eventType Type of event, e.g. light
* @param parameters Input parameters from URL, e.g. on sensor reports
*/
public void onEvent(String deviceName, String componentIndex, String eventType, Map<String, String> parameters) {
logger.trace("Dispatch event to device handler {}", deviceName);
for (ShellyDeviceListener listener : deviceListeners) {
try {
if (listener.onEvent(deviceName, componentIndex, eventType, parameters)) {
// event processed
break;
}
} catch (NullPointerException e) {
logger.debug("Unable to process callback: {} ({}), deviceName={}, type={}, index={}, parameters={}\n{}",
e.getMessage(), e.getClass(), deviceName, eventType, componentIndex, parameters.toString(),
e.getStackTrace());
// continue with next listener
public void onEvent(String ipAddress, String deviceName, String componentIndex, String eventType,
Map<String, String> parameters) {
logger.trace("{}: Dispatch event to thing handler", deviceName);
for (ShellyBaseHandler listener : deviceListeners) {
if (listener.onEvent(ipAddress, deviceName, componentIndex, eventType, parameters)) {
// event processed
return;
}
}
}

@Nullable
public ShellyBindingConfiguration getBindingConfig() {
return bindingConfig;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* Copyright (c) 2010-2020 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.shelly.internal.api;

import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

import com.google.gson.JsonSyntaxException;

/**
* The {@link CarNetException} implements an extension to the standard Exception class. This allows to keep also the
* result of the last API call (e.g. including the http status code in the message).
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class ShellyApiException extends Exception {
private static final long serialVersionUID = -5809459454769761821L;

private ShellyApiResult apiResult = new ShellyApiResult();
private static String EX_NONE = "none";

public ShellyApiException(Exception exception) {
super(exception);
}

public ShellyApiException(String message) {
super(message);
}

public ShellyApiException(ShellyApiResult res) {
super(EX_NONE);
apiResult = res;
}

public ShellyApiException(Exception exception, String message) {
super(message, exception);
}

public ShellyApiException(ShellyApiResult result, Exception exception) {
super(exception);
apiResult = result;
}

@Override
public String getMessage() {
return isEmpty() ? "" : nonNullString(super.getMessage());
}

@Override
public String toString() {
String message = nonNullString(super.getMessage());
String cause = getCauseClass().toString();
if (!isEmpty()) {
if (isUnknownHost()) {
String[] string = message.split(": "); // java.net.UnknownHostException: api.rach.io
message = MessageFormat.format("Unable to connect to {0} (Unknown host / Network down / Low signal)",
string[1]);
} else if (isMalformedURL()) {
message = MessageFormat.format("Invalid URL: {0}", apiResult.getUrl());
} else if (isTimeout()) {
message = MessageFormat.format("Device unreachable or API Timeout ({0})", apiResult.getUrl());
} else {
message = MessageFormat.format("{0} ({1})", message, cause);
}
} else {
message = apiResult.toString();
}
return message;
}

public boolean isApiException() {
return getCauseClass() == ShellyApiException.class;
}

public boolean isTimeout() {
Class<?> extype = !isEmpty() ? getCauseClass() : null;
return (extype != null) && ((extype == TimeoutException.class) || (extype == ExecutionException.class)
|| (extype == InterruptedException.class) || getMessage().toLowerCase().contains("timeout"));
}

public boolean isHttpAccessUnauthorized() {
return apiResult.isHttpAccessUnauthorized();
}

public boolean isUnknownHost() {
return getCauseClass() == MalformedURLException.class;
}

public boolean isMalformedURL() {
return getCauseClass() == UnknownHostException.class;
}

public boolean IsJSONException() {
return getCauseClass() == JsonSyntaxException.class;
}

public ShellyApiResult getApiResult() {
return apiResult;
}

private boolean isEmpty() {
return nonNullString(super.getMessage()).equals(EX_NONE);
}

private static String nonNullString(@Nullable String s) {
return s != null ? s : "";
}

private Class<?> getCauseClass() {
Throwable cause = getCause();
if (getCause() != null) {
return cause.getClass();
}
return ShellyApiException.class;
}
}
Loading

0 comments on commit 66a11d6

Please sign in to comment.