-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSentinelCam_CloudBridge_AppsScript.js
More file actions
335 lines (269 loc) · 9.86 KB
/
Copy pathSentinelCam_CloudBridge_AppsScript.js
File metadata and controls
335 lines (269 loc) · 9.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// ============================================================
// SentinelCam CloudBridge — Google Apps Script Backend
// Project : SentinelCam-ESP32
// Version : 1.1
//
// Purpose:
// Receives motion-triggered JPEG images from ESP32-CAM,
// saves them to Google Drive, and sends Pushover alerts.
//
// Deploy:
// Google Apps Script → Deploy as Web App
// Execute as : Me
// Access : Anyone
//
// ============================================================
// ============================================================
// CONFIGURATION — Replace these values in your PRIVATE copy
// ============================================================
const DRIVE_FOLDER_ID = "YOUR_GOOGLE_DRIVE_FOLDER_ID"; // Google Drive folder ID
const PUSHOVER_USER_KEY = "YOUR_PUSHOVER_USER_KEY"; // Pushover user key
const PUSHOVER_APP_TOKEN = "YOUR_PUSHOVER_APP_TOKEN"; // Pushover application token
const DEVICE_SECRET = "YOUR_CUSTOM_SECRET_KEY"; // Must match Arduino DEVICE_SECRET
// Pushover priority:
// -2 = silent, -1 = low, 0 = normal, 1 = high, 2 = emergency
const PUSHOVER_PRIORITY = 0;
// Pushover sound examples:
// "pushover", "siren", "spacealarm", "alien", "persistent", "echo", "none"
const PUSHOVER_SOUND = "pushover";
// Send notification as soon as first image is saved.
// true = faster alert
// false = alert after all images are uploaded
const NOTIFY_ON_FIRST_IMAGE = true;
// Share saved event folder/files as "Anyone with the link".
// This makes the Drive link easier to open from Pushover.
// If you want stricter privacy, set this to false.
const ENABLE_LINK_SHARING = true;
// ============================================================
// doGet — Browser health check
// Open the Web App URL in browser to confirm deployment.
// ============================================================
function doGet(e) {
return jsonResponse({
success: true,
service: "SentinelCam CloudBridge",
message: "ESP32-CAM Web App is running. Use POST from ESP32-CAM."
});
}
// ============================================================
// doPost — Main entry point for ESP32-CAM uploads
// ============================================================
function doPost(e) {
try {
// ----- 1. Validate request body -----
if (!e || !e.postData || !e.postData.contents) {
return jsonResponse({
success: false,
message: "No data received. Send JSON using HTTP POST."
});
}
// ----- 2. Parse JSON -----
var data;
try {
data = JSON.parse(e.postData.contents);
} catch (parseErr) {
return jsonResponse({
success: false,
message: "JSON parse error: " + parseErr.toString()
});
}
// ----- 3. Verify shared secret -----
if (!data.secret || data.secret !== DEVICE_SECRET) {
Logger.log("Unauthorized request blocked.");
return jsonResponse({
success: false,
message: "Unauthorized: secret mismatch."
});
}
// ----- 4. Extract and normalize fields -----
var eventId = String(data.eventId || "unknown_event");
var imageIndex = parseInt(data.imageIndex, 10) || 1;
var totalImages = parseInt(data.totalImages, 10) || 1;
// Supports both field names:
// - imageData = current Arduino sketch
// - imageBase64 = future/alternative clients
var imageBase64 = data.imageBase64 || data.imageData;
var deviceName = String(data.deviceName || "ESP32-CAM");
var timestamp = String(
data.timestamp ||
Utilities.formatDate(new Date(), "UTC", "yyyy-MM-dd_HH-mm-ss")
);
if (!imageBase64 || imageBase64.length === 0) {
return jsonResponse({
success: false,
message: "No image data found in request."
});
}
// Keep event folder names safe.
eventId = eventId.replace(/[^a-zA-Z0-9_\-]/g, "_");
Logger.log(
"Event: " + eventId +
" | Image: " + imageIndex + "/" + totalImages +
" | Device: " + deviceName
);
// ----- 5. Open parent Drive folder -----
var parentFolder;
try {
parentFolder = DriveApp.getFolderById(DRIVE_FOLDER_ID);
} catch (folderErr) {
return jsonResponse({
success: false,
message: "Cannot open Drive folder. Check DRIVE_FOLDER_ID. Error: " + folderErr.toString()
});
}
// ----- 6. Create/find event subfolder -----
var eventFolder = getOrCreateFolder(parentFolder, eventId);
if (ENABLE_LINK_SHARING) {
eventFolder.setSharing(
DriveApp.Access.ANYONE_WITH_LINK,
DriveApp.Permission.VIEW
);
}
var folderUrl = eventFolder.getUrl();
// ----- 7. Decode base64 and save JPEG -----
var safeTimestamp = timestamp.replace(/[^a-zA-Z0-9_\-]/g, "_");
var filename = "motion_" + safeTimestamp + "_img" + imageIndex + ".jpg";
var imageBytes;
try {
imageBytes = Utilities.base64Decode(imageBase64);
} catch (decErr) {
return jsonResponse({
success: false,
message: "Base64 decode failed: " + decErr.toString()
});
}
var blob = Utilities.newBlob(imageBytes, "image/jpeg", filename);
var savedFile = eventFolder.createFile(blob);
if (ENABLE_LINK_SHARING) {
savedFile.setSharing(
DriveApp.Access.ANYONE_WITH_LINK,
DriveApp.Permission.VIEW
);
}
var fileUrl = savedFile.getUrl();
Logger.log("Saved file: " + filename + " -> " + fileUrl);
// ----- 8. Send Pushover notification -----
var shouldNotify = NOTIFY_ON_FIRST_IMAGE
? imageIndex === 1
: imageIndex === totalImages;
var pushoverStatus = shouldNotify
? sendPushover(deviceName, totalImages, folderUrl, timestamp, eventId)
: "Skipped for this image.";
Logger.log("Pushover status: " + pushoverStatus);
// ----- 9. Return JSON success -----
return jsonResponse({
success: true,
service: "SentinelCam CloudBridge",
message: "Image " + imageIndex + "/" + totalImages + " saved successfully.",
eventId: eventId,
imageIndex: imageIndex,
totalImages: totalImages,
fileUrl: fileUrl,
folderUrl: folderUrl,
pushover: pushoverStatus
});
} catch (err) {
Logger.log("doPost exception: " + err.toString() + "\n" + err.stack);
return jsonResponse({
success: false,
message: "Server exception: " + err.toString()
});
}
}
// ============================================================
// getOrCreateFolder — Reuse event folder or create new one
// ============================================================
function getOrCreateFolder(parent, name) {
var iter = parent.getFoldersByName(name);
if (iter.hasNext()) {
return iter.next();
}
Logger.log("Creating new event folder: " + name);
return parent.createFolder(name);
}
// ============================================================
// sendPushover — Send mobile security alert
// ============================================================
function sendPushover(deviceName, imageCount, folderUrl, timestamp, eventId) {
try {
var readableTime = timestamp
.replace(/_/g, " ")
.replace(/-/g, ":");
var msg =
"Motion detected by " + deviceName + "\n" +
"Time : " + readableTime + "\n" +
"Images : " + imageCount + " captured\n" +
"Event ID: " + eventId + "\n\n" +
"Open Google Drive to review captured evidence.";
var payload = {
"token" : PUSHOVER_APP_TOKEN,
"user" : PUSHOVER_USER_KEY,
"title" : "🛡️ SentinelCam Motion Alert",
"message" : msg,
// Pushover accepts form fields as strings.
// Using String() avoids priority-related HTTP 400 issues.
"priority" : String(PUSHOVER_PRIORITY),
"sound" : PUSHOVER_SOUND,
"url" : folderUrl,
"url_title" : "Open Captured Images"
};
// Emergency priority requires retry + expire.
if (PUSHOVER_PRIORITY === 2) {
payload["retry"] = "60"; // Retry every 60 seconds
payload["expire"] = "600"; // Stop after 10 minutes
}
var options = {
method: "post",
contentType: "application/x-www-form-urlencoded",
payload: payload,
muteHttpExceptions: true
};
var resp = UrlFetchApp.fetch(
"https://api.pushover.net/1/messages.json",
options
);
var code = resp.getResponseCode();
var body = resp.getContentText();
Logger.log("Pushover HTTP " + code + ": " + body);
if (code === 200) {
return "Notification sent successfully.";
}
return "Notification failed. HTTP " + code + ": " + body;
} catch (err) {
return "Pushover exception: " + err.toString();
}
}
// ============================================================
// jsonResponse — Return JSON output to ESP32/browser
// ============================================================
function jsonResponse(data) {
return ContentService
.createTextOutput(JSON.stringify(data))
.setMimeType(ContentService.MimeType.JSON);
}
// ============================================================
// TEST: Pushover notification
// Run manually inside Apps Script editor before deployment.
// ============================================================
function testPushover() {
var result = sendPushover(
"SentinelCam-TestDevice",
3,
"https://drive.google.com/drive/folders/YOUR_FOLDER_ID",
"2026-07-01_14-30-00",
"test_event_001"
);
Logger.log("Test Pushover result: " + result);
}
// ============================================================
// TEST: Google Drive folder access
// Run manually inside Apps Script editor before deployment.
// ============================================================
function testDriveAccess() {
try {
var folder = DriveApp.getFolderById(DRIVE_FOLDER_ID);
Logger.log("Drive folder OK: " + folder.getName() + " | URL: " + folder.getUrl());
} catch (err) {
Logger.log("Drive folder ERROR: " + err.toString());
}
}