added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.doorbird-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
</repository>
<feature name="openhab-binding-doorbird" description="Doorbird Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle dependency="true">mvn:net.java.dev.jna/jna/5.5.0</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.doorbird/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,163 @@
/**
* 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.doorbird.action;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.doorbird.internal.handler.DoorbellHandler;
import org.openhab.core.automation.annotation.ActionOutput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link DoorbirdActions} defines rule actions for the doorbird binding.
*
* @author Mark Hilbush - Initial contribution
*/
@ThingActionsScope(name = "doorbird")
@NonNullByDefault
public class DoorbirdActions implements ThingActions {
private static final Logger LOGGER = LoggerFactory.getLogger(DoorbirdActions.class);
private @Nullable DoorbellHandler handler;
public DoorbirdActions() {
LOGGER.debug("DoorbirdActions service created");
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof DoorbellHandler) {
this.handler = (DoorbellHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.handler;
}
private static IDoorbirdActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(DoorbirdActions.class.getName())) {
if (actions instanceof IDoorbirdActions) {
return (IDoorbirdActions) actions;
} else {
return (IDoorbirdActions) Proxy.newProxyInstance(IDoorbirdActions.class.getClassLoader(),
new Class[] { IDoorbirdActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("actions is not an instance of DoorbirdActions");
}
@RuleAction(label = "Restart Doorbird", description = "Restarts the Doorbird device")
public void restart() {
LOGGER.debug("Doorbird action 'restart' called");
if (handler != null) {
handler.actionRestart();
} else {
LOGGER.info("Doorbird Action service ThingHandler is null!");
}
}
public static void restart(@Nullable ThingActions actions) {
invokeMethodOf(actions).restart();
}
@RuleAction(label = "SIP Hangup", description = "Hangup SIP call")
public void sipHangup() {
LOGGER.debug("Doorbird action 'sipHangup' called");
if (handler != null) {
handler.actionSIPHangup();
} else {
LOGGER.info("Doorbird Action service ThingHandler is null!");
}
}
public static void sipHangup(@Nullable ThingActions actions) {
invokeMethodOf(actions).sipHangup();
}
@RuleAction(label = "Get Ring Time Limit", description = "Get the value of RING_TIME_LIMIT")
public @ActionOutput(name = "getRingTimeLimit", type = "java.lang.String") String getRingTimeLimit() {
LOGGER.debug("Doorbird action 'getRingTimeLimit' called");
if (handler != null) {
return handler.actionGetRingTimeLimit();
} else {
LOGGER.info("Doorbird Action service ThingHandler is null!");
return "";
}
}
public static String getRingTimeLimit(@Nullable ThingActions actions) {
return invokeMethodOf(actions).getRingTimeLimit();
}
@RuleAction(label = "Get Call Time Limit", description = "Get the value of CALL_TIME_LIMIT")
public @ActionOutput(name = "getCallTimeLimit", type = "java.lang.String") String getCallTimeLimit() {
LOGGER.debug("Doorbird action 'getCallTimeLimit' called");
if (handler != null) {
return handler.actionGetCallTimeLimit();
} else {
LOGGER.info("Doorbird Action service ThingHandler is null!");
return "";
}
}
public static String getCallTimeLimit(@Nullable ThingActions actions) {
return invokeMethodOf(actions).getCallTimeLimit();
}
@RuleAction(label = "Get Last Error Code", description = "Get the value of LASTERRORCODE")
public @ActionOutput(name = "getLastErrorCode", type = "java.lang.String") String getLastErrorCode() {
LOGGER.debug("Doorbird action 'getLastErrorCode' called");
if (handler != null) {
return handler.actionGetLastErrorCode();
} else {
LOGGER.info("Doorbird Action service ThingHandler is null!");
return "";
}
}
public static String getLastErrorCode(@Nullable ThingActions actions) {
return invokeMethodOf(actions).getLastErrorCode();
}
@RuleAction(label = "Get Last Error Text", description = "Get the value of LASTERRORTEXT")
public @ActionOutput(name = "getLastErrorText", type = "java.lang.String") String getLastErrorText() {
LOGGER.debug("Doorbird action 'getLastErrorText' called");
if (handler != null) {
return handler.actionGetLastErrorText();
} else {
LOGGER.info("Doorbird Action service ThingHandler is null!");
return "";
}
}
public static String getLastErrorText(@Nullable ThingActions actions) {
return invokeMethodOf(actions).getLastErrorText();
}
}

View File

@@ -0,0 +1,37 @@
/**
* 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.doorbird.action;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link IDoorbirdActions} defines the interface for all thing actions supported by the binding.
* These methods, parameters, and return types are explained in {@link DoorbirdActions}.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public interface IDoorbirdActions {
public void restart();
public void sipHangup();
public String getRingTimeLimit();
public String getCallTimeLimit();
public String getLastErrorCode();
public String getLastErrorText();
}

View File

@@ -0,0 +1,61 @@
/**
* 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.doorbird.internal;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link DoorbirdBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class DoorbirdBindingConstants {
public static final String BINDING_ID = "doorbird";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_D101 = new ThingTypeUID(BINDING_ID, "d101");
public static final ThingTypeUID THING_TYPE_D210X = new ThingTypeUID(BINDING_ID, "d210x");
public static final ThingTypeUID THING_TYPE_A1081 = new ThingTypeUID(BINDING_ID, "a1081");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(THING_TYPE_D101, THING_TYPE_D210X, THING_TYPE_A1081).collect(Collectors.toSet());
// List of all Channel IDs
public static final String CHANNEL_DOORBELL = "doorbell";
public static final String CHANNEL_DOORBELL_TIMESTAMP = "doorbellTimestamp";
public static final String CHANNEL_DOORBELL_IMAGE = "doorbellImage";
public static final String CHANNEL_MOTION = "motion";
public static final String CHANNEL_MOTION_TIMESTAMP = "motionTimestamp";
public static final String CHANNEL_MOTION_IMAGE = "motionImage";
public static final String CHANNEL_LIGHT = "light";
public static final String CHANNEL_OPENDOOR1 = "openDoor1";
public static final String CHANNEL_OPENDOOR2 = "openDoor2";
public static final String CHANNEL_OPENDOOR3 = "openDoor3";
public static final String CHANNEL_IMAGE = "image";
public static final String CHANNEL_IMAGE_TIMESTAMP = "imageTimestamp";
public static final String CHANNEL_DOORBELL_HISTORY_INDEX = "doorbellHistoryIndex";
public static final String CHANNEL_DOORBELL_HISTORY_IMAGE = "doorbellHistoryImage";
public static final String CHANNEL_DOORBELL_HISTORY_TIMESTAMP = "doorbellHistoryTimestamp";
public static final String CHANNEL_MOTION_HISTORY_INDEX = "motionHistoryIndex";
public static final String CHANNEL_MOTION_HISTORY_IMAGE = "motionHistoryImage";
public static final String CHANNEL_MOTION_HISTORY_TIMESTAMP = "motionHistoryTimestamp";
public static final String CHANNEL_DOORBELL_IMAGE_MONTAGE = "doorbellMontage";
public static final String CHANNEL_MOTION_IMAGE_MONTAGE = "motionMontage";
}

View File

@@ -0,0 +1,67 @@
/**
* 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.doorbird.internal;
import static org.openhab.binding.doorbird.internal.DoorbirdBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.doorbird.internal.handler.ControllerHandler;
import org.openhab.binding.doorbird.internal.handler.DoorbellHandler;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link DoorbirdHandlerFactory} is responsible for creating Doorbird thing
* handlers.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.doorbird", service = ThingHandlerFactory.class)
public class DoorbirdHandlerFactory extends BaseThingHandlerFactory {
private final TimeZoneProvider timeZoneProvider;
private final HttpClient httpClient;
@Activate
public DoorbirdHandlerFactory(@Reference TimeZoneProvider timeZoneProvider,
@Reference HttpClientFactory httpClientFactory) {
this.timeZoneProvider = timeZoneProvider;
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_D101.equals(thingTypeUID) || THING_TYPE_D210X.equals(thingTypeUID)) {
return new DoorbellHandler(thing, timeZoneProvider, httpClient);
} else if (THING_TYPE_A1081.equals(thingTypeUID)) {
return new ControllerHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,56 @@
/**
* 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.doorbird.internal.api;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Authorization} is responsible for managing the host and
* authorization strings used in Doorbird API calls.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class Authorization {
private final String host;
private final String userId;
private final String userPassword;
private final String authorization;
public Authorization(String host, String userId, String userPassword) {
this.host = host;
this.userId = userId;
this.userPassword = userPassword;
this.authorization = new String(Base64.getEncoder().encode((userId + ":" + userPassword).getBytes()),
StandardCharsets.UTF_8);
}
public String getHost() {
return host;
}
public String getUserId() {
return userId;
}
public String getUserPassword() {
return userPassword;
}
public String getAuthorization() {
return authorization;
}
}

View File

@@ -0,0 +1,255 @@
/**
* 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.doorbird.internal.api;
import java.io.IOException;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.core.io.net.http.HttpRequestBuilder;
import org.openhab.core.library.types.RawType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* The {@link DoorbirdAPI} class exposes the functionality provided by the Doorbird API.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public final class DoorbirdAPI {
private static final long API_REQUEST_TIMEOUT_SECONDS = 16L;
// Single Gson instance shared by multiple classes
private static final Gson GSON = new Gson();
private final Logger logger = LoggerFactory.getLogger(DoorbirdAPI.class);
private @Nullable Authorization authorization;
private @Nullable HttpClient httpClient;
public static Gson getGson() {
return (GSON);
}
public static <T> T fromJson(String json, Class<T> dataClass) {
return GSON.fromJson(json, dataClass);
}
public void setAuthorization(String doorbirdHost, String userId, String userPassword) {
this.authorization = new Authorization(doorbirdHost, userId, userPassword);
}
public void setHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}
public @Nullable DoorbirdInfo getDoorbirdInfo() {
DoorbirdInfo doorbirdInfo = null;
try {
String infoResponse = executeGetRequest("/bha-api/info.cgi");
logger.debug("Doorbird returned json response: {}", infoResponse);
doorbirdInfo = new DoorbirdInfo(infoResponse);
} catch (IOException e) {
logger.info("Unable to communicate with Doorbird: {}", e.getMessage());
} catch (JsonSyntaxException e) {
logger.info("Unable to parse Doorbird response: {}", e.getMessage());
} catch (DoorbirdUnauthorizedException e) {
logAuthorizationError("getDoorbirdName");
}
return doorbirdInfo;
}
public @Nullable SipStatus getSipStatus() {
SipStatus sipStatus = null;
try {
String statusResponse = executeGetRequest("/bha-api/sip.cgi&action=status");
logger.debug("Doorbird returned json response: {}", statusResponse);
sipStatus = new SipStatus(statusResponse);
} catch (IOException e) {
logger.info("Unable to communicate with Doorbird: {}", e.getMessage());
} catch (JsonSyntaxException e) {
logger.info("Unable to parse Doorbird response: {}", e.getMessage());
} catch (DoorbirdUnauthorizedException e) {
logAuthorizationError("getSipStatus");
}
return sipStatus;
}
public void lightOn() {
try {
String response = executeGetRequest("/bha-api/light-on.cgi");
logger.debug("Response={}", response);
} catch (IOException e) {
logger.debug("IOException turning on light: {}", e.getMessage());
} catch (DoorbirdUnauthorizedException e) {
logAuthorizationError("lightOn");
}
}
public void restart() {
try {
String response = executeGetRequest("/bha-api/restart.cgi");
logger.debug("Response={}", response);
} catch (IOException e) {
logger.debug("IOException restarting device: {}", e.getMessage());
} catch (DoorbirdUnauthorizedException e) {
logAuthorizationError("restart");
}
}
public void sipHangup() {
try {
String response = executeGetRequest("/bha-api/sip.cgi?action=hangup");
logger.debug("Response={}", response);
} catch (IOException e) {
logger.debug("IOException hanging up SIP call: {}", e.getMessage());
} catch (DoorbirdUnauthorizedException e) {
logAuthorizationError("sipHangup");
}
}
public @Nullable DoorbirdImage downloadCurrentImage() {
return downloadImage("/bha-api/image.cgi");
}
public @Nullable DoorbirdImage downloadDoorbellHistoryImage(String imageNumber) {
return downloadImage("/bha-api/history.cgi?event=doorbell&index=" + imageNumber);
}
public @Nullable DoorbirdImage downloadMotionHistoryImage(String imageNumber) {
return downloadImage("/bha-api/history.cgi?event=motionsensor&index=" + imageNumber);
}
public void openDoorController(String controllerId, String doorNumber) {
openDoor("/bha-api/open-door.cgi?r=" + controllerId + "@" + doorNumber);
}
public void openDoorDoorbell(String doorNumber) {
openDoor("/bha-api/open-door.cgi?r=" + doorNumber);
}
private void openDoor(String urlFragment) {
try {
String response = executeGetRequest(urlFragment);
logger.debug("Response={}", response);
} catch (IOException e) {
logger.debug("IOException opening door: {}", e.getMessage());
} catch (DoorbirdUnauthorizedException e) {
logAuthorizationError("openDoor");
}
}
private @Nullable synchronized DoorbirdImage downloadImage(String urlFragment) {
Authorization auth = authorization;
if (auth == null) {
logAuthorizationError("downloadImage");
return null;
}
HttpClient client = httpClient;
if (client == null) {
logger.info("Unable to download image because httpClient is not set");
return null;
}
String errorMsg;
try {
String url = buildUrl(auth, urlFragment);
logger.debug("Downloading image from doorbird: {}", url);
Request request = client.newRequest(url);
request.method(HttpMethod.GET);
request.header("Authorization", "Basic " + auth.getAuthorization());
request.timeout(API_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
ContentResponse contentResponse = request.send();
switch (contentResponse.getStatus()) {
case HttpStatus.OK_200:
DoorbirdImage doorbirdImage = new DoorbirdImage();
doorbirdImage.setImage(new RawType(contentResponse.getContent(),
contentResponse.getHeaders().get(HttpHeader.CONTENT_TYPE)));
doorbirdImage.setTimestamp(convertXTimestamp(contentResponse.getHeaders().get("X-Timestamp")));
return doorbirdImage;
default:
errorMsg = String.format("HTTP GET failed: %d, %s", contentResponse.getStatus(),
contentResponse.getReason());
break;
}
} catch (TimeoutException e) {
errorMsg = "TimeoutException: Call to Doorbird API timed out";
} catch (ExecutionException e) {
errorMsg = String.format("ExecutionException: %s", e.getMessage());
} catch (InterruptedException e) {
errorMsg = String.format("InterruptedException: %s", e.getMessage());
Thread.currentThread().interrupt();
}
logger.debug("{}", errorMsg);
return null;
}
private long convertXTimestamp(@Nullable String timestamp) {
// Convert Unix Epoch string timestamp to long value
// Use current time if passed null string or if conversion fails
long value;
if (timestamp != null) {
try {
value = Integer.parseInt(timestamp);
} catch (NumberFormatException e) {
logger.debug("X-Timestamp header is not a number: {}", timestamp);
value = ZonedDateTime.now().toEpochSecond();
}
} else {
value = ZonedDateTime.now().toEpochSecond();
}
return value;
}
private String buildUrl(Authorization auth, String path) {
return "http://" + auth.getHost() + path;
}
private synchronized String executeGetRequest(String urlFragment)
throws IOException, DoorbirdUnauthorizedException {
Authorization auth = authorization;
if (auth == null) {
throw new DoorbirdUnauthorizedException();
}
String url = buildUrl(auth, urlFragment);
logger.debug("Executing doorbird API request: {}", url);
// @formatter:off
return HttpRequestBuilder.getFrom(url)
.withTimeout(Duration.ofSeconds(API_REQUEST_TIMEOUT_SECONDS))
.withHeader("Authorization", "Basic " + auth.getAuthorization())
.withHeader("charset", "utf-8")
.withHeader("Accept-language", "en-us")
.getContentAsString();
// @formatter:on
}
private void logAuthorizationError(String operation) {
logger.info("Authorization info is not set or is incorrect on call to '{}' API", operation);
}
}

View File

@@ -0,0 +1,44 @@
/**
* 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.doorbird.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.RawType;
/**
* The {@link DoorbirdImage} represents an image received from a doorbird.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class DoorbirdImage {
private @Nullable RawType image;
private long timestamp;
public @Nullable RawType getImage() {
return image;
}
public void setImage(RawType image) {
this.image = image;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}

View File

@@ -0,0 +1,100 @@
/**
* 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.doorbird.internal.api;
import java.util.ArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.doorbird.internal.model.DoorbirdInfoDTO;
import org.openhab.binding.doorbird.internal.model.DoorbirdInfoDTO.DoorbirdInfoBha;
import org.openhab.binding.doorbird.internal.model.DoorbirdInfoDTO.DoorbirdInfoBha.DoorbirdInfoArray;
import com.google.gson.JsonSyntaxException;
/**
* The {@link DoorbirdInfo} holds information about the Doorbird.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class DoorbirdInfo {
private @Nullable String returnCode;
private @Nullable String firmwareVersion;
private @Nullable String buildNumber;
private @Nullable String primaryMacAddress;
private @Nullable String wifiMacAddress;
private @Nullable String deviceType;
private @Nullable String controllerId;
private ArrayList<String> relays = new ArrayList<>();
@SuppressWarnings("null")
public DoorbirdInfo(String infoJson) throws JsonSyntaxException {
DoorbirdInfoDTO info = DoorbirdAPI.fromJson(infoJson, DoorbirdInfoDTO.class);
if (info != null) {
DoorbirdInfoBha bha = info.bha;
returnCode = bha.returnCode;
if (bha.doorbirdInfoArray.length == 1) {
DoorbirdInfoArray doorbirdInfo = bha.doorbirdInfoArray[0];
firmwareVersion = doorbirdInfo.firmwareVersion;
buildNumber = doorbirdInfo.buildNumber;
primaryMacAddress = doorbirdInfo.primaryMacAddress;
wifiMacAddress = doorbirdInfo.wifiMacAddress;
deviceType = doorbirdInfo.deviceType;
for (String relay : doorbirdInfo.relays) {
relays.add(relay);
String[] parts = relay.split("@");
if (parts.length == 2) {
controllerId = parts[0];
}
}
}
}
}
public @Nullable String getReturnCode() {
return returnCode;
}
public @Nullable String getFirmwareVersion() {
return firmwareVersion;
}
public @Nullable String getBuildNumber() {
return buildNumber;
}
public @Nullable String getPrimaryMacAddress() {
return primaryMacAddress;
}
public @Nullable String getWifiMacAddress() {
return wifiMacAddress;
}
public @Nullable String getDeviceType() {
return deviceType;
}
public @Nullable String getControllerId() {
return controllerId;
}
public ArrayList<String> getRelays() {
return relays;
}
public void addRelay(String relay) {
relays.add(relay);
}
}

View File

@@ -0,0 +1,34 @@
/**
* 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.doorbird.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link DoorbirdUnauthorizedException} is responsible for
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class DoorbirdUnauthorizedException extends Exception {
private static final long serialVersionUID = 1L;
public DoorbirdUnauthorizedException() {
super("Authorization is not set");
}
public DoorbirdUnauthorizedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,92 @@
/**
* 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.doorbird.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.doorbird.internal.model.SipStatusDTO;
import org.openhab.binding.doorbird.internal.model.SipStatusDTO.SipStatusBha;
import org.openhab.binding.doorbird.internal.model.SipStatusDTO.SipStatusBha.SipStatusArray;
import com.google.gson.JsonSyntaxException;
/**
* The {@link SipStatus} holds SIP status information retrieved from the Doorbell
* that is used in the binding handler.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class SipStatus {
private @Nullable String returnCode;
private @Nullable String speakerVolume;
private @Nullable String microphoneVolume;
private @Nullable String lastErrorCode;
private @Nullable String lastErrorText;
private @Nullable String ringTimeLimit;
private @Nullable String callTimeLimit;
@SuppressWarnings("null")
public SipStatus(String sipStatusJson) throws JsonSyntaxException {
SipStatusDTO sipStatus = DoorbirdAPI.fromJson(sipStatusJson, SipStatusDTO.class);
if (sipStatus != null) {
SipStatusBha bha = sipStatus.bha;
returnCode = bha.returnCode;
// SIP array should have only one entry
if (bha.sipStatusArray.length == 1) {
SipStatusArray sip = bha.sipStatusArray[0];
speakerVolume = sip.speakerVolume;
microphoneVolume = sip.microphoneVolume;
lastErrorCode = sip.lastErrorCode;
lastErrorText = sip.lastErrorText;
ringTimeLimit = sip.ringTimeLimit;
callTimeLimit = sip.callTimeLimit;
}
}
}
public String getReturnCode() {
String value = returnCode;
return value != null ? value : "";
}
public String getSpeakerVolume() {
String value = speakerVolume;
return value != null ? value : "";
}
public String getMicrophoneVolume() {
String value = microphoneVolume;
return value != null ? value : "";
}
public String getLastErrorCode() {
String value = lastErrorCode;
return value != null ? value : "";
}
public String getLastErrorText() {
String value = lastErrorText;
return value != null ? value : "";
}
public String getRingTimeLimit() {
String value = ringTimeLimit;
return value != null ? value : "";
}
public String getCallTimeLimit() {
String value = callTimeLimit;
return value != null ? value : "";
}
}

View File

@@ -0,0 +1,40 @@
/**
* 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.doorbird.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link DoorbirdConfig} class contains fields mapping thing configuration parameters
* for the Doorbird A1081 Controller..
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class ControllerConfiguration {
/**
* Hostname or IP address of the Doorbird doorbell to which the controller is assigned
*/
public @Nullable String doorbirdHost;
/**
* User ID of the Doorbird doorbell to which the controller is assigned
*/
public @Nullable String userId;
/**
* Password of the Doorbird doorbell to which the controller is assigned
*/
public @Nullable String userPassword;
}

View File

@@ -0,0 +1,65 @@
/**
* 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.doorbird.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link DoorbellConfig} class contains fields mapping thing configuration parameters
* for doorbell thing types.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class DoorbellConfiguration {
/**
* Hostname or IP address of doorbell
*/
public @Nullable String doorbirdHost;
/**
* User ID used for API requests
*/
public @Nullable String userId;
/**
* Password used in API requests, and to decrypt doorbird events
*/
public @Nullable String userPassword;
/**
* Rate at which image channel will be updated
*/
public @Nullable Integer imageRefreshRate;
/**
* Delay to set doorbell channel OFF after doorbell event
*/
public @Nullable Integer doorbellOffDelay;
/**
* Delay to set motion channel OFF after motion event
*/
public @Nullable Integer motionOffDelay;
/**
* Number of images in doorbell and motion montages
*/
public @Nullable Integer montageNumImages;
/**
* Scale factor for montages
*/
public @Nullable Integer montageScaleFactor;
}

View File

@@ -0,0 +1,112 @@
/**
* 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.doorbird.internal.handler;
import static org.openhab.binding.doorbird.internal.DoorbirdBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.doorbird.internal.api.DoorbirdAPI;
import org.openhab.binding.doorbird.internal.api.DoorbirdInfo;
import org.openhab.binding.doorbird.internal.config.ControllerConfiguration;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ControllerHandler} is responsible for handling commands
* to the A1081 Controller.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class ControllerHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(ControllerHandler.class);
private @Nullable String controllerId;
private DoorbirdAPI api = new DoorbirdAPI();
public ControllerHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
ControllerConfiguration config = getConfigAs(ControllerConfiguration.class);
String host = config.doorbirdHost;
if (host == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Doorbird host not provided");
return;
}
String user = config.userId;
if (user == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "User ID not provided");
return;
}
String password = config.userPassword;
if (password == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "User password not provided");
return;
}
api.setAuthorization(host, user, password);
// Get the Id of the controller for use in the open door API
controllerId = getControllerId();
if (controllerId != null) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Doorbird not configured with a Controller");
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Got command {} for channel {} of thing {}", command, channelUID, getThing().getUID());
switch (channelUID.getId()) {
case CHANNEL_OPENDOOR1:
handleOpenDoor(command, "1");
break;
case CHANNEL_OPENDOOR2:
handleOpenDoor(command, "2");
break;
case CHANNEL_OPENDOOR3:
handleOpenDoor(command, "3");
break;
}
}
private void handleOpenDoor(Command command, String doorNumber) {
String id = controllerId;
if (id == null) {
logger.debug("Unable to handle open door command because controller ID is not set");
return;
}
if (command.equals(OnOffType.ON)) {
api.openDoorController(id, doorNumber);
}
}
private @Nullable String getControllerId() {
DoorbirdInfo info = api.getDoorbirdInfo();
return info == null ? null : info.getControllerId();
}
}

View File

@@ -0,0 +1,536 @@
/**
* 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.doorbird.internal.handler;
import static org.openhab.binding.doorbird.internal.DoorbirdBindingConstants.*;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import javax.imageio.ImageIO;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.doorbird.action.DoorbirdActions;
import org.openhab.binding.doorbird.internal.api.DoorbirdAPI;
import org.openhab.binding.doorbird.internal.api.DoorbirdImage;
import org.openhab.binding.doorbird.internal.api.SipStatus;
import org.openhab.binding.doorbird.internal.config.DoorbellConfiguration;
import org.openhab.binding.doorbird.internal.listener.DoorbirdUdpListener;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.CommonTriggerEvents;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link DoorbellHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class DoorbellHandler extends BaseThingHandler {
private static final long MONTAGE_UPDATE_DELAY_SECONDS = 5L;
// Maximum number of doorbell and motion history images stored on Doorbird backend
private static final int MAX_HISTORY_IMAGES = 50;
private final Logger logger = LoggerFactory.getLogger(DoorbellHandler.class);
// Get a dedicated threadpool for the long-running listener thread
private final ScheduledExecutorService doorbirdScheduler = ThreadPoolManager
.getScheduledPool("doorbirdListener" + "-" + thing.getUID().getId());
private @Nullable ScheduledFuture<?> listenerJob;
private final DoorbirdUdpListener udpListener;
private @Nullable ScheduledFuture<?> imageRefreshJob;
private @Nullable ScheduledFuture<?> doorbellOffJob;
private @Nullable ScheduledFuture<?> motionOffJob;
private @NonNullByDefault({}) DoorbellConfiguration config;
private DoorbirdAPI api = new DoorbirdAPI();
private final TimeZoneProvider timeZoneProvider;
private final HttpClient httpClient;
public DoorbellHandler(Thing thing, TimeZoneProvider timeZoneProvider, HttpClient httpClient) {
super(thing);
this.timeZoneProvider = timeZoneProvider;
this.httpClient = httpClient;
udpListener = new DoorbirdUdpListener(this);
}
@Override
public void initialize() {
config = getConfigAs(DoorbellConfiguration.class);
String host = config.doorbirdHost;
if (host == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Doorbird host not provided");
return;
}
String user = config.userId;
if (user == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "User ID not provided");
return;
}
String password = config.userPassword;
if (password == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "User password not provided");
return;
}
api.setAuthorization(host, user, password);
api.setHttpClient(httpClient);
startImageRefreshJob();
startUDPListenerJob();
updateStatus(ThingStatus.ONLINE);
}
@Override
public void dispose() {
stopUDPListenerJob();
stopImageRefreshJob();
stopDoorbellOffJob();
stopMotionOffJob();
super.dispose();
}
// Callback used by listener to get Doorbird host name
public @Nullable String getDoorbirdHost() {
return config.doorbirdHost;
}
// Callback used by listener to get Doorbird password
public @Nullable String getUserId() {
return config.userId;
}
// Callback used by listener to get Doorbird password
public @Nullable String getUserPassword() {
return config.userPassword;
}
// Callback used by listener to update doorbell channel
public void updateDoorbellChannel(long timestamp) {
logger.debug("Handler: Update DOORBELL channels for thing {}", getThing().getUID());
DoorbirdImage dbImage = api.downloadCurrentImage();
if (dbImage != null) {
RawType image = dbImage.getImage();
updateState(CHANNEL_DOORBELL_IMAGE, image != null ? image : UnDefType.UNDEF);
updateState(CHANNEL_DOORBELL_TIMESTAMP, getLocalDateTimeType(dbImage.getTimestamp()));
}
triggerChannel(CHANNEL_DOORBELL, CommonTriggerEvents.PRESSED);
startDoorbellOffJob();
updateDoorbellMontage();
}
// Callback used by listener to update motion channel
public void updateMotionChannel(long timestamp) {
logger.debug("Handler: Update MOTION channels for thing {}", getThing().getUID());
DoorbirdImage dbImage = api.downloadCurrentImage();
if (dbImage != null) {
RawType image = dbImage.getImage();
updateState(CHANNEL_MOTION_IMAGE, image != null ? image : UnDefType.UNDEF);
updateState(CHANNEL_MOTION_TIMESTAMP, getLocalDateTimeType(dbImage.getTimestamp()));
}
updateState(CHANNEL_MOTION, OnOffType.ON);
startMotionOffJob();
updateMotionMontage();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Got command {} for channel {} of thing {}", command, channelUID, getThing().getUID());
switch (channelUID.getId()) {
case CHANNEL_DOORBELL_IMAGE:
if (command instanceof RefreshType) {
refreshDoorbellImageFromHistory();
}
break;
case CHANNEL_MOTION_IMAGE:
if (command instanceof RefreshType) {
refreshMotionImageFromHistory();
}
break;
case CHANNEL_LIGHT:
handleLight(command);
break;
case CHANNEL_OPENDOOR1:
handleOpenDoor(command, "1");
break;
case CHANNEL_OPENDOOR2:
handleOpenDoor(command, "2");
break;
case CHANNEL_IMAGE:
if (command instanceof RefreshType) {
handleGetImage();
}
break;
case CHANNEL_DOORBELL_HISTORY_INDEX:
case CHANNEL_MOTION_HISTORY_INDEX:
if (command instanceof RefreshType) {
// On REFRESH, get the first history image
handleHistoryImage(channelUID, new DecimalType(1));
} else {
// Get the history image specified in the command
handleHistoryImage(channelUID, command);
}
break;
case CHANNEL_DOORBELL_IMAGE_MONTAGE:
if (command instanceof RefreshType) {
updateDoorbellMontage();
}
break;
case CHANNEL_MOTION_IMAGE_MONTAGE:
if (command instanceof RefreshType) {
updateMotionMontage();
}
break;
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(DoorbirdActions.class);
}
public void actionRestart() {
api.restart();
}
public void actionSIPHangup() {
api.sipHangup();
}
public String actionGetRingTimeLimit() {
return getSipStatusValue(SipStatus::getRingTimeLimit);
}
public String actionGetCallTimeLimit() {
return getSipStatusValue(SipStatus::getCallTimeLimit);
}
public String actionGetLastErrorCode() {
return getSipStatusValue(SipStatus::getLastErrorCode);
}
public String actionGetLastErrorText() {
return getSipStatusValue(SipStatus::getLastErrorText);
}
private String getSipStatusValue(Function<SipStatus, String> function) {
String value = "";
SipStatus sipStatus = api.getSipStatus();
if (sipStatus != null) {
value = function.apply(sipStatus);
}
return value;
}
private void refreshDoorbellImageFromHistory() {
logger.debug("Handler: REFRESH doorbell image channel using most recent doorbell history image");
scheduler.execute(() -> {
DoorbirdImage dbImage = api.downloadDoorbellHistoryImage("1");
if (dbImage != null) {
RawType image = dbImage.getImage();
updateState(CHANNEL_DOORBELL_IMAGE, image != null ? image : UnDefType.UNDEF);
updateState(CHANNEL_DOORBELL_TIMESTAMP, getLocalDateTimeType(dbImage.getTimestamp()));
}
updateState(CHANNEL_DOORBELL, OnOffType.OFF);
});
}
private void refreshMotionImageFromHistory() {
logger.debug("Handler: REFRESH motion image channel using most recent motion history image");
scheduler.execute(() -> {
DoorbirdImage dbImage = api.downloadMotionHistoryImage("1");
if (dbImage != null) {
RawType image = dbImage.getImage();
updateState(CHANNEL_MOTION_IMAGE, image != null ? image : UnDefType.UNDEF);
updateState(CHANNEL_MOTION_TIMESTAMP, getLocalDateTimeType(dbImage.getTimestamp()));
}
updateState(CHANNEL_MOTION, OnOffType.OFF);
});
}
private void handleLight(Command command) {
// It's only possible to energize the light relay
if (command.equals(OnOffType.ON)) {
api.lightOn();
}
}
private void handleOpenDoor(Command command, String doorNumber) {
// It's only possible to energize the open door relay
if (command.equals(OnOffType.ON)) {
api.openDoorDoorbell(doorNumber);
}
}
private void handleGetImage() {
scheduler.execute(this::updateImageAndTimestamp);
}
private void handleHistoryImage(ChannelUID channelUID, Command command) {
if (!(command instanceof DecimalType)) {
logger.debug("History index must be of type DecimalType");
return;
}
int value = ((DecimalType) command).intValue();
if (value < 0 || value > MAX_HISTORY_IMAGES) {
logger.debug("History index must be in range 1 to {}", MAX_HISTORY_IMAGES);
return;
}
boolean isDoorbell = CHANNEL_DOORBELL_HISTORY_INDEX.equals(channelUID.getId());
String imageChannelId = isDoorbell ? CHANNEL_DOORBELL_HISTORY_IMAGE : CHANNEL_MOTION_HISTORY_IMAGE;
String timestampChannelId = isDoorbell ? CHANNEL_DOORBELL_HISTORY_TIMESTAMP : CHANNEL_MOTION_HISTORY_TIMESTAMP;
DoorbirdImage dbImage = isDoorbell ? api.downloadDoorbellHistoryImage(command.toString())
: api.downloadMotionHistoryImage(command.toString());
if (dbImage != null) {
RawType image = dbImage.getImage();
updateState(imageChannelId, image != null ? image : UnDefType.UNDEF);
updateState(timestampChannelId, getLocalDateTimeType(dbImage.getTimestamp()));
}
}
private void startImageRefreshJob() {
Integer imageRefreshRate = config.imageRefreshRate;
if (imageRefreshRate != null) {
imageRefreshJob = scheduler.scheduleWithFixedDelay(() -> {
try {
updateImageAndTimestamp();
} catch (RuntimeException e) {
logger.debug("Refresh image job got unhandled exception: {}", e.getMessage(), e);
}
}, 8L, imageRefreshRate, TimeUnit.SECONDS);
logger.debug("Scheduled job to refresh image channel every {} seconds", imageRefreshRate);
}
}
private void stopImageRefreshJob() {
if (imageRefreshJob != null) {
imageRefreshJob.cancel(true);
imageRefreshJob = null;
logger.debug("Canceling image refresh job");
}
}
private void startUDPListenerJob() {
logger.debug("Listener job is scheduled to start in 5 seconds");
listenerJob = doorbirdScheduler.schedule(udpListener, 5, TimeUnit.SECONDS);
}
private void stopUDPListenerJob() {
if (listenerJob != null) {
listenerJob.cancel(true);
udpListener.shutdown();
logger.debug("Canceling listener job");
}
}
private void startDoorbellOffJob() {
Integer offDelay = config.doorbellOffDelay;
if (offDelay == null) {
return;
}
if (doorbellOffJob != null) {
doorbellOffJob.cancel(true);
}
doorbellOffJob = scheduler.schedule(() -> {
logger.debug("Update channel 'doorbell' to OFF for thing {}", getThing().getUID());
triggerChannel(CHANNEL_DOORBELL, CommonTriggerEvents.RELEASED);
}, offDelay, TimeUnit.SECONDS);
}
private void stopDoorbellOffJob() {
if (doorbellOffJob != null) {
doorbellOffJob.cancel(true);
doorbellOffJob = null;
logger.debug("Canceling doorbell off job");
}
}
private void startMotionOffJob() {
Integer offDelay = config.motionOffDelay;
if (offDelay == null) {
return;
}
if (motionOffJob != null) {
motionOffJob.cancel(true);
}
motionOffJob = scheduler.schedule(() -> {
logger.debug("Update channel 'motion' to OFF for thing {}", getThing().getUID());
updateState(CHANNEL_MOTION, OnOffType.OFF);
}, offDelay, TimeUnit.SECONDS);
}
private void stopMotionOffJob() {
if (motionOffJob != null) {
motionOffJob.cancel(true);
motionOffJob = null;
logger.debug("Canceling motion off job");
}
}
private void updateDoorbellMontage() {
if (config.montageNumImages == 0) {
return;
}
logger.debug("Scheduling DOORBELL montage update to run in {} seconds", MONTAGE_UPDATE_DELAY_SECONDS);
scheduler.schedule(() -> {
updateMontage(CHANNEL_DOORBELL_IMAGE_MONTAGE);
}, MONTAGE_UPDATE_DELAY_SECONDS, TimeUnit.SECONDS);
}
private void updateMotionMontage() {
if (config.montageNumImages == 0) {
return;
}
logger.debug("Scheduling MOTION montage update to run in {} seconds", MONTAGE_UPDATE_DELAY_SECONDS);
scheduler.schedule(() -> {
updateMontage(CHANNEL_MOTION_IMAGE_MONTAGE);
}, MONTAGE_UPDATE_DELAY_SECONDS, TimeUnit.SECONDS);
}
private void updateMontage(String channelId) {
logger.debug("Update montage for channel '{}'", channelId);
ArrayList<BufferedImage> images = getImages(channelId);
if (!images.isEmpty()) {
State state = createMontage(images);
if (state != null) {
logger.debug("Got a montage. Updating channel '{}' with image montage", channelId);
updateState(channelId, state);
return;
}
}
logger.debug("Updating channel '{}' with NULL image montage", channelId);
updateState(channelId, UnDefType.NULL);
}
// Get an array list of history images
private ArrayList<BufferedImage> getImages(String channelId) {
ArrayList<BufferedImage> images = new ArrayList<>();
Integer numberOfImages = config.montageNumImages;
if (numberOfImages != null) {
for (int imageNumber = 1; imageNumber <= numberOfImages; imageNumber++) {
logger.trace("Downloading montage image {} for channel '{}'", imageNumber, channelId);
DoorbirdImage historyImage = CHANNEL_DOORBELL_IMAGE_MONTAGE.equals(channelId)
? api.downloadDoorbellHistoryImage(String.valueOf(imageNumber))
: api.downloadMotionHistoryImage(String.valueOf(imageNumber));
if (historyImage != null) {
RawType image = historyImage.getImage();
if (image != null) {
try {
BufferedImage i = ImageIO.read(new ByteArrayInputStream(image.getBytes()));
images.add(i);
} catch (IOException e) {
logger.debug("IOException creating BufferedImage from downloaded image: {}",
e.getMessage());
}
}
}
}
if (images.size() < numberOfImages) {
logger.debug("Some images could not be downloaded: wanted={}, actual={}", numberOfImages,
images.size());
}
}
return images;
}
// Assemble the array of images into a single scaled image
private @Nullable State createMontage(ArrayList<BufferedImage> images) {
State state = null;
Integer montageScaleFactor = config.montageScaleFactor;
if (montageScaleFactor != null) {
// Assume all images are the same size, as the Doorbird image resolution cannot
// be changed by the user
int height = (int) (images.get(0).getHeight() * (montageScaleFactor / 100.0));
int width = (int) (images.get(0).getWidth() * (montageScaleFactor / 100.0));
int widthTotal = width * images.size();
logger.debug("Dimensions of final montage image: w={}, h={}", widthTotal, height);
// Create concatenated image
int currentWidth = 0;
BufferedImage concatImage = new BufferedImage(widthTotal, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = concatImage.createGraphics();
logger.debug("Concatenating images array into single image");
for (int j = 0; j < images.size(); j++) {
g2d.drawImage(images.get(j), currentWidth, 0, width, height, null);
currentWidth += width;
}
g2d.dispose();
// Convert image to a state
logger.debug("Rendering image to byte array and converting to RawType state");
byte[] imageBytes = convertImageToByteArray(concatImage);
if (imageBytes != null) {
state = new RawType(imageBytes, "image/png");
}
}
return state;
}
private byte @Nullable [] convertImageToByteArray(BufferedImage image) {
byte[] data = null;
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
ImageIO.write(image, "png", out);
data = out.toByteArray();
} catch (IOException ioe) {
logger.debug("IOException occurred converting image to byte array", ioe);
}
return data;
}
private void updateImageAndTimestamp() {
DoorbirdImage dbImage = api.downloadCurrentImage();
if (dbImage != null) {
RawType image = dbImage.getImage();
updateState(CHANNEL_IMAGE, image != null ? image : UnDefType.UNDEF);
updateState(CHANNEL_IMAGE_TIMESTAMP, getLocalDateTimeType(dbImage.getTimestamp()));
}
}
private DateTimeType getLocalDateTimeType(long dateTimeSeconds) {
return new DateTimeType(Instant.ofEpochSecond(dateTimeSeconds).atZone(timeZoneProvider.getTimeZone()));
}
}

View File

@@ -0,0 +1,256 @@
/**
* 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.doorbird.internal.listener;
import java.net.DatagramPacket;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.goterl.lazycode.lazysodium.LazySodiumJava;
import com.goterl.lazycode.lazysodium.SodiumJava;
import com.goterl.lazycode.lazysodium.exceptions.SodiumException;
import com.goterl.lazycode.lazysodium.interfaces.PwHash;
import com.sun.jna.NativeLong;
/**
* The {@link DoorbirdEvent} is responsible for decoding event packets received
* from the Doorbird.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class DoorbirdEvent {
private final Logger logger = LoggerFactory.getLogger(DoorbirdEvent.class);
// These values are extracted from the UDP packet
private byte version;
private int opslimit;
private long memlimit;
private byte[] salt = new byte[16];
private byte[] nonce = new byte[8];
private byte[] ciphertext = new byte[34];
// Starting 6 characters from the user name
private @Nullable String eventIntercomId;
// Doorbell number for doorbell event, or "motion" for motion events
private @Nullable String eventId;
// Timestamp of event
private long eventTimestamp;
private boolean isDoorbellEvent;
/*
* We want a single instance of LazySodium. Also, try to load the libsodium library
* from multiple sources.
*
* To load the libsodium library,
* - first try to load the library from the resources that are bundled with
* the LazySodium jar. (i.e. new SodiumJava())
* - if that fails with an UnsatisfiedLinkError, then try to load the library
* from the operating system (i.e. new SodiumJava("sodium").
* - if both of these attempts fail, the binding will be functional, except for
* its ability to decrypt the UDP events.
*/
@NonNullByDefault
private static class LazySodiumJavaHolder {
private static final Logger LOGGER = LoggerFactory.getLogger(LazySodiumJavaHolder.class);
static final @Nullable LazySodiumJava LAZY_SODIUM_JAVA_INSTANCE = loadLazySodiumJava();
private static @Nullable LazySodiumJava loadLazySodiumJava() {
LOGGER.debug("LazySodium has not been loaded yet. Try to load it now.");
LazySodiumJava lazySodiumJava = null;
try {
lazySodiumJava = new LazySodiumJava(new SodiumJava());
LOGGER.debug("Successfully loaded bundled libsodium crypto library!!");
} catch (UnsatisfiedLinkError e1) {
try {
LOGGER.debug("Unable to load bundled libsodium crypto library!! Try to load OS version.", e1);
lazySodiumJava = new LazySodiumJava(new SodiumJava("sodium"));
LOGGER.debug("Successfully loaded libsodium crypto library from operating system!!");
} catch (UnsatisfiedLinkError e2) {
LOGGER.info("Failed to load libsodium crypto library!!", e2);
LOGGER.info("Try manually installing libsodium on your OS if libsodium supports your architecture");
}
}
return lazySodiumJava;
}
}
public static @Nullable LazySodiumJava getLazySodiumJavaInstance() {
return LazySodiumJavaHolder.LAZY_SODIUM_JAVA_INSTANCE;
}
// Will be true if this is a valid Doorbird event
public boolean isDoorbellEvent() {
return isDoorbellEvent;
}
// Contains the intercomId for valid Doorbird events
public @Nullable String getIntercomId() {
return eventIntercomId;
}
// Contains the eventId for valid Doorbird events
public @Nullable String getEventId() {
return eventId;
}
// Contains the timestamp for valid Doorbird events
public long getTimestamp() {
return eventTimestamp;
}
/*
* The following functions support the decryption of the doorbell event
* using the LazySodium wrapper for the libsodium crypto library
*/
public void decrypt(DatagramPacket p, String password) {
isDoorbellEvent = false;
int length = p.getLength();
byte[] data = Arrays.copyOf(p.getData(), length);
// A valid event contains a 3 byte signature followed by the decryption version
if (length < 4) {
return;
}
// Only the first 5 characters of the password are used to generate the decryption key
if (password.length() < 5) {
logger.info("Invalid password length, must be at least 5 characters");
return;
}
String passwordFirstFive = password.substring(0, 5);
try {
// Load the message into the ByteBuffer
ByteBuffer bb = ByteBuffer.allocate(length);
bb.put(data, 0, length);
bb.rewind();
// Check for proper event signature
if (!isValidSignature(bb)) {
logger.trace("Received event not a doorbell event: {}", new String(data, StandardCharsets.US_ASCII));
return;
}
// Get the decryption version
version = getVersion(bb);
if (version == 1) {
// Decrypt using version 1 decryption scheme
decryptV1(bb, passwordFirstFive);
} else {
logger.info("Don't know how to decrypt version {} doorbell event", version);
}
} catch (IndexOutOfBoundsException e) {
logger.info("IndexOutOfBoundsException decrypting doorbell event", e);
} catch (BufferUnderflowException e) {
logger.info("BufferUnderflowException decrypting doorbell event", e);
}
}
private boolean isValidSignature(ByteBuffer bb) throws IndexOutOfBoundsException, BufferUnderflowException {
// Check the first three bytes for the proper signature
return (bb.get() & 0xFF) == 0xDE && (bb.get() & 0xFF) == 0xAD && (bb.get() & 0xFF) == 0xBE;
}
private byte getVersion(ByteBuffer bb) throws IndexOutOfBoundsException, BufferUnderflowException {
// Extract the decryption version from the packet
return bb.get();
}
private void decryptV1(ByteBuffer bb, String password5) throws IndexOutOfBoundsException, BufferUnderflowException {
LazySodiumJava sodium = getLazySodiumJavaInstance();
if (sodium == null) {
logger.debug("Unable to decrypt event because libsodium is not loaded");
return;
}
if (bb.capacity() != 70) {
logger.info("Received malformed version 1 doorbell event, length not 70 bytes");
return;
}
// opslimit and memlimit are 4 bytes each
opslimit = bb.getInt();
memlimit = bb.getInt();
// Get salt, nonce, and ciphertext arrays
bb.get(salt, 0, salt.length);
bb.get(nonce, 0, nonce.length);
bb.get(ciphertext, 0, ciphertext.length);
// Create the hash, which will be used to decrypt the ciphertext
byte[] hash;
try {
logger.trace("Calling cryptoPwHash with passwordFirstFive='{}', opslimit={}, memlimit={}, salt='{}'",
password5, opslimit, memlimit, HexUtils.bytesToHex(salt, " "));
String hashAsString = sodium.cryptoPwHash(password5, 32, salt, opslimit, new NativeLong(memlimit),
PwHash.Alg.PWHASH_ALG_ARGON2I13);
hash = HexUtils.hexToBytes(hashAsString);
} catch (SodiumException e) {
logger.info("Got SodiumException", e);
return;
}
// Set up the variables for the decryption algorithm
byte[] m = new byte[30];
long[] mLen = new long[30];
byte[] nSec = null;
byte[] c = ciphertext;
long cLen = ciphertext.length;
byte[] ad = null;
long adLen = 0;
byte[] nPub = nonce;
byte[] k = hash;
// Decrypt the ciphertext
logger.trace("Call cryptoAeadChaCha20Poly1305Decrypt with ciphertext='{}', nonce='{}', key='{}'",
HexUtils.bytesToHex(ciphertext, " "), HexUtils.bytesToHex(nonce, " "), HexUtils.bytesToHex(k, " "));
boolean success = sodium.cryptoAeadChaCha20Poly1305Decrypt(m, mLen, nSec, c, cLen, ad, adLen, nPub, k);
if (!success) {
/*
* Don't log at debug level since the decryption will fail for events encrypted with
* passwords other than the password contained in the thing configuration (reference API
* documentation for details)
*/
logger.trace("Decryption FAILED");
return;
}
int decryptedTextLength = (int) mLen[0];
if (decryptedTextLength != 18L) {
logger.info("Length of decrypted text is invalid, must be 18 bytes");
return;
}
// Get event fields from decrypted text
logger.debug("Received and successfully decrypted a Doorbird event!!");
ByteBuffer b = ByteBuffer.allocate(decryptedTextLength);
b.put(m, 0, decryptedTextLength);
b.rewind();
byte[] buf = new byte[8];
b.get(buf, 0, 6);
eventIntercomId = new String(buf, 0, 6).trim();
b.get(buf, 0, 8);
eventId = new String(buf, 0, 8).trim();
eventTimestamp = b.getInt();
logger.debug("Event is eventId='{}', intercomId='{}', timestamp={}", eventId, eventIntercomId, eventTimestamp);
isDoorbellEvent = true;
}
}

