Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion bundles/org.openhab.binding.dahuadoor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ VTO2202 is a single-button outdoor station; VTO3211 is a dual-button outdoor sta
| 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) |
| maxImages | integer | No | 20 | Maximum number of timestamped snapshots to keep (0 disables cleanup). For VTO3211 this applies per button. |
| useHttps | boolean | No | false | Use HTTPS (port 443) for snapshot and door-open requests. Enable if the device has HTTPS turned on in its network settings. When disabled, plain HTTP (port 80) is used. |
| enableWebRTC | boolean | No | false | Enables local go2rtc sidecar management and publishes a `webrtc-url` channel. |
| go2rtcPath | text | No | | Absolute path to the go2rtc binary (required when `enableWebRTC=true`). |
Expand All @@ -44,6 +45,12 @@ VTO2202 is a single-button outdoor station; VTO3211 is a dual-button outdoor sta
| localSipPort | integer | No | 5060 | Local UDP SIP listening port. |
| sipRealm | text | No | VDP | SIP authentication realm (default for Dahua VTO devices). |

**Snapshot persistence:**
The binding stores snapshots in `snapshotPath` and reloads the latest snapshot on startup to restore the `door-image` channels.
For VTO2202, the latest file is `Doorbell.jpg` and timestamped files are named `Doorbell_YYYY-MM-DD_HH-mm-ss.jpg`.
For VTO3211, the latest files are `Doorbell-1.jpg` and `Doorbell-2.jpg`, with timestamped files named `Doorbell-1_YYYY-MM-DD_HH-mm-ss.jpg` and `Doorbell-2_YYYY-MM-DD_HH-mm-ss.jpg`.
Timestamped files are capped by `maxImages` (0 disables cleanup).

**Note on SIP configuration:**
To enable SIP call signaling, set `enableSip=true` and configure at least one value in `sipExtension`.
If your VTO requires a dedicated SIP password, set `sipPassword`; otherwise the binding uses `password`.
Expand Down Expand Up @@ -104,6 +111,7 @@ Thing dahuadoor:vto2202:frontdoor "Front Door Station" @ "Entrance" [
username="admin",
password="password123",
snapshotPath="/var/lib/openhab/door-images",
maxImages=20,
useHttps=false
]
```
Expand Down Expand Up @@ -141,6 +149,7 @@ Thing dahuadoor:vto3211:entrance "Entrance Station" @ "Entrance" [
username="admin",
password="password123",
snapshotPath="/var/lib/openhab/door-images",
maxImages=20,
useHttps=false
]
```
Expand Down Expand Up @@ -185,7 +194,7 @@ end
Intercom operation is implemented with WebRTC via the `go2rtc` binary.
It converts the Dahua RTP audio/video stream into browser-compatible WebRTC. The audio stream is transcoded using `ffmpeg`. Hence both tools are needed.
When `enableWebRTC=true`, the binding starts everything automatically when a call is received.
The binding registers itself at the VTO. Define one or more terminals (for example `9901#2` or `9901#2,9901#3`, type `public`) and list those accounts in `sipExtension` as a comma-separated list. Use the matching `sipPassword` (single password for all accounts).
The binding registers itself at the VTO similar to a VTH device. Create one or more SIP terminals in the VTO menu (type `public`) for the number of parallel connections you expect (for example `9901#2`, `9901#3`, `9901#4`), then list those accounts in `sipExtension` as a comma-separated list. Use the matching `sipPassword` (single password for all accounts).
You can try Dahua's default password for initial testing, but do not use it in production. Consult your VTO manual for setup details.

### Tool installation
Expand Down Expand Up @@ -236,6 +245,7 @@ Thing dahuadoor:vto2202:frontdoor "Front Door Station" @ "Entrance" [
username="admin",
password="password123",
snapshotPath="/var/lib/openhab/door-images",
maxImages=20,
enableWebRTC=true,
go2rtcPath="/usr/local/bin/go2rtc",
go2rtcApiPort=1984,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.net.InetAddress;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -192,6 +194,8 @@ public void initialize() {
updateState(CHANNEL_SIP_CALL_STATE, new StringType(SipClient.SipCallState.IDLE.name()));
}

restoreLastSnapshots();

// Set status to UNKNOWN - will be set to ONLINE when first DHIP event is received
updateStatus(ThingStatus.UNKNOWN);
}
Expand Down Expand Up @@ -333,6 +337,14 @@ private void stopWebRtc() {
}

public void saveSnapshot(byte @Nullable [] buffer) {
saveSnapshot(buffer, null);
}

public void saveSnapshot(byte @Nullable [] buffer, int lockNumber) {
saveSnapshot(buffer, Integer.valueOf(lockNumber));
}

