mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[dahuadoor] Initial contribution (#20172)
* Initial commit Signed-off-by: Sven Schad <svnsssd@gmail.com>
This commit is contained in:
@@ -76,6 +76,7 @@
|
||||
/bundles/org.openhab.binding.cm11a/ @BobRak
|
||||
/bundles/org.openhab.binding.comfoair/ @boehan
|
||||
/bundles/org.openhab.binding.coolmasternet/ @projectgus
|
||||
/bundles/org.openhab.binding.dahuadoor/ @svnsssd
|
||||
/bundles/org.openhab.binding.daikin/ @caffineehacker
|
||||
/bundles/org.openhab.binding.dali/ @rs22
|
||||
/bundles/org.openhab.binding.danfossairunit/ @pravussum
|
||||
|
||||
@@ -366,6 +366,11 @@
|
||||
<artifactId>org.openhab.binding.coolmasternet</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.dahuadoor</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.daikin</artifactId>
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,138 @@
|
||||
# DahuaDoor Binding
|
||||
|
||||
This binding integrates Dahua VTO Villastation door controllers with openHAB, enabling doorbell notifications, camera snapshots, and remote door control.
|
||||
|
||||
## Supported Things
|
||||
|
||||
| Thing Type | Thing ID | Description |
|
||||
| ---------- | --------- | ------------------------------------------------ |
|
||||
| VTO2202 | `vto2202` | Dahua VTO2202 outdoor station with single button |
|
||||
| VTO3211 | `vto3211` | Dahua VTO3211 outdoor station with dual buttons |
|
||||
|
||||
## Discovery
|
||||
|
||||
Automatic discovery is not supported.
|
||||
Things must be manually configured.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
### VTO2202/VTO3211 Device
|
||||
|
||||
Single-button outdoor station.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| ------------ | ---- | -------- | ---------------------------------------------------------------------------- |
|
||||
| hostname | text | Yes | Hostname or IP address of the device (e.g., 192.168.1.100) |
|
||||
| username | text | Yes | Username to access the device |
|
||||
| password | text | Yes | Password to access the device |
|
||||
| snapshotPath | text | Yes | Linux path where image files are stored (e.g., /var/lib/openhab/door-images) |
|
||||
|
||||
**Note:** Windows paths are not currently supported.
|
||||
|
||||
## Channels
|
||||
|
||||
### VTO2202 Channels (Single Button)
|
||||
|
||||
| Channel ID | Type | Read/Write | Description |
|
||||
| ----------- | ------- | ---------- | --------------------------------------------------------- |
|
||||
| bell-button | Trigger | Read | Triggers when doorbell button is pressed (event: PRESSED) |
|
||||
| door-image | Image | Read | Camera snapshot taken when doorbell is pressed |
|
||||
| open-door-1 | Switch | Write | Command to open door relay 1 |
|
||||
| open-door-2 | Switch | Write | Command to open door relay 2 |
|
||||
|
||||
### VTO3211 Channels (Dual Button)
|
||||
|
||||
| Channel ID | Type | Read/Write | Description |
|
||||
| ------------- | ------- | ---------- | -------------------------------------------------- |
|
||||
| bell-button-1 | Trigger | Read | Triggers when button 1 is pressed (event: PRESSED) |
|
||||
| bell-button-2 | Trigger | Read | Triggers when button 2 is pressed (event: PRESSED) |
|
||||
| door-image-1 | Image | Read | Camera snapshot when button 1 is pressed |
|
||||
| door-image-2 | Image | Read | Camera snapshot when button 2 is pressed |
|
||||
| open-door-1 | Switch | Write | Command to open door relay 1 |
|
||||
| open-door-2 | Switch | Write | Command to open door relay 2 |
|
||||
|
||||
## Full Example
|
||||
|
||||
### VTO2202 Example (Single Button)
|
||||
|
||||
#### Thing Configuration
|
||||
|
||||
```java
|
||||
Thing dahuadoor:vto2202:frontdoor "Front Door Station" @ "Entrance" [
|
||||
hostname="192.168.1.100",
|
||||
username="admin",
|
||||
password="password123",
|
||||
snapshotPath="/var/lib/openhab/door-images"
|
||||
]
|
||||
```
|
||||
|
||||
#### Item Configuration
|
||||
|
||||
```java
|
||||
Switch OpenFrontDoor "Open Front Door" <door> { channel="dahuadoor:vto2202:frontdoor:open-door-1" }
|
||||
Image FrontDoorImage "Front Door Camera" <camera> { channel="dahuadoor:vto2202:frontdoor:door-image" }
|
||||
```
|
||||
|
||||
#### Rule Configuration
|
||||
|
||||
Send smartphone notification with camera image when doorbell is pressed (requires openHAB Cloud Connector):
|
||||
|
||||
```java
|
||||
rule "Doorbell Notification"
|
||||
when
|
||||
Channel "dahuadoor:vto2202:frontdoor:bell-button" triggered PRESSED
|
||||
then
|
||||
sendBroadcastNotification("Visitor at the door", "door",
|
||||
"entrance", "Entrance", "door-notifications", null,
|
||||
"item:FrontDoorImage",
|
||||
"Open Door=command:OpenFrontDoor:ON", null)
|
||||
end
|
||||
```
|
||||
|
||||
### VTO3211 Example (Dual Button)
|
||||
|
||||
#### Thing Configuration
|
||||
|
||||
```java
|
||||
Thing dahuadoor:vto3211:entrance "Entrance Station" @ "Entrance" [
|
||||
hostname="192.168.1.101",
|
||||
username="admin",
|
||||
password="password123",
|
||||
snapshotPath="/var/lib/openhab/door-images"
|
||||
]
|
||||
```
|
||||
|
||||
#### Item Configuration
|
||||
|
||||
```java
|
||||
Switch OpenApartment1 "Open Apartment 1" <door> { channel="dahuadoor:vto3211:entrance:open-door-1" }
|
||||
Switch OpenApartment2 "Open Apartment 2" <door> { channel="dahuadoor:vto3211:entrance:open-door-2" }
|
||||
Image Apartment1Image "Apartment 1 Camera" <camera> { channel="dahuadoor:vto3211:entrance:door-image-1" }
|
||||
Image Apartment2Image "Apartment 2 Camera" <camera> { channel="dahuadoor:vto3211:entrance:door-image-2" }
|
||||
```
|
||||
|
||||
#### Rule Configuration
|
||||
|
||||
Send notifications for both buttons:
|
||||
|
||||
```java
|
||||
rule "Apartment 1 Doorbell"
|
||||
when
|
||||
Channel "dahuadoor:vto3211:entrance:bell-button-1" triggered PRESSED
|
||||
then
|
||||
sendBroadcastNotification("Visitor at Apartment 1", "door",
|
||||
"entrance", "Entrance", "door-notifications", null,
|
||||
"item:Apartment1Image",
|
||||
"Open Door=command:OpenApartment1:ON", null)
|
||||
end
|
||||
|
||||
rule "Apartment 2 Doorbell"
|
||||
when
|
||||
Channel "dahuadoor:vto3211:entrance:bell-button-2" triggered PRESSED
|
||||
then
|
||||
sendBroadcastNotification("Visitor at Apartment 2", "door",
|
||||
"entrance", "Entrance", "door-notifications", null,
|
||||
"item:Apartment2Image",
|
||||
"Open Door=command:OpenApartment2:ON", null)
|
||||
end
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>5.2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.dahuadoor</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: DahuaDoor Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.dahuadoor-${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-dahuadoor" description="DahuaDoor Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.dahuadoor/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
+575
@@ -0,0 +1,575 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 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.dahuadoor.internal;
|
||||
|
||||
import static org.openhab.binding.dahuadoor.internal.DahuaDoorBindingConstants.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.dahuadoor.internal.dahuaeventhandler.DHIPEventListener;
|
||||
import org.openhab.binding.dahuadoor.internal.dahuaeventhandler.DahuaEventClient;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.RawType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
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.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* The {@link DahuaDoorBaseHandler} is responsible for handling commands, which
|
||||
* are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Sven Schad - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class DahuaDoorBaseHandler extends BaseThingHandler implements DHIPEventListener {
|
||||
|
||||
protected final Logger logger = LoggerFactory.getLogger(DahuaDoorBaseHandler.class);
|
||||
protected @Nullable DahuaDoorConfiguration config;
|
||||
|
||||
protected Gson gson = new Gson();
|
||||
|
||||
protected @Nullable DahuaEventClient client = null;
|
||||
|
||||
public DahuaDoorBaseHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
DahuaEventClient localClient = client;
|
||||
if (localClient == null) {
|
||||
logger.warn("Client not initialized, cannot handle command");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_OPEN_DOOR_1:
|
||||
if (command instanceof OnOffType) {
|
||||
if (command == OnOffType.ON) {
|
||||
localClient.openDoor(1);
|
||||
updateState(channelUID, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_OPEN_DOOR_2:
|
||||
if (command instanceof OnOffType) {
|
||||
if (command == OnOffType.ON) {
|
||||
localClient.openDoor(2);
|
||||
updateState(channelUID, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void errorInformer(String msgError) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, msgError);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
DahuaDoorConfiguration localConfig = getConfigAs(DahuaDoorConfiguration.class);
|
||||
config = localConfig;
|
||||
|
||||
// Validate required configuration
|
||||
if (localConfig.hostname.isBlank() || localConfig.username.isBlank() || localConfig.password.isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error-missing-credentials");
|
||||
return;
|
||||
}
|
||||
|
||||
if (localConfig.snapshotPath.isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/offline.conf-error-missing-snapshot-path");
|
||||
return;
|
||||
}
|
||||
|
||||
client = new DahuaEventClient(localConfig.hostname, localConfig.username, localConfig.password, this,
|
||||
this::errorInformer);
|
||||
|
||||
// Set status to UNKNOWN - will be set to ONLINE when first DHIP event is received
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
DahuaEventClient localClient = client;
|
||||
if (localClient != null) {
|
||||
localClient.dispose();
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void saveSnapshot(byte @Nullable [] buffer) {
|
||||
final DahuaDoorConfiguration localConfig = config;
|
||||
if (localConfig == null) {
|
||||
logger.warn("Configuration not initialized");
|
||||
return;
|
||||
}
|
||||
if (localConfig.snapshotPath.isEmpty()) {
|
||||
logger.warn("Path for Snapshots is invalid");
|
||||
return;
|
||||
}
|
||||
if (buffer == null) {
|
||||
logger.warn("cannot save empty buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure snapshot directory exists
|
||||
File snapshotDir = new File(localConfig.snapshotPath);
|
||||
if (!snapshotDir.exists() && !snapshotDir.mkdirs()) {
|
||||
logger.warn("Could not create snapshot directory '{}', check permissions", localConfig.snapshotPath);
|
||||
return;
|
||||
}
|
||||
|
||||
String timestamp = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ROOT).format(new Date());
|
||||
String filename = localConfig.snapshotPath + "/DoorBell_" + timestamp + ".jpg";
|
||||
|
||||
try (FileOutputStream fos = new FileOutputStream(new File(filename))) {
|
||||
fos.write(buffer);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not write image to file '{}', check permissions and path", filename, e);
|
||||
}
|
||||
|
||||
// Write buffer directly to latest snapshot file (avoids copy-from-source failures)
|
||||
String latestSnapshotFilename = localConfig.snapshotPath + "/Doorbell.jpg";
|
||||
try (FileOutputStream fos = new FileOutputStream(new File(latestSnapshotFilename))) {
|
||||
fos.write(buffer);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not write latest snapshot to '{}', check permissions and path", latestSnapshotFilename,
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChannelImage(byte @Nullable [] buffer) {
|
||||
if (buffer == null || buffer.length == 0) {
|
||||
updateState(CHANNEL_DOOR_IMAGE, UnDefType.UNDEF);
|
||||
return;
|
||||
}
|
||||
RawType image = new RawType(buffer, "image/jpeg");
|
||||
updateState(CHANNEL_DOOR_IMAGE, image);
|
||||
}
|
||||
|
||||
protected void handleButtonPressed() {
|
||||
Channel channel = this.getThing().getChannel(CHANNEL_BELL_BUTTON);
|
||||
if (channel == null) {
|
||||
logger.warn("Bell button channel not found");
|
||||
return;
|
||||
}
|
||||
triggerChannel(channel.getUID(), "PRESSED");
|
||||
|
||||
DahuaEventClient localClient = client;
|
||||
if (localClient == null) {
|
||||
logger.warn("Client not initialized, cannot retrieve doorbell image");
|
||||
return;
|
||||
}
|
||||
byte[] buffer = localClient.requestImage();
|
||||
updateChannelImage(buffer);
|
||||
saveSnapshot(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(JsonObject eventData) {
|
||||
// Set thing ONLINE when first event is received (confirms successful
|
||||
// connection)
|
||||
if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
try {
|
||||
logger.trace("JSON{}", eventData);
|
||||
JsonObject jsonObj = eventData.getAsJsonObject("params");
|
||||
if (jsonObj == null) {
|
||||
logger.debug("Missing 'params' object in DahuaDoor event. Raw payload: {}", eventData.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
JsonArray firstLevel = jsonObj.getAsJsonArray("eventList");
|
||||
if (firstLevel == null || firstLevel.isEmpty()) {
|
||||
logger.debug("Missing or empty 'eventList' array in DahuaDoor event. Raw payload: {}",
|
||||
eventData.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
JsonObject eventList = firstLevel.get(0).getAsJsonObject();
|
||||
if (!eventList.has("Code")) {
|
||||
logger.debug("Missing 'Code' in event data. Raw payload: {}", eventData.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
String eventCodeString = eventList.get("Code").getAsString();
|
||||
DahuaEventCode eventCode = DahuaEventCode.fromString(eventCodeString);
|
||||
JsonObject eventPayload = eventList.getAsJsonObject("Data");
|
||||
if (firstLevel.size() > 1) {
|
||||
logger.debug("Event Manager subscription reply: {}", eventCodeString);
|
||||
} else {
|
||||
switch (eventCode) {
|
||||
case CALL_NO_ANSWERED:
|
||||
handleVTOCall();
|
||||
break;
|
||||
case IGNORE_INVITE:
|
||||
handleVTHAnswer();
|
||||
break;
|
||||
case VIDEO_MOTION:
|
||||
handleMotionEvent();
|
||||
break;
|
||||
case RTSP_SESSION_DISCONNECT:
|
||||
handleRTSPDisconnect(eventList, eventPayload);
|
||||
break;
|
||||
case BACK_KEY_LIGHT:
|
||||
handleBackKeyLight(eventPayload);
|
||||
break;
|
||||
case TIME_CHANGE:
|
||||
handleTimeChange(eventPayload);
|
||||
break;
|
||||
case NTP_ADJUST_TIME:
|
||||
handleNTPAdjust(eventPayload);
|
||||
break;
|
||||
case KEEP_LIGHT_ON:
|
||||
handleKeepLightOn(eventPayload);
|
||||
break;
|
||||
case VIDEO_BLIND:
|
||||
handleVideoBlind(eventList);
|
||||
break;
|
||||
case FINGER_PRINT_CHECK:
|
||||
handleFingerPrintCheck(eventPayload);
|
||||
break;
|
||||
case DOOR_CARD:
|
||||
handleDoorCard(eventList, eventPayload);
|
||||
break;
|
||||
case SIP_REGISTER_RESULT:
|
||||
handleSIPRegisterResult(eventList, eventPayload);
|
||||
break;
|
||||
case ACCESS_CONTROL:
|
||||
handleAccessControl(eventPayload);
|
||||
break;
|
||||
case CALL_SNAP:
|
||||
handleCallSnap(eventPayload);
|
||||
break;
|
||||
case HUNGUP_PHONE:
|
||||
handleHungupPhone(eventList, eventPayload);
|
||||
break;
|
||||
case HANGUP_PHONE:
|
||||
handleHangupPhone(eventList, eventPayload);
|
||||
break;
|
||||
case HANGUP:
|
||||
handleHangup(eventList, eventPayload);
|
||||
break;
|
||||
case INVITE:
|
||||
handleInvite(eventList, eventPayload);
|
||||
break;
|
||||
case ALARM_LOCAL:
|
||||
handleAlarmLocal(eventList, eventPayload);
|
||||
break;
|
||||
case ACCESS_SNAP:
|
||||
handleAccessSnap(eventPayload);
|
||||
break;
|
||||
case REQUEST_CALL_STATE:
|
||||
handleRequestCallState(eventList, eventPayload);
|
||||
break;
|
||||
case PASSIVE_HANGUP:
|
||||
handlePassiveHangup(eventList, eventPayload);
|
||||
break;
|
||||
case PROFILE_ALARM_TRANSMIT:
|
||||
handleProfileAlarmTransit(eventList, eventPayload);
|
||||
break;
|
||||
case NEW_FILE:
|
||||
handleNewFile(eventList, eventPayload);
|
||||
break;
|
||||
case UPDATE_FILE:
|
||||
handleUpdateFile(eventList, eventPayload);
|
||||
break;
|
||||
case REBOOT:
|
||||
handleReboot(eventList, eventPayload);
|
||||
break;
|
||||
case SECURITY_IM_EXPORT:
|
||||
handleSecurityImport(eventList, eventPayload);
|
||||
break;
|
||||
case DGS_ERROR_REPORT:
|
||||
handleDGSErrorReport(eventList, eventPayload);
|
||||
break;
|
||||
case UPGRADE:
|
||||
handleUpgrade(eventList, eventPayload);
|
||||
break;
|
||||
case SEND_CARD:
|
||||
handleSendCard(eventList, eventPayload);
|
||||
break;
|
||||
case ADD_CARD:
|
||||
handleAddCard(eventList, eventPayload);
|
||||
break;
|
||||
case DOOR_STATUS:
|
||||
handleDoorStatus(eventList, eventPayload);
|
||||
break;
|
||||
case DOOR_CONTROL:
|
||||
handleDoorControl(eventList, eventPayload);
|
||||
break;
|
||||
case DOOR_NOT_CLOSED:
|
||||
handleDoorNotClosed(eventList, eventPayload);
|
||||
break;
|
||||
case NETWORK_CHANGE:
|
||||
handleNetworkChanged(eventList, eventPayload);
|
||||
break;
|
||||
case UNKNOWN:
|
||||
default:
|
||||
logger.debug("Unknown event received. JSON{}", gson.toJson(eventData));
|
||||
}
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
logger.debug("Invalid JSON structure while handling DahuaDoor event. Raw payload: {}", eventData.toString(),
|
||||
e);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
logger.debug("Missing expected array elements in DahuaDoor event. Raw payload: {}", eventData.toString(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleNetworkChanged(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: NetworkChange, Action {}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleDoorNotClosed(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: DoorNotClosed, Action {}, Name{}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("Name").getAsString(), eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleDoorControl(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: DoorControl, Action {}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleDoorStatus(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: DoorStatus, Action {}, Status: {}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("Status").getAsString(), eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleAddCard(JsonObject eventList, JsonObject eventData) {
|
||||
JsonObject cardData = eventData.getAsJsonArray("Data").get(0).getAsJsonObject();
|
||||
logger.debug(
|
||||
"Event: AddCard, Action {}: CardNo {}, UserID {}, UserName {}, CardStatus {}, CardType {}, Doors: Door 0={}, Door1={}",
|
||||
eventList.get("Action").getAsString(), cardData.get("CardNo").getAsString(),
|
||||
cardData.get("UserID").getAsString(), cardData.get("UserName").getAsString(),
|
||||
cardData.get("CardStatus").getAsString(), cardData.get("CardType").getAsString(),
|
||||
cardData.getAsJsonArray("Doors").get(0).getAsString(),
|
||||
cardData.getAsJsonArray("Doors").get(1).getAsString());
|
||||
}
|
||||
|
||||
protected void handleSendCard(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: SendCard, Action {}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleUpgrade(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: Upgrade, Action {}, with State{}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("State").getAsString(), eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleDGSErrorReport(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: DGSErrorReport, Action {}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleSecurityImport(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: SecurityImExport, Action {}, LocaleTime {}, Status {}",
|
||||
eventList.get("Action").getAsString(), eventData.get("LocaleTime").getAsString(),
|
||||
eventData.get("Status").getAsString());
|
||||
}
|
||||
|
||||
protected void handleReboot(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: Reboot, Action {}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleUpdateFile(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: UpdateFile, Action {}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleNewFile(JsonObject eventList, JsonObject eventData) {
|
||||
String action = eventList.has("Action") ? eventList.get("Action").getAsString() : "unknown";
|
||||
String file = eventData.has("File") ? eventData.get("File").getAsString() : "unknown";
|
||||
String folder = eventData.has("Filter") ? eventData.get("Filter").getAsString() : "unknown";
|
||||
// not a typo: Filter works for folder, seems to be a naming error in the Dahua firmware
|
||||
String localeTime = eventData.has("LocaleTime") ? eventData.get("LocaleTime").getAsString() : "unknown";
|
||||
String index = eventList.has("Index") ? eventList.get("Index").getAsString() : "unknown";
|
||||
|
||||
logger.debug("Event: NewFile, Action {}, File {}, Folder {}, LocaleTime {}, Index {}", action, file, folder,
|
||||
localeTime, index);
|
||||
}
|
||||
|
||||
protected void handleProfileAlarmTransit(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: ProfileAlarmTransmit, Action {}, AlarmType {}, DevSrcType {}, SenseMethod {}",
|
||||
eventList.get("Action").getAsString(), eventData.get("AlarmType").getAsString(),
|
||||
eventData.get("DevSrcType").getAsString(), eventData.get("SenseMethod").getAsString());
|
||||
}
|
||||
|
||||
protected void handlePassiveHangup(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: PassiveHangup, Action {}, LocaleTime {}, Index {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("LocaleTime").getAsString(), eventData.get("Index").getAsString());
|
||||
}
|
||||
|
||||
protected void handleRequestCallState(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: RequestCallState, Action {}, LocaleTime {}, Index {}",
|
||||
eventList.get("Action").getAsString(), eventData.get("LocaleTime").getAsString(),
|
||||
eventData.get("Index").getAsString());
|
||||
}
|
||||
|
||||
protected void handleAccessSnap(JsonObject eventData) {
|
||||
logger.debug("Event: AccessSnap, FTP upload to {}", eventData.get("FtpUrl").getAsString());
|
||||
}
|
||||
|
||||
protected void handleAlarmLocal(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: AlarmLocal, Action {}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleInvite(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: Invite, Action {}, CallID {}, Lock Number {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("CallID").getAsString(), eventData.get("LockNum").getAsString());
|
||||
}
|
||||
|
||||
protected void handleHangup(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: Hangup, Action {}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleHungupPhone(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: HungupPhone, Action {}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleHangupPhone(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event: HangupPhone, Action {}, LocaleTime {}", eventList.get("Action").getAsString(),
|
||||
eventData.get("LocaleTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleCallSnap(JsonObject eventData) {
|
||||
logger.debug("Event: CallSnap, DeviceType {}, RemoteID {}, RemoteIP {}, CallStatus {}",
|
||||
eventData.get("DeviceType").getAsString(), eventData.get("RemoteID").getAsString(),
|
||||
eventData.get("RemoteIP").getAsString(),
|
||||
eventData.getAsJsonArray("ChannelStates").get(0).getAsString());
|
||||
}
|
||||
|
||||
protected void handleAccessControl(JsonObject eventData) {
|
||||
logger.debug("Event: AccessControl, Name {}, Method {}, ReaderID {}, UserID {}",
|
||||
eventData.get("Name").getAsString(), eventData.get("Method").getAsString(),
|
||||
eventData.get("ReaderID").getAsString(), eventData.get("UserID").getAsString());
|
||||
}
|
||||
|
||||
protected void handleSIPRegisterResult(JsonObject eventList, JsonObject eventData) {
|
||||
if ("Pulse".equals(eventList.get("Action").getAsString())) {
|
||||
if (eventData.get("Success").getAsBoolean()) {
|
||||
logger.debug("Event SIPRegisterResult, Success");
|
||||
} else {
|
||||
logger.debug("Event SIPRegisterResult, Failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleDoorCard(JsonObject eventList, JsonObject eventData) {
|
||||
if ("Pulse".equals(eventList.get("Action").getAsString())) {
|
||||
logger.debug("DoorCard {} was used at door", eventData.get("Number").getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleFingerPrintCheck(JsonObject eventData) {
|
||||
if (eventData.get("FingerPrintID").getAsInt() > -1) {
|
||||
int finger = eventData.get("FingerPrintID").getAsInt();
|
||||
logger.debug("Event FingerPrintCheck success, Finger number {}, User {}", finger, "User" + finger);
|
||||
} else {
|
||||
logger.debug("Event FingerPrintCheck failed, unknown Finger");
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleVideoBlind(JsonObject eventList) {
|
||||
if ("Start".equals(eventList.get("Action").getAsString())) {
|
||||
logger.debug("Event VideoBlind started");
|
||||
} else if ("Stop".equals(eventList.get("Action").getAsString())) {
|
||||
logger.debug("Event VideoBlind stopped");
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleKeepLightOn(JsonObject eventData) {
|
||||
if ("On".equals(eventData.get("Status").getAsString())) {
|
||||
logger.debug("Event KeepLightOn");
|
||||
} else if ("Off".equals(eventData.get("Status").getAsString())) {
|
||||
logger.debug("Event KeepLightOff");
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleNTPAdjust(JsonObject eventData) {
|
||||
if (eventData.get("result").getAsBoolean()) {
|
||||
logger.debug("Event NTPAdjustTime with {} success", eventData.get("Address").getAsString());
|
||||
} else {
|
||||
logger.debug("Event NTPAdjustTime failed");
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleTimeChange(JsonObject eventData) {
|
||||
logger.debug("Event TimeChange, BeforeModifyTime: {}, ModifiedTime: {}",
|
||||
eventData.get("BeforeModifyTime").getAsString(), eventData.get("ModifiedTime").getAsString());
|
||||
}
|
||||
|
||||
protected void handleBackKeyLight(JsonObject eventData) {
|
||||
logger.debug("Event BackKeyLight with State {} ", eventData.get("State").getAsString());
|
||||
}
|
||||
|
||||
protected void handleRTSPDisconnect(JsonObject eventList, JsonObject eventData) {
|
||||
if ("Start".equals(eventList.get("Action").getAsString())) {
|
||||
logger.debug("Event Rtsp-Session from {} disconnected",
|
||||
eventData.get("Device").getAsString().replace("::ffff:", ""));
|
||||
} else if ("Stop".equals(eventList.get("Action").getAsString())) {
|
||||
logger.debug("Event Rtsp-Session from {} connected",
|
||||
eventData.get("Device").getAsString().replace("::ffff:", ""));
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleMotionEvent() {
|
||||
logger.debug("Event VideoMotion");
|
||||
}
|
||||
|
||||
protected void handleVTHAnswer() {
|
||||
logger.debug("Event VTH answered call from VTO");
|
||||
}
|
||||
|
||||
protected void handleVTOCall() {
|
||||
logger.debug("Event Call from VTO - subclass should override this method");
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method to handle button press events with lock number identification.
|
||||
* Subclasses must implement this to handle single or multi-button devices.
|
||||
*
|
||||
* @param lockNumber The lock number from the Invite event (1 or 2)
|
||||
*/
|
||||
protected abstract void onButtonPressed(int lockNumber);
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 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.dahuadoor.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link DahuaDoorBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Sven Schad - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DahuaDoorBindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "dahuadoor";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_VTO2202 = new ThingTypeUID(BINDING_ID, "vto2202");
|
||||
public static final ThingTypeUID THING_TYPE_VTO3211 = new ThingTypeUID(BINDING_ID, "vto3211");
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_BELL_BUTTON = "bell-button";
|
||||
public static final String CHANNEL_BELL_BUTTON_1 = "bell-button-1";
|
||||
public static final String CHANNEL_BELL_BUTTON_2 = "bell-button-2";
|
||||
public static final String CHANNEL_DOOR_IMAGE = "door-image";
|
||||
public static final String CHANNEL_DOOR_IMAGE_1 = "door-image-1";
|
||||
public static final String CHANNEL_DOOR_IMAGE_2 = "door-image-2";
|
||||
public static final String CHANNEL_OPEN_DOOR_1 = "open-door-1";
|
||||
public static final String CHANNEL_OPEN_DOOR_2 = "open-door-2";
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 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.dahuadoor.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link DahuaDoorConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Sven Schad - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DahuaDoorConfiguration {
|
||||
|
||||
/**
|
||||
* Configuration parameters for the Dahua door device, including hostname, authentication
|
||||
* credentials, and snapshot path.
|
||||
*/
|
||||
public String hostname = "";
|
||||
public String username = "";
|
||||
public String password = "";
|
||||
public String snapshotPath = "";
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 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.dahuadoor.internal;
|
||||
|
||||
import static org.openhab.binding.dahuadoor.internal.DahuaDoorBindingConstants.*;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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.Component;
|
||||
|
||||
/**
|
||||
* The {@link DahuaDoorHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Sven Schad - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.dahuadoor", service = ThingHandlerFactory.class)
|
||||
public class DahuaDoorHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_VTO2202, THING_TYPE_VTO3211);
|
||||
|
||||
@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_VTO2202.equals(thingTypeUID)) {
|
||||
return new DahuaVto2202Handler(thing);
|
||||
} else if (THING_TYPE_VTO3211.equals(thingTypeUID)) {
|
||||
return new DahuaVto3211Handler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 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.dahuadoor.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Enum representing all known Dahua VTO event codes.
|
||||
*
|
||||
* @author Sven Schad - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum DahuaEventCode {
|
||||
ACCESS_CONTROL("AccessControl"),
|
||||
ACCESS_SNAP("AccessSnap"),
|
||||
ADD_CARD("AddCard"),
|
||||
ALARM_LOCAL("AlarmLocal"),
|
||||
BACK_KEY_LIGHT("BackKeyLight"),
|
||||
CALL_NO_ANSWERED("CallNoAnswered"),
|
||||
CALL_SNAP("CallSnap"),
|
||||
DGS_ERROR_REPORT("DGSErrorReport"),
|
||||
DOOR_CARD("DoorCard"),
|
||||
DOOR_CONTROL("DoorControl"),
|
||||
DOOR_NOT_CLOSED("DoorNotClosed"),
|
||||
DOOR_STATUS("DoorStatus"),
|
||||
FINGER_PRINT_CHECK("FingerPrintCheck"),
|
||||
HANGUP("Hangup"),
|
||||
HANGUP_PHONE("HangupPhone"),
|
||||
HUNGUP_PHONE("HungupPhone"),
|
||||
IGNORE_INVITE("IgnoreInvite"),
|
||||
INVITE("Invite"),
|
||||
KEEP_LIGHT_ON("KeepLightOn"),
|
||||
NETWORK_CHANGE("NetworkChange"),
|
||||
NEW_FILE("NewFile"),
|
||||
NTP_ADJUST_TIME("NTPAdjustTime"),
|
||||
PASSIVE_HANGUP("PassiveHangup"),
|
||||
PROFILE_ALARM_TRANSMIT("ProfileAlarmTransmit"),
|
||||
REBOOT("Reboot"),
|
||||
REQUEST_CALL_STATE("RequestCallState"),
|
||||
RTSP_SESSION_DISCONNECT("RtspSessionDisconnect"),
|
||||
SECURITY_IM_EXPORT("SecurityImExport"),
|
||||
SEND_CARD("SendCard"),
|
||||
SIP_REGISTER_RESULT("SIPRegisterResult"),
|
||||
TIME_CHANGE("TimeChange"),
|
||||
UPDATE_FILE("UpdateFile"),
|
||||
UPGRADE("Upgrade"),
|
||||
VIDEO_BLIND("VideoBlind"),
|
||||
VIDEO_MOTION("VideoMotion"),
|
||||
UNKNOWN("");
|
||||
|
||||
private final String code;
|
||||
|
||||
DahuaEventCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to the corresponding enum value.
|
||||
*
|
||||
* @param code the event code string
|
||||
* @return the matching enum value, or UNKNOWN if not found
|
||||
*/
|
||||
public static DahuaEventCode fromString(@Nullable String code) {
|
||||
if (code == null || code.isEmpty()) {
|
||||
return UNKNOWN;
|
||||
}
|
||||
for (DahuaEventCode eventCode : values()) {
|
||||
if (eventCode.code.equals(code)) {
|
||||
return eventCode;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 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.dahuadoor.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.dahuadoor.internal.dahuaeventhandler.DahuaEventClient;
|
||||
import org.openhab.core.library.types.RawType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.Thing;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* The {@link DahuaVto2202Handler} handles VTO2202 devices (single button)
|
||||
*
|
||||
* @author Sven Schad - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DahuaVto2202Handler extends DahuaDoorBaseHandler {
|
||||
|
||||
public DahuaVto2202Handler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleInvite(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event Invite from VTO2202 (single button)");
|
||||
// VTO2202 has only one button, always use lockNumber 1
|
||||
onButtonPressed(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleVTOCall() {
|
||||
logger.debug("Event Call from VTO2202 (single button)");
|
||||
// VTO2202 has only one button, always use lockNumber 1
|
||||
onButtonPressed(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onButtonPressed(int lockNumber) {
|
||||
logger.debug("Button pressed on VTO2202 (lockNumber ignored, single button)");
|
||||
|
||||
// Trigger bell button channel synchronously (fast, no blocking)
|
||||
Channel channel = this.getThing().getChannel(DahuaDoorBindingConstants.CHANNEL_BELL_BUTTON);
|
||||
if (channel == null) {
|
||||
logger.warn("Bell button channel not found");
|
||||
return;
|
||||
}
|
||||
triggerChannel(channel.getUID(), "PRESSED");
|
||||
|
||||
// Retrieve image asynchronously to avoid blocking the keepAlive event loop
|
||||
DahuaEventClient localClient = client;
|
||||
if (localClient == null) {
|
||||
logger.warn("Client not initialized, cannot retrieve doorbell image");
|
||||
return;
|
||||
}
|
||||
|
||||
scheduler.submit(() -> {
|
||||
byte[] buffer = localClient.requestImage();
|
||||
if (buffer != null && buffer.length > 0) {
|
||||
// Update image channel
|
||||
RawType image = new RawType(buffer, "image/jpeg");
|
||||
updateState(DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE, image);
|
||||
|
||||
// Save snapshot
|
||||
saveSnapshot(buffer);
|
||||
} else {
|
||||
logger.warn("Received empty or null image buffer from VTO2202");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 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.dahuadoor.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.dahuadoor.internal.dahuaeventhandler.DahuaEventClient;
|
||||
import org.openhab.core.library.types.RawType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.Thing;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* The {@link DahuaVto3211Handler} handles VTO3211 devices (dual button with LockNum detection)
|
||||
*
|
||||
* @author Sven Schad - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DahuaVto3211Handler extends DahuaDoorBaseHandler {
|
||||
|
||||
public DahuaVto3211Handler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleInvite(JsonObject eventList, JsonObject eventData) {
|
||||
logger.debug("Event Invite from VTO3211 (dual button)");
|
||||
|
||||
// Extract LockNum from event data to determine which button was pressed
|
||||
int lockNumber = 1; // default to button 1
|
||||
JsonElement lockNumElement = eventData.get("LockNum");
|
||||
if (lockNumElement != null && !lockNumElement.isJsonNull()) {
|
||||
lockNumber = lockNumElement.getAsInt();
|
||||
logger.debug("Extracted LockNum: {}", lockNumber);
|
||||
} else {
|
||||
logger.warn("LockNum not found in Invite event, defaulting to button 1");
|
||||
}
|
||||
|
||||
onButtonPressed(lockNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleVTOCall() {
|
||||
logger.debug("Event Call from VTO3211 (dual button) - no LockNum available, defaulting to button 1");
|
||||
// VTOCall events don't contain LockNum, default to button 1
|
||||
onButtonPressed(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onButtonPressed(int lockNumber) {
|
||||
logger.debug("Button {} pressed on VTO3211", lockNumber);
|
||||
|
||||
// Determine channel IDs based on lock number
|
||||
String bellButtonChannelId;
|
||||
String doorImageChannelId;
|
||||
|
||||
if (lockNumber == 2) {
|
||||
bellButtonChannelId = DahuaDoorBindingConstants.CHANNEL_BELL_BUTTON_2;
|
||||
doorImageChannelId = DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE_2;
|
||||
} else {
|
||||
// Default to button 1 for lockNumber 1 or any other value
|
||||
bellButtonChannelId = DahuaDoorBindingConstants.CHANNEL_BELL_BUTTON_1;
|
||||
doorImageChannelId = DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE_1;
|
||||
}
|
||||
|
||||
// Trigger bell button channel
|
||||
Channel bellChannel = this.getThing().getChannel(bellButtonChannelId);
|
||||
if (bellChannel == null) {
|
||||
logger.warn("Bell button channel '{}' not found", bellButtonChannelId);
|
||||
return;
|
||||
}
|
||||
triggerChannel(bellChannel.getUID(), "PRESSED");
|
||||
|
||||
// Retrieve image asynchronously to avoid blocking the keepAlive event loop
|
||||
DahuaEventClient localClient = client;
|
||||
if (localClient == null) {
|
||||
logger.warn("Client not initialized, cannot retrieve doorbell image");
|
||||
return;
|
||||
}
|
||||
|
||||
final String doorImageChannelIdFinal = doorImageChannelId;
|
||||
scheduler.submit(() -> {
|
||||
byte[] buffer = localClient.requestImage();
|
||||
if (buffer != null && buffer.length > 0) {
|
||||
// Update image channel for the specific button
|
||||
RawType image = new RawType(buffer, "image/jpeg");
|
||||
updateState(doorImageChannelIdFinal, image);
|
||||
|
||||
// Save snapshot image
|
||||
saveSnapshot(buffer);
|
||||
} else {
|
||||
logger.warn("Received empty or null image buffer from VTO3211 button {}", lockNumber);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 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.dahuadoor.internal.dahuaeventhandler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* The {@link DHIPEventListener} interface defines the callback for DHIP protocol events.
|
||||
*
|
||||
* @author Sven Schad - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface DHIPEventListener {
|
||||
|
||||
/**
|
||||
* Handles incoming DHIP events from the Dahua device.
|
||||
*
|
||||
* @param eventData JSON object containing the event data from the device
|
||||
*/
|
||||
void onEvent(JsonObject eventData);
|
||||
}
|
||||
+835
@@ -0,0 +1,835 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2026 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.dahuadoor.internal.dahuaeventhandler;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.common.ThreadPoolManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* The {@link DahuaEventClient} client polls the Dahua device
|
||||
*
|
||||
*
|
||||
* @author Sven Schad - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DahuaEventClient implements Runnable {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DahuaEventClient.class);
|
||||
|
||||
private final String host;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private int id = 0; // Our Request / Response ID that must be in all requests and initiated by us
|
||||
private int sessionId = 0; // Session ID will be returned after successful login
|
||||
private String fakeIpAddr = "(null)"; // WebGUI: mask our real IP
|
||||
private String clientType = ""; // WebGUI: We do not show up in logs or online users
|
||||
private int keepAliveInterval = 60;
|
||||
private long lastKeepAlive = 0;
|
||||
|
||||
private DHIPEventListener eventListener;
|
||||
|
||||
private @Nullable Socket sock;
|
||||
private final Gson gson = new Gson();
|
||||
private volatile boolean execThread = true;
|
||||
private Consumer<String> errorInformer;
|
||||
private ByteBuffer residualBuffer = ByteBuffer.allocate(0); // Buffer for incomplete frames across reads
|
||||
|
||||
private static final int HTTP_TIMEOUT_SECONDS = 10;
|
||||
private static final String SNAPSHOT_PATH = "/cgi-bin/snapshot.cgi";
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
|
||||
public DahuaEventClient(String host, String username, String password, DHIPEventListener eventListener,
|
||||
Consumer<String> errorInformer) {
|
||||
this.host = host;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.eventListener = eventListener;
|
||||
this.errorInformer = errorInformer;
|
||||
this.execThread = true;
|
||||
ThreadPoolManager.getPool("binding.dahuadoor").submit(this);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
this.execThread = false;
|
||||
// Close socket to unblock any pending read() operations
|
||||
final Socket localSock = sock;
|
||||
if (localSock != null) {
|
||||
try {
|
||||
localSock.close();
|
||||
} catch (IOException e) {
|
||||
logger.trace("Error closing socket during dispose: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a JPEG snapshot image from the device via HTTP.
|
||||
*
|
||||
* @return JPEG image bytes, or null if the request failed
|
||||
*/
|
||||
public byte @Nullable [] requestImage() {
|
||||
try {
|
||||
SnapshotHttpResponse response = sendSnapshotRequest(null);
|
||||
if (response.statusCode == 200) {
|
||||
return response.body;
|
||||
}
|
||||
if (response.statusCode == 401) {
|
||||
String challenge = response.headers.get("www-authenticate");
|
||||
if (challenge != null) {
|
||||
String digestHeader = createDigestAuthorizationHeader(challenge, SNAPSHOT_PATH);
|
||||
if (digestHeader != null) {
|
||||
SnapshotHttpResponse authResponse = sendSnapshotRequest(digestHeader);
|
||||
if (authResponse.statusCode == 200) {
|
||||
return authResponse.body;
|
||||
}
|
||||
logger.debug("Authenticated snapshot request failed with HTTP status {} from {}",
|
||||
authResponse.statusCode, host);
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug("Snapshot request failed with HTTP status {} from {}", response.statusCode, host);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Could not retrieve snapshot from {}", host, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the specified door via HTTP.
|
||||
*
|
||||
* @param doorNo door number (1 or 2)
|
||||
*/
|
||||
public void openDoor(int doorNo) {
|
||||
try {
|
||||
String path = "/cgi-bin/accessControl.cgi?action=openDoor&UserID=101&Type=Remote&channel=" + doorNo;
|
||||
OpenDoorHttpResponse response = sendOpenDoorRequest(path, null);
|
||||
if (response.statusCode == 200) {
|
||||
logger.debug("Open Door Success");
|
||||
} else if (response.statusCode == 401) {
|
||||
String challenge = response.wwwAuthenticate;
|
||||
if (challenge != null) {
|
||||
String digestHeader = createDigestAuthorizationHeader(challenge, path);
|
||||
if (digestHeader != null) {
|
||||
OpenDoorHttpResponse authResponse = sendOpenDoorRequest(path, digestHeader);
|
||||
if (authResponse.statusCode == 200) {
|
||||
logger.debug("Open Door Success (with authentication)");
|
||||
} else {
|
||||
logger.debug("Open door request failed with HTTP status {} for door {} on {}",
|
||||
authResponse.statusCode, doorNo, host);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug("Open door request failed with HTTP status {} for door {} on {}", response.statusCode,
|
||||
doorNo, host);
|
||||
}
|
||||
} else {
|
||||
logger.debug("Open door request failed with HTTP status {} for door {} on {}", response.statusCode,
|
||||
doorNo, host);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Could not open door {} on {}", doorNo, host, e);
|
||||
}
|
||||
}
|
||||
|
||||
private SnapshotHttpResponse sendSnapshotRequest(@Nullable String authorizationHeader) throws Exception {
|
||||
try (Socket socket = new Socket()) {
|
||||
socket.connect(new InetSocketAddress(host, 80), HTTP_TIMEOUT_SECONDS * 1000);
|
||||
socket.setSoTimeout(HTTP_TIMEOUT_SECONDS * 1000);
|
||||
StringBuilder request = new StringBuilder();
|
||||
request.append("GET ").append(SNAPSHOT_PATH).append(" HTTP/1.1\r\n");
|
||||
request.append("Host: ").append(host).append("\r\n");
|
||||
request.append("Connection: close\r\n");
|
||||
request.append("Accept: */*\r\n");
|
||||
if (authorizationHeader != null) {
|
||||
request.append("Authorization: ").append(authorizationHeader).append("\r\n");
|
||||
}
|
||||
request.append("\r\n");
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
outputStream.write(request.toString().getBytes(StandardCharsets.ISO_8859_1));
|
||||
outputStream.flush();
|
||||
byte[] rawResponse = readHttpResponse(socket.getInputStream());
|
||||
return parseSnapshotResponse(rawResponse);
|
||||
}
|
||||
}
|
||||
|
||||
private OpenDoorHttpResponse sendOpenDoorRequest(String path, @Nullable String authorizationHeader)
|
||||
throws Exception {
|
||||
try (Socket socket = new Socket()) {
|
||||
socket.connect(new InetSocketAddress(host, 80), HTTP_TIMEOUT_SECONDS * 1000);
|
||||
socket.setSoTimeout(HTTP_TIMEOUT_SECONDS * 1000);
|
||||
StringBuilder request = new StringBuilder();
|
||||
request.append("GET ").append(path).append(" HTTP/1.1\r\n");
|
||||
request.append("Host: ").append(host).append("\r\n");
|
||||
request.append("Connection: close\r\n");
|
||||
if (authorizationHeader != null) {
|
||||
request.append("Authorization: ").append(authorizationHeader).append("\r\n");
|
||||
}
|
||||
request.append("\r\n");
|
||||
OutputStream outputStream = socket.getOutputStream();
|
||||
outputStream.write(request.toString().getBytes(StandardCharsets.ISO_8859_1));
|
||||
outputStream.flush();
|
||||
byte[] rawResponse = readHttpResponse(socket.getInputStream());
|
||||
return parseOpenDoorResponse(rawResponse);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] readHttpResponse(InputStream inputStream) throws Exception {
|
||||
ByteArrayOutputStream headerStream = new ByteArrayOutputStream();
|
||||
int state = 0;
|
||||
while (true) {
|
||||
int next = inputStream.read();
|
||||
if (next == -1) {
|
||||
break;
|
||||
}
|
||||
headerStream.write(next);
|
||||
if (state == 0 && next == '\r') {
|
||||
state = 1;
|
||||
} else if (state == 1 && next == '\n') {
|
||||
state = 2;
|
||||
} else if (state == 2 && next == '\r') {
|
||||
state = 3;
|
||||
} else if (state == 3 && next == '\n') {
|
||||
break;
|
||||
} else {
|
||||
state = 0;
|
||||
}
|
||||
}
|
||||
byte[] headerBytes = headerStream.toByteArray();
|
||||
int contentLength = extractContentLength(headerBytes);
|
||||
ByteArrayOutputStream responseStream = new ByteArrayOutputStream();
|
||||
responseStream.write(headerBytes);
|
||||
byte[] buffer = new byte[4096];
|
||||
if (contentLength >= 0) {
|
||||
int remaining = contentLength;
|
||||
while (remaining > 0) {
|
||||
int read = inputStream.read(buffer, 0, Math.min(buffer.length, remaining));
|
||||
if (read == -1) {
|
||||
break;
|
||||
}
|
||||
responseStream.write(buffer, 0, read);
|
||||
remaining -= read;
|
||||
}
|
||||
} else {
|
||||
while (true) {
|
||||
try {
|
||||
int read = inputStream.read(buffer);
|
||||
if (read == -1) {
|
||||
break;
|
||||
}
|
||||
responseStream.write(buffer, 0, read);
|
||||
} catch (SocketTimeoutException e) {
|
||||
logger.debug("Snapshot response read timed out without Content-Length; using partial body");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return responseStream.toByteArray();
|
||||
}
|
||||
|
||||
private int extractContentLength(byte[] headerBytes) {
|
||||
String headerText = new String(headerBytes, StandardCharsets.ISO_8859_1);
|
||||
for (String line : headerText.split("\\r\\n")) {
|
||||
if (line.regionMatches(true, 0, "Content-Length:", 0, "Content-Length:".length())) {
|
||||
try {
|
||||
return Integer.parseInt(line.substring("Content-Length:".length()).trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private SnapshotHttpResponse parseSnapshotResponse(byte[] rawResponse) {
|
||||
int headerEnd = -1;
|
||||
for (int i = 0; i < rawResponse.length - 3; i++) {
|
||||
if (rawResponse[i] == '\r' && rawResponse[i + 1] == '\n' && rawResponse[i + 2] == '\r'
|
||||
&& rawResponse[i + 3] == '\n') {
|
||||
headerEnd = i + 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (headerEnd < 0) {
|
||||
return new SnapshotHttpResponse(0, Map.of(), new byte[0]);
|
||||
}
|
||||
String headerText = new String(rawResponse, 0, headerEnd, StandardCharsets.ISO_8859_1);
|
||||
String[] lines = headerText.split("\\r\\n");
|
||||
int statusCode = 0;
|
||||
if (lines.length > 0) {
|
||||
String[] statusParts = lines[0].split(" ");
|
||||
if (statusParts.length > 1) {
|
||||
try {
|
||||
statusCode = Integer.parseInt(statusParts[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
statusCode = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
for (int i = 1; i < lines.length; i++) {
|
||||
int colon = lines[i].indexOf(':');
|
||||
if (colon > 0) {
|
||||
headers.put(lines[i].substring(0, colon).trim().toLowerCase(Locale.ROOT),
|
||||
lines[i].substring(colon + 1).trim());
|
||||
}
|
||||
}
|
||||
byte[] body = Arrays.copyOfRange(rawResponse, headerEnd, rawResponse.length);
|
||||
return new SnapshotHttpResponse(statusCode, headers, body);
|
||||
}
|
||||
|
||||
private OpenDoorHttpResponse parseOpenDoorResponse(byte[] rawResponse) {
|
||||
String headerText = new String(rawResponse, StandardCharsets.ISO_8859_1);
|
||||
String[] lines = headerText.split("\\r\\n");
|
||||
int statusCode = 0;
|
||||
if (lines.length > 0) {
|
||||
String[] statusParts = lines[0].split(" ");
|
||||
if (statusParts.length > 1) {
|
||||
try {
|
||||
statusCode = Integer.parseInt(statusParts[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
statusCode = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
String wwwAuthenticate = null;
|
||||
for (int i = 1; i < lines.length; i++) {
|
||||
if (lines[i].regionMatches(true, 0, "WWW-Authenticate:", 0, "WWW-Authenticate:".length())) {
|
||||
wwwAuthenticate = lines[i].substring("WWW-Authenticate:".length()).trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new OpenDoorHttpResponse(statusCode, wwwAuthenticate);
|
||||
}
|
||||
|
||||
private @Nullable String createDigestAuthorizationHeader(String challenge, String requestPath) throws Exception {
|
||||
if (!challenge.toLowerCase(Locale.ROOT).startsWith("digest")) {
|
||||
return null;
|
||||
}
|
||||
Map<String, String> digestParams = parseDigestChallenge(challenge.substring(6).trim());
|
||||
String realm = digestParams.get("realm");
|
||||
String nonce = digestParams.get("nonce");
|
||||
if (realm == null || nonce == null) {
|
||||
return null;
|
||||
}
|
||||
String qop = digestParams.get("qop");
|
||||
if (qop != null && qop.contains(",")) {
|
||||
qop = qop.split(",")[0].trim();
|
||||
}
|
||||
String ha1 = md5Hex(username + ":" + realm + ":" + password);
|
||||
String ha2 = md5Hex("GET:" + requestPath);
|
||||
StringBuilder auth = new StringBuilder("Digest ");
|
||||
auth.append("username=\"").append(escapeDigestValue(username)).append("\"");
|
||||
auth.append(", realm=\"").append(escapeDigestValue(realm)).append("\"");
|
||||
auth.append(", nonce=\"").append(escapeDigestValue(nonce)).append("\"");
|
||||
auth.append(", uri=\"").append(requestPath).append("\"");
|
||||
String response;
|
||||
if (qop != null && !qop.isBlank()) {
|
||||
String nonceCount = "00000001";
|
||||
String cnonce = randomHex(16);
|
||||
response = md5Hex(ha1 + ":" + nonce + ":" + nonceCount + ":" + cnonce + ":" + qop + ":" + ha2);
|
||||
auth.append(", qop=").append(qop);
|
||||
auth.append(", nc=").append(nonceCount);
|
||||
auth.append(", cnonce=\"").append(cnonce).append("\"");
|
||||
} else {
|
||||
response = md5Hex(ha1 + ":" + nonce + ":" + ha2);
|
||||
}
|
||||
String opaque = digestParams.get("opaque");
|
||||
if (opaque != null) {
|
||||
auth.append(", opaque=\"").append(escapeDigestValue(opaque)).append("\"");
|
||||
}
|
||||
String algorithm = digestParams.get("algorithm");
|
||||
if (algorithm != null) {
|
||||
auth.append(", algorithm=").append(algorithm);
|
||||
}
|
||||
auth.append(", response=\"").append(response).append("\"");
|
||||
return auth.toString();
|
||||
}
|
||||
|
||||
private Map<String, String> parseDigestChallenge(String challenge) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
int index = 0;
|
||||
while (index < challenge.length()) {
|
||||
while (index < challenge.length()
|
||||
&& (challenge.charAt(index) == ',' || Character.isWhitespace(challenge.charAt(index)))) {
|
||||
index++;
|
||||
}
|
||||
int equals = challenge.indexOf('=', index);
|
||||
if (equals < 0) {
|
||||
break;
|
||||
}
|
||||
String key = challenge.substring(index, equals).trim().toLowerCase(Locale.ROOT);
|
||||
index = equals + 1;
|
||||
String value;
|
||||
if (index < challenge.length() && challenge.charAt(index) == '"') {
|
||||
index++;
|
||||
int end = challenge.indexOf('"', index);
|
||||
if (end < 0) {
|
||||
break;
|
||||
}
|
||||
value = challenge.substring(index, end);
|
||||
index = end + 1;
|
||||
} else {
|
||||
int comma = challenge.indexOf(',', index);
|
||||
if (comma < 0) {
|
||||
value = challenge.substring(index).trim();
|
||||
index = challenge.length();
|
||||
} else {
|
||||
value = challenge.substring(index, comma).trim();
|
||||
index = comma + 1;
|
||||
}
|
||||
}
|
||||
result.put(key, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String md5Hex(String value) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
byte[] hash = digest.digest(value.getBytes(StandardCharsets.ISO_8859_1));
|
||||
StringBuilder builder = new StringBuilder(hash.length * 2);
|
||||
for (byte b : hash) {
|
||||
builder.append(String.format("%02x", b));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String randomHex(int length) {
|
||||
byte[] bytes = new byte[length / 2];
|
||||
SECURE_RANDOM.nextBytes(bytes);
|
||||
StringBuilder builder = new StringBuilder(length);
|
||||
for (byte b : bytes) {
|
||||
builder.append(String.format("%02x", b));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String escapeDigestValue(String value) {
|
||||
return value.replace("\\\\", "\\\\\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
private record SnapshotHttpResponse(int statusCode, Map<String, String> headers, byte[] body) {
|
||||
}
|
||||
|
||||
private record OpenDoorHttpResponse(int statusCode, @Nullable String wwwAuthenticate) {
|
||||
}
|
||||
|
||||
public String generateMd5Hash(String dahuaRandom, String dahuaRealm, String username, String password)
|
||||
throws Exception {
|
||||
final String passwordDbHash = md5(String.join(":", username, dahuaRealm, password)).toUpperCase(Locale.ROOT);
|
||||
final String pass = String.join(":", username, dahuaRandom, passwordDbHash);
|
||||
return md5(pass).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5 hash function for Dahua DHIP protocol authentication.
|
||||
* Note: MD5 is cryptographically weak, but is required by the Dahua device API
|
||||
* for digest authentication. This is a protocol limitation, not a design choice.
|
||||
*/
|
||||
private String md5(String input) throws Exception {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
|
||||
byte[] array = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : array) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public void keepAlive(int delay) {
|
||||
final Socket localSock = sock;
|
||||
if (localSock == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.trace("Started keepAlive thread");
|
||||
while (execThread) {
|
||||
Map<String, Object> queryArgs = new HashMap<>();
|
||||
queryArgs.put("method", "global.keepAlive");
|
||||
queryArgs.put("magic", "0x1234");
|
||||
queryArgs.put("params", Map.of("timeout", delay, "active", true));
|
||||
queryArgs.put("id", this.id);
|
||||
queryArgs.put("session", this.sessionId);
|
||||
|
||||
try {
|
||||
send(new Gson().toJson(queryArgs));
|
||||
} catch (IOException e) {
|
||||
logger.trace("Error sending keepAlive", e);
|
||||
return;
|
||||
}
|
||||
lastKeepAlive = System.currentTimeMillis();
|
||||
boolean keepAliveReceived = false;
|
||||
|
||||
while (lastKeepAlive + delay * 1000 > System.currentTimeMillis()) {
|
||||
ArrayList<String> data;
|
||||
try {
|
||||
data = receive();
|
||||
for (String packet : data) {
|
||||
JsonObject jsonPacket = gson.fromJson(packet, JsonObject.class);
|
||||
if (jsonPacket != null) {
|
||||
if (jsonPacket.has("result")) {
|
||||
logger.trace("keepAlive back");
|
||||
keepAliveReceived = true;
|
||||
} else if ("client.notifyEventStream".equals(jsonPacket.get("method").getAsString())) {
|
||||
eventListener.onEvent(jsonPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketTimeoutException e) {
|
||||
// Read timeout: no data yet, keep waiting until the keep-alive deadline.
|
||||
logger.trace("keepAlive receive timeout, continuing to wait");
|
||||
} catch (IOException e) {
|
||||
logger.trace("Error receiving keepAlive response", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!keepAliveReceived) {
|
||||
logger.trace("keepAlive failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void send(String packet) throws IOException {
|
||||
byte[] payloadBytes = packet.getBytes(StandardCharsets.UTF_8);
|
||||
ByteBuffer buffer = ByteBuffer.allocate(32 + payloadBytes.length);
|
||||
buffer.order(ByteOrder.BIG_ENDIAN);
|
||||
buffer.putInt(0x20000000);
|
||||
buffer.putInt(0x44484950);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.putInt(sessionId);
|
||||
buffer.putInt(id);
|
||||
buffer.putInt(payloadBytes.length);
|
||||
buffer.putInt(0);
|
||||
buffer.putInt(payloadBytes.length);
|
||||
buffer.putInt(0);
|
||||
|
||||
if (buffer.position() != 32) {
|
||||
logger.trace("Binary header != 32");
|
||||
return;
|
||||
}
|
||||
|
||||
id += 1;
|
||||
|
||||
final Socket localSock = sock;
|
||||
if (localSock == null) {
|
||||
throw new IOException("Socket is not connected");
|
||||
}
|
||||
|
||||
buffer.put(payloadBytes);
|
||||
localSock.getOutputStream().write(buffer.array());
|
||||
}
|
||||
|
||||
public ArrayList<String> receive() throws IOException {
|
||||
ArrayList<String> p2pReturnData = new ArrayList<String>();
|
||||
byte[] buffer = new byte[8192];
|
||||
byte[] header = new byte[32];
|
||||
ByteBuffer bbuffer;
|
||||
int lenRecved = 1;
|
||||
int timeout = 5;
|
||||
|
||||
final Socket localSock = sock;
|
||||
if (localSock == null) {
|
||||
logger.debug("Socket is not connected");
|
||||
throw new IOException("Socket is not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
localSock.setSoTimeout(timeout * 1000); // Set timeout in milliseconds
|
||||
InputStream input = localSock.getInputStream();
|
||||
int bytesRead = input.read(buffer);
|
||||
if (bytesRead < 0) {
|
||||
// End of stream - connection closed
|
||||
throw new IOException("Connection closed by remote host");
|
||||
}
|
||||
|
||||
// Combine residual buffer with new data
|
||||
if (residualBuffer.hasRemaining()) {
|
||||
ByteBuffer combined = ByteBuffer.allocate(residualBuffer.remaining() + bytesRead);
|
||||
combined.put(residualBuffer);
|
||||
combined.put(buffer, 0, bytesRead);
|
||||
combined.flip();
|
||||
bbuffer = combined;
|
||||
residualBuffer = ByteBuffer.allocate(0); // Clear residual
|
||||
} else {
|
||||
bbuffer = ByteBuffer.wrap(buffer, 0, bytesRead);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.trace("IOException in receive(): {}", e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
|
||||
while (bbuffer.hasRemaining()) {
|
||||
// Ensure we have enough bytes for at least the magic value
|
||||
if (bbuffer.remaining() < Long.BYTES) {
|
||||
// Save remaining bytes for next read
|
||||
residualBuffer = ByteBuffer.allocate(bbuffer.remaining());
|
||||
residualBuffer.put(bbuffer);
|
||||
residualBuffer.flip();
|
||||
break;
|
||||
}
|
||||
|
||||
bbuffer.order(ByteOrder.BIG_ENDIAN);
|
||||
if (bbuffer.getLong(bbuffer.position()) == 0x2000000044484950L) {
|
||||
// Ensure we have a full header before reading it
|
||||
if (bbuffer.remaining() < 32) {
|
||||
// Save remaining bytes for next read
|
||||
residualBuffer = ByteBuffer.allocate(bbuffer.remaining());
|
||||
residualBuffer.put(bbuffer);
|
||||
residualBuffer.flip();
|
||||
break;
|
||||
}
|
||||
bbuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
// DHIP Protocol Header Structure (32 bytes):
|
||||
// Offset 0-7: Magic value (0x2000000044484950)
|
||||
// Offset 8-11: Session ID
|
||||
// Offset 12-15: Sequence number
|
||||
// Offset 16-19: Length of received data (lenRecved)
|
||||
// Offset 20-23: Reserved
|
||||
// Offset 24-27: Expected length for multi-part messages
|
||||
// Offset 28-31: Reserved
|
||||
int frameStart = bbuffer.position();
|
||||
lenRecved = bbuffer.getInt(frameStart + 16);
|
||||
bbuffer.get(header, 0, 32); // advances position by 32 to payload start
|
||||
|
||||
} else {
|
||||
if (lenRecved > 0) {
|
||||
// Ensure we have the full payload before reading it
|
||||
if (bbuffer.remaining() < lenRecved) {
|
||||
// Save from header start to preserve full frame for the next read.
|
||||
ByteBuffer residualSlice = bbuffer.duplicate();
|
||||
int headerStartPosition = Math.max(0, bbuffer.position() - 32);
|
||||
residualSlice.position(headerStartPosition);
|
||||
residualBuffer = ByteBuffer.allocate(residualSlice.remaining());
|
||||
residualBuffer.put(residualSlice);
|
||||
residualBuffer.flip();
|
||||
break;
|
||||
}
|
||||
String p2pData = new String(bbuffer.array(), bbuffer.arrayOffset() + bbuffer.position(), lenRecved,
|
||||
StandardCharsets.UTF_8);
|
||||
bbuffer.position(bbuffer.position() + lenRecved);
|
||||
p2pReturnData.add(p2pData);
|
||||
lenRecved = 0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return p2pReturnData;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public boolean login() {
|
||||
logger.trace("Start login");
|
||||
|
||||
Map<String, Object> queryArgs = new HashMap<>();
|
||||
queryArgs.put("id", this.id);
|
||||
queryArgs.put("magic", "0x1234");
|
||||
queryArgs.put("method", "global.login");
|
||||
queryArgs.put("params", Map.of("clientType", this.clientType, "ipAddr", this.fakeIpAddr, "loginType", "Direct",
|
||||
"password", "", "userName", this.username));
|
||||
queryArgs.put("session", 0);
|
||||
|
||||
try {
|
||||
send(new Gson().toJson(queryArgs));
|
||||
ArrayList<String> data = receive();
|
||||
if (data.isEmpty()) {
|
||||
logger.trace("global.login [random]");
|
||||
return false;
|
||||
}
|
||||
Map<String, Object> jsonData = new Gson().fromJson(data.get(0), Map.class);
|
||||
if (jsonData == null || !jsonData.containsKey("session") || !jsonData.containsKey("params")) {
|
||||
logger.trace("Invalid JSON response from login");
|
||||
return false;
|
||||
}
|
||||
Object sessionObj = jsonData.get("session");
|
||||
if (sessionObj instanceof Number) {
|
||||
this.sessionId = ((Number) sessionObj).intValue();
|
||||
} else {
|
||||
logger.trace("Invalid session type in response");
|
||||
return false;
|
||||
}
|
||||
Map<String, Object> params = (Map<String, Object>) jsonData.get("params");
|
||||
if (params == null) {
|
||||
logger.trace("Missing params in response");
|
||||
return false;
|
||||
}
|
||||
String random = (String) params.get("random");
|
||||
String realm = (String) params.get("realm");
|
||||
|
||||
if (random == null || realm == null) {
|
||||
logger.trace("Login failed: missing random or realm");
|
||||
return false;
|
||||
}
|
||||
|
||||
String hashedCredentials = generateMd5Hash(random, realm, this.username, this.password);
|
||||
|
||||
queryArgs = new HashMap<>();
|
||||
queryArgs.put("id", this.id);
|
||||
queryArgs.put("magic", "0x1234");
|
||||
queryArgs.put("method", "global.login");
|
||||
queryArgs.put("session", this.sessionId);
|
||||
queryArgs.put("params", Map.of("userName", this.username, "password", hashedCredentials, "clientType",
|
||||
this.clientType, "ipAddr", this.fakeIpAddr, "loginType", "Direct", "authorityType", "Default"));
|
||||
|
||||
send(new Gson().toJson(queryArgs));
|
||||
data = receive();
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
jsonData = new Gson().fromJson(data.get(0), Map.class);
|
||||
if (jsonData != null) {
|
||||
if (Boolean.TRUE.equals(jsonData.get("result"))) {
|
||||
logger.trace("Login success");
|
||||
Object paramsObj = jsonData.get("params");
|
||||
if (paramsObj instanceof Map) {
|
||||
Map<String, Object> paramsMap = (Map<String, Object>) paramsObj;
|
||||
Object intervalObj = paramsMap.get("keepAliveInterval");
|
||||
if (intervalObj instanceof Number) {
|
||||
this.keepAliveInterval = ((Number) intervalObj).intValue();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Object errorObj = jsonData.get("error");
|
||||
if (errorObj instanceof Map) {
|
||||
Map<?, ?> errorMap = (Map<?, ?>) errorObj;
|
||||
Object code = errorMap.get("code");
|
||||
Object message = errorMap.get("message");
|
||||
logger.trace("Login failed: {} {}", code, message);
|
||||
} else {
|
||||
logger.trace("Login failed: {}", jsonData);
|
||||
}
|
||||
} else {
|
||||
logger.trace("Login failed: empty or invalid JSON response");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.trace("Login error: {}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
boolean error = false;
|
||||
int loginTries = 0;
|
||||
while (execThread) {
|
||||
if (error) {
|
||||
try {
|
||||
for (int i = 0; i < 12 && execThread; i++) {
|
||||
Thread.sleep(5000);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
logger.debug("Thread interrupted during error wait", e);
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
error = true;
|
||||
try {
|
||||
Socket localSock = new Socket();
|
||||
localSock.connect(new InetSocketAddress(host, 5000), 5000); // 5s connect timeout
|
||||
sock = localSock;
|
||||
localSock.setSoTimeout(5000); // Set timeout to 5 seconds
|
||||
error = false;
|
||||
|
||||
if (!login()) {
|
||||
loginTries++;
|
||||
if (loginTries > 4) {
|
||||
errorInformer.accept("Login failed. Verify the host configuration and credentials.");
|
||||
execThread = false;
|
||||
}
|
||||
try {
|
||||
Socket sockToClose = sock;
|
||||
if (sockToClose != null) {
|
||||
sockToClose.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.trace("Error closing socket after login failure", e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, Object> queryArgs = new HashMap<>();
|
||||
queryArgs.put("id", this.id);
|
||||
queryArgs.put("magic", "0x1234");
|
||||
queryArgs.put("method", "eventManager.attach");
|
||||
queryArgs.put("params", Map.of("codes", new String[] { "All" }));
|
||||
queryArgs.put("session", this.sessionId);
|
||||
|
||||
send(new Gson().toJson(queryArgs));
|
||||
ArrayList<String> data = receive();
|
||||
JsonObject jsonData = data.isEmpty() ? null : gson.fromJson(data.get(0), JsonObject.class);
|
||||
if (jsonData == null || !jsonData.has("result")) {
|
||||
logger.trace("Failure eventManager.attach");
|
||||
try {
|
||||
Socket sockToClose = sock;
|
||||
if (sockToClose != null) {
|
||||
sockToClose.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.trace("Error closing socket after attach failure", e);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
for (String packet : data) {
|
||||
JsonObject jsonPacket = gson.fromJson(packet, JsonObject.class);
|
||||
if (jsonPacket != null && jsonPacket.has("method")) {
|
||||
String method = jsonPacket.get("method").getAsString();
|
||||
if ("client.notifyEventStream".equals(method)) {
|
||||
eventListener.onEvent(jsonPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
keepAlive(this.keepAliveInterval);
|
||||
logger.trace("Failure no keep alive received");
|
||||
} catch (Exception e) {
|
||||
logger.trace("Socket open failed", e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
Socket sockToClose = sock;
|
||||
if (sockToClose != null) {
|
||||
sockToClose.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.trace("Error while closing socket", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="dahuadoor" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>DahuaDoor Binding</name>
|
||||
<description>This is the binding for VTO2202 Dahua door controller.</description>
|
||||
<connection>local</connection>
|
||||
|
||||
</addon:addon>
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
# add-on
|
||||
|
||||
addon.dahuadoor.name = DahuaDoor Binding
|
||||
addon.dahuadoor.description = This is the binding for VTO2202 Dahua door controller.
|
||||
|
||||
# thing types
|
||||
|
||||
thing-type.dahuadoor.vto2202.label = Dahua VTO2202 Door Station
|
||||
thing-type.dahuadoor.vto2202.description = Dahua VTO2202 outdoor station with single button
|
||||
thing-type.dahuadoor.vto2202.channel.open-door-1.label = Open Door 1
|
||||
thing-type.dahuadoor.vto2202.channel.open-door-1.description = Controls door relay 1
|
||||
thing-type.dahuadoor.vto2202.channel.open-door-2.label = Open Door 2
|
||||
thing-type.dahuadoor.vto2202.channel.open-door-2.description = Controls door relay 2
|
||||
thing-type.dahuadoor.vto3211.label = Dahua VTO3211 Door Station
|
||||
thing-type.dahuadoor.vto3211.description = Dahua VTO3211 outdoor station with dual buttons
|
||||
thing-type.dahuadoor.vto3211.channel.bell-button-1.label = Door Button 1 Trigger
|
||||
thing-type.dahuadoor.vto3211.channel.bell-button-1.description = Trigger channel for button 1
|
||||
thing-type.dahuadoor.vto3211.channel.bell-button-2.label = Door Button 2 Trigger
|
||||
thing-type.dahuadoor.vto3211.channel.bell-button-2.description = Trigger channel for button 2
|
||||
thing-type.dahuadoor.vto3211.channel.door-image-1.label = Door Image 1
|
||||
thing-type.dahuadoor.vto3211.channel.door-image-1.description = Image of door when button 1 is pressed
|
||||
thing-type.dahuadoor.vto3211.channel.door-image-2.label = Door Image 2
|
||||
thing-type.dahuadoor.vto3211.channel.door-image-2.description = Image of door when button 2 is pressed
|
||||
thing-type.dahuadoor.vto3211.channel.open-door-1.label = Open Door 1
|
||||
thing-type.dahuadoor.vto3211.channel.open-door-1.description = Relay control to open door 1
|
||||
thing-type.dahuadoor.vto3211.channel.open-door-2.label = Open Door 2
|
||||
thing-type.dahuadoor.vto3211.channel.open-door-2.description = Relay control to open door 2
|
||||
|
||||
# thing types config
|
||||
|
||||
thing-type.config.dahuadoor.vto2202.hostname.label = Hostname
|
||||
thing-type.config.dahuadoor.vto2202.hostname.description = Hostname or IP address of the device
|
||||
thing-type.config.dahuadoor.vto2202.password.label = Password
|
||||
thing-type.config.dahuadoor.vto2202.password.description = Password to access the device
|
||||
thing-type.config.dahuadoor.vto2202.snapshotPath.label = Snapshot Path
|
||||
thing-type.config.dahuadoor.vto2202.snapshotPath.description = Path where snapshots will be saved (e.g., /var/lib/openhab/door-images)
|
||||
thing-type.config.dahuadoor.vto2202.username.label = Username
|
||||
thing-type.config.dahuadoor.vto2202.username.description = Username to access the device
|
||||
thing-type.config.dahuadoor.vto3211.hostname.label = Hostname
|
||||
thing-type.config.dahuadoor.vto3211.hostname.description = Hostname or IP address of the device
|
||||
thing-type.config.dahuadoor.vto3211.password.label = Password
|
||||
thing-type.config.dahuadoor.vto3211.password.description = Password to access the device
|
||||
thing-type.config.dahuadoor.vto3211.snapshotPath.label = Snapshot Path
|
||||
thing-type.config.dahuadoor.vto3211.snapshotPath.description = Path where snapshots will be saved (e.g., /var/lib/openhab/door-images)
|
||||
thing-type.config.dahuadoor.vto3211.username.label = Username
|
||||
thing-type.config.dahuadoor.vto3211.username.description = Username to access the device
|
||||
|
||||
# channel types
|
||||
|
||||
channel-type.dahuadoor.bell-button.label = Doorbell Button
|
||||
channel-type.dahuadoor.bell-button.description = Triggered when doorbell button is pressed.
|
||||
channel-type.dahuadoor.door-image.label = Door Image
|
||||
channel-type.dahuadoor.door-image.description = Snapshot image taken when doorbell is pressed.
|
||||
channel-type.dahuadoor.open-door.label = Open Door
|
||||
channel-type.dahuadoor.open-door.description = Controls the door lock relay.
|
||||
|
||||
# thing status descriptions
|
||||
|
||||
offline.conf-error-missing-credentials = Hostname, username and password must be configured.
|
||||
offline.conf-error-missing-snapshot-path = Snapshot path must be configured.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="dahuadoor"
|
||||
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">
|
||||
|
||||
<channel-type id="bell-button">
|
||||
<kind>trigger</kind>
|
||||
<label>Doorbell Button</label>
|
||||
<description>Triggered when doorbell button is pressed.</description>
|
||||
<tags>
|
||||
<tag>Alarm</tag>
|
||||
</tags>
|
||||
<event>
|
||||
<options>
|
||||
<option value="PRESSED">Pressed</option>
|
||||
</options>
|
||||
</event>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="door-image">
|
||||
<item-type>Image</item-type>
|
||||
<label>Door Image</label>
|
||||
<description>Snapshot image taken when doorbell is pressed.</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="open-door">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Open Door</label>
|
||||
<description>Controls the door lock relay.</description>
|
||||
<tags>
|
||||
<tag>Switch</tag>
|
||||
</tags>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="dahuadoor"
|
||||
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="vto2202">
|
||||
<label>Dahua VTO2202 Door Station</label>
|
||||
<description>Dahua VTO2202 outdoor station with single button</description>
|
||||
<semantic-equipment-tag>Doorbell</semantic-equipment-tag>
|
||||
|
||||
<channels>
|
||||
<channel id="bell-button" typeId="bell-button"/>
|
||||
<channel id="door-image" typeId="door-image"/>
|
||||
<channel id="open-door-1" typeId="open-door">
|
||||
<label>Open Door 1</label>
|
||||
<description>Controls door relay 1</description>
|
||||
</channel>
|
||||
<channel id="open-door-2" typeId="open-door">
|
||||
<label>Open Door 2</label>
|
||||
<description>Controls door relay 2</description>
|
||||
</channel>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="hostname" type="text" required="true">
|
||||
<context>network-address</context>
|
||||
<label>Hostname</label>
|
||||
<description>Hostname or IP address of the device</description>
|
||||
</parameter>
|
||||
<parameter name="username" type="text" required="true">
|
||||
<context>username</context>
|
||||
<label>Username</label>
|
||||
<description>Username to access the device</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="true">
|
||||
<context>password</context>
|
||||
<label>Password</label>
|
||||
<description>Password to access the device</description>
|
||||
</parameter>
|
||||
<parameter name="snapshotPath" type="text" required="true">
|
||||
<label>Snapshot Path</label>
|
||||
<description>Path where snapshots will be saved (e.g., /var/lib/openhab/door-images)</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="dahuadoor"
|
||||
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="vto3211">
|
||||
<label>Dahua VTO3211 Door Station</label>
|
||||
<description>Dahua VTO3211 outdoor station with dual buttons</description>
|
||||
<semantic-equipment-tag>Doorbell</semantic-equipment-tag>
|
||||
|
||||
<channels>
|
||||
<channel id="bell-button-1" typeId="bell-button">
|
||||
<label>Door Button 1 Trigger</label>
|
||||
<description>Trigger channel for button 1</description>
|
||||
</channel>
|
||||
<channel id="bell-button-2" typeId="bell-button">
|
||||
<label>Door Button 2 Trigger</label>
|
||||
<description>Trigger channel for button 2</description>
|
||||
</channel>
|
||||
<channel id="door-image-1" typeId="door-image">
|
||||
<label>Door Image 1</label>
|
||||
<description>Image of door when button 1 is pressed</description>
|
||||
</channel>
|
||||
<channel id="door-image-2" typeId="door-image">
|
||||
<label>Door Image 2</label>
|
||||
<description>Image of door when button 2 is pressed</description>
|
||||
</channel>
|
||||
<channel id="open-door-1" typeId="open-door">
|
||||
<label>Open Door 1</label>
|
||||
<description>Relay control to open door 1</description>
|
||||
</channel>
|
||||
<channel id="open-door-2" typeId="open-door">
|
||||
<label>Open Door 2</label>
|
||||
<description>Relay control to open door 2</description>
|
||||
</channel>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="hostname" type="text" required="true">
|
||||
<context>network-address</context>
|
||||
<label>Hostname</label>
|
||||
<description>Hostname or IP address of the device</description>
|
||||
</parameter>
|
||||
<parameter name="username" type="text" required="true">
|
||||
<context>username</context>
|
||||
<label>Username</label>
|
||||
<description>Username to access the device</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="true">
|
||||
<context>password</context>
|
||||
<label>Password</label>
|
||||
<description>Password to access the device</description>
|
||||
</parameter>
|
||||
<parameter name="snapshotPath" type="text" required="true">
|
||||
<label>Snapshot Path</label>
|
||||
<description>Path where snapshots will be saved (e.g., /var/lib/openhab/door-images)</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -111,6 +111,7 @@
|
||||
<module>org.openhab.binding.cm11a</module>
|
||||
<module>org.openhab.binding.comfoair</module>
|
||||
<module>org.openhab.binding.coolmasternet</module>
|
||||
<module>org.openhab.binding.dahuadoor</module>
|
||||
<module>org.openhab.binding.daikin</module>
|
||||
<module>org.openhab.binding.dali</module>
|
||||
<module>org.openhab.binding.danfossairunit</module>
|
||||
|
||||
Reference in New Issue
Block a user