View File

@@ -0,0 +1,160 @@
/**
* 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.doorbird.internal.listener;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.doorbird.internal.handler.DoorbellHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link DoorbirdUdpListener} is responsible for receiving
* UDP braodcasts from the Doorbird doorbell.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class DoorbirdUdpListener extends Thread {
// Doorbird devices report status on a UDP port
private static final int UDP_PORT = 6524;
// How long to wait in milliseconds for a UDP packet
private static final int SOCKET_TIMEOUT_MILLISECONDS = 3000;
private static final int BUFFER_SIZE = 80;
private final Logger logger = LoggerFactory.getLogger(DoorbirdUdpListener.class);
private final DoorbirdEvent event = new DoorbirdEvent();
// Used for callbacks to handler
private final DoorbellHandler thingHandler;
// UDP socket used to receive status events from doorbell
private @Nullable DatagramSocket socket;
private byte @Nullable [] lastData;
private int lastDataLength;
private long lastDataTime;
public DoorbirdUdpListener(DoorbellHandler thingHandler) {
this.thingHandler = thingHandler;
}
@Override
public void run() {
receivePackets();
}
public void shutdown() {
if (socket != null) {
socket.close();
logger.debug("Listener closing listener socket");
socket = null;
}
}
private void receivePackets() {
try {
DatagramSocket s = new DatagramSocket(null);
s.setSoTimeout(SOCKET_TIMEOUT_MILLISECONDS);
s.setReuseAddress(true);
InetSocketAddress address = new InetSocketAddress(UDP_PORT);
s.bind(address);
socket = s;
logger.debug("Listener got UDP socket on port {} with timeout {}", UDP_PORT, SOCKET_TIMEOUT_MILLISECONDS);
} catch (SocketException e) {
logger.debug("Listener got SocketException: {}", e.getMessage(), e);
socket = null;
return;
}
DatagramPacket packet = new DatagramPacket(new byte[BUFFER_SIZE], BUFFER_SIZE);
while (socket != null) {
try {
socket.receive(packet);
processPacket(packet);
} catch (SocketTimeoutException e) {
// Nothing to do on socket timeout
} catch (IOException e) {
logger.debug("Listener got IOException waiting for datagram: {}", e.getMessage());
socket = null;
}
}
logger.debug("Listener exiting");
}
private void processPacket(DatagramPacket packet) {
logger.trace("Got datagram of length {} from {}", packet.getLength(), packet.getAddress().getHostAddress());
// Check for duplicate packet
if (isDuplicate(packet)) {
logger.trace("Dropping duplicate packet");
return;
}
String userId = thingHandler.getUserId();
String userPassword = thingHandler.getUserPassword();
if (userId == null || userPassword == null) {
logger.info("Doorbird user id and/or password is not set in configuration");
return;
}
try {
event.decrypt(packet, userPassword);
} catch (RuntimeException e) {
// The libsodium library might generate a runtime exception if the packet is malformed
logger.info("DoorbirdEvent got unhandled exception: {}", e.getMessage(), e);
return;
}
if (event.isDoorbellEvent()) {
if ("motion".equalsIgnoreCase(event.getEventId())) {
thingHandler.updateMotionChannel(event.getTimestamp());
} else {
String intercomId = event.getIntercomId();
if (intercomId != null && userId.toLowerCase().startsWith(intercomId.toLowerCase())) {
thingHandler.updateDoorbellChannel(event.getTimestamp());
} else {
logger.info("Received doorbell event for unknown device: {}", event.getIntercomId());
}
}
}
}
private boolean isDuplicate(DatagramPacket packet) {
boolean packetIsDuplicate = false;
if (lastData != null && lastDataLength == packet.getLength()) {
// Packet must be received within 750 ms of previous packet to be considered a duplicate
if ((System.currentTimeMillis() - lastDataTime) < 750) {
// Compare packets byte-for-byte
if (Arrays.equals(lastData, Arrays.copyOf(packet.getData(), packet.getLength()))) {
packetIsDuplicate = true;
}
}
}
// Remember this packet for duplicate check
lastDataLength = packet.getLength();
lastData = Arrays.copyOf(packet.getData(), lastDataLength);
lastDataTime = System.currentTimeMillis();
return packetIsDuplicate;
}
}

View File

@@ -0,0 +1,81 @@
/**
* 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.doorbird.internal.model;
import com.google.gson.annotations.SerializedName;
/**
* The {@link DoorbirdInfoDTO} models the JSON response returned by the Doorbird in response
* to calling the info.cgi API.
*
* @author Mark Hilbush - Initial contribution
*/
public class DoorbirdInfoDTO {
/**
* Top level container of information about the Doorbird configuration
*/
@SerializedName("BHA")
public DoorbirdInfoBha bha;
public class DoorbirdInfoBha {
/**
* Return code from the Doorbird
*/
@SerializedName("RETURNCODE")
public String returnCode;
/**
* Contains information about the Doorbird configuration
*/
@SerializedName("VERSION")
public DoorbirdInfoArray[] doorbirdInfoArray;
public class DoorbirdInfoArray {
/**
* Doorbird's firmware version
*/
@SerializedName("FIRMWARE")
public String firmwareVersion;
/**
* Doorbird's build number
*/
@SerializedName("BUILD_NUMBER")
public String buildNumber;
/**
* MAC address of Doorbird's wired interface
*/
@SerializedName("PRIMARY_MAC_ADDR")
public String primaryMacAddress;
/**
* MAC address of Doorbird's wifi interface
*/
@SerializedName("WIFI_MAC_ADDR")
public String wifiMacAddress;
/**
* Array of relays supported by this Doorbird
*/
@SerializedName("RELAYS")
public String[] relays;
/**
* Doorbird's model name
*/
@SerializedName("DEVICE-TYPE")
public String deviceType;
}
}
}

