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,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true" />
<attribute name="maven.pomderived" value="true" />
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true" />
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true" />
<attribute name="maven.pomderived" value="true" />
<attribute name="test" value="true" />
</attributes>
</classpathentry>
<classpathentry kind="con"
path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true" />
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true" />
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes" />
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.doorbird</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,240 @@
# Doorbird Binding
Binding for Doorbird D101 and D210x video doorbells.
## Supported Things
The following thing types are supported:
| Device | Thing ID |
|----------------------------------|-----------|
| Doorbird D101/D201/D205/D1101V Doorbell | d101 |
| Doorbird D210x Doorbell | d210x |
| Doorbird A1081 Controller | a1081 |
## Thing Configuration
### D101/D201/D205/D1101V and D210x Doorbell
The following configuration parameters are available on the Doorbird D101/D201/D205/D1101V and D210x Doorbell things:
| Parameter | Parameter ID | Required/Optional | Description |
|--------------------------|--------------------|-------------------|-------------|
| Hostname | doorbirdHost | Required | The hostname or IP address of the Doorbird device. |
| User ID | userId | Required | User Id of a Doorbird user that has permissions to access the API. The User ID and Password must be created using the Doorbird smart phone application. |
| Password | userPassword | Required | Password of a Doorbird user. |
| Image Refresh Rate | imageRefreshRate | Optional | Rate at which image channel should be automatically updated. Leave field blank (default) to disable refresh. |
| Doorbell Off Delay | doorbellOffDelay | Optional | Number of seconds to wait before setting doorbell channel OFF after a doorbell event. Leave field blank to disable. |
| Motion Off Delay | motionOffDelay | Optional | Number of seconds to wait before setting motion channel OFF after a motion event. Leave field blank to disable. |
| Montage Number of Images | montageNumImages | Required | Number of images to include in the doorbell and motion montage images. Default is 0. |
| Montage Scale Factor | montageScaleFactor | Required | Percent scaling factor for montage image. Default is 100. |
### A1081 Controller
The following configuration parameters are available on the Doorbird A1081 Controller thing:
| Parameter | Parameter ID | Required/Optional | Description |
|--------------------------|--------------|-------------------|-------------|
| Hostname | doorbirdHost | Required | The hostname or IP address of the Doorbird device. |
| User ID | userId | Required | User Id of a Doorbird user that has permissions to access the API. The User ID and Password must be created using the Doorbird smart phone application. |
| Password | userPassword | Required | Password of a Doorbird user. |
## Discovery
Auto-discovery is not supported at this time.
## Channels
The following channels are supported by the binding for the Doorbird D101/D201/D205 and D210x Doorbell thing types.
| Channel ID | Item Type | Description |
|--------------------------|-----------|---------------------------------------------------|
| doorbell | Trigger | Generates PRESSED event when doorbell is pressed |
| doorbellTimestamp | DateTime | Timestamp when doorbell was pressed |
| doorbellImage | Image | Image captured when the doorbell was pressed |
| doorbellHistoryIndex | Number | Index of historical image for doorbell press |
| doorbellHistoryTimestamp | DateTime | Time when doorbell was pressed for history image |
| doorbellHistoryImage | Image | Historical image for doorbell press |
| doorbellMontage | Image | Concatenation of first n doorbell history images |
| motion | Switch | Changes to ON when the device detects motion |
| motionTimestamp | DateTime | Timestamp when motion sensor was triggered |
| motionImage | Image | Image captured when motion was detected |
| motionHistoryIndex | Number | Index of Historical image for motion |
| motionHistoryTimestamp | DateTime | Time when motion was detected for history image |
| motionHistoryImage | Image | Historical image for motion sensor |
| motionMontage | Image | Concatenation of first n motion history images |
| light | Switch | Activates the light relay |
| openDoor1 | Switch | Activates the door 1 relay |
| openDoor2 | Switch | Activates the door 2 relay (D210x only) |
| image | Image | Image from the doorbird camera |
| imageTimestamp | DateTime | Time when image was captured from device |
The following channels are supported by the binding for the Doorbird A1081 Controller thing type.
| Channel ID | Item Type | Description |
|--------------------------|-----------|---------------------------------------------------|
| openDoor1 | Switch | Activates the door 1 relay |
| openDoor2 | Switch | Activates the door 2 relay |
| openDoor3 | Switch | Activates the door 3 relay |
## Profiles
Using the system default switch profile *rawbutton-on-off-switch* in a *doorbell* channel item definition will cause ON/OFF
states to be set when the doorbell is pressed and released.
See *Items* example below.
## Rule Actions
The binding supports the following actions.
In classic rules these are accessible as shown in this example (adjust getActions with your ThingId):
### void restart()
Restarts the Doorbird device.
### void sipHangup()
Hangs up a SIP call.
### String getRingTimeLimit()
Get the value of the SIP status parameter RING_TIME_LIMIT.
### String getCallTimeLimit()
Get the value of the SIP status parameter CALL_TIME_LIMIT.
### String getLastErrorCode()
Get the value of the SIP status parameter LASTERRORCODE.
### String getLastErrorText()
Get the value of the SIP status parameter LASTERRORTEXT.
Example
```
val actions = getActions("doorbird","doorbird:d101:doorbell")
if(actions === null) {
logInfo("actions", "Actions not found, check thing ID")
return
}
actions.sipHangup()
var String ringTimeLimit = actions.getRingTimeLimit()
```
## Known Issues
The Doorbird uses the UDP protocol on port 6524 to broadcast events for Doorbird actions, such as doorbell pressed, motion detected, etc.
If the Doorbord is on a separate subnet or VLAN from openHAB, those UDP packets will not route by default.
In that case, the Doorbird binding will not receive those events.
Either put the Doorbird and openHAB on the same subnet/VLAN, or set up your network to explicitly route those UDP packets.
## Example
### Things
```
Thing doorbird:d101:doorbell "Doorbird D101 Doorbell" [doorbirdHost="192.168.1.100",userId="dtfubb0004",userPassword="HG7afc5TvN",imageRefreshRate=60,doorbellOffDelay=3,motionOffDelay=30,montageNumImages=3,montageScaleFactor=35]
Thing doorbird:a1081:controller "Doorbird A1081 Controller" [doorbirdHost="192.168.1.100",userId="dtfubb0004",userPassword="HG7afc5TvN"]
```
### Items
```
Switch Doorbell_Pressed "Doorbell Pressed [%s]" <switch> ["Switch"] { channel="doorbird:d101:doorbell:doorbell" [profile="rawbutton-on-off-switch"] }
DateTime Doorbell_PressedTimestamp "Doorbell Pressed Timestamp [%1$tA, %1$tm/%1$td/%1$tY %1$tl:%1$tM %1$tp]" <time> { channel="doorbird:d101:doorbell:doorbellTimestamp" }
Image Doorbell_PressedImage "Doorbell Pressed Image [%s]" { channel="doorbird:d101:doorbell:doorbellImage" }
Switch Doorbell_Motion "Doorbell Motion [%s]" <switch> ["Switch"] { channel="doorbird:d101:doorbell:motion" }
DateTime Doorbell_MotionTimestamp "Doorbell Motion Timestamp [%1$tA, %1$tm/%1$td/%1$tY %1$tl:%1$tM %1$tp]" <time> { channel="doorbird:d101:doorbell:motionTimestamp" }
Image Doorbell_MotionDetectedImage "Motion Detected Image [%s]" { channel="doorbird:d101:doorbell:motionImage" }
Switch Doorbell_Light "Doorbell Light [%s]" <switch> ["Switch"] { channel="doorbird:d101:doorbell:light", expire="5s,command=OFF" }
Switch Doorbell_OpenDoor1 "Doorbell Open Door 1 [%s]" <switch> ["Switch"] { channel="doorbird:d101:doorbell:openDoor1", expire="5s,command=OFF" }
Image Doorbell_Image "Doorbell Image [%s]" { channel="doorbird:d101:doorbell:image" }
Number Doorbell_DoorbellHistoryIndex "Doorbell History Index [%.0f]" <none> { channel="doorbird:d101:doorbell:doorbellHistoryIndex" }
DateTime Doorbell_DoorbellHistoryTimestamp "Doorbell History Timestamp [%1$tA, %1$tm/%1$td/%1$tY %1$tl:%1$tM %1$tp]" <time> { channel="doorbird:d101:doorbell:doorbellHistoryTimestamp" }
Image Doorbell_DoorbellHistoryImage "Doorbell History Image [%s]" { channel="doorbird:d101:doorbell:doorbellHistoryImage" }
Number Doorbell_MotionHistoryIndex "Motion History Index [%.0f]" <none> { channel="doorbird:d101:doorbell:motionHistoryIndex" }
DateTime Doorbell_MotionHistoryTimestamp "Motion History Timestamp [%1$tA, %1$tm/%1$td/%1$tY %1$tl:%1$tM %1$tp]" <time> { channel="doorbird:d101:doorbell:motionHistoryTimestamp" }
Image Doorbell_MotionHistoryImage "Motion History Image [%s]" { channel="doorbird:d101:doorbell:motionHistoryImage" }
Image Doorbell_DoorbellMontage "Doorbell History Montage [%s]" { channel="doorbird:d101:doorbell:doorbellMontage" }
Image Doorbell_MotionMontage "Motion History Montage [%s]" { channel="doorbird:d101:doorbell:motionMontage" }
```
### Sitemap
```
Frame {
Text label="Doorbird" {
Frame label="Image" {
Image item=Doorbell_Image
}
Frame label="Events" {
Text item=Doorbell_Pressed
Text item=Doorbell_PressedTimestamp
Image item=Doorbell_PressedImage
Text item=Doorbell_Motion
Text item=Doorbell_MotionTimestamp
Image item=Doorbell_MotionImage
}
Frame label="Actions" {
Switch item=Doorbell_OpenDoor1
Switch item=Doorbell_Light
}
Frame label="History" {
Setpoint item=Doorbell_DoorbellHistoryIndex minValue=1 maxValue=50 step=1
Switch item=Doorbell_DoorbellHistoryIndex label="Reset Index []" mappings=[1="Reset"]
Text item=Doorbell_DoorbellHistoryTimestamp
Image item=Doorbell_DoorbellHistoryImage
Setpoint item=Doorbell_MotionHistoryIndex minValue=1 maxValue=50 step=1
Switch item=Doorbell_MotionHistoryIndex label="Reset Index []" mappings=[1="Reset"]
Text item=Doorbell_MotionHistoryTimestamp
Image item=Doorbell_MotionHistoryImage
}
Frame label="Doorbell Montage" {
Image item=Doorbell_DoorbellMontage
}
Frame label="Motion Montage" {
Image item=Doorbell_MotionMontage
}
}
}
```
### Rule
Using the doorbell trigger channel to detect if the doorbell has been pressed:
```
rule "Doorbell Button Pressed"
when
Channel "doorbird:d101:doorbell:doorbell" triggered PRESSED
then
// Do something when the doorbell is pressed
end
```
Alternatively, detecting a doorbell press using an item that references the *rawbutton-on-off-switch* profile:
```
rule "Doorbell Button Pressed"
when
Item Doorbell_Pressed received command ON
then
// Do something when the doorbell is pressed
end
```
Using the doorbell motion channel to detect motion:
```
rule "Motion Detected"
when
Item Doorbell_Motion received command ON
then
// Do something when motion is detected
end
```

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.doorbird</artifactId>
<name>openHAB Add-ons :: Bundles :: Doorbird Binding</name>
<properties>
<dep.noembedding>jna</dep.noembedding>
</properties>
<dependencies>
<dependency>
<groupId>com.goterl.lazycode</groupId>
<artifactId>lazysodium-java</artifactId>
<version>4.2.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>co.libly</groupId>
<artifactId>resource-loader</artifactId>
<version>1.3.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.5.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

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());
}
}