added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
33
bundles/org.openhab.binding.doorbird/.classpath
Normal file
33
bundles/org.openhab.binding.doorbird/.classpath
Normal 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>
|
||||
23
bundles/org.openhab.binding.doorbird/.project
Normal file
23
bundles/org.openhab.binding.doorbird/.project
Normal 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>
|
||||
13
bundles/org.openhab.binding.doorbird/NOTICE
Normal file
13
bundles/org.openhab.binding.doorbird/NOTICE
Normal 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
|
||||
240
bundles/org.openhab.binding.doorbird/README.md
Normal file
240
bundles/org.openhab.binding.doorbird/README.md
Normal 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
|
||||
```
|
||||
42
bundles/org.openhab.binding.doorbird/pom.xml
Normal file
42
bundles/org.openhab.binding.doorbird/pom.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 : "";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user