View File

@@ -0,0 +1,123 @@
/**
* 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.doorbird.internal.model;
import com.google.gson.annotations.SerializedName;
/**
* The {@link SipStatusDTO} models the JSON response returned by the Doorbird in response
* to calling the sip.cgi status API.
*
* @author Mark Hilbush - Initial contribution
*/
public class SipStatusDTO {
/**
* Top level container of information about the Doorbird status
*/
@SerializedName("BHA")
public SipStatusBha bha;
public class SipStatusBha {
/**
* Return code from the Doorbird
*/
@SerializedName("RETURNCODE")
public String returnCode;
/**
* Contains information about the Doorbird SIP status
*/
@SerializedName("SIP")
public SipStatusArray[] sipStatusArray;
public class SipStatusArray {
@SerializedName("ENABLE")
public String enable;
@SerializedName("PRIORITIZE_APP")
public String prioritizeApp;
@SerializedName("REGISTER_URL")
public String registerUrl;
@SerializedName("REGISTER_USER")
public String registerUser;
@SerializedName("REGISTER_PASSWORD")
public String registerPassword;
@SerializedName("AUTOCALL_MOTIONSENSOR_URL")
public String autocallMotionSensorUrl;
@SerializedName("AUTOCALL_DOORBELL_URL")
public String autocallDoorbellUrl;
/**
* Speaker volume
*/
@SerializedName("SPK_VOLUME")
public String speakerVolume;
/**
* Microphone volume
*/
@SerializedName("MIC_VOLUME")
public String microphoneVolume;
@SerializedName("DTMF")
public String dtmf;
@SerializedName("relais:1")
public String relais1;
@SerializedName("relais:2")
public String relais2;
@SerializedName("LIGHT_PASSCODE")
public String lightPasscode;
@SerializedName("INCOMING_CALL_ENABLE")
public String incomingCallEnable;
@SerializedName("INCOMING_CALL_USER")
public String incomingCallUser;
@SerializedName("ANC")
public String autoNoiseCancellation;
/**
* SIP call last error code
*/
@SerializedName("LASTERRORCODE")
public String lastErrorCode;
/**
* SIP call last error code
*/
@SerializedName("LASTERRORTEXT")
public String lastErrorText;
/**
* Maximum SIP ring time
*/
@SerializedName("RING_TIME_LIMIT")
public String ringTimeLimit;
/**
* Maximum SIP call time
*/
@SerializedName("CALL_TIME_LIMIT")
public String callTimeLimit;
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="doorbird" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Doorbird Binding</name>
<description>This is the binding for Doorbird video doorbells.</description>
<author>Mark Hilbush</author>
</binding:binding>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:doorbird:config">
<parameter name="doorbirdHost" type="text" required="true">
<label>Host</label>
<description>Hostname or IP address of Doorbird</description>
<context>network-address</context>
</parameter>
<parameter name="userId" type="text" required="true">
<label>User ID</label>
<description>Doorbird user ID with API permissions enabled</description>
</parameter>
<parameter name="userPassword" type="text" required="true">
<label>Password</label>
<description>Doorbird user password</description>
<context>password</context>
</parameter>
<parameter name="imageRefreshRate" type="integer" min="2" max="600">
<label>Image Refresh Rate</label>
<description>Image refresh rate in seconds (blank to disable)</description>
</parameter>
<parameter name="doorbellOffDelay" type="integer" min="1">
<label>Doorbell Released Delay</label>
<description>Delay in seconds after a doorbell event to send RELEASED event (blank to disable)</description>
</parameter>
<parameter name="motionOffDelay" type="integer" min="1">
<label>Motion Off Delay</label>
<description>Delay in seconds to set motion channel OFF after motion event (blank to disable)</description>
</parameter>
<parameter name="montageNumImages" type="integer" min="0" max="6">
<label>Montage Number of Images</label>
<description>Number of images to include in history montage</description>
<default>0</default>
</parameter>
<parameter name="montageScaleFactor" type="integer" min="1" max="100">
<label>Montage Scale Factor</label>
<description>Scaling factor percentage to apply to history montage (e.g use 25 for 25%)</description>
<default>25</default>
</parameter>
</config-description>
<config-description uri="thing-type:controller:config">
<parameter name="doorbirdHost" type="text" required="true">
<label>Host</label>
<description>Hostname or IP address of Doorbird</description>
<context>network-address</context>
</parameter>
<parameter name="userId" type="text" required="true">
<label>User ID</label>
<description>Doorbird user ID with API permissions enabled</description>
</parameter>
<parameter name="userPassword" type="text" required="true">
<label>Password</label>
<description>Doorbird user password</description>
<context>password</context>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="doorbird"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="d101">
<label>Doorbird Doorbell D101/D201/D205/D1101V</label>
<description>Doorbird doorbell model D101/D201/D205/D1101V</description>
<channels>
<channel id="doorbell" typeId="system.rawbutton">
<label>Doorbell</label>
<description>Trigger for doorbell press</description>
</channel>
<channel id="doorbellTimestamp" typeId="doorbellTimestamp"/>
<channel id="doorbellImage" typeId="doorbellImage"/>
<channel id="doorbellHistoryIndex" typeId="doorbellHistoryIndex"/>
<channel id="doorbellHistoryTimestamp" typeId="doorbellHistoryTimestamp"/>
<channel id="doorbellHistoryImage" typeId="doorbellHistoryImage"/>
<channel id="doorbellMontage" typeId="doorbellMontage"/>
<channel id="motion" typeId="system.motion"/>
<channel id="motionTimestamp" typeId="motionTimestamp"/>
<channel id="motionImage" typeId="motionImage"/>
<channel id="motionHistoryIndex" typeId="motionHistoryIndex"/>
<channel id="motionHistoryTimestamp" typeId="motionHistoryTimestamp"/>
<channel id="motionHistoryImage" typeId="motionHistoryImage"/>
<channel id="motionMontage" typeId="motionMontage"/>
<channel id="light" typeId="light"/>
<channel id="openDoor1" typeId="openDoor1"/>
<channel id="image" typeId="image"/>
<channel id="imageTimestamp" typeId="imageTimestamp"/>
</channels>
<config-description-ref uri="thing-type:doorbird:config"/>
</thing-type>
<thing-type id="d210x">
<label>Doorbird D210x Doorbell</label>
<description>Doorbird doorbell model D210x</description>
<channels>
<channel id="doorbell" typeId="system.rawbutton">
<label>Doorbell</label>
<description>Trigger for doorbell press</description>
</channel>
<channel id="doorbellTimestamp" typeId="doorbellTimestamp"/>
<channel id="doorbellImage" typeId="doorbellImage"/>
<channel id="doorbellHistoryIndex" typeId="doorbellHistoryIndex"/>
<channel id="doorbellHistoryTimestamp" typeId="doorbellHistoryTimestamp"/>
<channel id="doorbellHistoryImage" typeId="doorbellHistoryImage"/>
<channel id="doorbellMontage" typeId="doorbellMontage"/>
<channel id="motion" typeId="system.motion"/>
<channel id="motionTimestamp" typeId="motionTimestamp"/>
<channel id="motionImage" typeId="motionImage"/>
<channel id="motionHistoryIndex" typeId="motionHistoryIndex"/>
<channel id="motionHistoryTimestamp" typeId="motionHistoryTimestamp"/>
<channel id="motionHistoryImage" typeId="motionHistoryImage"/>
<channel id="motionMontage" typeId="motionMontage"/>
<channel id="light" typeId="light"/>
<channel id="openDoor1" typeId="openDoor1"/>
<channel id="openDoor2" typeId="openDoor2"/>
<channel id="image" typeId="image"/>
<channel id="imageTimestamp" typeId="imageTimestamp"/>
</channels>
<config-description-ref uri="thing-type:doorbird:config"/>
</thing-type>
<thing-type id="a1081">
<label>Doorbird A1081 Controller</label>
<description>Doorbird model A1081 Controller</description>
<channels>
<channel id="openDoor1" typeId="openDoor1"/>
<channel id="openDoor2" typeId="openDoor2"/>
<channel id="openDoor3" typeId="openDoor3"/>
</channels>
<config-description-ref uri="thing-type:controller:config"/>
</thing-type>
<channel-type id="doorbellTimestamp">
<item-type>DateTime</item-type>
<label>Doorbell Timestamp</label>
<description>Time when doorbell was last pressed</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="doorbellImage">
<item-type>Image</item-type>
<label>Doorbell Pressed Image</label>
<description>Image when doorbell was last pressed</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="motionTimestamp">
<item-type>DateTime</item-type>
<label>Motion Timestamp</label>
<description>Time when motion was last detected</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="motionImage">
<item-type>Image</item-type>
<label>Motion Detected Image</label>
<description>Image when motion was last detected</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="light">
<item-type>Switch</item-type>
<label>Light</label>
<description>Energize the light relay</description>
<state pattern="%s"></state>
</channel-type>
<channel-type id="openDoor1">
<item-type>Switch</item-type>
<label>Open Door 1</label>
<description>Energize opendoor / alarm output relay 1</description>
<state pattern="%s"></state>
</channel-type>
<channel-type id="openDoor2">
<item-type>Switch</item-type>
<label>Open Door 2</label>
<description>Energize opendoor / alarm output relay 2</description>
<state pattern="%s"></state>
</channel-type>
<channel-type id="openDoor3">
<item-type>Switch</item-type>
<label>Open Door 3</label>
<description>Energize opendoor / alarm output relay 3</description>
<state pattern="%s"></state>
</channel-type>
<channel-type id="imageTimestamp">
<item-type>DateTime</item-type>
<label>Image Timestamp</label>
<description>Time when image was captured from device</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="image">
<item-type>Image</item-type>
<label>Image</label>
<description>Image from device</description>
</channel-type>
<channel-type id="doorbellHistoryIndex">
<item-type>Number</item-type>
<label>Doorbell History Index</label>
<description>Index of historical image for doorbell press</description>
<state min="1" max="50" step="1"></state>
</channel-type>
<channel-type id="doorbellHistoryTimestamp">
<item-type>DateTime</item-type>
<label>Doorbell History Timestamp</label>
<description>Time when doorbell was pressed for history image</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="doorbellHistoryImage">
<item-type>Image</item-type>
<label>Doorbell History Image</label>
<description>Historical image for doorbell press</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="motionHistoryIndex">
<item-type>Number</item-type>
<label>Motion History Index</label>
<description>Index of Historical image for motion</description>
<state min="1" max="50" step="1"></state>
</channel-type>
<channel-type id="motionHistoryTimestamp">
<item-type>DateTime</item-type>
<label>Motion History Timestamp</label>
<description>Time when motion was detected for history image</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="motionHistoryImage">
<item-type>Image</item-type>
<label>Motion History Image</label>
<description>Historical image for motion sensor</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="doorbellMontage">
<item-type>Image</item-type>
<label>Doorbell Montage Image</label>
<description>Montage of multiple doorbell history images</description>
</channel-type>
<channel-type id="motionMontage">
<item-type>Image</item-type>
<label>Motion Montage Image</label>
<description>Montage of multiple motion history images</description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,97 @@
/**
* 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.doorbird.internal;
import static org.junit.Assert.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.doorbird.internal.api.DoorbirdInfo;
/**
* The {@link DoorbirdInfoTest} is responsible for testing the functionality
* of Doorbird "info" message parsing.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class DoorbirdInfoTest {
private final String infoWithControllerId =
//@formatter:off
"{" +
"'BHA': {" +
"'RETURNCODE': '1'," +
"'VERSION': [{" +
"'FIRMWARE': '000109'," +
"'BUILD_NUMBER': '15120529'," +
"'PRIMARY_MAC_ADDR': '1CCAE3711111'," +
"'WIFI_MAC_ADDR': '1CCAE3799999'," +
"'RELAYS': ['1', '2', 'gggaaa@1', 'gggaaa@2']," +
"'DEVICE-TYPE': 'DoorBird D101'" +
"}]" +
"}" +
"}";
//@formatter:on
private final String infoWithoutControllerId =
//@formatter:off
"{" +
"'BHA': {" +
"'RETURNCODE': '1'," +
"'VERSION': [{" +
"'FIRMWARE': '000109'," +
"'BUILD_NUMBER': '15120529'," +
"'PRIMARY_MAC_ADDR': '1CCAE3711111'," +
"'WIFI_MAC_ADDR': '1CCAE3799999'," +
"'RELAYS': ['1', '2']," +
"'DEVICE-TYPE': 'DoorBird D101'" +
"}]" +
"}" +
"}";
//@formatter:on
@Test
public void testParsingWithoutControllerId() {
DoorbirdInfo info = new DoorbirdInfo(infoWithoutControllerId);
assertEquals("1", info.getReturnCode());
assertEquals("000109", info.getFirmwareVersion());
assertEquals("15120529", info.getBuildNumber());
assertEquals("1CCAE3711111", info.getPrimaryMacAddress());
assertEquals("1CCAE3799999", info.getWifiMacAddress());
assertEquals("DoorBird D101", info.getDeviceType());
assertTrue(info.getRelays().contains("1"));
assertTrue(info.getRelays().contains("2"));
assertFalse(info.getRelays().contains("3"));
}
@Test
public void testGetControllerId() {
DoorbirdInfo info = new DoorbirdInfo(infoWithControllerId);
assertEquals("gggaaa", info.getControllerId());
assertTrue(info.getRelays().contains("gggaaa@1"));
assertTrue(info.getRelays().contains("gggaaa@2"));
assertFalse(info.getRelays().contains("unknown"));
}
@Test
public void testControllerIdIsNull() {
DoorbirdInfo info = new DoorbirdInfo(infoWithoutControllerId);
assertNull(info.getControllerId());
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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.doorbird.internal;
import static org.junit.Assert.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.doorbird.internal.api.SipStatus;
/**
* The {@link SipStatusTest} is responsible for testing the functionality
* of Doorbird "sipStatus" message parsing.
*
* @author Mark Hilbush - Initial contribution
*/
@NonNullByDefault
public class SipStatusTest {
private final String sipStatusJson =
//@formatter:off
"{" +
"'BHA': {" +
"'RETURNCODE': '1'," +
"'SIP': [{" +
"'ENABLE': '10'," +
"'PRIORITIZE_APP': '1'," +
"'REGISTER_URL': '192.168.178.1'," +
"'REGISTER_USER': 'xxxxx'," +
"'REGISTER_PASSWORD': 'yyyyy'," +
"'AUTOCALL_MOTIONSENSOR_URL': 'motion-url'," +
"'AUTOCALL_DOORBELL_URL': 'doorbell-url'," +
"'SPK_VOLUME': '70'," +
"'MIC_VOLUME': '33'," +
"'DTMF': '1'," +
"'relais:1': '0'," +
"'relais:2': '1'," +
"'LIGHT_PASSCODE': 'light-passcode'," +
"'INCOMING_CALL_ENABLE': '0'," +
"'INCOMING_CALL_USER': 'abcde'," +
"'ANC': '1'," +
"'LASTERRORCODE': '901'," +
"'LASTERRORTEXT': 'OK'," +
"'RING_TIME_LIMIT': '60'," +
"'CALL_TIME_LIMIT': '180'" +
"}]" +
"}" +
"}";
//@formatter:on
@Test
public void testParsing() {
SipStatus sipStatus = new SipStatus(sipStatusJson);
assertEquals("70", sipStatus.getSpeakerVolume());
assertEquals("33", sipStatus.getMicrophoneVolume());
assertEquals("901", sipStatus.getLastErrorCode());
assertEquals("OK", sipStatus.getLastErrorText());
assertEquals("60", sipStatus.getRingTimeLimit());
assertEquals("180", sipStatus.getCallTimeLimit());
}
}