private void saveSnapshot(byte @Nullable [] buffer, @Nullable Integer lockNumber) {
final DahuaDoorConfiguration localConfig = config;
if (localConfig == null) {
logger.warn("Configuration not initialized");
Expand All @@ -354,8 +366,9 @@ public void saveSnapshot(byte @Nullable [] buffer) {
return;
}

String suffix = lockNumber == null ? "" : "-" + lockNumber;
String timestamp = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ROOT).format(new Date());
String filename = localConfig.snapshotPath + "/DoorBell_" + timestamp + ".jpg";
String filename = localConfig.snapshotPath + "/Doorbell" + suffix + "_" + timestamp + ".jpg";

try (FileOutputStream fos = new FileOutputStream(new File(filename))) {
fos.write(buffer);
Expand All @@ -364,22 +377,106 @@ public void saveSnapshot(byte @Nullable [] buffer) {
}

// Write buffer directly to latest snapshot file (avoids copy-from-source failures)
String latestSnapshotFilename = localConfig.snapshotPath + "/Doorbell.jpg";
String latestSnapshotFilename = localConfig.snapshotPath + "/Doorbell" + suffix + ".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);
}

cleanupOldSnapshots(lockNumber);
}

private void cleanupOldSnapshots(@Nullable Integer lockNumber) {
final DahuaDoorConfiguration localConfig = config;
if (localConfig == null || localConfig.snapshotPath.isEmpty() || localConfig.maxImages <= 0) {
return;
}

Path snapshotDir = Path.of(localConfig.snapshotPath);
if (!Files.isDirectory(snapshotDir)) {
return;
}

String suffix = lockNumber == null ? "" : "-" + lockNumber;
String prefix = "Doorbell" + suffix + "_";
List<Path> candidates = new ArrayList<>();

try (var stream = Files.list(snapshotDir)) {
stream.filter(path -> {
String name = path.getFileName().toString();
return name.startsWith(prefix) && name.endsWith(".jpg");
}).forEach(candidates::add);
} catch (IOException e) {
logger.warn("Could not list snapshot directory '{}', check permissions and path", localConfig.snapshotPath,
e);
return;
}

int maxImages = localConfig.maxImages;
if (candidates.size() <= maxImages) {
return;
}

candidates.sort((left, right) -> left.getFileName().toString().compareTo(right.getFileName().toString()));
int deleteCount = candidates.size() - maxImages;
for (int i = 0; i < deleteCount; i++) {
Path candidate = candidates.get(i);
try {
Files.deleteIfExists(candidate);
} catch (IOException e) {
logger.warn("Could not delete snapshot '{}', check permissions and path", candidate, e);
}
}
}

private void updateChannelImage(byte @Nullable [] buffer) {
updateImageChannel(CHANNEL_DOOR_IMAGE, buffer);
}

protected void updateImageChannel(String channelId, byte @Nullable [] buffer) {
if (buffer == null || buffer.length == 0) {
updateState(CHANNEL_DOOR_IMAGE, UnDefType.UNDEF);
updateState(channelId, UnDefType.UNDEF);
return;
}
RawType image = new RawType(buffer, "image/jpeg");
updateState(CHANNEL_DOOR_IMAGE, image);
updateState(channelId, image);
}

protected byte @Nullable [] readLatestSnapshot() {
return readLatestSnapshotInternal(null);
}

protected byte @Nullable [] readLatestSnapshot(int lockNumber) {
return readLatestSnapshotInternal(Integer.valueOf(lockNumber));
}

private byte @Nullable [] readLatestSnapshotInternal(@Nullable Integer lockNumber) {
final DahuaDoorConfiguration localConfig = config;
if (localConfig == null || localConfig.snapshotPath.isEmpty()) {
return null;
}

String suffix = lockNumber == null ? "" : "-" + lockNumber;
String latestSnapshotFilename = localConfig.snapshotPath + "/Doorbell" + suffix + ".jpg";
return readSnapshotFile(latestSnapshotFilename);
}

private byte @Nullable [] readSnapshotFile(String filename) {
Path path = Path.of(filename);
if (!Files.isRegularFile(path)) {
return null;
}
try {
return Files.readAllBytes(path);
} catch (IOException e) {
logger.warn("Could not read snapshot from '{}', check permissions and path", filename, e);
return null;
}
}

protected void restoreLastSnapshots() {
}

