Skip to content

Commit 5ca79a0

Browse files
svnsssdolemr
authored andcommitted
[dahuadoor] Restore snapshots and enforce retention (openhab#20692)
* [dahuadoor] add door image persistence on startup Signed-off-by: Sven Schad <svnsssd@gmail.com> Signed-off-by: olemr <olemr@olemr.com>
1 parent 37eab92 commit 5ca79a0

8 files changed

Lines changed: 149 additions & 15 deletions

File tree

bundles/org.openhab.binding.dahuadoor/README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ VTO2202 is a single-button outdoor station; VTO3211 is a dual-button outdoor sta
3030
| username | text | Yes | | Username to access the device |
3131
| password | text | Yes | | Password to access the device |
3232
| snapshotPath | text | Yes | | Linux path where image files are stored (e.g., /var/lib/openhab/door-images) |
33+
| maxImages | integer | No | 20 | Maximum number of timestamped snapshots to keep (0 disables cleanup). For VTO3211 this applies per button. |
3334
| 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. |
3435
| enableWebRTC | boolean | No | false | Enables local go2rtc sidecar management and publishes a `webrtc-url` channel. |
3536
| go2rtcPath | text | No | | Absolute path to the go2rtc binary (required when `enableWebRTC=true`). |
@@ -44,6 +45,12 @@ VTO2202 is a single-button outdoor station; VTO3211 is a dual-button outdoor sta
4445
| localSipPort | integer | No | 5060 | Local UDP SIP listening port. |
4546
| sipRealm | text | No | VDP | SIP authentication realm (default for Dahua VTO devices). |
4647

48+
**Snapshot persistence:**
49+
The binding stores snapshots in `snapshotPath` and reloads the latest snapshot on startup to restore the `door-image` channels.
50+
For VTO2202, the latest file is `Doorbell.jpg` and timestamped files are named `Doorbell_YYYY-MM-DD_HH-mm-ss.jpg`.
51+
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`.
52+
Timestamped files are capped by `maxImages` (0 disables cleanup).
53+
4754
**Note on SIP configuration:**
4855
To enable SIP call signaling, set `enableSip=true` and configure at least one value in `sipExtension`.
4956
If your VTO requires a dedicated SIP password, set `sipPassword`; otherwise the binding uses `password`.
@@ -104,6 +111,7 @@ Thing dahuadoor:vto2202:frontdoor "Front Door Station" @ "Entrance" [
104111
username="admin",
105112
password="password123",
106113
snapshotPath="/var/lib/openhab/door-images",
114+
maxImages=20,
107115
useHttps=false
108116
]
109117
```
@@ -141,6 +149,7 @@ Thing dahuadoor:vto3211:entrance "Entrance Station" @ "Entrance" [
141149
username="admin",
142150
password="password123",
143151
snapshotPath="/var/lib/openhab/door-images",
152+
maxImages=20,
144153
useHttps=false
145154
]
146155
```
@@ -185,7 +194,7 @@ end
185194
Intercom operation is implemented with WebRTC via the `go2rtc` binary.
186195
It converts the Dahua RTP audio/video stream into browser-compatible WebRTC. The audio stream is transcoded using `ffmpeg`. Hence both tools are needed.
187196
When `enableWebRTC=true`, the binding starts everything automatically when a call is received.
188-
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).
197+
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).
189198
You can try Dahua's default password for initial testing, but do not use it in production. Consult your VTO manual for setup details.
190199

191200
### Tool installation
@@ -236,6 +245,7 @@ Thing dahuadoor:vto2202:frontdoor "Front Door Station" @ "Entrance" [
236245
username="admin",
237246
password="password123",
238247
snapshotPath="/var/lib/openhab/door-images",
248+
maxImages=20,
239249
enableWebRTC=true,
240250
go2rtcPath="/usr/local/bin/go2rtc",
241251
go2rtcApiPort=1984,

bundles/org.openhab.binding.dahuadoor/src/main/java/org/openhab/binding/dahuadoor/internal/DahuaDoorBaseHandler.java

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.net.InetAddress;
2222
import java.net.URLEncoder;
2323
import java.nio.charset.StandardCharsets;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
2426
import java.text.SimpleDateFormat;
2527
import java.util.ArrayList;
2628
import java.util.Arrays;
@@ -192,6 +194,8 @@ public void initialize() {
192194
updateState(CHANNEL_SIP_CALL_STATE, new StringType(SipClient.SipCallState.IDLE.name()));
193195
}
194196

197+
restoreLastSnapshots();
198+
195199
// Set status to UNKNOWN - will be set to ONLINE when first DHIP event is received
196200
updateStatus(ThingStatus.UNKNOWN);
197201
}
@@ -333,6 +337,14 @@ private void stopWebRtc() {
333337
}
334338

335339
public void saveSnapshot(byte @Nullable [] buffer) {
340+
saveSnapshot(buffer, null);
341+
}
342+
343+
public void saveSnapshot(byte @Nullable [] buffer, int lockNumber) {
344+
saveSnapshot(buffer, Integer.valueOf(lockNumber));
345+
}
346+
347+
private void saveSnapshot(byte @Nullable [] buffer, @Nullable Integer lockNumber) {
336348
final DahuaDoorConfiguration localConfig = config;
337349
if (localConfig == null) {
338350
logger.warn("Configuration not initialized");
@@ -354,8 +366,9 @@ public void saveSnapshot(byte @Nullable [] buffer) {
354366
return;
355367
}
356368

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

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

366379
// Write buffer directly to latest snapshot file (avoids copy-from-source failures)
367-
String latestSnapshotFilename = localConfig.snapshotPath + "/Doorbell.jpg";
380+
String latestSnapshotFilename = localConfig.snapshotPath + "/Doorbell" + suffix + ".jpg";
368381
try (FileOutputStream fos = new FileOutputStream(new File(latestSnapshotFilename))) {
369382
fos.write(buffer);
370383
} catch (IOException e) {
371384
logger.warn("Could not write latest snapshot to '{}', check permissions and path", latestSnapshotFilename,
372385
e);
373386
}
387+
388+
cleanupOldSnapshots(lockNumber);
389+
}
390+
391+
private void cleanupOldSnapshots(@Nullable Integer lockNumber) {
392+
final DahuaDoorConfiguration localConfig = config;
393+
if (localConfig == null || localConfig.snapshotPath.isEmpty() || localConfig.maxImages <= 0) {
394+
return;
395+
}
396+
397+
Path snapshotDir = Path.of(localConfig.snapshotPath);
398+
if (!Files.isDirectory(snapshotDir)) {
399+
return;
400+
}
401+
402+
String suffix = lockNumber == null ? "" : "-" + lockNumber;
403+
String prefix = "Doorbell" + suffix + "_";
404+
List<Path> candidates = new ArrayList<>();
405+
406+
try (var stream = Files.list(snapshotDir)) {
407+
stream.filter(path -> {
408+
String name = path.getFileName().toString();
409+
return name.startsWith(prefix) && name.endsWith(".jpg");
410+
}).forEach(candidates::add);
411+
} catch (IOException e) {
412+
logger.warn("Could not list snapshot directory '{}', check permissions and path", localConfig.snapshotPath,
413+
e);
414+
return;
415+
}
416+
417+
int maxImages = localConfig.maxImages;
418+
if (candidates.size() <= maxImages) {
419+
return;
420+
}
421+
422+
candidates.sort((left, right) -> left.getFileName().toString().compareTo(right.getFileName().toString()));
423+
int deleteCount = candidates.size() - maxImages;
424+
for (int i = 0; i < deleteCount; i++) {
425+
Path candidate = candidates.get(i);
426+
try {
427+
Files.deleteIfExists(candidate);
428+
} catch (IOException e) {
429+
logger.warn("Could not delete snapshot '{}', check permissions and path", candidate, e);
430+
}
431+
}
374432
}
375433

376434
private void updateChannelImage(byte @Nullable [] buffer) {
435+
updateImageChannel(CHANNEL_DOOR_IMAGE, buffer);
436+
}
437+
438+
protected void updateImageChannel(String channelId, byte @Nullable [] buffer) {
377439
if (buffer == null || buffer.length == 0) {
378-
updateState(CHANNEL_DOOR_IMAGE, UnDefType.UNDEF);
440+
updateState(channelId, UnDefType.UNDEF);
379441
return;
380442
}
381443
RawType image = new RawType(buffer, "image/jpeg");
382-
updateState(CHANNEL_DOOR_IMAGE, image);
444+
updateState(channelId, image);
445+
}
446+
447+
protected byte @Nullable [] readLatestSnapshot() {
448+
return readLatestSnapshotInternal(null);
449+
}
450+
451+
protected byte @Nullable [] readLatestSnapshot(int lockNumber) {
452+
return readLatestSnapshotInternal(Integer.valueOf(lockNumber));
453+
}
454+
455+
private byte @Nullable [] readLatestSnapshotInternal(@Nullable Integer lockNumber) {
456+
final DahuaDoorConfiguration localConfig = config;
457+
if (localConfig == null || localConfig.snapshotPath.isEmpty()) {
458+
return null;
459+
}
460+
461+
String suffix = lockNumber == null ? "" : "-" + lockNumber;
462+
String latestSnapshotFilename = localConfig.snapshotPath + "/Doorbell" + suffix + ".jpg";
463+
return readSnapshotFile(latestSnapshotFilename);
464+
}
465+
466+
private byte @Nullable [] readSnapshotFile(String filename) {
467+
Path path = Path.of(filename);
468+
if (!Files.isRegularFile(path)) {
469+
return null;
470+
}
471+
try {
472+
return Files.readAllBytes(path);
473+
} catch (IOException e) {
474+
logger.warn("Could not read snapshot from '{}', check permissions and path", filename, e);
475+
return null;
476+
}
477+
}
478+
479+
protected void restoreLastSnapshots() {
383480
}
384481

385482
protected void handleButtonPressed() {

bundles/org.openhab.binding.dahuadoor/src/main/java/org/openhab/binding/dahuadoor/internal/DahuaDoorConfiguration.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class DahuaDoorConfiguration {
3030
public String username = "";
3131
public String password = "";
3232
public String snapshotPath = "";
33+
public int maxImages = 20;
3334
public boolean useHttps = false;
3435

3536
// WebRTC / go2rtc settings

bundles/org.openhab.binding.dahuadoor/src/main/java/org/openhab/binding/dahuadoor/internal/DahuaVto2202Handler.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import org.eclipse.jdt.annotation.NonNullByDefault;
1616
import org.openhab.binding.dahuadoor.internal.dahuaeventhandler.DahuaEventClient;
1717
import org.openhab.binding.dahuadoor.internal.media.PlayStreamServlet;
18-
import org.openhab.core.library.types.RawType;
1918
import org.openhab.core.thing.Channel;
2019
import org.openhab.core.thing.Thing;
2120

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

42+
@Override
43+
protected void restoreLastSnapshots() {
44+
byte[] buffer = readLatestSnapshot();
45+
updateImageChannel(DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE, buffer);
46+
}
47+
4348
@Override
4449
protected void onButtonPressed(int lockNumber) {
4550
logger.debug("Button pressed on VTO2202 (lockNumber ignored, single button)");
@@ -63,8 +68,7 @@ protected void onButtonPressed(int lockNumber) {
6368
byte[] buffer = localClient.requestImage();
6469
if (buffer != null && buffer.length > 0) {
6570
// Update image channel
66-
RawType image = new RawType(buffer, "image/jpeg");
67-
updateState(DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE, image);
71+
updateImageChannel(DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE, buffer);
6872

6973
// Save snapshot
7074
saveSnapshot(buffer);

bundles/org.openhab.binding.dahuadoor/src/main/java/org/openhab/binding/dahuadoor/internal/DahuaVto3211Handler.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import org.eclipse.jdt.annotation.NonNullByDefault;
1616
import org.openhab.binding.dahuadoor.internal.dahuaeventhandler.DahuaEventClient;
1717
import org.openhab.binding.dahuadoor.internal.media.PlayStreamServlet;
18-
import org.openhab.core.library.types.RawType;
1918
import org.openhab.core.thing.Channel;
2019
import org.openhab.core.thing.Thing;
2120

@@ -51,15 +50,25 @@ protected void handleInvite(JsonObject eventList, JsonObject eventData) {
5150
handleResolvedDoorbellEvent("DHIP", lockNumber);
5251
}
5352

53+
@Override
54+
protected void restoreLastSnapshots() {
55+
byte[] buffer1 = readLatestSnapshot(1);
56+
updateImageChannel(DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE_1, buffer1);
57+
58+
byte[] buffer2 = readLatestSnapshot(2);
59+
updateImageChannel(DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE_2, buffer2);
60+
}
61+
5462
@Override
5563
protected void onButtonPressed(int lockNumber) {
56-
logger.debug("Button {} pressed on VTO3211", lockNumber);
64+
int resolvedLockNumber = lockNumber == 2 ? 2 : 1;
65+
logger.debug("Button {} pressed on VTO3211", resolvedLockNumber);
5766

5867
// Determine channel IDs based on lock number
5968
String bellButtonChannelId;
6069
String doorImageChannelId;
6170

62-
if (lockNumber == 2) {
71+
if (resolvedLockNumber == 2) {
6372
bellButtonChannelId = DahuaDoorBindingConstants.CHANNEL_BELL_BUTTON_2;
6473
doorImageChannelId = DahuaDoorBindingConstants.CHANNEL_DOOR_IMAGE_2;
6574
} else {
@@ -88,13 +97,12 @@ protected void onButtonPressed(int lockNumber) {
8897
byte[] buffer = localClient.requestImage();
8998
if (buffer != null && buffer.length > 0) {
9099
// Update image channel for the specific button
91-
RawType image = new RawType(buffer, "image/jpeg");
92-
updateState(doorImageChannelIdFinal, image);
100+
updateImageChannel(doorImageChannelIdFinal, buffer);
93101

94102
// Save snapshot image
95-
saveSnapshot(buffer);
103+
saveSnapshot(buffer, resolvedLockNumber);
96104
} else {
97-
logger.warn("Received empty or null image buffer from VTO3211 button {}", lockNumber);
105+
logger.warn("Received empty or null image buffer from VTO3211 button {}", resolvedLockNumber);
98106
}
99107
});
100108
}

bundles/org.openhab.binding.dahuadoor/src/main/resources/OH-INF/i18n/dahuadoor.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ thing-type.config.dahuadoor.vto2202.hostname.label = Hostname
4040
thing-type.config.dahuadoor.vto2202.hostname.description = Hostname or IP address of the device
4141
thing-type.config.dahuadoor.vto2202.localSipPort.label = Local SIP Port
4242
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.
43+
thing-type.config.dahuadoor.vto2202.maxImages.label = Max Images
44+
thing-type.config.dahuadoor.vto2202.maxImages.description = Maximum number of timestamped snapshots to keep (0 disables cleanup)
4345
thing-type.config.dahuadoor.vto2202.password.label = Password
4446
thing-type.config.dahuadoor.vto2202.password.description = Password to access the device
4547
thing-type.config.dahuadoor.vto2202.rtspChannel.label = RTSP Channel
@@ -74,6 +76,8 @@ thing-type.config.dahuadoor.vto3211.hostname.label = Hostname
7476
thing-type.config.dahuadoor.vto3211.hostname.description = Hostname or IP address of the device
7577
thing-type.config.dahuadoor.vto3211.localSipPort.label = Local SIP Port
7678
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.
79+
thing-type.config.dahuadoor.vto3211.maxImages.label = Max Images
80+
thing-type.config.dahuadoor.vto3211.maxImages.description = Maximum number of timestamped snapshots to keep per button (0 disables cleanup)
7781
thing-type.config.dahuadoor.vto3211.password.label = Password
7882
thing-type.config.dahuadoor.vto3211.password.description = Password to access the device
7983
thing-type.config.dahuadoor.vto3211.rtspChannel.label = RTSP Channel

bundles/org.openhab.binding.dahuadoor/src/main/resources/OH-INF/thing/vto2202.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
<label>Snapshot Path</label>
5050
<description>Path where snapshots will be saved (e.g., /var/lib/openhab/door-images)</description>
5151
</parameter>
52+
<parameter name="maxImages" type="integer">
53+
<label>Max Images</label>
54+
<description>Maximum number of timestamped snapshots to keep (0 disables cleanup)</description>
55+
<default>20</default>
56+
</parameter>
5257
<parameter name="useHttps" type="boolean">
5358
<label>Use HTTPS</label>
5459
<description>Use HTTPS (port 443) for snapshot and door-open requests. Enable if the device has HTTPS turned on.

bundles/org.openhab.binding.dahuadoor/src/main/resources/OH-INF/thing/vto3211.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@
6363
<label>Snapshot Path</label>
6464
<description>Path where snapshots will be saved (e.g., /var/lib/openhab/door-images)</description>
6565
</parameter>
66+
<parameter name="maxImages" type="integer">
67+
<label>Max Images</label>
68+
<description>Maximum number of timestamped snapshots to keep per button (0 disables cleanup)</description>
69+
<default>20</default>
70+
</parameter>
6671
<parameter name="useHttps" type="boolean">
6772
<label>Use HTTPS</label>
6873
<description>Use HTTPS (port 443) for snapshot and door-open requests. Enable if the device has HTTPS turned on.

0 commit comments

Comments
 (0)