protected void handleButtonPressed() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class DahuaDoorConfiguration {
public String username = "";
public String password = "";
public String snapshotPath = "";
public int maxImages = 20;
public boolean useHttps = false;

// WebRTC / go2rtc settings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.dahuadoor.internal.dahuaeventhandler.DahuaEventClient;
import org.openhab.binding.dahuadoor.internal.media.PlayStreamServlet;
import org.openhab.core.library.types.RawType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;

Expand All @@ -40,6 +39,12 @@ protected void handleInvite(JsonObject eventList, JsonObject eventData) {
handleResolvedDoorbellEvent("DHIP", 1);
}

@Override
protected void restoreLastSnapshots() {
byte[] buffer = readLatestSnapshot();
updateImageChannel(DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE, buffer);
}

@Override
protected void onButtonPressed(int lockNumber) {
logger.debug("Button pressed on VTO2202 (lockNumber ignored, single button)");
Expand All @@ -63,8 +68,7 @@ protected void onButtonPressed(int lockNumber) {
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);
updateImageChannel(DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE, buffer);

// Save snapshot
saveSnapshot(buffer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.dahuadoor.internal.dahuaeventhandler.DahuaEventClient;
import org.openhab.binding.dahuadoor.internal.media.PlayStreamServlet;
import org.openhab.core.library.types.RawType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;

Expand Down Expand Up @@ -51,15 +50,25 @@ protected void handleInvite(JsonObject eventList, JsonObject eventData) {
handleResolvedDoorbellEvent("DHIP", lockNumber);
}

@Override
protected void restoreLastSnapshots() {
byte[] buffer1 = readLatestSnapshot(1);
updateImageChannel(DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE_1, buffer1);

byte[] buffer2 = readLatestSnapshot(2);
updateImageChannel(DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE_2, buffer2);
}

@Override
protected void onButtonPressed(int lockNumber) {
logger.debug("Button {} pressed on VTO3211", lockNumber);
int resolvedLockNumber = lockNumber == 2 ? 2 : 1;
logger.debug("Button {} pressed on VTO3211", resolvedLockNumber);

// Determine channel IDs based on lock number
String bellButtonChannelId;
String doorImageChannelId;

if (lockNumber == 2) {
if (resolvedLockNumber == 2) {
bellButtonChannelId = DahuaDoorBindingConstants.CHANNEL_BELL_BUTTON_2;
doorImageChannelId = DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE_2;
} else {
Expand Down Expand Up @@ -88,13 +97,12 @@ protected void onButtonPressed(int lockNumber) {
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);
updateImageChannel(doorImageChannelIdFinal, buffer);

// Save snapshot image
saveSnapshot(buffer);
saveSnapshot(buffer, resolvedLockNumber);
} else {
logger.warn("Received empty or null image buffer from VTO3211 button {}", lockNumber);
logger.warn("Received empty or null image buffer from VTO3211 button {}", resolvedLockNumber);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ 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.localSipPort.label = Local SIP Port
thing-type.config.dahuadoor.vto2202.localSipPort.description = Local UDP port for SIP client (default 5060). Must be unique per thing if multiple SIP clients are used.
thing-type.config.dahuadoor.vto2202.maxImages.label = Max Images
thing-type.config.dahuadoor.vto2202.maxImages.description = Maximum number of timestamped snapshots to keep (0 disables cleanup)
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.rtspChannel.label = RTSP Channel
Expand Down Expand Up @@ -74,6 +76,8 @@ 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.localSipPort.label = Local SIP Port
thing-type.config.dahuadoor.vto3211.localSipPort.description = Local UDP port for SIP client (default 5060). Must be unique per thing if multiple SIP clients are used.
thing-type.config.dahuadoor.vto3211.maxImages.label = Max Images
thing-type.config.dahuadoor.vto3211.maxImages.description = Maximum number of timestamped snapshots to keep per button (0 disables cleanup)
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.rtspChannel.label = RTSP Channel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
<label>Snapshot Path</label>
<description>Path where snapshots will be saved (e.g., /var/lib/openhab/door-images)</description>
</parameter>
<parameter name="maxImages" type="integer">
<label>Max Images</label>
<description>Maximum number of timestamped snapshots to keep (0 disables cleanup)</description>
<default>20</default>
</parameter>
<parameter name="useHttps" type="boolean">
<label>Use HTTPS</label>
<description>Use HTTPS (port 443) for snapshot and door-open requests. Enable if the device has HTTPS turned on.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@
<label>Snapshot Path</label>
<description>Path where snapshots will be saved (e.g., /var/lib/openhab/door-images)</description>
</parameter>
<parameter name="maxImages" type="integer">
<label>Max Images</label>
<description>Maximum number of timestamped snapshots to keep per button (0 disables cleanup)</description>
<default>20</default>
</parameter>
<parameter name="useHttps" type="boolean">
<label>Use HTTPS</label>
<description>Use HTTPS (port 443) for snapshot and door-open requests. Enable if the device has HTTPS turned on.
Expand Down
Loading