Initial commit - Smart Rack Security
This commit is contained in:
commit
709c7a06d1
|
|
@ -0,0 +1,18 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[compose.yaml]
|
||||
indent_size = 4
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
APP_NAME="Smart Rack Security"
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://your-app.railway.app
|
||||
|
||||
APP_LOCALE=id
|
||||
APP_FALLBACK_LOCALE=id
|
||||
APP_FAKER_LOCALE=id_ID
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_LEVEL=error
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=
|
||||
DB_USERNAME=
|
||||
DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
CACHE_STORE=file
|
||||
QUEUE_CONNECTION=sync
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
auth.json
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/.fleet
|
||||
/.idea
|
||||
/.vscode
|
||||
lora_test.json
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
# Panduan Test API - Smart Rack Security
|
||||
|
||||
## Base URL
|
||||
```
|
||||
http://localhost:8000/api
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Test Sensor PIR
|
||||
|
||||
### Kirim data PIR (gerakan terdeteksi)
|
||||
```
|
||||
POST /api/pir/data
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": 1,
|
||||
"motion_detected": true,
|
||||
"motion_intensity": 85,
|
||||
"duration_seconds": 10,
|
||||
"detection_zone": "center"
|
||||
}
|
||||
```
|
||||
|
||||
### Kirim data PIR (tidak ada gerakan)
|
||||
```
|
||||
POST /api/pir/data
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": 1,
|
||||
"motion_detected": false,
|
||||
"motion_intensity": 0,
|
||||
"duration_seconds": 0,
|
||||
"detection_zone": "center"
|
||||
}
|
||||
```
|
||||
|
||||
### Ambil data PIR terbaru
|
||||
```
|
||||
GET /api/pir/readings?limit=10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Test Sensor Getaran (SW-420)
|
||||
|
||||
### Kirim data getaran (abnormal)
|
||||
```
|
||||
POST /api/vibration/data
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": 1,
|
||||
"x_axis": 3.5,
|
||||
"y_axis": 2.8,
|
||||
"z_axis": 4.1,
|
||||
"threshold": 2.0
|
||||
}
|
||||
```
|
||||
|
||||
### Kirim data getaran (normal)
|
||||
```
|
||||
POST /api/vibration/data
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": 1,
|
||||
"x_axis": 0.1,
|
||||
"y_axis": 0.2,
|
||||
"z_axis": 0.1,
|
||||
"threshold": 2.0
|
||||
}
|
||||
```
|
||||
|
||||
### Ambil data getaran terbaru
|
||||
```
|
||||
GET /api/vibration/readings?limit=10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Test Reed Switch (Door Access)
|
||||
|
||||
### Kirim data pintu terbuka
|
||||
```
|
||||
POST /api/door-access/data
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": 1,
|
||||
"door_opened": true,
|
||||
"access_method": "manual",
|
||||
"duration_seconds": 5,
|
||||
"door_location": "main_entrance",
|
||||
"is_forced_entry": false
|
||||
}
|
||||
```
|
||||
|
||||
### Kirim data pintu tertutup
|
||||
```
|
||||
POST /api/door-access/data
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": 1,
|
||||
"door_opened": false,
|
||||
"access_method": "manual",
|
||||
"duration_seconds": 0,
|
||||
"door_location": "main_entrance",
|
||||
"is_forced_entry": false
|
||||
}
|
||||
```
|
||||
|
||||
### Ambil data door access terbaru
|
||||
```
|
||||
GET /api/door-access/readings?limit=10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Test LoRa Gateway
|
||||
|
||||
### Kirim data LoRa dari gateway
|
||||
```
|
||||
POST /api/lora/receive
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"node_id": "LORA_001",
|
||||
"gateway_id": "GATEWAY_001",
|
||||
"payload": "SENSOR|PIR|1|85|10|center",
|
||||
"rssi": -75,
|
||||
"snr": 8.5
|
||||
}
|
||||
```
|
||||
|
||||
### Kirim heartbeat
|
||||
```
|
||||
POST /api/lora/receive
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"node_id": "LORA_001",
|
||||
"gateway_id": "GATEWAY_001",
|
||||
"payload": "HEARTBEAT|85|-75|3600",
|
||||
"rssi": -75,
|
||||
"snr": 8.5
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cara Test Menggunakan Postman
|
||||
|
||||
1. Buka Postman
|
||||
2. Buat request baru
|
||||
3. Pilih method POST
|
||||
4. Masukkan URL: `http://localhost:8000/api/pir/data`
|
||||
5. Pilih tab Body → raw → JSON
|
||||
6. Paste JSON di atas
|
||||
7. Klik Send
|
||||
8. Cek response — harus `"success": true`
|
||||
|
||||
## Cara Test Menggunakan Browser (GET only)
|
||||
|
||||
Buka langsung di browser:
|
||||
- http://localhost:8000/api/pir/readings
|
||||
- http://localhost:8000/api/vibration/readings
|
||||
- http://localhost:8000/api/door-access/readings
|
||||
|
|
@ -0,0 +1,715 @@
|
|||
# 📡 Smart Rack LoRa Communication API
|
||||
|
||||
Backend API untuk komunikasi LoRa (Long Range) yang dapat menerima dan mengirim data dari/ke sensor-sensor IoT dengan jangkauan jauh dan konsumsi daya rendah.
|
||||
|
||||
## 📡 **API Endpoints**
|
||||
|
||||
### 1. **Terima Message LoRa dari Gateway**
|
||||
```http
|
||||
POST /api/lora/receive
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"node_id": "LORA_001",
|
||||
"gateway_id": "GATEWAY_001",
|
||||
"payload": "SENSOR|VIBRATION|1.5,2.3,1.8|2.0",
|
||||
"rssi": -85.5,
|
||||
"snr": 8.2,
|
||||
"spreading_factor": 7,
|
||||
"frequency": 868.1,
|
||||
"bandwidth": 125000,
|
||||
"received_at": "2026-04-28T10:30:00Z",
|
||||
"metadata": {
|
||||
"gateway_location": "Building_A",
|
||||
"channel": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response Success:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "LoRa message received successfully",
|
||||
"data": {
|
||||
"message_id": 129,
|
||||
"node_id": "LORA_001",
|
||||
"message_type": "sensor_data",
|
||||
"signal_quality": "good",
|
||||
"estimated_distance": 2.5,
|
||||
"processing_result": {
|
||||
"success": true,
|
||||
"action": "vibration_data_saved",
|
||||
"vibration_reading_id": 45,
|
||||
"magnitude": 3.21,
|
||||
"status": "warning",
|
||||
"is_abnormal": true
|
||||
},
|
||||
"received_at": "2026-04-28T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Kirim Command ke LoRa Node**
|
||||
```http
|
||||
POST /api/lora/send-command
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"node_id": "LORA_001",
|
||||
"action": "set_threshold",
|
||||
"parameters": ["2.5", "vibration"],
|
||||
"gateway_id": "GATEWAY_001",
|
||||
"priority": "high"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Command sent to LoRa node",
|
||||
"data": {
|
||||
"message_id": 130,
|
||||
"command_id": "CMD_507f1f77bcf86cd799439011",
|
||||
"node_id": "LORA_001",
|
||||
"action": "set_threshold",
|
||||
"payload": "COMMAND|set_threshold|2.5,vibration|CMD_507f1f77bcf86cd799439011",
|
||||
"transmission_result": {
|
||||
"success": true,
|
||||
"simulated": true
|
||||
},
|
||||
"transmitted_at": "2026-04-28T10:35:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Kirim Konfigurasi ke LoRa Node**
|
||||
```http
|
||||
POST /api/lora/send-config
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"node_id": "LORA_001",
|
||||
"parameter": "sleep_time",
|
||||
"value": "300",
|
||||
"gateway_id": "GATEWAY_001"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Configuration sent to LoRa node",
|
||||
"data": {
|
||||
"message_id": 131,
|
||||
"config_id": "CFG_507f1f77bcf86cd799439012",
|
||||
"node_id": "LORA_001",
|
||||
"parameter": "sleep_time",
|
||||
"value": "300",
|
||||
"payload": "CONFIG|sleep_time|300|CFG_507f1f77bcf86cd799439012",
|
||||
"transmission_result": {
|
||||
"success": true,
|
||||
"simulated": true
|
||||
},
|
||||
"transmitted_at": "2026-04-28T10:40:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **Ambil Messages LoRa Terbaru**
|
||||
```http
|
||||
GET /api/lora/messages?node_id=LORA_001&direction=inbound&message_type=sensor_data&limit=50
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 129,
|
||||
"node_id": "LORA_001",
|
||||
"gateway_id": "GATEWAY_001",
|
||||
"direction": "inbound",
|
||||
"message_type": "sensor_data",
|
||||
"payload": "SENSOR|VIBRATION|1.5,2.3,1.8|2.0",
|
||||
"parsed_data": {
|
||||
"sensor_type": "VIBRATION",
|
||||
"x_axis": 1.5,
|
||||
"y_axis": 2.3,
|
||||
"z_axis": 1.8,
|
||||
"threshold": 2.0
|
||||
},
|
||||
"rssi": -85.5,
|
||||
"snr": 8.2,
|
||||
"spreading_factor": 7,
|
||||
"frequency": 868.1,
|
||||
"signal_quality": "good",
|
||||
"estimated_distance": 2.5,
|
||||
"is_processed": true,
|
||||
"status": "processed",
|
||||
"received_at": "2026-04-28T10:30:00Z",
|
||||
"device": {
|
||||
"id": 1,
|
||||
"name": "LoRa Vibration Sensor A",
|
||||
"location": "Ruang Server"
|
||||
}
|
||||
}
|
||||
],
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 5. **Ambil Statistik LoRa Communication**
|
||||
```http
|
||||
GET /api/lora/statistics?node_id=LORA_001&hours=24
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"total_messages": 150,
|
||||
"inbound_messages": 120,
|
||||
"outbound_messages": 30,
|
||||
"processed_messages": 145,
|
||||
"failed_messages": 5,
|
||||
"acknowledged_messages": 25,
|
||||
"success_rate": 96.67,
|
||||
"avg_rssi": -82.3,
|
||||
"avg_snr": 7.8,
|
||||
"message_types": {
|
||||
"sensor_data": 100,
|
||||
"heartbeat": 20,
|
||||
"command": 25,
|
||||
"ack": 3,
|
||||
"config": 2
|
||||
},
|
||||
"node_statistics": {
|
||||
"LORA_001": {
|
||||
"message_count": 85,
|
||||
"avg_rssi": -80.5,
|
||||
"last_seen": "2026-04-28T10:30:00Z"
|
||||
},
|
||||
"LORA_002": {
|
||||
"message_count": 65,
|
||||
"avg_rssi": -88.2,
|
||||
"last_seen": "2026-04-28T10:25:00Z"
|
||||
}
|
||||
},
|
||||
"signal_quality_distribution": {
|
||||
"excellent": 15,
|
||||
"good": 45,
|
||||
"fair": 60,
|
||||
"poor": 25,
|
||||
"unknown": 5
|
||||
},
|
||||
"latest_message": {
|
||||
"id": 129,
|
||||
"node_id": "LORA_001",
|
||||
"message_type": "sensor_data",
|
||||
"received_at": "2026-04-28T10:30:00Z"
|
||||
}
|
||||
},
|
||||
"period_hours": 24
|
||||
}
|
||||
```
|
||||
|
||||
### 6. **Process Unprocessed Messages**
|
||||
```http
|
||||
POST /api/lora/process-messages
|
||||
```
|
||||
|
||||
## 📊 **LoRa Message Format**
|
||||
|
||||
### **Sensor Data Payload Format:**
|
||||
```
|
||||
SENSOR|{SENSOR_TYPE}|{DATA}|{ADDITIONAL_PARAMS}
|
||||
```
|
||||
|
||||
**Contoh:**
|
||||
- **Vibration**: `SENSOR|VIBRATION|1.5,2.3,1.8|2.0`
|
||||
- **PIR**: `SENSOR|PIR|1|85|120|front`
|
||||
- **Door**: `SENSOR|DOOR|1|EMP-1234|45|keycard|front_door`
|
||||
|
||||
### **Command Payload Format:**
|
||||
```
|
||||
COMMAND|{ACTION}|{PARAMETERS}|{COMMAND_ID}
|
||||
```
|
||||
|
||||
**Contoh:**
|
||||
- `COMMAND|reboot||CMD_123456`
|
||||
- `COMMAND|set_threshold|2.5,vibration|CMD_789012`
|
||||
- `COMMAND|read_sensors||CMD_345678`
|
||||
|
||||
### **Config Payload Format:**
|
||||
```
|
||||
CONFIG|{PARAMETER}|{VALUE}|{CONFIG_ID}
|
||||
```
|
||||
|
||||
**Contoh:**
|
||||
- `CONFIG|sleep_time|300|CFG_123456`
|
||||
- `CONFIG|tx_power|14|CFG_789012`
|
||||
- `CONFIG|threshold|2.0|CFG_345678`
|
||||
|
||||
### **Heartbeat Payload Format:**
|
||||
```
|
||||
HEARTBEAT|{BATTERY_LEVEL}|{SIGNAL_STRENGTH}|{UPTIME}
|
||||
```
|
||||
|
||||
**Contoh:**
|
||||
- `HEARTBEAT|85|75|3600`
|
||||
|
||||
### **ACK Payload Format:**
|
||||
```
|
||||
ACK|{COMMAND_ID}|{STATUS}|{MESSAGE}
|
||||
```
|
||||
|
||||
**Contoh:**
|
||||
- `ACK|CMD_123456|success|Command executed`
|
||||
- `ACK|CFG_789012|failed|Invalid parameter`
|
||||
|
||||
## 🔧 **LoRa Parameters**
|
||||
|
||||
### **Signal Quality Levels:**
|
||||
- **Excellent**: RSSI ≥ -70 dBm
|
||||
- **Good**: -85 dBm ≤ RSSI < -70 dBm
|
||||
- **Fair**: -100 dBm ≤ RSSI < -85 dBm
|
||||
- **Poor**: RSSI < -100 dBm
|
||||
|
||||
### **Spreading Factor (SF):**
|
||||
- **SF7**: Fastest data rate, shortest range
|
||||
- **SF8-SF10**: Balanced speed and range
|
||||
- **SF11-SF12**: Slowest data rate, longest range
|
||||
|
||||
### **Frequency Bands:**
|
||||
- **EU868**: 868.1 - 868.5 MHz (Europe)
|
||||
- **US915**: 902 - 928 MHz (North America)
|
||||
- **AS923**: 923 MHz (Asia)
|
||||
|
||||
### **Bandwidth Options:**
|
||||
- **125 kHz**: Standard bandwidth
|
||||
- **250 kHz**: Higher data rate
|
||||
- **500 kHz**: Highest data rate
|
||||
|
||||
## 📡 **Database Schema**
|
||||
|
||||
### **lo_ra_messages table:**
|
||||
```sql
|
||||
- id (bigint, primary key)
|
||||
- device_id (foreign key to devices, nullable)
|
||||
- node_id (string) - LoRa node identifier
|
||||
- gateway_id (string) - LoRa gateway identifier
|
||||
- direction (enum: inbound, outbound)
|
||||
- message_type (enum: sensor_data, command, heartbeat, ack, config)
|
||||
- payload (text) - Raw LoRa message
|
||||
- parsed_data (json) - Parsed sensor data
|
||||
- rssi (float) - Signal strength
|
||||
- snr (float) - Signal-to-noise ratio
|
||||
- spreading_factor (integer) - LoRa SF (7-12)
|
||||
- frequency (float) - Frequency in MHz
|
||||
- bandwidth (integer) - Bandwidth in Hz
|
||||
- is_processed (boolean) - Processing status
|
||||
- is_acknowledged (boolean) - ACK status
|
||||
- status (string) - Message status
|
||||
- error_message (text) - Error details
|
||||
- metadata (json) - Additional parameters
|
||||
- transmitted_at (timestamp) - Transmission time
|
||||
- received_at (timestamp) - Reception time
|
||||
- created_at, updated_at
|
||||
```
|
||||
|
||||
## 🔌 **Integrasi LoRa Device**
|
||||
|
||||
### **Arduino/ESP32 + LoRa Module Example:**
|
||||
```cpp
|
||||
#include <SPI.h>
|
||||
#include <LoRa.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
// LoRa pins
|
||||
#define SS 18
|
||||
#define RST 14
|
||||
#define DIO0 26
|
||||
|
||||
String nodeId = "LORA_001";
|
||||
float vibrationThreshold = 2.0;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
// Initialize LoRa
|
||||
LoRa.setPins(SS, RST, DIO0);
|
||||
if (!LoRa.begin(868E6)) {
|
||||
Serial.println("Starting LoRa failed!");
|
||||
while (1);
|
||||
}
|
||||
|
||||
// LoRa configuration
|
||||
LoRa.setSpreadingFactor(7);
|
||||
LoRa.setSignalBandwidth(125E3);
|
||||
LoRa.setCodingRate4(5);
|
||||
LoRa.setTxPower(14);
|
||||
|
||||
Serial.println("LoRa Sensor Node Started");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Read sensors
|
||||
float x = readAccelX();
|
||||
float y = readAccelY();
|
||||
float z = readAccelZ();
|
||||
|
||||
// Send sensor data
|
||||
sendVibrationData(x, y, z, vibrationThreshold);
|
||||
|
||||
// Check for incoming commands
|
||||
checkForCommands();
|
||||
|
||||
// Send heartbeat every 5 minutes
|
||||
static unsigned long lastHeartbeat = 0;
|
||||
if (millis() - lastHeartbeat > 300000) {
|
||||
sendHeartbeat();
|
||||
lastHeartbeat = millis();
|
||||
}
|
||||
|
||||
delay(10000); // Send data every 10 seconds
|
||||
}
|
||||
|
||||
void sendVibrationData(float x, float y, float z, float threshold) {
|
||||
String payload = "SENSOR|VIBRATION|" + String(x) + "," + String(y) + "," + String(z) + "|" + String(threshold);
|
||||
|
||||
LoRa.beginPacket();
|
||||
LoRa.print(payload);
|
||||
LoRa.endPacket();
|
||||
|
||||
Serial.println("Sent: " + payload);
|
||||
}
|
||||
|
||||
void sendHeartbeat() {
|
||||
int batteryLevel = readBatteryLevel();
|
||||
int signalStrength = LoRa.packetRssi();
|
||||
unsigned long uptime = millis() / 1000;
|
||||
|
||||
String payload = "HEARTBEAT|" + String(batteryLevel) + "|" + String(signalStrength) + "|" + String(uptime);
|
||||
|
||||
LoRa.beginPacket();
|
||||
LoRa.print(payload);
|
||||
LoRa.endPacket();
|
||||
|
||||
Serial.println("Heartbeat sent: " + payload);
|
||||
}
|
||||
|
||||
void checkForCommands() {
|
||||
int packetSize = LoRa.parsePacket();
|
||||
if (packetSize) {
|
||||
String command = "";
|
||||
while (LoRa.available()) {
|
||||
command += (char)LoRa.read();
|
||||
}
|
||||
|
||||
Serial.println("Received command: " + command);
|
||||
processCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
void processCommand(String command) {
|
||||
// Parse command: COMMAND|action|parameters|command_id
|
||||
int firstPipe = command.indexOf('|');
|
||||
int secondPipe = command.indexOf('|', firstPipe + 1);
|
||||
int thirdPipe = command.indexOf('|', secondPipe + 1);
|
||||
|
||||
if (firstPipe == -1) return;
|
||||
|
||||
String type = command.substring(0, firstPipe);
|
||||
String action = command.substring(firstPipe + 1, secondPipe);
|
||||
String parameters = command.substring(secondPipe + 1, thirdPipe);
|
||||
String commandId = command.substring(thirdPipe + 1);
|
||||
|
||||
if (type == "COMMAND") {
|
||||
if (action == "reboot") {
|
||||
sendAck(commandId, "success", "Rebooting");
|
||||
ESP.restart();
|
||||
} else if (action == "set_threshold") {
|
||||
vibrationThreshold = parameters.toFloat();
|
||||
sendAck(commandId, "success", "Threshold updated");
|
||||
} else if (action == "read_sensors") {
|
||||
float x = readAccelX();
|
||||
float y = readAccelY();
|
||||
float z = readAccelZ();
|
||||
sendVibrationData(x, y, z, vibrationThreshold);
|
||||
sendAck(commandId, "success", "Sensors read");
|
||||
} else {
|
||||
sendAck(commandId, "failed", "Unknown command");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sendAck(String commandId, String status, String message) {
|
||||
String payload = "ACK|" + commandId + "|" + status + "|" + message;
|
||||
|
||||
LoRa.beginPacket();
|
||||
LoRa.print(payload);
|
||||
LoRa.endPacket();
|
||||
|
||||
Serial.println("ACK sent: " + payload);
|
||||
}
|
||||
|
||||
float readAccelX() { return random(-300, 300) / 100.0; }
|
||||
float readAccelY() { return random(-300, 300) / 100.0; }
|
||||
float readAccelZ() { return random(-300, 300) / 100.0; }
|
||||
int readBatteryLevel() { return random(20, 100); }
|
||||
```
|
||||
|
||||
### **Python LoRa Gateway Example:**
|
||||
```python
|
||||
import serial
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
class LoRaGateway:
|
||||
def __init__(self, serial_port='/dev/ttyUSB0', api_url='http://localhost:8000/api/lora'):
|
||||
self.serial_port = serial_port
|
||||
self.api_url = api_url
|
||||
self.gateway_id = 'GATEWAY_001'
|
||||
|
||||
def start_listening(self):
|
||||
try:
|
||||
ser = serial.Serial(self.serial_port, 115200, timeout=1)
|
||||
print(f"LoRa Gateway started on {self.serial_port}")
|
||||
|
||||
while True:
|
||||
if ser.in_waiting > 0:
|
||||
line = ser.readline().decode('utf-8').strip()
|
||||
if line:
|
||||
self.process_lora_message(line)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Gateway error: {e}")
|
||||
|
||||
def process_lora_message(self, raw_message):
|
||||
try:
|
||||
# Parse LoRa message format: NODE_ID:PAYLOAD:RSSI:SNR
|
||||
parts = raw_message.split(':')
|
||||
if len(parts) >= 4:
|
||||
node_id = parts[0]
|
||||
payload = parts[1]
|
||||
rssi = float(parts[2])
|
||||
snr = float(parts[3])
|
||||
|
||||
# Send to backend API
|
||||
self.send_to_backend(node_id, payload, rssi, snr)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Message processing error: {e}")
|
||||
|
||||
def send_to_backend(self, node_id, payload, rssi, snr):
|
||||
try:
|
||||
data = {
|
||||
'node_id': node_id,
|
||||
'gateway_id': self.gateway_id,
|
||||
'payload': payload,
|
||||
'rssi': rssi,
|
||||
'snr': snr,
|
||||
'spreading_factor': 7,
|
||||
'frequency': 868.1,
|
||||
'bandwidth': 125000,
|
||||
'received_at': datetime.now().isoformat(),
|
||||
'metadata': {
|
||||
'gateway_location': 'Building_A',
|
||||
'channel': 0
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(f"{self.api_url}/receive", json=data)
|
||||
|
||||
if response.status_code == 201:
|
||||
print(f"Message sent to backend: {node_id} -> {payload}")
|
||||
else:
|
||||
print(f"Backend error: {response.status_code}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Backend communication error: {e}")
|
||||
|
||||
# Usage
|
||||
if __name__ == "__main__":
|
||||
gateway = LoRaGateway()
|
||||
gateway.start_listening()
|
||||
```
|
||||
|
||||
## 🧪 **Testing API**
|
||||
|
||||
### **Test dengan cURL:**
|
||||
```bash
|
||||
# Test receive LoRa message (vibration sensor)
|
||||
curl -X POST http://localhost:8000/api/lora/receive \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"node_id": "LORA_001",
|
||||
"gateway_id": "GATEWAY_001",
|
||||
"payload": "SENSOR|VIBRATION|2.5,3.2,1.8|2.0",
|
||||
"rssi": -82.5,
|
||||
"snr": 7.8,
|
||||
"spreading_factor": 7,
|
||||
"frequency": 868.1
|
||||
}'
|
||||
|
||||
# Test receive LoRa message (PIR sensor)
|
||||
curl -X POST http://localhost:8000/api/lora/receive \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"node_id": "LORA_002",
|
||||
"payload": "SENSOR|PIR|1|85|120|front",
|
||||
"rssi": -88.2,
|
||||
"snr": 6.5
|
||||
}'
|
||||
|
||||
# Test receive LoRa message (door access)
|
||||
curl -X POST http://localhost:8000/api/lora/receive \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"node_id": "LORA_003",
|
||||
"payload": "SENSOR|DOOR|1|EMP-1234|45|keycard|front_door",
|
||||
"rssi": -75.8,
|
||||
"snr": 9.2
|
||||
}'
|
||||
|
||||
# Send command to LoRa node
|
||||
curl -X POST http://localhost:8000/api/lora/send-command \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"node_id": "LORA_001",
|
||||
"action": "set_threshold",
|
||||
"parameters": ["2.5", "vibration"],
|
||||
"priority": "high"
|
||||
}'
|
||||
|
||||
# Send config to LoRa node
|
||||
curl -X POST http://localhost:8000/api/lora/send-config \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"node_id": "LORA_001",
|
||||
"parameter": "sleep_time",
|
||||
"value": "300"
|
||||
}'
|
||||
|
||||
# Get LoRa messages
|
||||
curl "http://localhost:8000/api/lora/messages?node_id=LORA_001&limit=10"
|
||||
|
||||
# Get LoRa statistics
|
||||
curl "http://localhost:8000/api/lora/statistics?hours=24"
|
||||
```
|
||||
|
||||
## 📈 **LoRa Dashboard Features**
|
||||
|
||||
### **Real-time Monitoring:**
|
||||
- Active LoRa nodes status
|
||||
- Signal quality per node
|
||||
- Message throughput
|
||||
- Battery levels
|
||||
- Network coverage map
|
||||
|
||||
### **Communication Management:**
|
||||
- Send commands to nodes
|
||||
- Configure node parameters
|
||||
- Monitor acknowledgments
|
||||
- Retry failed transmissions
|
||||
|
||||
## 🔒 **Security & Best Practices**
|
||||
|
||||
### **LoRa Security:**
|
||||
- **Payload Encryption**: Encrypt sensitive sensor data
|
||||
- **Node Authentication**: Validate node IDs
|
||||
- **Message Integrity**: Check message checksums
|
||||
- **Replay Protection**: Prevent message replay attacks
|
||||
|
||||
### **Network Optimization:**
|
||||
- **Adaptive Data Rate (ADR)**: Optimize SF and TX power
|
||||
- **Duty Cycle Compliance**: Respect regional regulations
|
||||
- **Gateway Load Balancing**: Distribute traffic across gateways
|
||||
- **Message Prioritization**: Handle urgent messages first
|
||||
|
||||
## 🚀 **Deployment & Configuration**
|
||||
|
||||
### **Environment Variables:**
|
||||
```env
|
||||
# LoRa Gateway Settings
|
||||
LORA_GATEWAY_URL=http://localhost:8080/lora/send
|
||||
LORA_DEFAULT_FREQUENCY=868.1
|
||||
LORA_DEFAULT_SF=7
|
||||
LORA_DEFAULT_TX_POWER=14
|
||||
|
||||
# LoRa Network Settings
|
||||
LORA_NETWORK_ID=1
|
||||
LORA_APP_KEY=your_app_key_here
|
||||
LORA_DEVICE_EUI_PREFIX=LORA_
|
||||
|
||||
# Processing Settings
|
||||
LORA_AUTO_PROCESS=true
|
||||
LORA_RETRY_FAILED_MESSAGES=true
|
||||
LORA_MAX_RETRY_ATTEMPTS=3
|
||||
```
|
||||
|
||||
### **Konfigurasi LoRa Network:**
|
||||
```php
|
||||
// config/lora.php
|
||||
return [
|
||||
'gateway' => [
|
||||
'url' => env('LORA_GATEWAY_URL', 'http://localhost:8080/lora/send'),
|
||||
'timeout' => 10,
|
||||
'retry_attempts' => 3
|
||||
],
|
||||
'network' => [
|
||||
'frequency' => env('LORA_DEFAULT_FREQUENCY', 868.1),
|
||||
'spreading_factor' => env('LORA_DEFAULT_SF', 7),
|
||||
'tx_power' => env('LORA_DEFAULT_TX_POWER', 14),
|
||||
'bandwidth' => 125000
|
||||
],
|
||||
'processing' => [
|
||||
'auto_process' => env('LORA_AUTO_PROCESS', true),
|
||||
'batch_size' => 100,
|
||||
'retry_failed' => env('LORA_RETRY_FAILED_MESSAGES', true)
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
## 📞 **Troubleshooting**
|
||||
|
||||
### **Common Issues:**
|
||||
1. **Poor signal quality**: Adjust SF or TX power
|
||||
2. **Message loss**: Check gateway connectivity
|
||||
3. **Processing delays**: Increase batch processing
|
||||
4. **Node offline**: Check battery and coverage
|
||||
|
||||
### **Monitoring Commands:**
|
||||
```bash
|
||||
# Check LoRa logs
|
||||
tail -f storage/logs/laravel.log | grep LoRa
|
||||
|
||||
# Process unprocessed messages
|
||||
curl -X POST http://localhost:8000/api/lora/process-messages
|
||||
|
||||
# Check node statistics
|
||||
curl "http://localhost:8000/api/lora/statistics?node_id=LORA_001"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Backend Smart Rack LoRa Communication siap digunakan!** 🎉
|
||||
|
||||
Sistem akan otomatis:
|
||||
- ✅ Menerima data sensor via LoRa dengan jangkauan jauh
|
||||
- ✅ Parsing dan processing data sensor secara otomatis
|
||||
- ✅ Mengirim command dan konfigurasi ke LoRa nodes
|
||||
- ✅ Monitoring kualitas sinyal dan status nodes
|
||||
- ✅ Integrasi seamless dengan sensor vibration, PIR, dan door access
|
||||
- ✅ Menyediakan statistik dan analytics LoRa network
|
||||
- ✅ Support multiple LoRa gateways dan nodes
|
||||
- ✅ Battery monitoring dan low power management
|
||||
|
|
@ -0,0 +1,473 @@
|
|||
# 👁️ Smart Rack PIR Motion Detection API
|
||||
|
||||
Backend API untuk menerima data sensor PIR (Passive Infrared) dan mengirim notifikasi otomatis jika terdeteksi gerakan tidak wajar atau mencurigakan.
|
||||
|
||||
## 📡 **API Endpoints**
|
||||
|
||||
### 1. **Kirim Data Sensor PIR**
|
||||
```http
|
||||
POST /api/pir/data
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": 1,
|
||||
"motion_detected": true,
|
||||
"motion_intensity": 75,
|
||||
"duration_seconds": 120,
|
||||
"detection_zone": "front",
|
||||
"motion_start": "2026-04-28T10:30:00Z",
|
||||
"motion_end": "2026-04-28T10:32:00Z",
|
||||
"metadata": {
|
||||
"sensor_type": "PIR_HC-SR501",
|
||||
"firmware_version": "2.1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response Normal (Jam Kerja):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "PIR data received successfully",
|
||||
"data": {
|
||||
"id": 125,
|
||||
"motion_detected": true,
|
||||
"motion_type": "normal",
|
||||
"is_suspicious": false,
|
||||
"is_authorized_time": true,
|
||||
"alert_sent": false,
|
||||
"recorded_at": "2026-04-28T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response Mencurigakan (Alert Triggered):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "PIR data received successfully",
|
||||
"data": {
|
||||
"id": 126,
|
||||
"motion_detected": true,
|
||||
"motion_type": "unauthorized",
|
||||
"is_suspicious": true,
|
||||
"is_authorized_time": false,
|
||||
"alert_sent": true,
|
||||
"recorded_at": "2026-04-28T22:15:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Ambil Data PIR Terbaru**
|
||||
```http
|
||||
GET /api/pir/readings?device_id=1&limit=50&motion_type=suspicious&detection_zone=front
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 126,
|
||||
"device_id": 1,
|
||||
"motion_detected": true,
|
||||
"motion_intensity": 85,
|
||||
"duration_seconds": 180,
|
||||
"is_authorized_time": false,
|
||||
"is_suspicious": true,
|
||||
"motion_type": "unauthorized",
|
||||
"detection_zone": "front",
|
||||
"motion_start": "2026-04-28T22:15:00Z",
|
||||
"motion_end": "2026-04-28T22:18:00Z",
|
||||
"recorded_at": "2026-04-28T22:15:00Z",
|
||||
"device": {
|
||||
"id": 1,
|
||||
"name": "PIR Rak Server A",
|
||||
"location": "Ruang Server"
|
||||
}
|
||||
}
|
||||
],
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Ambil Statistik Gerakan**
|
||||
```http
|
||||
GET /api/pir/statistics?device_id=1&hours=24
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"total_readings": 200,
|
||||
"motion_detected_count": 45,
|
||||
"suspicious_count": 8,
|
||||
"unauthorized_count": 3,
|
||||
"normal_count": 34,
|
||||
"motion_percentage": 22.5,
|
||||
"suspicious_percentage": 4.0,
|
||||
"avg_intensity": 65.5,
|
||||
"avg_duration": 85.2,
|
||||
"max_intensity": 95,
|
||||
"max_duration": 300,
|
||||
"latest_motion": {
|
||||
"id": 126,
|
||||
"motion_type": "unauthorized",
|
||||
"intensity": 85,
|
||||
"recorded_at": "2026-04-28T22:15:00Z"
|
||||
},
|
||||
"detection_zones": {
|
||||
"front": 15,
|
||||
"back": 8,
|
||||
"side": 12,
|
||||
"center": 10
|
||||
}
|
||||
},
|
||||
"period_hours": 24
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **Test Notifikasi PIR**
|
||||
```http
|
||||
POST /api/test-pir-notification
|
||||
```
|
||||
|
||||
## 🚨 **Sistem Deteksi & Alert**
|
||||
|
||||
### **Tipe Gerakan:**
|
||||
- **none**: Tidak ada gerakan terdeteksi
|
||||
- **normal**: Gerakan normal dalam jam kerja
|
||||
- **suspicious**: Gerakan mencurigakan (intensitas/durasi tinggi)
|
||||
- **unauthorized**: Gerakan di luar jam kerja
|
||||
|
||||
### **Kondisi Mencurigakan:**
|
||||
1. **Gerakan di luar jam kerja** (Senin-Jumat 08:00-17:00)
|
||||
2. **Intensitas tinggi** (>80%)
|
||||
3. **Durasi lama** (>300 detik)
|
||||
4. **Zona sensitif** dengan aktivitas tidak normal
|
||||
|
||||
### **Jam Kerja (Authorized Time):**
|
||||
- **Senin - Jumat**: 08:00 - 17:00
|
||||
- **Sabtu - Minggu**: Tidak ada jam kerja (semua gerakan dianggap unauthorized)
|
||||
|
||||
### **Zona Deteksi:**
|
||||
- **front**: Depan rak (akses utama)
|
||||
- **back**: Belakang rak (area maintenance)
|
||||
- **side**: Samping rak (area sekunder)
|
||||
- **center**: Tengah ruangan (area umum)
|
||||
|
||||
## 🔔 **Sistem Notifikasi**
|
||||
|
||||
### **Priority Level:**
|
||||
- **high**: Gerakan unauthorized (di luar jam kerja)
|
||||
- **medium**: Gerakan suspicious (dalam jam kerja tapi mencurigakan)
|
||||
- **low**: Gerakan normal dengan intensitas tinggi
|
||||
|
||||
### **Jenis Notifikasi:**
|
||||
1. **📧 Email** - Semua alert
|
||||
2. **📱 SMS** - Hanya priority HIGH (unauthorized)
|
||||
3. **🔔 Push Notification** - Real-time ke security app
|
||||
4. **🔗 Webhook** - Ke sistem CCTV/security eksternal
|
||||
5. **📝 Security Log** - Audit trail lengkap
|
||||
|
||||
### **Contoh Notifikasi:**
|
||||
```
|
||||
🚨 SECURITY ALERT: Gerakan unauthorized pada PIR Rak Server A zona front (luar jam kerja)
|
||||
Intensitas: 85%. Durasi: 180s
|
||||
Waktu: 2026-04-28 22:15:00
|
||||
```
|
||||
|
||||
## 📊 **Database Schema**
|
||||
|
||||
### **pir_readings table:**
|
||||
```sql
|
||||
- id (bigint, primary key)
|
||||
- device_id (foreign key to devices)
|
||||
- motion_detected (boolean) - Gerakan terdeteksi
|
||||
- motion_intensity (integer 0-100) - Intensitas gerakan
|
||||
- duration_seconds (integer) - Durasi gerakan
|
||||
- is_authorized_time (boolean) - Dalam jam kerja
|
||||
- is_suspicious (boolean) - Gerakan mencurigakan
|
||||
- motion_type (enum: none, normal, suspicious, unauthorized)
|
||||
- detection_zone (enum: front, back, side, center)
|
||||
- metadata (json) - Data tambahan sensor
|
||||
- motion_start (timestamp) - Waktu mulai gerakan
|
||||
- motion_end (timestamp) - Waktu selesai gerakan
|
||||
- recorded_at (timestamp) - Waktu pembacaan
|
||||
- created_at, updated_at
|
||||
```
|
||||
|
||||
## 🔌 **Integrasi IoT Device**
|
||||
|
||||
### **Arduino/ESP32 Example:**
|
||||
```cpp
|
||||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
int pirPin = 2;
|
||||
int motionStartTime = 0;
|
||||
bool motionActive = false;
|
||||
|
||||
void setup() {
|
||||
pinMode(pirPin, INPUT);
|
||||
Serial.begin(115200);
|
||||
// WiFi setup...
|
||||
}
|
||||
|
||||
void loop() {
|
||||
int pirState = digitalRead(pirPin);
|
||||
|
||||
if (pirState == HIGH && !motionActive) {
|
||||
// Motion started
|
||||
motionActive = true;
|
||||
motionStartTime = millis();
|
||||
Serial.println("Motion detected!");
|
||||
|
||||
} else if (pirState == LOW && motionActive) {
|
||||
// Motion ended
|
||||
motionActive = false;
|
||||
int duration = (millis() - motionStartTime) / 1000;
|
||||
|
||||
sendPirData(true, 75, duration, "front");
|
||||
Serial.println("Motion ended, data sent");
|
||||
}
|
||||
|
||||
delay(100);
|
||||
}
|
||||
|
||||
void sendPirData(bool detected, int intensity, int duration, String zone) {
|
||||
HTTPClient http;
|
||||
http.begin("http://your-server.com/api/pir/data");
|
||||
http.addHeader("Content-Type", "application/json");
|
||||
|
||||
StaticJsonDocument<300> doc;
|
||||
doc["device_id"] = 1;
|
||||
doc["motion_detected"] = detected;
|
||||
doc["motion_intensity"] = intensity;
|
||||
doc["duration_seconds"] = duration;
|
||||
doc["detection_zone"] = zone;
|
||||
|
||||
String jsonString;
|
||||
serializeJson(doc, jsonString);
|
||||
|
||||
int httpResponseCode = http.POST(jsonString);
|
||||
|
||||
if (httpResponseCode > 0) {
|
||||
String response = http.getString();
|
||||
Serial.println("Response: " + response);
|
||||
}
|
||||
|
||||
http.end();
|
||||
}
|
||||
```
|
||||
|
||||
### **Python Example:**
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
import RPi.GPIO as GPIO
|
||||
|
||||
PIR_PIN = 18
|
||||
|
||||
def setup_pir():
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(PIR_PIN, GPIO.IN)
|
||||
|
||||
def send_pir_data(device_id, motion_detected, intensity, duration, zone):
|
||||
url = "http://your-server.com/api/pir/data"
|
||||
|
||||
data = {
|
||||
"device_id": device_id,
|
||||
"motion_detected": motion_detected,
|
||||
"motion_intensity": intensity,
|
||||
"duration_seconds": duration,
|
||||
"detection_zone": zone,
|
||||
"metadata": {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"sensor_type": "PIR_HC-SR501",
|
||||
"gpio_pin": PIR_PIN
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, json=data)
|
||||
return response.json()
|
||||
|
||||
def monitor_motion():
|
||||
motion_start = None
|
||||
|
||||
while True:
|
||||
if GPIO.input(PIR_PIN):
|
||||
if motion_start is None:
|
||||
motion_start = time.time()
|
||||
print("Motion detected!")
|
||||
else:
|
||||
if motion_start is not None:
|
||||
duration = int(time.time() - motion_start)
|
||||
intensity = min(100, duration * 2) # Simple intensity calculation
|
||||
|
||||
result = send_pir_data(1, True, intensity, duration, "front")
|
||||
print(f"Motion data sent: {result}")
|
||||
|
||||
motion_start = None
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
# Usage
|
||||
setup_pir()
|
||||
monitor_motion()
|
||||
```
|
||||
|
||||
## 🧪 **Testing API**
|
||||
|
||||
### **Test dengan cURL:**
|
||||
```bash
|
||||
# Test normal motion (jam kerja)
|
||||
curl -X POST http://localhost:8000/api/pir/data \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_id": 1,
|
||||
"motion_detected": true,
|
||||
"motion_intensity": 60,
|
||||
"duration_seconds": 45,
|
||||
"detection_zone": "front"
|
||||
}'
|
||||
|
||||
# Test suspicious motion (intensitas tinggi)
|
||||
curl -X POST http://localhost:8000/api/pir/data \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_id": 1,
|
||||
"motion_detected": true,
|
||||
"motion_intensity": 90,
|
||||
"duration_seconds": 350,
|
||||
"detection_zone": "back"
|
||||
}'
|
||||
|
||||
# Test unauthorized motion (simulasi luar jam kerja)
|
||||
curl -X POST http://localhost:8000/api/pir/data \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_id": 1,
|
||||
"motion_detected": true,
|
||||
"motion_intensity": 75,
|
||||
"duration_seconds": 120,
|
||||
"detection_zone": "front",
|
||||
"metadata": {
|
||||
"simulated_time": "2026-04-28T22:30:00Z"
|
||||
}
|
||||
}'
|
||||
|
||||
# Get statistics
|
||||
curl "http://localhost:8000/api/pir/statistics?device_id=1&hours=24"
|
||||
|
||||
# Test PIR notification
|
||||
curl -X POST http://localhost:8000/api/test-pir-notification
|
||||
```
|
||||
|
||||
## 📈 **Security Dashboard**
|
||||
|
||||
### **Real-time Monitoring:**
|
||||
- Current motion status per zone
|
||||
- Alert count by priority (24h)
|
||||
- Unauthorized access attempts
|
||||
- Motion patterns analysis
|
||||
- Device status monitoring
|
||||
|
||||
### **Security Reports:**
|
||||
- Daily motion summary
|
||||
- Unauthorized access log
|
||||
- Peak activity hours
|
||||
- Zone-based statistics
|
||||
- Alert response times
|
||||
|
||||
## 🔒 **Security Features**
|
||||
|
||||
### **Smart Detection:**
|
||||
- **Time-based analysis**: Jam kerja vs luar jam kerja
|
||||
- **Pattern recognition**: Gerakan berulang mencurigakan
|
||||
- **Zone-based rules**: Aturan berbeda per zona
|
||||
- **Intensity thresholds**: Deteksi gerakan abnormal
|
||||
|
||||
### **Multi-level Alerts:**
|
||||
- **Immediate**: Push notification real-time
|
||||
- **Escalation**: SMS untuk alert critical
|
||||
- **Logging**: Audit trail lengkap
|
||||
- **Integration**: Webhook ke sistem CCTV
|
||||
|
||||
## 🚀 **Deployment & Configuration**
|
||||
|
||||
### **Environment Variables:**
|
||||
```env
|
||||
# PIR Settings
|
||||
PIR_WEBHOOK_URL=https://your-cctv-system.com/alerts
|
||||
WORK_START_HOUR=8
|
||||
WORK_END_HOUR=17
|
||||
SUSPICIOUS_INTENSITY_THRESHOLD=80
|
||||
SUSPICIOUS_DURATION_THRESHOLD=300
|
||||
|
||||
# Notification settings
|
||||
SECURITY_EMAIL=security@yourcompany.com
|
||||
SECURITY_SMS_NUMBER=+6281234567890
|
||||
```
|
||||
|
||||
### **Konfigurasi Jam Kerja:**
|
||||
```php
|
||||
// config/pir.php
|
||||
return [
|
||||
'work_hours' => [
|
||||
'start' => 8, // 08:00
|
||||
'end' => 17, // 17:00
|
||||
'weekdays_only' => true
|
||||
],
|
||||
'thresholds' => [
|
||||
'suspicious_intensity' => 80,
|
||||
'suspicious_duration' => 300,
|
||||
'high_intensity' => 90
|
||||
],
|
||||
'zones' => [
|
||||
'front' => ['priority' => 'high'],
|
||||
'back' => ['priority' => 'medium'],
|
||||
'side' => ['priority' => 'medium'],
|
||||
'center' => ['priority' => 'low']
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
## 📞 **Troubleshooting**
|
||||
|
||||
### **Common Issues:**
|
||||
1. **False positives**: Adjust intensity threshold
|
||||
2. **Missing alerts**: Check notification service logs
|
||||
3. **Wrong time detection**: Verify server timezone
|
||||
4. **High CPU usage**: Implement data cleanup job
|
||||
|
||||
### **Monitoring Commands:**
|
||||
```bash
|
||||
# Check PIR logs
|
||||
tail -f storage/logs/laravel.log | grep PIR
|
||||
|
||||
# Database cleanup
|
||||
php artisan pir:cleanup-old-data
|
||||
|
||||
# Test notification system
|
||||
php artisan pir:test-notifications
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Backend Smart Rack PIR Motion Detection siap digunakan!** 🎉
|
||||
|
||||
Sistem akan otomatis:
|
||||
- ✅ Menerima data sensor PIR real-time
|
||||
- ✅ Mendeteksi gerakan mencurigakan berdasarkan waktu, intensitas, dan durasi
|
||||
- ✅ Membedakan jam kerja vs luar jam kerja
|
||||
- ✅ Mengirim notifikasi multi-channel (email, SMS, push, webhook)
|
||||
- ✅ Menyimpan audit trail lengkap untuk analisis keamanan
|
||||
- ✅ Menyediakan statistik dan laporan keamanan
|
||||
- ✅ Integrasi mudah dengan sistem CCTV dan security lainnya
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
|
||||
|
||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
|
||||
## Laravel Sponsors
|
||||
|
||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||
|
||||
### Premium Partners
|
||||
|
||||
- **[Vehikl](https://vehikl.com)**
|
||||
- **[Tighten Co.](https://tighten.co)**
|
||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||
- **[64 Robots](https://64robots.com)**
|
||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
||||
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||
- **[Redberry](https://redberry.international/laravel-development)**
|
||||
- **[Active Logic](https://activelogic.com)**
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
|
|
@ -0,0 +1,560 @@
|
|||
# 🚪 Smart Rack Door Access (Reed Switch) API
|
||||
|
||||
Backend API untuk menerima data sensor Reed Switch yang mendeteksi buka tutup pintu dan mengirim notifikasi otomatis jika terdeteksi akses mencurigakan atau tidak sah.
|
||||
|
||||
## 📡 **API Endpoints**
|
||||
|
||||
### 1. **Kirim Data Sensor Reed Switch**
|
||||
```http
|
||||
POST /api/door-access/data
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": 1,
|
||||
"door_opened": true,
|
||||
"access_method": "keycard",
|
||||
"user_id_card": "EMP-1234",
|
||||
"duration_seconds": 45,
|
||||
"door_location": "front_door",
|
||||
"is_forced_entry": false,
|
||||
"door_opened_at": "2026-04-28T10:30:00Z",
|
||||
"door_closed_at": "2026-04-28T10:30:45Z",
|
||||
"metadata": {
|
||||
"sensor_type": "Reed_Switch_Magnetic",
|
||||
"firmware_version": "3.1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response Authorized (Jam Kerja + ID Card):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Door access data received successfully",
|
||||
"data": {
|
||||
"id": 127,
|
||||
"door_opened": true,
|
||||
"access_type": "authorized",
|
||||
"is_authorized": true,
|
||||
"is_suspicious": false,
|
||||
"alert_sent": false,
|
||||
"priority": "info",
|
||||
"recorded_at": "2026-04-28T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response Unauthorized (Alert Triggered):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Door access data received successfully",
|
||||
"data": {
|
||||
"id": 128,
|
||||
"door_opened": true,
|
||||
"access_type": "forced_entry",
|
||||
"is_authorized": false,
|
||||
"is_suspicious": true,
|
||||
"alert_sent": true,
|
||||
"priority": "critical",
|
||||
"recorded_at": "2026-04-28T22:15:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Ambil Data Door Access Terbaru**
|
||||
```http
|
||||
GET /api/door-access/readings?device_id=1&limit=50&access_type=unauthorized&door_location=front_door&suspicious=true
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 128,
|
||||
"device_id": 1,
|
||||
"door_opened": true,
|
||||
"is_authorized_access": false,
|
||||
"access_type": "forced_entry",
|
||||
"access_method": "force",
|
||||
"user_id_card": null,
|
||||
"duration_seconds": 180,
|
||||
"is_suspicious": true,
|
||||
"door_location": "front_door",
|
||||
"is_forced_entry": true,
|
||||
"door_opened_at": "2026-04-28T22:15:00Z",
|
||||
"door_closed_at": "2026-04-28T22:18:00Z",
|
||||
"recorded_at": "2026-04-28T22:15:00Z",
|
||||
"device": {
|
||||
"id": 1,
|
||||
"name": "Reed Switch Pintu Utama",
|
||||
"location": "Ruang Server"
|
||||
}
|
||||
}
|
||||
],
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Ambil Statistik Door Access**
|
||||
```http
|
||||
GET /api/door-access/statistics?device_id=1&hours=24
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"total_access_attempts": 85,
|
||||
"door_opened_count": 78,
|
||||
"authorized_count": 65,
|
||||
"unauthorized_count": 13,
|
||||
"suspicious_count": 8,
|
||||
"forced_entry_count": 2,
|
||||
"authorized_percentage": 76.47,
|
||||
"suspicious_percentage": 9.41,
|
||||
"avg_duration": 42.5,
|
||||
"max_duration": 180,
|
||||
"latest_access": {
|
||||
"id": 128,
|
||||
"access_type": "forced_entry",
|
||||
"user_id_card": null,
|
||||
"recorded_at": "2026-04-28T22:15:00Z"
|
||||
},
|
||||
"access_types": {
|
||||
"authorized": 65,
|
||||
"unauthorized": 8,
|
||||
"after_hours": 5,
|
||||
"forced_entry": 2,
|
||||
"emergency": 3,
|
||||
"maintenance": 2
|
||||
},
|
||||
"door_locations": {
|
||||
"front_door": 45,
|
||||
"back_door": 20,
|
||||
"side_door": 15,
|
||||
"main_entrance": 5
|
||||
},
|
||||
"access_methods": {
|
||||
"keycard": 60,
|
||||
"manual": 15,
|
||||
"force": 2,
|
||||
"emergency": 3,
|
||||
"maintenance": 2,
|
||||
"unknown": 3
|
||||
}
|
||||
},
|
||||
"period_hours": 24
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **Test Notifikasi Door Access**
|
||||
```http
|
||||
POST /api/test-door-access-notification
|
||||
```
|
||||
|
||||
## 🚨 **Sistem Deteksi & Alert**
|
||||
|
||||
### **Tipe Akses:**
|
||||
- **authorized**: Akses sah dengan ID card dalam jam kerja
|
||||
- **unauthorized**: Akses tanpa ID card atau ID tidak valid
|
||||
- **after_hours**: Akses di luar jam kerja (07:00-18:00)
|
||||
- **forced_entry**: Paksa masuk (CRITICAL ALERT)
|
||||
- **emergency**: Akses darurat (diizinkan kapan saja)
|
||||
- **maintenance**: Akses maintenance dalam jam kerja
|
||||
|
||||
### **Kondisi Mencurigakan:**
|
||||
1. **Forced Entry** - Paksa masuk tanpa ID card
|
||||
2. **Akses luar jam kerja** tanpa ID card yang valid
|
||||
3. **Durasi pintu terbuka terlalu lama** (>180 detik)
|
||||
4. **Akses tanpa ID card** di jam kerja normal
|
||||
5. **ID card tidak valid** (format bukan EMP-XXXX)
|
||||
|
||||
### **Jam Kerja (Authorized Time):**
|
||||
- **Senin - Jumat**: 07:00 - 18:00
|
||||
- **Sabtu - Minggu**: Semua akses dianggap after_hours
|
||||
|
||||
### **Lokasi Pintu:**
|
||||
- **front_door**: Pintu depan (akses utama)
|
||||
- **back_door**: Pintu belakang (akses service)
|
||||
- **side_door**: Pintu samping (akses sekunder)
|
||||
- **main_entrance**: Pintu masuk utama gedung
|
||||
|
||||
### **Metode Akses:**
|
||||
- **keycard**: Menggunakan kartu akses (paling aman)
|
||||
- **manual**: Buka manual dengan kunci
|
||||
- **force**: Paksa masuk (trigger alert)
|
||||
- **emergency**: Akses darurat
|
||||
- **maintenance**: Akses maintenance
|
||||
- **unknown**: Metode tidak diketahui
|
||||
|
||||
## 🔔 **Sistem Notifikasi**
|
||||
|
||||
### **Priority Level:**
|
||||
- **critical**: Forced entry (paksa masuk)
|
||||
- **high**: Unauthorized access, after hours tanpa ID
|
||||
- **medium**: Emergency access, suspicious activity
|
||||
- **low**: Maintenance access
|
||||
- **info**: Authorized access normal
|
||||
|
||||
### **Jenis Notifikasi:**
|
||||
1. **📧 Email** - Semua alert
|
||||
2. **📱 SMS** - Priority CRITICAL & HIGH (forced entry, unauthorized)
|
||||
3. **🔔 Push Notification** - Real-time ke security app
|
||||
4. **🔗 Webhook** - Ke sistem access control eksternal
|
||||
5. **📝 Security Log** - Audit trail lengkap dengan user tracking
|
||||
|
||||
### **Contoh Notifikasi:**
|
||||
```
|
||||
🚨 SECURITY BREACH: PAKSA MASUK pada Reed Switch Pintu Utama di front_door
|
||||
Durasi: 180s. Waktu: 2026-04-28 22:15:00
|
||||
|
||||
🚨 DOOR ALERT: Akses unauthorized pada Reed Switch Pintu Utama di front_door tanpa ID
|
||||
Metode: manual. Durasi: 45s. Waktu: 2026-04-28 22:30:00
|
||||
```
|
||||
|
||||
## 📊 **Database Schema**
|
||||
|
||||
### **door_access_readings table:**
|
||||
```sql
|
||||
- id (bigint, primary key)
|
||||
- device_id (foreign key to devices)
|
||||
- door_opened (boolean) - Status pintu terbuka
|
||||
- is_authorized_access (boolean) - Akses sah/tidak
|
||||
- access_type (enum) - Tipe akses
|
||||
- access_method (enum) - Metode akses
|
||||
- user_id_card (string) - ID card pengguna
|
||||
- duration_seconds (integer) - Durasi pintu terbuka
|
||||
- is_suspicious (boolean) - Akses mencurigakan
|
||||
- door_location (enum) - Lokasi pintu
|
||||
- is_forced_entry (boolean) - Paksa masuk
|
||||
- metadata (json) - Data tambahan sensor
|
||||
- door_opened_at (timestamp) - Waktu pintu dibuka
|
||||
- door_closed_at (timestamp) - Waktu pintu ditutup
|
||||
- recorded_at (timestamp) - Waktu pembacaan
|
||||
- created_at, updated_at
|
||||
```
|
||||
|
||||
## 🔌 **Integrasi IoT Device**
|
||||
|
||||
### **Arduino/ESP32 Example:**
|
||||
```cpp
|
||||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <RFID.h>
|
||||
|
||||
int reedSwitchPin = 2;
|
||||
int rfidReaderPin = 4;
|
||||
bool doorState = false;
|
||||
unsigned long doorOpenTime = 0;
|
||||
String lastIdCard = "";
|
||||
|
||||
void setup() {
|
||||
pinMode(reedSwitchPin, INPUT_PULLUP);
|
||||
Serial.begin(115200);
|
||||
// WiFi setup...
|
||||
// RFID setup...
|
||||
}
|
||||
|
||||
void loop() {
|
||||
bool currentDoorState = !digitalRead(reedSwitchPin); // Reed switch logic
|
||||
|
||||
if (currentDoorState != doorState) {
|
||||
doorState = currentDoorState;
|
||||
|
||||
if (doorState) {
|
||||
// Door opened
|
||||
doorOpenTime = millis();
|
||||
lastIdCard = readRFIDCard(); // Read ID card if available
|
||||
Serial.println("Door opened, ID: " + lastIdCard);
|
||||
|
||||
} else {
|
||||
// Door closed
|
||||
unsigned long duration = (millis() - doorOpenTime) / 1000;
|
||||
|
||||
sendDoorAccessData(true, lastIdCard, duration, "front_door");
|
||||
Serial.println("Door closed, data sent");
|
||||
|
||||
lastIdCard = "";
|
||||
}
|
||||
}
|
||||
|
||||
delay(100);
|
||||
}
|
||||
|
||||
String readRFIDCard() {
|
||||
// Implementasi baca RFID card
|
||||
// Return format: "EMP-1234" atau "" jika tidak ada
|
||||
return "EMP-1234"; // Simulasi
|
||||
}
|
||||
|
||||
void sendDoorAccessData(bool opened, String idCard, int duration, String location) {
|
||||
HTTPClient http;
|
||||
http.begin("http://your-server.com/api/door-access/data");
|
||||
http.addHeader("Content-Type", "application/json");
|
||||
|
||||
StaticJsonDocument<400> doc;
|
||||
doc["device_id"] = 1;
|
||||
doc["door_opened"] = opened;
|
||||
doc["access_method"] = idCard.length() > 0 ? "keycard" : "manual";
|
||||
doc["user_id_card"] = idCard.length() > 0 ? idCard : nullptr;
|
||||
doc["duration_seconds"] = duration;
|
||||
doc["door_location"] = location;
|
||||
doc["is_forced_entry"] = (idCard.length() == 0 && duration > 10);
|
||||
|
||||
String jsonString;
|
||||
serializeJson(doc, jsonString);
|
||||
|
||||
int httpResponseCode = http.POST(jsonString);
|
||||
|
||||
if (httpResponseCode > 0) {
|
||||
String response = http.getString();
|
||||
Serial.println("Response: " + response);
|
||||
}
|
||||
|
||||
http.end();
|
||||
}
|
||||
```
|
||||
|
||||
### **Python Example (Raspberry Pi):**
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
import RPi.GPIO as GPIO
|
||||
from mfrc522 import SimpleMFRC522
|
||||
|
||||
REED_SWITCH_PIN = 18
|
||||
reader = SimpleMFRC522()
|
||||
|
||||
def setup_reed_switch():
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(REED_SWITCH_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
|
||||
def read_rfid_card():
|
||||
try:
|
||||
id, text = reader.read_no_block()
|
||||
if id:
|
||||
return f"EMP-{id % 10000:04d}" # Format ke EMP-XXXX
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
def send_door_access_data(device_id, door_opened, id_card, duration, location):
|
||||
url = "http://your-server.com/api/door-access/data"
|
||||
|
||||
access_method = "keycard" if id_card else "manual"
|
||||
is_forced = not id_card and duration > 10
|
||||
|
||||
data = {
|
||||
"device_id": device_id,
|
||||
"door_opened": door_opened,
|
||||
"access_method": access_method,
|
||||
"user_id_card": id_card,
|
||||
"duration_seconds": duration,
|
||||
"door_location": location,
|
||||
"is_forced_entry": is_forced,
|
||||
"metadata": {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"sensor_type": "Reed_Switch_Magnetic",
|
||||
"gpio_pin": REED_SWITCH_PIN
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, json=data)
|
||||
return response.json()
|
||||
|
||||
def monitor_door_access():
|
||||
door_open = False
|
||||
door_open_time = None
|
||||
current_id_card = None
|
||||
|
||||
while True:
|
||||
# Read reed switch (False = door closed, True = door open)
|
||||
door_state = not GPIO.input(REED_SWITCH_PIN)
|
||||
|
||||
if door_state and not door_open:
|
||||
# Door just opened
|
||||
door_open = True
|
||||
door_open_time = time.time()
|
||||
current_id_card = read_rfid_card()
|
||||
print(f"Door opened, ID Card: {current_id_card}")
|
||||
|
||||
elif not door_state and door_open:
|
||||
# Door just closed
|
||||
door_open = False
|
||||
duration = int(time.time() - door_open_time) if door_open_time else 0
|
||||
|
||||
result = send_door_access_data(1, True, current_id_card, duration, "front_door")
|
||||
print(f"Door access data sent: {result}")
|
||||
|
||||
current_id_card = None
|
||||
door_open_time = None
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
# Usage
|
||||
setup_reed_switch()
|
||||
monitor_door_access()
|
||||
```
|
||||
|
||||
## 🧪 **Testing API**
|
||||
|
||||
### **Test dengan cURL:**
|
||||
```bash
|
||||
# Test authorized access (jam kerja + ID card)
|
||||
curl -X POST http://localhost:8000/api/door-access/data \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_id": 1,
|
||||
"door_opened": true,
|
||||
"access_method": "keycard",
|
||||
"user_id_card": "EMP-1234",
|
||||
"duration_seconds": 30,
|
||||
"door_location": "front_door"
|
||||
}'
|
||||
|
||||
# Test unauthorized access (tanpa ID card)
|
||||
curl -X POST http://localhost:8000/api/door-access/data \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_id": 1,
|
||||
"door_opened": true,
|
||||
"access_method": "manual",
|
||||
"duration_seconds": 60,
|
||||
"door_location": "back_door"
|
||||
}'
|
||||
|
||||
# Test forced entry (CRITICAL ALERT)
|
||||
curl -X POST http://localhost:8000/api/door-access/data \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_id": 1,
|
||||
"door_opened": true,
|
||||
"access_method": "force",
|
||||
"duration_seconds": 180,
|
||||
"door_location": "front_door",
|
||||
"is_forced_entry": true
|
||||
}'
|
||||
|
||||
# Get statistics
|
||||
curl "http://localhost:8000/api/door-access/statistics?device_id=1&hours=24"
|
||||
|
||||
# Test door access notification
|
||||
curl -X POST http://localhost:8000/api/test-door-access-notification
|
||||
```
|
||||
|
||||
## 📈 **Security Dashboard**
|
||||
|
||||
### **Real-time Monitoring:**
|
||||
- Current door status (open/closed)
|
||||
- Active access attempts
|
||||
- Unauthorized access count (24h)
|
||||
- Forced entry alerts
|
||||
- User access patterns
|
||||
|
||||
### **Access Control Reports:**
|
||||
- Daily access summary by user
|
||||
- Unauthorized access attempts
|
||||
- After-hours access log
|
||||
- Door usage statistics by location
|
||||
- ID card usage tracking
|
||||
|
||||
## 🔒 **Security Features**
|
||||
|
||||
### **Smart Access Control:**
|
||||
- **ID Card Validation**: Format EMP-XXXX validation
|
||||
- **Time-based Rules**: Working hours vs after hours
|
||||
- **Duration Monitoring**: Alert untuk pintu terbuka terlalu lama
|
||||
- **Forced Entry Detection**: Deteksi paksa masuk real-time
|
||||
- **User Tracking**: Track akses per ID card
|
||||
|
||||
### **Multi-level Security:**
|
||||
- **Immediate**: Push notification untuk forced entry
|
||||
- **Escalation**: SMS untuk unauthorized access
|
||||
- **Audit Trail**: Log lengkap semua akses
|
||||
- **Integration**: Webhook ke sistem access control
|
||||
|
||||
## 🚀 **Deployment & Configuration**
|
||||
|
||||
### **Environment Variables:**
|
||||
```env
|
||||
# Door Access Settings
|
||||
DOOR_ACCESS_WEBHOOK_URL=https://your-access-control.com/alerts
|
||||
WORK_START_HOUR=7
|
||||
WORK_END_HOUR=18
|
||||
MAX_DOOR_OPEN_DURATION=180
|
||||
|
||||
# ID Card Settings
|
||||
ID_CARD_FORMAT_REGEX=^EMP-\d{4}$
|
||||
VALID_ID_CARDS=EMP-1234,EMP-5678,EMP-9012
|
||||
|
||||
# Notification settings
|
||||
SECURITY_EMAIL=security@yourcompany.com
|
||||
SECURITY_SMS_NUMBER=+6281234567890
|
||||
```
|
||||
|
||||
### **Konfigurasi Access Control:**
|
||||
```php
|
||||
// config/door_access.php
|
||||
return [
|
||||
'work_hours' => [
|
||||
'start' => 7, // 07:00
|
||||
'end' => 18, // 18:00
|
||||
'weekdays_only' => true
|
||||
],
|
||||
'thresholds' => [
|
||||
'max_door_open_duration' => 180, // 3 minutes
|
||||
'suspicious_duration' => 120, // 2 minutes
|
||||
],
|
||||
'id_card' => [
|
||||
'format_regex' => '^EMP-\d{4}$',
|
||||
'required_for_access' => true
|
||||
],
|
||||
'locations' => [
|
||||
'front_door' => ['priority' => 'high', 'require_id' => true],
|
||||
'back_door' => ['priority' => 'medium', 'require_id' => true],
|
||||
'side_door' => ['priority' => 'medium', 'require_id' => false],
|
||||
'main_entrance' => ['priority' => 'high', 'require_id' => true]
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
## 📞 **Troubleshooting**
|
||||
|
||||
### **Common Issues:**
|
||||
1. **False alarms**: Adjust door open duration threshold
|
||||
2. **ID card not detected**: Check RFID reader connection
|
||||
3. **Missing alerts**: Verify notification service configuration
|
||||
4. **Wrong access type**: Check working hours configuration
|
||||
|
||||
### **Monitoring Commands:**
|
||||
```bash
|
||||
# Check door access logs
|
||||
tail -f storage/logs/laravel.log | grep "Door Access"
|
||||
|
||||
# Database cleanup
|
||||
php artisan door-access:cleanup-old-data
|
||||
|
||||
# Test access control system
|
||||
php artisan door-access:test-system
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Backend Smart Rack Door Access (Reed Switch) siap digunakan!** 🎉
|
||||
|
||||
Sistem akan otomatis:
|
||||
- ✅ Menerima data sensor Reed Switch real-time
|
||||
- ✅ Mendeteksi akses mencurigakan berdasarkan ID card, waktu, dan metode
|
||||
- ✅ Membedakan akses sah vs tidak sah
|
||||
- ✅ Mengirim notifikasi multi-channel untuk security breach
|
||||
- ✅ Menyimpan audit trail lengkap untuk compliance
|
||||
- ✅ Menyediakan statistik akses dan laporan keamanan
|
||||
- ✅ Integrasi mudah dengan sistem access control dan CCTV
|
||||
- ✅ Support multiple door locations dan access methods
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
# 🔔 Smart Rack Vibration Monitoring API
|
||||
|
||||
Backend API untuk menerima data sensor getar dan mengirim notifikasi otomatis jika getaran tidak wajar.
|
||||
|
||||
## 📡 **API Endpoints**
|
||||
|
||||
### 1. **Kirim Data Sensor Getar**
|
||||
```http
|
||||
POST /api/vibration/data
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"device_id": 1,
|
||||
"x_axis": 0.5,
|
||||
"y_axis": 1.2,
|
||||
"z_axis": 0.8,
|
||||
"threshold": 2.0,
|
||||
"metadata": {
|
||||
"sensor_type": "accelerometer",
|
||||
"firmware_version": "1.2.3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response Success:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Vibration data received successfully",
|
||||
"data": {
|
||||
"id": 123,
|
||||
"magnitude": 1.56,
|
||||
"status": "normal",
|
||||
"is_abnormal": false,
|
||||
"alert_sent": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response Abnormal (Alert Triggered):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Vibration data received successfully",
|
||||
"data": {
|
||||
"id": 124,
|
||||
"magnitude": 3.45,
|
||||
"status": "critical",
|
||||
"is_abnormal": true,
|
||||
"alert_sent": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Ambil Data Vibration Terbaru**
|
||||
```http
|
||||
GET /api/vibration/readings?device_id=1&limit=50&status=critical
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 124,
|
||||
"device_id": 1,
|
||||
"x_axis": 2.1,
|
||||
"y_axis": 2.8,
|
||||
"z_axis": 1.9,
|
||||
"magnitude": 3.45,
|
||||
"is_abnormal": true,
|
||||
"threshold": 2.0,
|
||||
"status": "critical",
|
||||
"recorded_at": "2026-04-28T10:30:00Z",
|
||||
"device": {
|
||||
"id": 1,
|
||||
"name": "Rak Server A",
|
||||
"location": "Ruang Server"
|
||||
}
|
||||
}
|
||||
],
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Ambil Statistik Getaran**
|
||||
```http
|
||||
GET /api/vibration/statistics?device_id=1&hours=24
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"total_readings": 150,
|
||||
"normal_count": 120,
|
||||
"warning_count": 25,
|
||||
"critical_count": 5,
|
||||
"abnormal_percentage": 20.0,
|
||||
"avg_magnitude": 1.85,
|
||||
"max_magnitude": 4.2,
|
||||
"latest_reading": {
|
||||
"id": 124,
|
||||
"magnitude": 3.45,
|
||||
"status": "critical",
|
||||
"recorded_at": "2026-04-28T10:30:00Z"
|
||||
}
|
||||
},
|
||||
"period_hours": 24
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **Test Notifikasi**
|
||||
```http
|
||||
POST /api/test-notification
|
||||
```
|
||||
|
||||
## 🚨 **Sistem Alert & Notifikasi**
|
||||
|
||||
### **Status Getaran:**
|
||||
- **normal**: magnitude ≤ threshold
|
||||
- **warning**: threshold < magnitude ≤ threshold × 1.5
|
||||
- **critical**: magnitude > threshold × 1.5
|
||||
|
||||
### **Jenis Notifikasi:**
|
||||
1. **📧 Email** - Ke admin sistem
|
||||
2. **📱 SMS** - Untuk alert critical
|
||||
3. **🔔 Push Notification** - Real-time ke mobile app
|
||||
4. **🔗 Webhook** - Ke sistem monitoring eksternal
|
||||
5. **📝 Log** - Audit trail lengkap
|
||||
|
||||
### **Contoh Notifikasi:**
|
||||
```
|
||||
🚨 ALERT: Getaran abnormal pada Rak Server A
|
||||
Magnitude: 3.45 (Normal: <2.0)
|
||||
Status: critical
|
||||
Lokasi: Ruang Server
|
||||
Waktu: 2026-04-28 10:30:00
|
||||
Sumbu: X=2.1, Y=2.8, Z=1.9
|
||||
```
|
||||
|
||||
## 🔧 **Konfigurasi Threshold**
|
||||
|
||||
### **Default Threshold:** 2.0
|
||||
### **Custom Threshold per Device:**
|
||||
```json
|
||||
{
|
||||
"device_id": 1,
|
||||
"threshold": 1.5, // Lebih sensitif
|
||||
"x_axis": 0.8,
|
||||
"y_axis": 1.2,
|
||||
"z_axis": 0.9
|
||||
}
|
||||
```
|
||||
|
||||
### **Threshold Recommendations:**
|
||||
- **Server Rack**: 1.5 - 2.0 (sensitif)
|
||||
- **Storage Rack**: 2.0 - 3.0 (normal)
|
||||
- **Network Equipment**: 1.0 - 1.5 (sangat sensitif)
|
||||
|
||||
## 📊 **Database Schema**
|
||||
|
||||
### **vibration_readings table:**
|
||||
```sql
|
||||
- id (bigint, primary key)
|
||||
- device_id (foreign key to devices)
|
||||
- x_axis (float) - Getaran sumbu X
|
||||
- y_axis (float) - Getaran sumbu Y
|
||||
- z_axis (float) - Getaran sumbu Z
|
||||
- magnitude (float) - Total magnitude
|
||||
- is_abnormal (boolean) - Status abnormal
|
||||
- threshold (float) - Batas normal
|
||||
- status (enum: normal, warning, critical)
|
||||
- metadata (json) - Data tambahan
|
||||
- recorded_at (timestamp) - Waktu pembacaan
|
||||
- created_at, updated_at
|
||||
```
|
||||
|
||||
## 🔌 **Integrasi IoT Device**
|
||||
|
||||
### **Arduino/ESP32 Example:**
|
||||
```cpp
|
||||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
void sendVibrationData(float x, float y, float z) {
|
||||
HTTPClient http;
|
||||
http.begin("http://your-server.com/api/vibration/data");
|
||||
http.addHeader("Content-Type", "application/json");
|
||||
|
||||
StaticJsonDocument<200> doc;
|
||||
doc["device_id"] = 1;
|
||||
doc["x_axis"] = x;
|
||||
doc["y_axis"] = y;
|
||||
doc["z_axis"] = z;
|
||||
doc["threshold"] = 2.0;
|
||||
|
||||
String jsonString;
|
||||
serializeJson(doc, jsonString);
|
||||
|
||||
int httpResponseCode = http.POST(jsonString);
|
||||
|
||||
if (httpResponseCode > 0) {
|
||||
String response = http.getString();
|
||||
Serial.println("Response: " + response);
|
||||
}
|
||||
|
||||
http.end();
|
||||
}
|
||||
```
|
||||
|
||||
### **Python Example:**
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
def send_vibration_data(device_id, x, y, z, threshold=2.0):
|
||||
url = "http://your-server.com/api/vibration/data"
|
||||
|
||||
data = {
|
||||
"device_id": device_id,
|
||||
"x_axis": x,
|
||||
"y_axis": y,
|
||||
"z_axis": z,
|
||||
"threshold": threshold,
|
||||
"metadata": {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"sensor_type": "MPU6050"
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, json=data)
|
||||
return response.json()
|
||||
|
||||
# Contoh penggunaan
|
||||
result = send_vibration_data(1, 0.5, 1.2, 0.8)
|
||||
print(f"Status: {result['data']['status']}")
|
||||
print(f"Alert sent: {result['data']['alert_sent']}")
|
||||
```
|
||||
|
||||
## 🧪 **Testing API**
|
||||
|
||||
### **Test dengan cURL:**
|
||||
```bash
|
||||
# Test normal vibration
|
||||
curl -X POST http://localhost:8000/api/vibration/data \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_id": 1,
|
||||
"x_axis": 0.5,
|
||||
"y_axis": 0.8,
|
||||
"z_axis": 0.6,
|
||||
"threshold": 2.0
|
||||
}'
|
||||
|
||||
# Test abnormal vibration (akan trigger alert)
|
||||
curl -X POST http://localhost:8000/api/vibration/data \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"device_id": 1,
|
||||
"x_axis": 2.5,
|
||||
"y_axis": 3.2,
|
||||
"z_axis": 2.8,
|
||||
"threshold": 2.0
|
||||
}'
|
||||
|
||||
# Get statistics
|
||||
curl "http://localhost:8000/api/vibration/statistics?device_id=1&hours=24"
|
||||
|
||||
# Test notification
|
||||
curl -X POST http://localhost:8000/api/test-notification
|
||||
```
|
||||
|
||||
## 📈 **Monitoring Dashboard**
|
||||
|
||||
### **Real-time Metrics:**
|
||||
- Current vibration level
|
||||
- Alert count (24h)
|
||||
- Device status
|
||||
- Threshold violations
|
||||
- Historical trends
|
||||
|
||||
### **Alert Management:**
|
||||
- View active alerts
|
||||
- Mark alerts as resolved
|
||||
- Configure notification settings
|
||||
- Export alert reports
|
||||
|
||||
## 🔒 **Security & Best Practices**
|
||||
|
||||
### **API Security:**
|
||||
- Rate limiting per device
|
||||
- API key authentication
|
||||
- Input validation & sanitization
|
||||
- HTTPS only in production
|
||||
|
||||
### **Data Retention:**
|
||||
- Keep raw data: 30 days
|
||||
- Keep aggregated data: 1 year
|
||||
- Auto-cleanup old records
|
||||
|
||||
### **Performance:**
|
||||
- Batch insert for high-frequency data
|
||||
- Database indexing on device_id + timestamp
|
||||
- Caching for statistics queries
|
||||
|
||||
## 🚀 **Deployment**
|
||||
|
||||
### **Environment Variables:**
|
||||
```env
|
||||
# Notification settings
|
||||
VIBRATION_WEBHOOK_URL=https://your-webhook.com/alerts
|
||||
NOTIFICATION_EMAIL=admin@yourcompany.com
|
||||
SMS_API_KEY=your-sms-api-key
|
||||
|
||||
# Alert thresholds
|
||||
DEFAULT_VIBRATION_THRESHOLD=2.0
|
||||
CRITICAL_MULTIPLIER=1.5
|
||||
```
|
||||
|
||||
### **Cron Jobs:**
|
||||
```bash
|
||||
# Cleanup old data (daily)
|
||||
0 2 * * * php artisan vibration:cleanup
|
||||
|
||||
# Generate daily reports
|
||||
0 8 * * * php artisan vibration:daily-report
|
||||
```
|
||||
|
||||
## 📞 **Support**
|
||||
|
||||
Jika ada pertanyaan atau masalah:
|
||||
1. Check log files: `storage/logs/laravel.log`
|
||||
2. Verify database connection
|
||||
3. Test API endpoints dengan Postman
|
||||
4. Check notification service logs
|
||||
|
||||
---
|
||||
|
||||
**Backend Smart Rack Vibration Monitoring siap digunakan!** 🎉
|
||||
|
||||
Sistem akan otomatis:
|
||||
- ✅ Menerima data sensor getar
|
||||
- ✅ Menghitung magnitude getaran
|
||||
- ✅ Mendeteksi getaran abnormal
|
||||
- ✅ Mengirim notifikasi real-time
|
||||
- ✅ Menyimpan data untuk analisis
|
||||
- ✅ Menyediakan statistik dan laporan
|
||||
|
|
@ -0,0 +1,481 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DoorAccessReading;
|
||||
use App\Models\Device;
|
||||
use App\Models\Alert;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DoorAccessController extends Controller
|
||||
{
|
||||
/**
|
||||
* Terima data sensor Reed Switch (Door Access) dari IoT device
|
||||
*/
|
||||
public function receiveData(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Validasi input
|
||||
$validator = Validator::make($request->all(), [
|
||||
'device_id' => 'required|exists:devices,id',
|
||||
'door_opened' => 'required|boolean',
|
||||
'access_method' => 'nullable|string|in:keycard,manual,force,emergency,maintenance,unknown',
|
||||
'user_id_card' => 'nullable|string|max:50',
|
||||
'duration_seconds' => 'nullable|integer|min:0',
|
||||
'door_location' => 'nullable|string|in:front_door,back_door,side_door,main_entrance',
|
||||
'is_forced_entry' => 'nullable|boolean',
|
||||
'door_opened_at' => 'nullable|date',
|
||||
'door_closed_at' => 'nullable|date|after:door_opened_at',
|
||||
'metadata' => 'nullable|array'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
$recordedAt = now();
|
||||
|
||||
// Hitung durasi jika ada door_opened_at dan door_closed_at
|
||||
$durationSeconds = $data['duration_seconds'] ?? 0;
|
||||
if (isset($data['door_opened_at']) && isset($data['door_closed_at'])) {
|
||||
$openedAt = Carbon::parse($data['door_opened_at']);
|
||||
$closedAt = Carbon::parse($data['door_closed_at']);
|
||||
$durationSeconds = $openedAt->diffInSeconds($closedAt);
|
||||
}
|
||||
|
||||
// Cek apakah dalam jam kerja
|
||||
$isAuthorizedTime = $this->checkAuthorizedTime($recordedAt);
|
||||
|
||||
// Tentukan apakah akses sah
|
||||
$isAuthorizedAccess = $this->determineAuthorizedAccess(
|
||||
$data['door_opened'],
|
||||
$data['access_method'] ?? 'unknown',
|
||||
$data['user_id_card'] ?? null,
|
||||
$isAuthorizedTime,
|
||||
$data['is_forced_entry'] ?? false
|
||||
);
|
||||
|
||||
// Tentukan apakah akses mencurigakan
|
||||
$isSuspicious = $this->determineSuspiciousAccess(
|
||||
$data['door_opened'],
|
||||
$isAuthorizedAccess,
|
||||
$durationSeconds,
|
||||
$data['access_method'] ?? 'unknown',
|
||||
$data['user_id_card'] ?? null,
|
||||
$data['is_forced_entry'] ?? false
|
||||
);
|
||||
|
||||
// Tentukan tipe akses
|
||||
$accessType = $this->determineAccessType(
|
||||
$isAuthorizedAccess,
|
||||
$isAuthorizedTime,
|
||||
$data['access_method'] ?? 'unknown',
|
||||
$data['is_forced_entry'] ?? false
|
||||
);
|
||||
|
||||
// Simpan data Door Access
|
||||
$doorReading = DoorAccessReading::create([
|
||||
'device_id' => $data['device_id'],
|
||||
'door_opened' => $data['door_opened'],
|
||||
'is_authorized_access' => $isAuthorizedAccess,
|
||||
'access_type' => $accessType,
|
||||
'access_method' => $data['access_method'] ?? 'unknown',
|
||||
'user_id_card' => $data['user_id_card'] ?? null,
|
||||
'duration_seconds' => $durationSeconds,
|
||||
'is_suspicious' => $isSuspicious,
|
||||
'door_location' => $data['door_location'] ?? 'main_entrance',
|
||||
'is_forced_entry' => $data['is_forced_entry'] ?? false,
|
||||
'metadata' => $data['metadata'] ?? null,
|
||||
'door_opened_at' => $data['door_opened_at'] ?? null,
|
||||
'door_closed_at' => $data['door_closed_at'] ?? null,
|
||||
'recorded_at' => $recordedAt
|
||||
]);
|
||||
|
||||
// Jika akses mencurigakan atau tidak sah, buat alert dan kirim notifikasi
|
||||
$alertSent = false;
|
||||
if ($isSuspicious || !$isAuthorizedAccess || $accessType === 'forced_entry') {
|
||||
$this->handleSuspiciousAccess($doorReading);
|
||||
$alertSent = true;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Door access data received successfully',
|
||||
'data' => [
|
||||
'id' => $doorReading->id,
|
||||
'door_opened' => $doorReading->door_opened,
|
||||
'access_type' => $accessType,
|
||||
'is_authorized' => $isAuthorizedAccess,
|
||||
'is_suspicious' => $isSuspicious,
|
||||
'alert_sent' => $alertSent,
|
||||
'priority' => $doorReading->getPriorityLevel(),
|
||||
'recorded_at' => $recordedAt->toISOString()
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Door access data receive error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to process door access data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil data door access terbaru
|
||||
*/
|
||||
public function getLatestReadings(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deviceId = $request->get('device_id');
|
||||
$limit = $request->get('limit', 50);
|
||||
$accessType = $request->get('access_type');
|
||||
$doorLocation = $request->get('door_location');
|
||||
$suspicious = $request->get('suspicious');
|
||||
|
||||
$query = DoorAccessReading::with('device')
|
||||
->orderBy('recorded_at', 'desc');
|
||||
|
||||
if ($deviceId) {
|
||||
$query->where('device_id', $deviceId);
|
||||
}
|
||||
|
||||
if ($accessType) {
|
||||
$query->where('access_type', $accessType);
|
||||
}
|
||||
|
||||
if ($doorLocation) {
|
||||
$query->where('door_location', $doorLocation);
|
||||
}
|
||||
|
||||
if ($suspicious !== null) {
|
||||
$query->where('is_suspicious', $suspicious === 'true');
|
||||
}
|
||||
|
||||
$readings = $query->limit($limit)->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $readings,
|
||||
'count' => $readings->count()
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch door access data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil statistik door access
|
||||
*/
|
||||
public function getStatistics(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deviceId = $request->get('device_id');
|
||||
$hours = $request->get('hours', 24);
|
||||
|
||||
$query = DoorAccessReading::where('recorded_at', '>=', now()->subHours($hours));
|
||||
|
||||
if ($deviceId) {
|
||||
$query->where('device_id', $deviceId);
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'total_access_attempts' => $query->count(),
|
||||
'door_opened_count' => $query->where('door_opened', true)->count(),
|
||||
'authorized_count' => $query->where('is_authorized_access', true)->count(),
|
||||
'unauthorized_count' => $query->where('is_authorized_access', false)->count(),
|
||||
'suspicious_count' => $query->where('is_suspicious', true)->count(),
|
||||
'forced_entry_count' => $query->where('is_forced_entry', true)->count(),
|
||||
'avg_duration' => $query->where('door_opened', true)->avg('duration_seconds'),
|
||||
'max_duration' => $query->max('duration_seconds'),
|
||||
'latest_access' => $query->orderBy('recorded_at', 'desc')->first(),
|
||||
'access_types' => $this->getAccessTypeStatistics($query),
|
||||
'door_locations' => $this->getDoorLocationStatistics($query),
|
||||
'access_methods' => $this->getAccessMethodStatistics($query)
|
||||
];
|
||||
|
||||
// Hitung persentase
|
||||
if ($stats['total_access_attempts'] > 0) {
|
||||
$stats['authorized_percentage'] = round(($stats['authorized_count'] / $stats['total_access_attempts']) * 100, 2);
|
||||
$stats['suspicious_percentage'] = round(($stats['suspicious_count'] / $stats['total_access_attempts']) * 100, 2);
|
||||
} else {
|
||||
$stats['authorized_percentage'] = 0;
|
||||
$stats['suspicious_percentage'] = 0;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $stats,
|
||||
'period_hours' => $hours
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch statistics',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah waktu dalam jam kerja
|
||||
*/
|
||||
private function checkAuthorizedTime(Carbon $timestamp): bool
|
||||
{
|
||||
// Jam kerja: Senin-Jumat 07:00-18:00
|
||||
$workStart = 7; // 07:00
|
||||
$workEnd = 18; // 18:00
|
||||
|
||||
$hour = $timestamp->hour;
|
||||
$isWeekday = $timestamp->isWeekday();
|
||||
|
||||
return $isWeekday && $hour >= $workStart && $hour < $workEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan apakah akses sah
|
||||
*/
|
||||
private function determineAuthorizedAccess(bool $doorOpened, string $accessMethod, ?string $userIdCard, bool $isAuthorizedTime, bool $isForcedEntry): bool
|
||||
{
|
||||
if ($isForcedEntry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($accessMethod === 'emergency') {
|
||||
return true; // Emergency access selalu dianggap sah
|
||||
}
|
||||
|
||||
if ($accessMethod === 'keycard' && $userIdCard && $this->isValidIdCard($userIdCard)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($accessMethod === 'maintenance' && $isAuthorizedTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan apakah akses mencurigakan
|
||||
*/
|
||||
private function determineSuspiciousAccess(bool $doorOpened, bool $isAuthorized, int $duration, string $accessMethod, ?string $userIdCard, bool $isForcedEntry): bool
|
||||
{
|
||||
if ($isForcedEntry) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$isAuthorized && $accessMethod !== 'emergency') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($duration > 180) { // Pintu terbuka lebih dari 3 menit
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($doorOpened && !$userIdCard && $accessMethod === 'manual') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan tipe akses
|
||||
*/
|
||||
private function determineAccessType(bool $isAuthorized, bool $isAuthorizedTime, string $accessMethod, bool $isForcedEntry): string
|
||||
{
|
||||
if ($isForcedEntry) {
|
||||
return 'forced_entry';
|
||||
}
|
||||
|
||||
if ($accessMethod === 'emergency') {
|
||||
return 'emergency';
|
||||
}
|
||||
|
||||
if ($accessMethod === 'maintenance') {
|
||||
return 'maintenance';
|
||||
}
|
||||
|
||||
if ($isAuthorized && $isAuthorizedTime) {
|
||||
return 'authorized';
|
||||
}
|
||||
|
||||
if (!$isAuthorizedTime) {
|
||||
return 'after_hours';
|
||||
}
|
||||
|
||||
return 'unauthorized';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validasi ID card (simulasi)
|
||||
*/
|
||||
private function isValidIdCard(string $idCard): bool
|
||||
{
|
||||
// Format: EMP-XXXX (4 digit angka)
|
||||
return preg_match('/^EMP-\d{4}$/', $idCard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle akses mencurigakan - buat alert dan kirim notifikasi
|
||||
*/
|
||||
private function handleSuspiciousAccess(DoorAccessReading $reading): void
|
||||
{
|
||||
try {
|
||||
$device = $reading->device;
|
||||
|
||||
// Tentukan priority berdasarkan tipe akses
|
||||
$priority = match($reading->access_type) {
|
||||
'forced_entry' => 'critical',
|
||||
'unauthorized' => 'high',
|
||||
'after_hours' => 'high',
|
||||
'emergency' => 'medium',
|
||||
'maintenance' => 'low',
|
||||
default => 'medium'
|
||||
};
|
||||
|
||||
// Buat alert
|
||||
$alert = Alert::create([
|
||||
'device_id' => $reading->device_id,
|
||||
'type' => 'door_access_alert',
|
||||
'priority' => $priority,
|
||||
'title' => 'Akses Pintu Mencurigakan Terdeteksi',
|
||||
'message' => $this->generateAlertMessage($reading, $device),
|
||||
'data' => [
|
||||
'door_reading_id' => $reading->id,
|
||||
'access_type' => $reading->access_type,
|
||||
'access_method' => $reading->access_method,
|
||||
'user_id_card' => $reading->user_id_card,
|
||||
'duration' => $reading->duration_seconds,
|
||||
'door_location' => $reading->door_location,
|
||||
'is_forced_entry' => $reading->is_forced_entry,
|
||||
'is_authorized_access' => $reading->is_authorized_access
|
||||
],
|
||||
'is_read' => false
|
||||
]);
|
||||
|
||||
// Kirim notifikasi
|
||||
$this->sendNotification($alert, $reading);
|
||||
|
||||
Log::info("Suspicious door access alert created", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_id' => $reading->device_id,
|
||||
'access_type' => $reading->access_type,
|
||||
'user_id_card' => $reading->user_id_card
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to handle suspicious door access: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate pesan alert berdasarkan data door access
|
||||
*/
|
||||
private function generateAlertMessage(DoorAccessReading $reading, Device $device): string
|
||||
{
|
||||
$locationInfo = $reading->door_location ? " di {$reading->door_location}" : '';
|
||||
$userInfo = $reading->user_id_card ? " oleh {$reading->user_id_card}" : ' tanpa ID card';
|
||||
$methodInfo = $reading->access_method !== 'unknown' ? " menggunakan {$reading->access_method}" : '';
|
||||
|
||||
if ($reading->is_forced_entry) {
|
||||
return "PAKSA MASUK terdeteksi pada {$device->name}{$locationInfo}. Durasi: {$reading->duration_seconds} detik.";
|
||||
}
|
||||
|
||||
return "Akses {$reading->access_type} terdeteksi pada {$device->name}{$locationInfo}{$userInfo}{$methodInfo}. " .
|
||||
"Durasi: {$reading->duration_seconds} detik.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim notifikasi menggunakan NotificationService
|
||||
*/
|
||||
private function sendNotification(Alert $alert, DoorAccessReading $reading): void
|
||||
{
|
||||
try {
|
||||
$notificationService = new NotificationService();
|
||||
$success = $notificationService->sendDoorAccessAlert($alert, $reading);
|
||||
|
||||
if ($success) {
|
||||
Log::info("Door access notification sent successfully", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_name' => $reading->device->name,
|
||||
'access_type' => $reading->access_type
|
||||
]);
|
||||
} else {
|
||||
Log::warning("Door access notification failed", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_id' => $reading->device_id
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Door access notification service error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistik per tipe akses
|
||||
*/
|
||||
private function getAccessTypeStatistics($query): array
|
||||
{
|
||||
$types = ['authorized', 'unauthorized', 'after_hours', 'forced_entry', 'emergency', 'maintenance'];
|
||||
$typeStats = [];
|
||||
|
||||
foreach ($types as $type) {
|
||||
$typeStats[$type] = $query->where('access_type', $type)->count();
|
||||
}
|
||||
|
||||
return $typeStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistik per lokasi pintu
|
||||
*/
|
||||
private function getDoorLocationStatistics($query): array
|
||||
{
|
||||
$locations = ['front_door', 'back_door', 'side_door', 'main_entrance'];
|
||||
$locationStats = [];
|
||||
|
||||
foreach ($locations as $location) {
|
||||
$locationStats[$location] = $query->where('door_location', $location)->count();
|
||||
}
|
||||
|
||||
return $locationStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistik per metode akses
|
||||
*/
|
||||
private function getAccessMethodStatistics($query): array
|
||||
{
|
||||
$methods = ['keycard', 'manual', 'force', 'emergency', 'maintenance', 'unknown'];
|
||||
$methodStats = [];
|
||||
|
||||
foreach ($methods as $method) {
|
||||
$methodStats[$method] = $query->where('access_method', $method)->count();
|
||||
}
|
||||
|
||||
return $methodStats;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,462 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DoorReading;
|
||||
use App\Models\Device;
|
||||
use App\Models\Alert;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DoorController extends Controller
|
||||
{
|
||||
/**
|
||||
* Terima data sensor Reed Switch/Door dari IoT device
|
||||
*/
|
||||
public function receiveData(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Validasi input
|
||||
$validator = Validator::make($request->all(), [
|
||||
'device_id' => 'required|exists:devices,id',
|
||||
'door_open' => 'required|boolean',
|
||||
'door_location' => 'nullable|string|in:front_panel,back_panel,side_door,main_door',
|
||||
'open_duration_seconds' => 'nullable|integer|min:0',
|
||||
'door_opened_at' => 'nullable|date',
|
||||
'door_closed_at' => 'nullable|date|after:door_opened_at',
|
||||
'access_card_data' => 'nullable|array',
|
||||
'proper_closure' => 'nullable|boolean',
|
||||
'metadata' => 'nullable|array'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
$recordedAt = now();
|
||||
|
||||
// Cek apakah dalam jam kerja yang sah
|
||||
$isAuthorizedAccess = $this->checkAuthorizedAccess($recordedAt, $data);
|
||||
|
||||
// Hitung durasi terbuka
|
||||
$openDuration = $this->calculateOpenDuration($data);
|
||||
|
||||
// Deteksi pembukaan paksa
|
||||
$isForcedEntry = $this->detectForcedEntry($data, $isAuthorizedAccess, $openDuration);
|
||||
|
||||
// Tentukan tipe akses
|
||||
$accessType = $this->determineAccessType($data['door_open'], $isAuthorizedAccess, $isForcedEntry, $openDuration);
|
||||
|
||||
// Tentukan proper closure
|
||||
$properClosure = $data['proper_closure'] ?? true;
|
||||
if (!$data['door_open'] && !isset($data['proper_closure'])) {
|
||||
// Jika pintu ditutup tapi tidak ada info proper_closure, anggap normal
|
||||
$properClosure = true;
|
||||
}
|
||||
|
||||
// Simpan data door reading
|
||||
$doorReading = DoorReading::create([
|
||||
'device_id' => $data['device_id'],
|
||||
'door_open' => $data['door_open'],
|
||||
'is_authorized_access' => $isAuthorizedAccess,
|
||||
'is_forced_entry' => $isForcedEntry,
|
||||
'access_type' => $accessType,
|
||||
'door_location' => $data['door_location'] ?? 'main_door',
|
||||
'open_duration_seconds' => $openDuration,
|
||||
'proper_closure' => $properClosure,
|
||||
'access_card_data' => $data['access_card_data'] ?? null,
|
||||
'metadata' => $data['metadata'] ?? null,
|
||||
'door_opened_at' => $data['door_opened_at'] ?? null,
|
||||
'door_closed_at' => $data['door_closed_at'] ?? null,
|
||||
'recorded_at' => $recordedAt
|
||||
]);
|
||||
|
||||
// Jika akses tidak sah atau pembukaan paksa, buat alert
|
||||
$alertSent = false;
|
||||
if (!$isAuthorizedAccess || $isForcedEntry || $accessType === 'unauthorized') {
|
||||
$this->handleUnauthorizedAccess($doorReading);
|
||||
$alertSent = true;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Door sensor data received successfully',
|
||||
'data' => [
|
||||
'id' => $doorReading->id,
|
||||
'door_open' => $doorReading->door_open,
|
||||
'access_type' => $accessType,
|
||||
'is_authorized_access' => $isAuthorizedAccess,
|
||||
'is_forced_entry' => $isForcedEntry,
|
||||
'security_risk_level' => $doorReading->getSecurityRiskLevel(),
|
||||
'alert_sent' => $alertSent,
|
||||
'recorded_at' => $recordedAt->toISOString()
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Door sensor data receive error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to process door sensor data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil data door sensor terbaru
|
||||
*/
|
||||
public function getLatestReadings(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deviceId = $request->get('device_id');
|
||||
$limit = $request->get('limit', 50);
|
||||
$accessType = $request->get('access_type');
|
||||
$doorLocation = $request->get('door_location');
|
||||
$doorOpen = $request->get('door_open');
|
||||
|
||||
$query = DoorReading::with('device')
|
||||
->orderBy('recorded_at', 'desc');
|
||||
|
||||
if ($deviceId) {
|
||||
$query->where('device_id', $deviceId);
|
||||
}
|
||||
|
||||
if ($accessType) {
|
||||
$query->where('access_type', $accessType);
|
||||
}
|
||||
|
||||
if ($doorLocation) {
|
||||
$query->where('door_location', $doorLocation);
|
||||
}
|
||||
|
||||
if ($doorOpen !== null) {
|
||||
$query->where('door_open', $doorOpen === 'true');
|
||||
}
|
||||
|
||||
$readings = $query->limit($limit)->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $readings,
|
||||
'count' => $readings->count()
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch door sensor data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil statistik akses pintu
|
||||
*/
|
||||
public function getStatistics(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deviceId = $request->get('device_id');
|
||||
$hours = $request->get('hours', 24);
|
||||
|
||||
$query = DoorReading::where('recorded_at', '>=', now()->subHours($hours));
|
||||
|
||||
if ($deviceId) {
|
||||
$query->where('device_id', $deviceId);
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'total_readings' => $query->count(),
|
||||
'door_opened_count' => $query->where('door_open', true)->count(),
|
||||
'authorized_access_count' => $query->where('is_authorized_access', true)->count(),
|
||||
'unauthorized_access_count' => $query->where('is_authorized_access', false)->count(),
|
||||
'forced_entry_count' => $query->where('is_forced_entry', true)->count(),
|
||||
'normal_access_count' => $query->where('access_type', 'normal')->count(),
|
||||
'maintenance_access_count' => $query->where('access_type', 'maintenance')->count(),
|
||||
'avg_open_duration' => $query->where('door_open', true)->avg('open_duration_seconds'),
|
||||
'max_open_duration' => $query->max('open_duration_seconds'),
|
||||
'currently_open_doors' => $query->currentlyOpen()->count(),
|
||||
'latest_access' => $query->where('door_open', true)->orderBy('recorded_at', 'desc')->first(),
|
||||
'door_locations' => $this->getDoorLocationStatistics($query),
|
||||
'access_types' => $this->getAccessTypeStatistics($query),
|
||||
'security_incidents' => $query->where(function($q) {
|
||||
$q->where('is_forced_entry', true)
|
||||
->orWhere('is_authorized_access', false);
|
||||
})->count()
|
||||
];
|
||||
|
||||
// Hitung persentase
|
||||
if ($stats['total_readings'] > 0) {
|
||||
$stats['unauthorized_percentage'] = round(($stats['unauthorized_access_count'] / $stats['total_readings']) * 100, 2);
|
||||
$stats['security_incident_percentage'] = round(($stats['security_incidents'] / $stats['total_readings']) * 100, 2);
|
||||
} else {
|
||||
$stats['unauthorized_percentage'] = 0;
|
||||
$stats['security_incident_percentage'] = 0;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $stats,
|
||||
'period_hours' => $hours
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch door statistics',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doors that are currently open
|
||||
*/
|
||||
public function getCurrentlyOpenDoors(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deviceId = $request->get('device_id');
|
||||
|
||||
$query = DoorReading::currentlyOpen()->with('device');
|
||||
|
||||
if ($deviceId) {
|
||||
$query->where('device_id', $deviceId);
|
||||
}
|
||||
|
||||
$openDoors = $query->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $openDoors,
|
||||
'count' => $openDoors->count(),
|
||||
'message' => $openDoors->count() > 0 ? 'Found open doors' : 'All doors are closed'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch open doors',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah akses dalam jam kerja yang sah
|
||||
*/
|
||||
private function checkAuthorizedAccess(Carbon $timestamp, array $data): bool
|
||||
{
|
||||
// Jam kerja: Senin-Jumat 07:00-18:00
|
||||
$workStart = 7; // 07:00
|
||||
$workEnd = 18; // 18:00
|
||||
|
||||
$hour = $timestamp->hour;
|
||||
$isWeekday = $timestamp->isWeekday();
|
||||
$isWorkingHours = $isWeekday && $hour >= $workStart && $hour < $workEnd;
|
||||
|
||||
// Jika ada data kartu akses, dianggap sah
|
||||
if (isset($data['access_card_data']) && !empty($data['access_card_data'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Jika dalam jam kerja, dianggap sah
|
||||
return $isWorkingHours;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung durasi terbuka
|
||||
*/
|
||||
private function calculateOpenDuration(array $data): int
|
||||
{
|
||||
if (isset($data['door_opened_at']) && isset($data['door_closed_at'])) {
|
||||
$start = Carbon::parse($data['door_opened_at']);
|
||||
$end = Carbon::parse($data['door_closed_at']);
|
||||
return $start->diffInSeconds($end);
|
||||
}
|
||||
|
||||
return $data['open_duration_seconds'] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deteksi pembukaan paksa
|
||||
*/
|
||||
private function detectForcedEntry(array $data, bool $isAuthorizedAccess, int $openDuration): bool
|
||||
{
|
||||
// Indikator pembukaan paksa:
|
||||
// 1. Tidak ada otorisasi dan tidak ada kartu akses
|
||||
// 2. Durasi sangat singkat (<3 detik) - kemungkinan dipaksa lalu ditutup cepat
|
||||
// 3. Tidak ditutup dengan benar di luar jam kerja
|
||||
|
||||
if (!$isAuthorizedAccess && empty($data['access_card_data'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($openDuration > 0 && $openDuration < 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isset($data['proper_closure']) && !$data['proper_closure'] && !$isAuthorizedAccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan tipe akses
|
||||
*/
|
||||
private function determineAccessType(bool $doorOpen, bool $isAuthorizedAccess, bool $isForcedEntry, int $openDuration): string
|
||||
{
|
||||
if ($isForcedEntry) {
|
||||
return 'forced';
|
||||
}
|
||||
|
||||
if (!$isAuthorizedAccess) {
|
||||
return 'unauthorized';
|
||||
}
|
||||
|
||||
// Maintenance jika terbuka lama (>5 menit) dengan otorisasi
|
||||
if ($doorOpen && $openDuration > 300 && $isAuthorizedAccess) {
|
||||
return 'maintenance';
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle akses tidak sah - buat alert dan kirim notifikasi
|
||||
*/
|
||||
private function handleUnauthorizedAccess(DoorReading $reading): void
|
||||
{
|
||||
try {
|
||||
$device = $reading->device;
|
||||
|
||||
// Tentukan priority berdasarkan tipe akses
|
||||
$priority = match($reading->access_type) {
|
||||
'forced' => 'critical',
|
||||
'unauthorized' => 'high',
|
||||
default => 'medium'
|
||||
};
|
||||
|
||||
// Buat alert
|
||||
$alert = Alert::create([
|
||||
'device_id' => $reading->device_id,
|
||||
'type' => 'door_access',
|
||||
'priority' => $priority,
|
||||
'title' => 'Akses Pintu Tidak Sah Terdeteksi',
|
||||
'message' => $this->generateAlertMessage($reading, $device),
|
||||
'data' => [
|
||||
'door_reading_id' => $reading->id,
|
||||
'access_type' => $reading->access_type,
|
||||
'door_location' => $reading->door_location,
|
||||
'is_forced_entry' => $reading->is_forced_entry,
|
||||
'open_duration' => $reading->open_duration_seconds,
|
||||
'is_authorized_access' => $reading->is_authorized_access,
|
||||
'security_risk_level' => $reading->getSecurityRiskLevel()
|
||||
],
|
||||
'is_read' => false
|
||||
]);
|
||||
|
||||
// Kirim notifikasi
|
||||
$this->sendNotification($alert, $reading);
|
||||
|
||||
Log::info("Unauthorized door access alert created", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_id' => $reading->device_id,
|
||||
'access_type' => $reading->access_type,
|
||||
'door_location' => $reading->door_location
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to handle unauthorized door access: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate pesan alert berdasarkan data door
|
||||
*/
|
||||
private function generateAlertMessage(DoorReading $reading, Device $device): string
|
||||
{
|
||||
$accessInfo = $reading->is_authorized_access ? 'akses sah' : 'akses tidak sah';
|
||||
$locationInfo = $reading->door_location ? " pada {$reading->door_location}" : '';
|
||||
$durationInfo = $reading->open_duration_seconds > 0 ? " selama {$reading->open_duration_seconds} detik" : '';
|
||||
|
||||
$message = "Akses {$reading->access_type} terdeteksi pada {$device->name}{$locationInfo}. ";
|
||||
$message .= "Status: {$accessInfo}{$durationInfo}.";
|
||||
|
||||
if ($reading->is_forced_entry) {
|
||||
$message .= " PERINGATAN: Indikasi pembukaan paksa!";
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim notifikasi menggunakan NotificationService
|
||||
*/
|
||||
private function sendNotification(Alert $alert, DoorReading $reading): void
|
||||
{
|
||||
try {
|
||||
$notificationService = new NotificationService();
|
||||
$success = $notificationService->sendDoorAlert($alert, $reading);
|
||||
|
||||
if ($success) {
|
||||
Log::info("Door access notification sent successfully", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_name' => $reading->device->name,
|
||||
'access_type' => $reading->access_type
|
||||
]);
|
||||
} else {
|
||||
Log::warning("Door access notification failed", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_id' => $reading->device_id
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Door notification service error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistik per lokasi pintu
|
||||
*/
|
||||
private function getDoorLocationStatistics($query): array
|
||||
{
|
||||
$locations = ['front_panel', 'back_panel', 'side_door', 'main_door'];
|
||||
$locationStats = [];
|
||||
|
||||
foreach ($locations as $location) {
|
||||
$locationStats[$location] = $query->where('door_location', $location)->count();
|
||||
}
|
||||
|
||||
return $locationStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistik per tipe akses
|
||||
*/
|
||||
private function getAccessTypeStatistics($query): array
|
||||
{
|
||||
$types = ['normal', 'unauthorized', 'forced', 'maintenance'];
|
||||
$typeStats = [];
|
||||
|
||||
foreach ($types as $type) {
|
||||
$typeStats[$type] = $query->where('access_type', $type)->count();
|
||||
}
|
||||
|
||||
return $typeStats;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,476 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\LoRaMessage;
|
||||
use App\Models\Device;
|
||||
use App\Services\LoRaProcessingService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class LoRaController extends Controller
|
||||
{
|
||||
protected $loraProcessor;
|
||||
|
||||
public function __construct(LoRaProcessingService $loraProcessor)
|
||||
{
|
||||
$this->loraProcessor = $loraProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Terima data LoRa dari gateway
|
||||
*/
|
||||
public function receiveMessage(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Validasi input
|
||||
$validator = Validator::make($request->all(), [
|
||||
'node_id' => 'required|string|max:50',
|
||||
'gateway_id' => 'nullable|string|max:50',
|
||||
'payload' => 'required|string',
|
||||
'rssi' => 'nullable|numeric',
|
||||
'snr' => 'nullable|numeric',
|
||||
'spreading_factor' => 'nullable|integer|min:7|max:12',
|
||||
'frequency' => 'nullable|numeric',
|
||||
'bandwidth' => 'nullable|integer',
|
||||
'received_at' => 'nullable|date',
|
||||
'metadata' => 'nullable|array'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
|
||||
// Tentukan message type berdasarkan payload
|
||||
$messageType = $this->determineMessageType($data['payload']);
|
||||
|
||||
// Cari device berdasarkan node_id atau device_id
|
||||
$device = Device::where('device_id', $data['node_id'])
|
||||
->orWhere('name', 'like', '%' . $data['node_id'] . '%')
|
||||
->first();
|
||||
|
||||
// Jika tidak ketemu, pakai device pertama yang ada
|
||||
if (!$device) {
|
||||
$device = Device::where('type', 'sensor_node')->first();
|
||||
}
|
||||
|
||||
// Simpan LoRa message
|
||||
$loraMessage = LoRaMessage::create([
|
||||
'device_id' => $device?->id,
|
||||
'node_id' => $data['node_id'],
|
||||
'gateway_id' => $data['gateway_id'] ?? 'GATEWAY_001',
|
||||
'direction' => 'inbound',
|
||||
'message_type' => $messageType,
|
||||
'payload' => $data['payload'],
|
||||
'rssi' => $data['rssi'] ?? null,
|
||||
'snr' => $data['snr'] ?? null,
|
||||
'spreading_factor' => $data['spreading_factor'] ?? null,
|
||||
'frequency' => $data['frequency'] ?? null,
|
||||
'bandwidth' => $data['bandwidth'] ?? null,
|
||||
'is_processed' => false,
|
||||
'status' => 'received',
|
||||
'metadata' => $data['metadata'] ?? null,
|
||||
'received_at' => isset($data['received_at']) ? Carbon::parse($data['received_at']) : now()
|
||||
]);
|
||||
|
||||
// Process message secara asynchronous
|
||||
$processingResult = $this->loraProcessor->processInboundMessage($loraMessage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'LoRa message received successfully',
|
||||
'data' => [
|
||||
'message_id' => $loraMessage->id,
|
||||
'node_id' => $loraMessage->node_id,
|
||||
'message_type' => $messageType,
|
||||
'signal_quality' => $loraMessage->getSignalQuality(),
|
||||
'estimated_distance' => $loraMessage->estimateDistance(),
|
||||
'processing_result' => $processingResult,
|
||||
'received_at' => $loraMessage->received_at->toISOString()
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('LoRa message receive error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to process LoRa message',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim command ke LoRa node
|
||||
*/
|
||||
public function sendCommand(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'node_id' => 'required|string|max:50',
|
||||
'action' => 'required|string|in:reboot,config,read_sensors,set_threshold,sleep,wake',
|
||||
'parameters' => 'nullable|array',
|
||||
'gateway_id' => 'nullable|string|max:50',
|
||||
'priority' => 'nullable|string|in:low,medium,high,urgent'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
|
||||
// Generate command payload
|
||||
$commandId = uniqid('CMD_');
|
||||
$payload = LoRaMessage::generateCommandPayload(
|
||||
$data['action'],
|
||||
$data['parameters'] ?? [],
|
||||
$commandId
|
||||
);
|
||||
|
||||
// Simpan outbound message
|
||||
$loraMessage = LoRaMessage::create([
|
||||
'node_id' => $data['node_id'],
|
||||
'gateway_id' => $data['gateway_id'] ?? 'GATEWAY_001',
|
||||
'direction' => 'outbound',
|
||||
'message_type' => 'command',
|
||||
'payload' => $payload,
|
||||
'is_processed' => false,
|
||||
'status' => 'pending',
|
||||
'metadata' => [
|
||||
'command_id' => $commandId,
|
||||
'action' => $data['action'],
|
||||
'priority' => $data['priority'] ?? 'medium'
|
||||
],
|
||||
'transmitted_at' => now()
|
||||
]);
|
||||
|
||||
// Kirim ke LoRa gateway (implementasi tergantung gateway yang digunakan)
|
||||
$transmissionResult = $this->loraProcessor->transmitMessage($loraMessage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Command sent to LoRa node',
|
||||
'data' => [
|
||||
'message_id' => $loraMessage->id,
|
||||
'command_id' => $commandId,
|
||||
'node_id' => $data['node_id'],
|
||||
'action' => $data['action'],
|
||||
'payload' => $payload,
|
||||
'transmission_result' => $transmissionResult,
|
||||
'transmitted_at' => $loraMessage->transmitted_at->toISOString()
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('LoRa command send error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to send LoRa command',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim konfigurasi ke LoRa node
|
||||
*/
|
||||
public function sendConfig(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'node_id' => 'required|string|max:50',
|
||||
'parameter' => 'required|string|in:threshold,interval,sleep_time,tx_power,spreading_factor',
|
||||
'value' => 'required',
|
||||
'gateway_id' => 'nullable|string|max:50'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
|
||||
// Generate config payload
|
||||
$configId = uniqid('CFG_');
|
||||
$payload = LoRaMessage::generateConfigPayload(
|
||||
$data['parameter'],
|
||||
$data['value'],
|
||||
$configId
|
||||
);
|
||||
|
||||
// Simpan outbound message
|
||||
$loraMessage = LoRaMessage::create([
|
||||
'node_id' => $data['node_id'],
|
||||
'gateway_id' => $data['gateway_id'] ?? 'GATEWAY_001',
|
||||
'direction' => 'outbound',
|
||||
'message_type' => 'config',
|
||||
'payload' => $payload,
|
||||
'is_processed' => false,
|
||||
'status' => 'pending',
|
||||
'metadata' => [
|
||||
'config_id' => $configId,
|
||||
'parameter' => $data['parameter'],
|
||||
'value' => $data['value']
|
||||
],
|
||||
'transmitted_at' => now()
|
||||
]);
|
||||
|
||||
// Kirim ke LoRa gateway
|
||||
$transmissionResult = $this->loraProcessor->transmitMessage($loraMessage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Configuration sent to LoRa node',
|
||||
'data' => [
|
||||
'message_id' => $loraMessage->id,
|
||||
'config_id' => $configId,
|
||||
'node_id' => $data['node_id'],
|
||||
'parameter' => $data['parameter'],
|
||||
'value' => $data['value'],
|
||||
'payload' => $payload,
|
||||
'transmission_result' => $transmissionResult,
|
||||
'transmitted_at' => $loraMessage->transmitted_at->toISOString()
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('LoRa config send error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to send LoRa configuration',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil messages LoRa terbaru
|
||||
*/
|
||||
public function getMessages(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$nodeId = $request->get('node_id');
|
||||
$direction = $request->get('direction');
|
||||
$messageType = $request->get('message_type');
|
||||
$limit = $request->get('limit', 50);
|
||||
|
||||
$query = LoRaMessage::with('device')
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
if ($nodeId) {
|
||||
$query->where('node_id', $nodeId);
|
||||
}
|
||||
|
||||
if ($direction) {
|
||||
$query->where('direction', $direction);
|
||||
}
|
||||
|
||||
if ($messageType) {
|
||||
$query->where('message_type', $messageType);
|
||||
}
|
||||
|
||||
$messages = $query->limit($limit)->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $messages->map(function ($message) {
|
||||
return array_merge($message->toArray(), [
|
||||
'signal_quality' => $message->getSignalQuality(),
|
||||
'estimated_distance' => $message->estimateDistance(),
|
||||
'parsed_data' => $message->parsed_data
|
||||
]);
|
||||
}),
|
||||
'count' => $messages->count()
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch LoRa messages',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil statistik LoRa communication
|
||||
*/
|
||||
public function getStatistics(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$nodeId = $request->get('node_id');
|
||||
$hours = $request->get('hours', 24);
|
||||
|
||||
$query = LoRaMessage::where('created_at', '>=', now()->subHours($hours));
|
||||
|
||||
if ($nodeId) {
|
||||
$query->where('node_id', $nodeId);
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'total_messages' => $query->count(),
|
||||
'inbound_messages' => $query->where('direction', 'inbound')->count(),
|
||||
'outbound_messages' => $query->where('direction', 'outbound')->count(),
|
||||
'processed_messages' => $query->where('is_processed', true)->count(),
|
||||
'failed_messages' => $query->where('status', 'failed')->count(),
|
||||
'acknowledged_messages' => $query->where('is_acknowledged', true)->count(),
|
||||
'avg_rssi' => $query->whereNotNull('rssi')->avg('rssi'),
|
||||
'avg_snr' => $query->whereNotNull('snr')->avg('snr'),
|
||||
'message_types' => $this->getMessageTypeStatistics($query),
|
||||
'node_statistics' => $this->getNodeStatistics($query),
|
||||
'signal_quality_distribution' => $this->getSignalQualityDistribution($query),
|
||||
'latest_message' => $query->orderBy('created_at', 'desc')->first()
|
||||
];
|
||||
|
||||
// Hitung success rate
|
||||
if ($stats['total_messages'] > 0) {
|
||||
$stats['success_rate'] = round(($stats['processed_messages'] / $stats['total_messages']) * 100, 2);
|
||||
} else {
|
||||
$stats['success_rate'] = 0;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $stats,
|
||||
'period_hours' => $hours
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch statistics',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process unprocessed messages (untuk manual processing)
|
||||
*/
|
||||
public function processUnprocessedMessages(): JsonResponse
|
||||
{
|
||||
try {
|
||||
$unprocessedMessages = LoRaMessage::unprocessed()
|
||||
->inbound()
|
||||
->orderBy('received_at')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
$results = [];
|
||||
foreach ($unprocessedMessages as $message) {
|
||||
$result = $this->loraProcessor->processInboundMessage($message);
|
||||
$results[] = [
|
||||
'message_id' => $message->id,
|
||||
'node_id' => $message->node_id,
|
||||
'result' => $result
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Processed unprocessed messages',
|
||||
'data' => [
|
||||
'processed_count' => count($results),
|
||||
'results' => $results
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to process messages',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan message type berdasarkan payload
|
||||
*/
|
||||
private function determineMessageType(string $payload): string
|
||||
{
|
||||
$upperPayload = strtoupper($payload);
|
||||
|
||||
if (str_starts_with($upperPayload, 'SENSOR|')) {
|
||||
return 'sensor_data';
|
||||
} elseif (str_starts_with($upperPayload, 'HEARTBEAT|')) {
|
||||
return 'heartbeat';
|
||||
} elseif (str_starts_with($upperPayload, 'COMMAND|')) {
|
||||
return 'command';
|
||||
} elseif (str_starts_with($upperPayload, 'ACK|')) {
|
||||
return 'ack';
|
||||
} elseif (str_starts_with($upperPayload, 'CONFIG|')) {
|
||||
return 'config';
|
||||
} else {
|
||||
return 'sensor_data'; // Default assumption
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistik per message type
|
||||
*/
|
||||
private function getMessageTypeStatistics($query): array
|
||||
{
|
||||
$types = ['sensor_data', 'heartbeat', 'command', 'ack', 'config'];
|
||||
$typeStats = [];
|
||||
|
||||
foreach ($types as $type) {
|
||||
$typeStats[$type] = $query->where('message_type', $type)->count();
|
||||
}
|
||||
|
||||
return $typeStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistik per node
|
||||
*/
|
||||
private function getNodeStatistics($query): array
|
||||
{
|
||||
return $query->select('node_id')
|
||||
->selectRaw('COUNT(*) as message_count')
|
||||
->selectRaw('AVG(rssi) as avg_rssi')
|
||||
->selectRaw('MAX(created_at) as last_seen')
|
||||
->groupBy('node_id')
|
||||
->get()
|
||||
->keyBy('node_id')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribusi kualitas sinyal
|
||||
*/
|
||||
private function getSignalQualityDistribution($query): array
|
||||
{
|
||||
$messages = $query->whereNotNull('rssi')->get();
|
||||
$distribution = ['excellent' => 0, 'good' => 0, 'fair' => 0, 'poor' => 0, 'unknown' => 0];
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$quality = $message->getSignalQuality();
|
||||
$distribution[$quality]++;
|
||||
}
|
||||
|
||||
return $distribution;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,387 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\PirReading;
|
||||
use App\Models\Device;
|
||||
use App\Models\Alert;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class PirController extends Controller
|
||||
{
|
||||
/**
|
||||
* Terima data sensor PIR dari IoT device
|
||||
*/
|
||||
public function receiveData(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Validasi input
|
||||
$validator = Validator::make($request->all(), [
|
||||
'device_id' => 'required|exists:devices,id',
|
||||
'motion_detected' => 'required|boolean',
|
||||
'motion_intensity' => 'nullable|integer|min:0|max:100',
|
||||
'duration_seconds' => 'nullable|integer|min:0',
|
||||
'detection_zone' => 'nullable|string|in:front,back,side,center',
|
||||
'motion_start' => 'nullable|date',
|
||||
'motion_end' => 'nullable|date|after:motion_start',
|
||||
'metadata' => 'nullable|array'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
$recordedAt = now();
|
||||
|
||||
// Cek apakah dalam jam kerja
|
||||
$isAuthorizedTime = $this->checkAuthorizedTime($recordedAt);
|
||||
|
||||
// Tentukan intensitas default jika tidak ada
|
||||
$motionIntensity = $data['motion_intensity'] ?? ($data['motion_detected'] ? 50 : 0);
|
||||
|
||||
// Hitung durasi jika ada motion_start dan motion_end
|
||||
$durationSeconds = $data['duration_seconds'] ?? 0;
|
||||
if (isset($data['motion_start']) && isset($data['motion_end'])) {
|
||||
$start = Carbon::parse($data['motion_start']);
|
||||
$end = Carbon::parse($data['motion_end']);
|
||||
$durationSeconds = $start->diffInSeconds($end);
|
||||
}
|
||||
|
||||
// Tentukan apakah gerakan mencurigakan
|
||||
$isSuspicious = $this->determineSuspiciousMotion(
|
||||
$data['motion_detected'],
|
||||
$motionIntensity,
|
||||
$durationSeconds,
|
||||
$isAuthorizedTime
|
||||
);
|
||||
|
||||
// Tentukan tipe gerakan
|
||||
$motionType = $this->determineMotionType(
|
||||
$data['motion_detected'],
|
||||
$isSuspicious,
|
||||
$isAuthorizedTime,
|
||||
$motionIntensity,
|
||||
$durationSeconds
|
||||
);
|
||||
|
||||
// Simpan data PIR
|
||||
$pirReading = PirReading::create([
|
||||
'device_id' => $data['device_id'],
|
||||
'motion_detected' => $data['motion_detected'],
|
||||
'motion_intensity' => $motionIntensity,
|
||||
'duration_seconds' => $durationSeconds,
|
||||
'is_authorized_time' => $isAuthorizedTime,
|
||||
'is_suspicious' => $isSuspicious,
|
||||
'motion_type' => $motionType,
|
||||
'detection_zone' => $data['detection_zone'] ?? 'center',
|
||||
'metadata' => $data['metadata'] ?? null,
|
||||
'motion_start' => $data['motion_start'] ?? null,
|
||||
'motion_end' => $data['motion_end'] ?? null,
|
||||
'recorded_at' => $recordedAt
|
||||
]);
|
||||
|
||||
// Jika gerakan mencurigakan, buat alert dan kirim notifikasi
|
||||
$alertSent = false;
|
||||
if ($isSuspicious || $motionType === 'unauthorized') {
|
||||
$this->handleSuspiciousMotion($pirReading);
|
||||
$alertSent = true;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'PIR data received successfully',
|
||||
'data' => [
|
||||
'id' => $pirReading->id,
|
||||
'motion_detected' => $pirReading->motion_detected,
|
||||
'motion_type' => $motionType,
|
||||
'is_suspicious' => $isSuspicious,
|
||||
'is_authorized_time' => $isAuthorizedTime,
|
||||
'alert_sent' => $alertSent,
|
||||
'recorded_at' => $recordedAt->toISOString()
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('PIR data receive error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to process PIR data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil data PIR terbaru
|
||||
*/
|
||||
public function getLatestReadings(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deviceId = $request->get('device_id');
|
||||
$limit = $request->get('limit', 50);
|
||||
$motionType = $request->get('motion_type');
|
||||
$detectionZone = $request->get('detection_zone');
|
||||
|
||||
$query = PirReading::with('device')
|
||||
->orderBy('recorded_at', 'desc');
|
||||
|
||||
if ($deviceId) {
|
||||
$query->where('device_id', $deviceId);
|
||||
}
|
||||
|
||||
if ($motionType) {
|
||||
$query->where('motion_type', $motionType);
|
||||
}
|
||||
|
||||
if ($detectionZone) {
|
||||
$query->where('detection_zone', $detectionZone);
|
||||
}
|
||||
|
||||
$readings = $query->limit($limit)->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $readings,
|
||||
'count' => $readings->count()
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch PIR data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil statistik gerakan
|
||||
*/
|
||||
public function getStatistics(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deviceId = $request->get('device_id');
|
||||
$hours = $request->get('hours', 24);
|
||||
|
||||
$query = PirReading::where('recorded_at', '>=', now()->subHours($hours));
|
||||
|
||||
if ($deviceId) {
|
||||
$query->where('device_id', $deviceId);
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'total_readings' => $query->count(),
|
||||
'motion_detected_count' => $query->where('motion_detected', true)->count(),
|
||||
'suspicious_count' => $query->where('is_suspicious', true)->count(),
|
||||
'unauthorized_count' => $query->where('motion_type', 'unauthorized')->count(),
|
||||
'normal_count' => $query->where('motion_type', 'normal')->count(),
|
||||
'avg_intensity' => $query->where('motion_detected', true)->avg('motion_intensity'),
|
||||
'avg_duration' => $query->where('motion_detected', true)->avg('duration_seconds'),
|
||||
'max_intensity' => $query->max('motion_intensity'),
|
||||
'max_duration' => $query->max('duration_seconds'),
|
||||
'latest_motion' => $query->where('motion_detected', true)->orderBy('recorded_at', 'desc')->first(),
|
||||
'detection_zones' => $this->getZoneStatistics($query)
|
||||
];
|
||||
|
||||
// Hitung persentase
|
||||
if ($stats['total_readings'] > 0) {
|
||||
$stats['motion_percentage'] = round(($stats['motion_detected_count'] / $stats['total_readings']) * 100, 2);
|
||||
$stats['suspicious_percentage'] = round(($stats['suspicious_count'] / $stats['total_readings']) * 100, 2);
|
||||
} else {
|
||||
$stats['motion_percentage'] = 0;
|
||||
$stats['suspicious_percentage'] = 0;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $stats,
|
||||
'period_hours' => $hours
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch statistics',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah waktu dalam jam kerja
|
||||
*/
|
||||
private function checkAuthorizedTime(Carbon $timestamp): bool
|
||||
{
|
||||
// Jam kerja: Senin-Jumat 08:00-17:00
|
||||
$workStart = 8; // 08:00
|
||||
$workEnd = 17; // 17:00
|
||||
|
||||
$hour = $timestamp->hour;
|
||||
$isWeekday = $timestamp->isWeekday();
|
||||
|
||||
return $isWeekday && $hour >= $workStart && $hour < $workEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan apakah gerakan mencurigakan
|
||||
*/
|
||||
private function determineSuspiciousMotion(bool $motionDetected, int $intensity, int $duration, bool $isAuthorizedTime): bool
|
||||
{
|
||||
if (!$motionDetected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Gerakan mencurigakan jika:
|
||||
// 1. Di luar jam kerja
|
||||
// 2. Intensitas tinggi (>80)
|
||||
// 3. Durasi sangat lama (>300 detik)
|
||||
|
||||
if (!$isAuthorizedTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($intensity > 80) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($duration > 300) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan tipe gerakan
|
||||
*/
|
||||
private function determineMotionType(bool $motionDetected, bool $isSuspicious, bool $isAuthorizedTime, int $intensity, int $duration): string
|
||||
{
|
||||
if (!$motionDetected) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
if (!$isAuthorizedTime) {
|
||||
return 'unauthorized';
|
||||
}
|
||||
|
||||
if ($isSuspicious) {
|
||||
return 'suspicious';
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle gerakan mencurigakan - buat alert dan kirim notifikasi
|
||||
*/
|
||||
private function handleSuspiciousMotion(PirReading $reading): void
|
||||
{
|
||||
try {
|
||||
$device = $reading->device;
|
||||
|
||||
// Tentukan priority berdasarkan tipe gerakan
|
||||
$priority = match($reading->motion_type) {
|
||||
'unauthorized' => 'high',
|
||||
'suspicious' => 'medium',
|
||||
default => 'low'
|
||||
};
|
||||
|
||||
// Buat alert
|
||||
$alert = Alert::create([
|
||||
'device_id' => $reading->device_id,
|
||||
'type' => 'motion_detected',
|
||||
'priority' => $priority,
|
||||
'title' => 'Gerakan Mencurigakan Terdeteksi',
|
||||
'message' => $this->generateAlertMessage($reading, $device),
|
||||
'data' => [
|
||||
'pir_reading_id' => $reading->id,
|
||||
'motion_type' => $reading->motion_type,
|
||||
'intensity' => $reading->motion_intensity,
|
||||
'duration' => $reading->duration_seconds,
|
||||
'detection_zone' => $reading->detection_zone,
|
||||
'is_authorized_time' => $reading->is_authorized_time
|
||||
],
|
||||
'is_read' => false
|
||||
]);
|
||||
|
||||
// Kirim notifikasi
|
||||
$this->sendNotification($alert, $reading);
|
||||
|
||||
Log::info("Suspicious motion alert created", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_id' => $reading->device_id,
|
||||
'motion_type' => $reading->motion_type,
|
||||
'intensity' => $reading->motion_intensity
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to handle suspicious motion: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate pesan alert berdasarkan data PIR
|
||||
*/
|
||||
private function generateAlertMessage(PirReading $reading, Device $device): string
|
||||
{
|
||||
$timeInfo = $reading->is_authorized_time ? 'dalam jam kerja' : 'di luar jam kerja';
|
||||
$zoneInfo = $reading->detection_zone ? " di zona {$reading->detection_zone}" : '';
|
||||
|
||||
return "Gerakan {$reading->motion_type} terdeteksi pada {$device->name}{$zoneInfo} {$timeInfo}. " .
|
||||
"Intensitas: {$reading->motion_intensity}%, Durasi: {$reading->duration_seconds} detik.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim notifikasi menggunakan NotificationService
|
||||
*/
|
||||
private function sendNotification(Alert $alert, PirReading $reading): void
|
||||
{
|
||||
try {
|
||||
$notificationService = new NotificationService();
|
||||
$success = $notificationService->sendPirAlert($alert, $reading);
|
||||
|
||||
if ($success) {
|
||||
Log::info("PIR notification sent successfully", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_name' => $reading->device->name,
|
||||
'motion_type' => $reading->motion_type
|
||||
]);
|
||||
} else {
|
||||
Log::warning("PIR notification failed", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_id' => $reading->device_id
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('PIR notification service error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistik per zona deteksi
|
||||
*/
|
||||
private function getZoneStatistics($query): array
|
||||
{
|
||||
$zones = ['front', 'back', 'side', 'center'];
|
||||
$zoneStats = [];
|
||||
|
||||
foreach ($zones as $zone) {
|
||||
$zoneStats[$zone] = $query->where('detection_zone', $zone)->count();
|
||||
}
|
||||
|
||||
return $zoneStats;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,515 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ReedSwitchReading;
|
||||
use App\Models\Device;
|
||||
use App\Models\Alert;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ReedSwitchController extends Controller
|
||||
{
|
||||
/**
|
||||
* Terima data sensor Reed Switch dari IoT device
|
||||
*/
|
||||
public function receiveData(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Validasi input
|
||||
$validator = Validator::make($request->all(), [
|
||||
'device_id' => 'required|exists:devices,id',
|
||||
'door_open' => 'required|boolean',
|
||||
'access_method' => 'nullable|string|in:key,card,biometric,remote,force,unknown',
|
||||
'door_location' => 'nullable|string|in:front,back,side,main,emergency',
|
||||
'open_duration_seconds' => 'nullable|integer|min:0',
|
||||
'door_opened_at' => 'nullable|date',
|
||||
'door_closed_at' => 'nullable|date|after:door_opened_at',
|
||||
'metadata' => 'nullable|array'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
$recordedAt = now();
|
||||
|
||||
// Cek apakah dalam jam kerja
|
||||
$isAuthorizedTime = $this->checkAuthorizedTime($recordedAt);
|
||||
|
||||
// Set default values
|
||||
$accessMethod = $data['access_method'] ?? 'unknown';
|
||||
$doorLocation = $data['door_location'] ?? 'main';
|
||||
|
||||
// Hitung durasi jika ada door_opened_at dan door_closed_at
|
||||
$openDuration = $data['open_duration_seconds'] ?? 0;
|
||||
if (isset($data['door_opened_at']) && isset($data['door_closed_at'])) {
|
||||
$opened = Carbon::parse($data['door_opened_at']);
|
||||
$closed = Carbon::parse($data['door_closed_at']);
|
||||
$openDuration = $opened->diffInSeconds($closed);
|
||||
} elseif (isset($data['door_opened_at']) && $data['door_open']) {
|
||||
// Pintu masih terbuka, hitung dari sekarang
|
||||
$opened = Carbon::parse($data['door_opened_at']);
|
||||
$openDuration = $opened->diffInSeconds($recordedAt);
|
||||
}
|
||||
|
||||
// Deteksi pembukaan paksa
|
||||
$isForcedEntry = $this->detectForcedEntry(
|
||||
$data['door_open'],
|
||||
$accessMethod,
|
||||
$openDuration,
|
||||
$isAuthorizedTime
|
||||
);
|
||||
|
||||
// Tentukan apakah akses tidak sah
|
||||
$isUnauthorized = $this->determineUnauthorizedAccess(
|
||||
$data['door_open'],
|
||||
$isAuthorizedTime,
|
||||
$isForcedEntry,
|
||||
$accessMethod,
|
||||
$openDuration
|
||||
);
|
||||
|
||||
// Tentukan level akses
|
||||
$accessLevel = $this->determineAccessLevel(
|
||||
$isForcedEntry,
|
||||
$isAuthorizedTime,
|
||||
$accessMethod,
|
||||
$openDuration
|
||||
);
|
||||
|
||||
// Tentukan status pintu
|
||||
$doorStatus = $this->determineDoorStatus(
|
||||
$data['door_open'],
|
||||
$isForcedEntry,
|
||||
$openDuration
|
||||
);
|
||||
|
||||
// Simpan data Reed Switch
|
||||
$reedReading = ReedSwitchReading::create([
|
||||
'device_id' => $data['device_id'],
|
||||
'door_open' => $data['door_open'],
|
||||
'is_authorized' => !$isUnauthorized,
|
||||
'is_forced_entry' => $isForcedEntry,
|
||||
'access_method' => $accessMethod,
|
||||
'door_status' => $doorStatus,
|
||||
'open_duration_seconds' => $openDuration,
|
||||
'access_level' => $accessLevel,
|
||||
'door_location' => $doorLocation,
|
||||
'metadata' => $data['metadata'] ?? null,
|
||||
'door_opened_at' => $data['door_opened_at'] ?? null,
|
||||
'door_closed_at' => $data['door_closed_at'] ?? null,
|
||||
'recorded_at' => $recordedAt
|
||||
]);
|
||||
|
||||
// Jika akses tidak sah atau pembukaan paksa, buat alert dan kirim notifikasi
|
||||
$alertSent = false;
|
||||
if ($isUnauthorized || $isForcedEntry || $accessLevel !== 'normal') {
|
||||
$this->handleUnauthorizedAccess($reedReading);
|
||||
$alertSent = true;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Reed Switch data received successfully',
|
||||
'data' => [
|
||||
'id' => $reedReading->id,
|
||||
'door_open' => $reedReading->door_open,
|
||||
'door_status' => $doorStatus,
|
||||
'access_level' => $accessLevel,
|
||||
'is_authorized' => !$isUnauthorized,
|
||||
'is_forced_entry' => $isForcedEntry,
|
||||
'alert_sent' => $alertSent,
|
||||
'recorded_at' => $recordedAt->toISOString()
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Reed Switch data receive error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to process Reed Switch data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil data Reed Switch terbaru
|
||||
*/
|
||||
public function getLatestReadings(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deviceId = $request->get('device_id');
|
||||
$limit = $request->get('limit', 50);
|
||||
$accessLevel = $request->get('access_level');
|
||||
$doorLocation = $request->get('door_location');
|
||||
$doorStatus = $request->get('door_status');
|
||||
|
||||
$query = ReedSwitchReading::with('device')
|
||||
->orderBy('recorded_at', 'desc');
|
||||
|
||||
if ($deviceId) {
|
||||
$query->where('device_id', $deviceId);
|
||||
}
|
||||
|
||||
if ($accessLevel) {
|
||||
$query->where('access_level', $accessLevel);
|
||||
}
|
||||
|
||||
if ($doorLocation) {
|
||||
$query->where('door_location', $doorLocation);
|
||||
}
|
||||
|
||||
if ($doorStatus) {
|
||||
$query->where('door_status', $doorStatus);
|
||||
}
|
||||
|
||||
$readings = $query->limit($limit)->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $readings,
|
||||
'count' => $readings->count()
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch Reed Switch data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil statistik akses pintu
|
||||
*/
|
||||
public function getStatistics(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deviceId = $request->get('device_id');
|
||||
$hours = $request->get('hours', 24);
|
||||
|
||||
$query = ReedSwitchReading::where('recorded_at', '>=', now()->subHours($hours));
|
||||
|
||||
if ($deviceId) {
|
||||
$query->where('device_id', $deviceId);
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'total_readings' => $query->count(),
|
||||
'door_open_count' => $query->where('door_open', true)->count(),
|
||||
'unauthorized_count' => $query->where('is_authorized', false)->count(),
|
||||
'forced_entry_count' => $query->where('is_forced_entry', true)->count(),
|
||||
'normal_access_count' => $query->where('access_level', 'normal')->count(),
|
||||
'suspicious_access_count' => $query->where('access_level', 'suspicious')->count(),
|
||||
'emergency_access_count' => $query->where('access_level', 'emergency')->count(),
|
||||
'avg_open_duration' => $query->where('door_open', true)->avg('open_duration_seconds'),
|
||||
'max_open_duration' => $query->max('open_duration_seconds'),
|
||||
'latest_access' => $query->where('door_open', true)->orderBy('recorded_at', 'desc')->first(),
|
||||
'access_methods' => $this->getAccessMethodStatistics($query),
|
||||
'door_locations' => $this->getDoorLocationStatistics($query),
|
||||
'hourly_access' => $this->getHourlyAccessStatistics($query)
|
||||
];
|
||||
|
||||
// Hitung persentase
|
||||
if ($stats['total_readings'] > 0) {
|
||||
$stats['door_open_percentage'] = round(($stats['door_open_count'] / $stats['total_readings']) * 100, 2);
|
||||
$stats['unauthorized_percentage'] = round(($stats['unauthorized_count'] / $stats['total_readings']) * 100, 2);
|
||||
$stats['forced_entry_percentage'] = round(($stats['forced_entry_count'] / $stats['total_readings']) * 100, 2);
|
||||
} else {
|
||||
$stats['door_open_percentage'] = 0;
|
||||
$stats['unauthorized_percentage'] = 0;
|
||||
$stats['forced_entry_percentage'] = 0;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $stats,
|
||||
'period_hours' => $hours
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch statistics',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah waktu dalam jam kerja
|
||||
*/
|
||||
private function checkAuthorizedTime(Carbon $timestamp): bool
|
||||
{
|
||||
// Jam kerja: Senin-Jumat 07:00-18:00 (lebih fleksibel untuk akses pintu)
|
||||
$workStart = 7; // 07:00
|
||||
$workEnd = 18; // 18:00
|
||||
|
||||
$hour = $timestamp->hour;
|
||||
$isWeekday = $timestamp->isWeekday();
|
||||
|
||||
return $isWeekday && $hour >= $workStart && $hour < $workEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deteksi pembukaan paksa
|
||||
*/
|
||||
private function detectForcedEntry(bool $doorOpen, string $accessMethod, int $openDuration, bool $isAuthorizedTime): bool
|
||||
{
|
||||
if (!$doorOpen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pembukaan paksa jika:
|
||||
// 1. Metode akses adalah 'force'
|
||||
// 2. Metode akses tidak dikenal di luar jam kerja
|
||||
// 3. Pembukaan sangat cepat di luar jam kerja (hit and run)
|
||||
|
||||
if ($accessMethod === 'force') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($accessMethod === 'unknown' && !$isAuthorizedTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$isAuthorizedTime && $openDuration > 0 && $openDuration < 30) {
|
||||
return true; // Pembukaan cepat di luar jam kerja
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan apakah akses tidak sah
|
||||
*/
|
||||
private function determineUnauthorizedAccess(bool $doorOpen, bool $isAuthorizedTime, bool $isForcedEntry, string $accessMethod, int $openDuration): bool
|
||||
{
|
||||
if (!$doorOpen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Akses tidak sah jika:
|
||||
// 1. Pembukaan paksa
|
||||
// 2. Di luar jam kerja (kecuali emergency access)
|
||||
// 3. Durasi terbuka terlalu lama (>30 menit)
|
||||
// 4. Metode akses tidak valid
|
||||
|
||||
if ($isForcedEntry) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$isAuthorizedTime && $accessMethod !== 'emergency') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($openDuration > 1800) { // 30 menit
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($accessMethod === 'unknown') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan level akses
|
||||
*/
|
||||
private function determineAccessLevel(bool $isForcedEntry, bool $isAuthorizedTime, string $accessMethod, int $openDuration): string
|
||||
{
|
||||
if ($isForcedEntry) {
|
||||
return 'emergency';
|
||||
}
|
||||
|
||||
if (!$isAuthorizedTime) {
|
||||
return 'unauthorized';
|
||||
}
|
||||
|
||||
if ($openDuration > 1800 || $accessMethod === 'unknown') {
|
||||
return 'suspicious';
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan status pintu
|
||||
*/
|
||||
private function determineDoorStatus(bool $doorOpen, bool $isForcedEntry, int $openDuration): string
|
||||
{
|
||||
if ($isForcedEntry) {
|
||||
return 'forced';
|
||||
}
|
||||
|
||||
if (!$doorOpen) {
|
||||
return 'closed';
|
||||
}
|
||||
|
||||
if ($openDuration > 300) { // 5 menit
|
||||
return 'ajar'; // Pintu terbuka terlalu lama
|
||||
}
|
||||
|
||||
return 'open';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle akses tidak sah - buat alert dan kirim notifikasi
|
||||
*/
|
||||
private function handleUnauthorizedAccess(ReedSwitchReading $reading): void
|
||||
{
|
||||
try {
|
||||
$device = $reading->device;
|
||||
|
||||
// Tentukan priority berdasarkan level akses
|
||||
$priority = match($reading->access_level) {
|
||||
'emergency' => 'critical',
|
||||
'unauthorized' => 'high',
|
||||
'suspicious' => 'medium',
|
||||
default => 'low'
|
||||
};
|
||||
|
||||
// Buat alert
|
||||
$alert = Alert::create([
|
||||
'device_id' => $reading->device_id,
|
||||
'type' => 'door_access',
|
||||
'priority' => $priority,
|
||||
'title' => 'Akses Pintu Tidak Sah Terdeteksi',
|
||||
'message' => $this->generateAlertMessage($reading, $device),
|
||||
'data' => [
|
||||
'reed_switch_reading_id' => $reading->id,
|
||||
'access_level' => $reading->access_level,
|
||||
'access_method' => $reading->access_method,
|
||||
'door_status' => $reading->door_status,
|
||||
'door_location' => $reading->door_location,
|
||||
'open_duration' => $reading->open_duration_seconds,
|
||||
'is_forced_entry' => $reading->is_forced_entry,
|
||||
'is_authorized' => $reading->is_authorized
|
||||
],
|
||||
'is_read' => false
|
||||
]);
|
||||
|
||||
// Kirim notifikasi
|
||||
$this->sendNotification($alert, $reading);
|
||||
|
||||
Log::info("Unauthorized door access alert created", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_id' => $reading->device_id,
|
||||
'access_level' => $reading->access_level,
|
||||
'door_location' => $reading->door_location
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to handle unauthorized door access: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate pesan alert berdasarkan data Reed Switch
|
||||
*/
|
||||
private function generateAlertMessage(ReedSwitchReading $reading, Device $device): string
|
||||
{
|
||||
$timeInfo = $reading->checkAuthorizedTime() ? 'dalam jam kerja' : 'di luar jam kerja';
|
||||
$locationInfo = $reading->door_location ? " pintu {$reading->door_location}" : '';
|
||||
$methodInfo = $reading->access_method !== 'unknown' ? " menggunakan {$reading->access_method}" : ' dengan metode tidak dikenal';
|
||||
|
||||
$message = "Akses {$reading->access_level} terdeteksi pada {$device->name}{$locationInfo} {$timeInfo}{$methodInfo}.";
|
||||
|
||||
if ($reading->is_forced_entry) {
|
||||
$message .= " PEMBUKAAN PAKSA TERDETEKSI!";
|
||||
}
|
||||
|
||||
if ($reading->open_duration_seconds > 300) {
|
||||
$message .= " Pintu terbuka selama " . gmdate('H:i:s', $reading->open_duration_seconds) . ".";
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim notifikasi menggunakan NotificationService
|
||||
*/
|
||||
private function sendNotification(Alert $alert, ReedSwitchReading $reading): void
|
||||
{
|
||||
try {
|
||||
$notificationService = new NotificationService();
|
||||
$success = $notificationService->sendReedSwitchAlert($alert, $reading);
|
||||
|
||||
if ($success) {
|
||||
Log::info("Reed Switch notification sent successfully", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_name' => $reading->device->name,
|
||||
'access_level' => $reading->access_level
|
||||
]);
|
||||
} else {
|
||||
Log::warning("Reed Switch notification failed", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_id' => $reading->device_id
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Reed Switch notification service error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistik per metode akses
|
||||
*/
|
||||
private function getAccessMethodStatistics($query): array
|
||||
{
|
||||
$methods = ['key', 'card', 'biometric', 'remote', 'force', 'unknown'];
|
||||
$methodStats = [];
|
||||
|
||||
foreach ($methods as $method) {
|
||||
$methodStats[$method] = $query->where('access_method', $method)->count();
|
||||
}
|
||||
|
||||
return $methodStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistik per lokasi pintu
|
||||
*/
|
||||
private function getDoorLocationStatistics($query): array
|
||||
{
|
||||
$locations = ['front', 'back', 'side', 'main', 'emergency'];
|
||||
$locationStats = [];
|
||||
|
||||
foreach ($locations as $location) {
|
||||
$locationStats[$location] = $query->where('door_location', $location)->count();
|
||||
}
|
||||
|
||||
return $locationStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistik akses per jam
|
||||
*/
|
||||
private function getHourlyAccessStatistics($query): array
|
||||
{
|
||||
$hourlyStats = [];
|
||||
|
||||
for ($hour = 0; $hour < 24; $hour++) {
|
||||
$count = $query->whereRaw('HOUR(recorded_at) = ?', [$hour])->count();
|
||||
$hourlyStats[sprintf('%02d:00', $hour)] = $count;
|
||||
}
|
||||
|
||||
return $hourlyStats;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\VibrationReading;
|
||||
use App\Models\Device;
|
||||
use App\Models\Alert;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class VibrationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Terima data sensor getar dari IoT device
|
||||
*/
|
||||
public function receiveData(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Validasi input
|
||||
$validator = Validator::make($request->all(), [
|
||||
'device_id' => 'required|exists:devices,id',
|
||||
'x_axis' => 'required|numeric',
|
||||
'y_axis' => 'required|numeric',
|
||||
'z_axis' => 'required|numeric',
|
||||
'threshold' => 'nullable|numeric|min:0',
|
||||
'metadata' => 'nullable|array'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
|
||||
// Hitung magnitude getaran
|
||||
$magnitude = sqrt(
|
||||
pow($data['x_axis'], 2) +
|
||||
pow($data['y_axis'], 2) +
|
||||
pow($data['z_axis'], 2)
|
||||
);
|
||||
|
||||
// Set threshold default jika tidak ada
|
||||
$threshold = $data['threshold'] ?? 2.0;
|
||||
|
||||
// Tentukan status getaran
|
||||
$isAbnormal = $magnitude > $threshold;
|
||||
$status = $this->determineStatus($magnitude, $threshold);
|
||||
|
||||
// Simpan data vibration
|
||||
$vibrationReading = VibrationReading::create([
|
||||
'device_id' => $data['device_id'],
|
||||
'x_axis' => $data['x_axis'],
|
||||
'y_axis' => $data['y_axis'],
|
||||
'z_axis' => $data['z_axis'],
|
||||
'magnitude' => $magnitude,
|
||||
'is_abnormal' => $isAbnormal,
|
||||
'threshold' => $threshold,
|
||||
'status' => $status,
|
||||
'metadata' => $data['metadata'] ?? null,
|
||||
'recorded_at' => now()
|
||||
]);
|
||||
|
||||
// Jika getaran abnormal, buat alert dan kirim notifikasi
|
||||
if ($isAbnormal) {
|
||||
$this->handleAbnormalVibration($vibrationReading);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Vibration data received successfully',
|
||||
'data' => [
|
||||
'id' => $vibrationReading->id,
|
||||
'magnitude' => $magnitude,
|
||||
'status' => $status,
|
||||
'is_abnormal' => $isAbnormal,
|
||||
'alert_sent' => $isAbnormal
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Vibration data receive error: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to process vibration data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil data vibration terbaru
|
||||
*/
|
||||
public function getLatestReadings(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deviceId = $request->get('device_id');
|
||||
$limit = $request->get('limit', 50);
|
||||
$status = $request->get('status');
|
||||
|
||||
$query = VibrationReading::with('device')
|
||||
->orderBy('recorded_at', 'desc');
|
||||
|
||||
if ($deviceId) {
|
||||
$query->where('device_id', $deviceId);
|
||||
}
|
||||
|
||||
if ($status) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
$readings = $query->limit($limit)->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $readings,
|
||||
'count' => $readings->count()
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch vibration data',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil statistik getaran
|
||||
*/
|
||||
public function getStatistics(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deviceId = $request->get('device_id');
|
||||
$hours = $request->get('hours', 24);
|
||||
|
||||
$query = VibrationReading::where('recorded_at', '>=', now()->subHours($hours));
|
||||
|
||||
if ($deviceId) {
|
||||
$query->where('device_id', $deviceId);
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'total_readings' => $query->count(),
|
||||
'normal_count' => $query->where('status', 'normal')->count(),
|
||||
'warning_count' => $query->where('status', 'warning')->count(),
|
||||
'critical_count' => $query->where('status', 'critical')->count(),
|
||||
'abnormal_percentage' => 0,
|
||||
'avg_magnitude' => $query->avg('magnitude'),
|
||||
'max_magnitude' => $query->max('magnitude'),
|
||||
'latest_reading' => $query->orderBy('recorded_at', 'desc')->first()
|
||||
];
|
||||
|
||||
if ($stats['total_readings'] > 0) {
|
||||
$abnormalCount = $stats['warning_count'] + $stats['critical_count'];
|
||||
$stats['abnormal_percentage'] = round(($abnormalCount / $stats['total_readings']) * 100, 2);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $stats,
|
||||
'period_hours' => $hours
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch statistics',
|
||||
'error' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan status berdasarkan magnitude dan threshold
|
||||
*/
|
||||
private function determineStatus(float $magnitude, float $threshold): string
|
||||
{
|
||||
if ($magnitude <= $threshold) {
|
||||
return 'normal';
|
||||
} elseif ($magnitude <= $threshold * 1.5) {
|
||||
return 'warning';
|
||||
} else {
|
||||
return 'critical';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle getaran abnormal - buat alert dan kirim notifikasi
|
||||
*/
|
||||
private function handleAbnormalVibration(VibrationReading $reading): void
|
||||
{
|
||||
try {
|
||||
$device = $reading->device;
|
||||
|
||||
// Buat alert
|
||||
$alert = Alert::create([
|
||||
'device_id' => $reading->device_id,
|
||||
'type' => 'vibration_abnormal',
|
||||
'priority' => $reading->status === 'critical' ? 'high' : 'medium',
|
||||
'title' => 'Getaran Tidak Normal Terdeteksi',
|
||||
'message' => "Getaran abnormal terdeteksi pada {$device->name}. Magnitude: {$reading->magnitude}, Status: {$reading->status}",
|
||||
'data' => [
|
||||
'vibration_reading_id' => $reading->id,
|
||||
'magnitude' => $reading->magnitude,
|
||||
'threshold' => $reading->threshold,
|
||||
'status' => $reading->status,
|
||||
'axes' => [
|
||||
'x' => $reading->x_axis,
|
||||
'y' => $reading->y_axis,
|
||||
'z' => $reading->z_axis
|
||||
]
|
||||
],
|
||||
'is_read' => false
|
||||
]);
|
||||
|
||||
// Kirim notifikasi (implementasi sesuai kebutuhan)
|
||||
$this->sendNotification($alert, $reading);
|
||||
|
||||
Log::info("Abnormal vibration alert created", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_id' => $reading->device_id,
|
||||
'magnitude' => $reading->magnitude,
|
||||
'status' => $reading->status
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to handle abnormal vibration: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirim notifikasi menggunakan NotificationService
|
||||
*/
|
||||
private function sendNotification(Alert $alert, VibrationReading $reading): void
|
||||
{
|
||||
try {
|
||||
$notificationService = new NotificationService();
|
||||
$success = $notificationService->sendVibrationAlert($alert, $reading);
|
||||
|
||||
if ($success) {
|
||||
Log::info("Vibration notification sent successfully", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_name' => $reading->device->name,
|
||||
'magnitude' => $reading->magnitude,
|
||||
'status' => $reading->status
|
||||
]);
|
||||
} else {
|
||||
Log::warning("Vibration notification failed", [
|
||||
'alert_id' => $alert->id,
|
||||
'device_id' => $reading->device_id
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Notification service error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class ActivityLog extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id',
|
||||
'sensor_id',
|
||||
'event_type',
|
||||
'severity',
|
||||
'title',
|
||||
'description',
|
||||
'event_data',
|
||||
'location',
|
||||
'user_agent',
|
||||
'ip_address',
|
||||
'event_time',
|
||||
'is_acknowledged',
|
||||
'acknowledged_at',
|
||||
'acknowledged_by'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'event_data' => 'array',
|
||||
'event_time' => 'datetime',
|
||||
'acknowledged_at' => 'datetime',
|
||||
'is_acknowledged' => 'boolean'
|
||||
];
|
||||
|
||||
// Relasi ke device
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
// Relasi ke sensor
|
||||
public function sensor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Sensor::class);
|
||||
}
|
||||
|
||||
// Relasi ke alerts
|
||||
public function alerts(): HasMany
|
||||
{
|
||||
return $this->hasMany(Alert::class);
|
||||
}
|
||||
|
||||
// Scope untuk log hari ini
|
||||
public function scopeToday($query)
|
||||
{
|
||||
return $query->whereDate('event_time', today());
|
||||
}
|
||||
|
||||
// Scope berdasarkan severity
|
||||
public function scopeBySeverity($query, $severity)
|
||||
{
|
||||
return $query->where('severity', $severity);
|
||||
}
|
||||
|
||||
// Scope untuk log yang belum diakui
|
||||
public function scopeUnacknowledged($query)
|
||||
{
|
||||
return $query->where('is_acknowledged', false);
|
||||
}
|
||||
|
||||
// Scope berdasarkan event type
|
||||
public function scopeByEventType($query, $eventType)
|
||||
{
|
||||
return $query->where('event_type', $eventType);
|
||||
}
|
||||
|
||||
// Accessor untuk severity color
|
||||
public function getSeverityColorAttribute()
|
||||
{
|
||||
return match($this->severity) {
|
||||
'info' => 'blue',
|
||||
'warning' => 'yellow',
|
||||
'critical' => 'red',
|
||||
'error' => 'red',
|
||||
default => 'gray'
|
||||
};
|
||||
}
|
||||
|
||||
// Accessor untuk event type display
|
||||
public function getEventTypeDisplayAttribute()
|
||||
{
|
||||
return match($this->event_type) {
|
||||
'motion_detected' => 'Gerakan Terdeteksi',
|
||||
'door_opened' => 'Rak Dibuka',
|
||||
'door_closed' => 'Rak Ditutup',
|
||||
'vibration_detected' => 'Getaran Terdeteksi',
|
||||
'system_normal' => 'Sistem Normal',
|
||||
'device_offline' => 'Device Offline',
|
||||
'device_online' => 'Device Online',
|
||||
'low_battery' => 'Baterai Lemah',
|
||||
default => ucfirst(str_replace('_', ' ', $this->event_type))
|
||||
};
|
||||
}
|
||||
|
||||
// Method untuk acknowledge log
|
||||
public function acknowledge($acknowledgedBy = 'system')
|
||||
{
|
||||
$this->update([
|
||||
'is_acknowledged' => true,
|
||||
'acknowledged_at' => now(),
|
||||
'acknowledged_by' => $acknowledgedBy
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Alert extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id',
|
||||
'sensor_id',
|
||||
'activity_log_id',
|
||||
'alert_type',
|
||||
'priority',
|
||||
'status',
|
||||
'title',
|
||||
'message',
|
||||
'alert_data',
|
||||
'location',
|
||||
'triggered_at',
|
||||
'acknowledged_at',
|
||||
'resolved_at',
|
||||
'acknowledged_by',
|
||||
'resolved_by',
|
||||
'resolution_notes',
|
||||
'is_sent_notification',
|
||||
'notification_channels',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'alert_data' => 'array',
|
||||
'notification_channels' => 'array',
|
||||
'triggered_at' => 'datetime',
|
||||
'acknowledged_at' => 'datetime',
|
||||
'resolved_at' => 'datetime',
|
||||
'is_sent_notification' => 'boolean',
|
||||
];
|
||||
|
||||
// Relasi ke device
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
// Relasi ke sensor
|
||||
public function sensor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Sensor::class);
|
||||
}
|
||||
|
||||
// Relasi ke activity log
|
||||
public function activityLog(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ActivityLog::class);
|
||||
}
|
||||
|
||||
// Scope untuk alert aktif
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'active');
|
||||
}
|
||||
|
||||
// Scope berdasarkan priority
|
||||
public function scopeByPriority($query, $priority)
|
||||
{
|
||||
return $query->where('priority', $priority);
|
||||
}
|
||||
|
||||
// Scope untuk alert hari ini
|
||||
public function scopeToday($query)
|
||||
{
|
||||
return $query->whereDate('triggered_at', today());
|
||||
}
|
||||
|
||||
// Scope untuk alert yang belum diakui
|
||||
public function scopeUnacknowledged($query)
|
||||
{
|
||||
return $query->where('status', 'active');
|
||||
}
|
||||
|
||||
// Accessor untuk priority color
|
||||
public function getPriorityColorAttribute()
|
||||
{
|
||||
switch ($this->priority) {
|
||||
case 'low':
|
||||
return 'green';
|
||||
case 'medium':
|
||||
return 'yellow';
|
||||
case 'high':
|
||||
return 'orange';
|
||||
case 'critical':
|
||||
return 'red';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
// Accessor untuk status color
|
||||
public function getStatusColorAttribute()
|
||||
{
|
||||
switch ($this->status) {
|
||||
case 'active':
|
||||
return 'red';
|
||||
case 'acknowledged':
|
||||
return 'yellow';
|
||||
case 'resolved':
|
||||
return 'green';
|
||||
case 'dismissed':
|
||||
return 'gray';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
// Accessor untuk alert type display
|
||||
public function getAlertTypeDisplayAttribute()
|
||||
{
|
||||
switch ($this->alert_type) {
|
||||
case 'security_breach':
|
||||
return 'Pelanggaran Keamanan';
|
||||
case 'device_offline':
|
||||
return 'Device Offline';
|
||||
case 'sensor_offline':
|
||||
return 'Sensor Offline';
|
||||
case 'low_battery':
|
||||
return 'Baterai Lemah';
|
||||
case 'high_vibration':
|
||||
return 'Getaran Tinggi';
|
||||
case 'unauthorized_access':
|
||||
return 'Akses Tidak Sah';
|
||||
default:
|
||||
return ucfirst(str_replace('_', ' ', $this->alert_type));
|
||||
}
|
||||
}
|
||||
|
||||
// Method untuk acknowledge alert
|
||||
public function acknowledge($acknowledgedBy = 'system', $notes = null)
|
||||
{
|
||||
$this->update([
|
||||
'status' => 'acknowledged',
|
||||
'acknowledged_at' => now(),
|
||||
'acknowledged_by' => $acknowledgedBy,
|
||||
'resolution_notes' => $notes
|
||||
]);
|
||||
}
|
||||
|
||||
// Method untuk resolve alert
|
||||
public function resolve($resolvedBy = 'system', $notes = null)
|
||||
{
|
||||
$this->update([
|
||||
'status' => 'resolved',
|
||||
'resolved_at' => now(),
|
||||
'resolved_by' => $resolvedBy,
|
||||
'resolution_notes' => $notes,
|
||||
]);
|
||||
}
|
||||
|
||||
// Method untuk dismiss alert
|
||||
public function dismiss($dismissedBy = 'system', $notes = null)
|
||||
{
|
||||
$this->update([
|
||||
'status' => 'dismissed',
|
||||
'resolved_at' => now(),
|
||||
'resolved_by' => $dismissedBy,
|
||||
'resolution_notes' => $notes
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Device extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'device_id',
|
||||
'location',
|
||||
'type',
|
||||
'status',
|
||||
'ip_address',
|
||||
'mac_address',
|
||||
'signal_strength',
|
||||
'last_seen',
|
||||
'configuration',
|
||||
'description',
|
||||
'is_active'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'configuration' => 'array',
|
||||
'last_seen' => 'datetime',
|
||||
'is_active' => 'boolean',
|
||||
'signal_strength' => 'integer'
|
||||
];
|
||||
|
||||
// Relasi ke sensors
|
||||
public function sensors(): HasMany
|
||||
{
|
||||
return $this->hasMany(Sensor::class);
|
||||
}
|
||||
|
||||
// Relasi ke sensor readings
|
||||
public function sensorReadings(): HasMany
|
||||
{
|
||||
return $this->hasMany(SensorReading::class);
|
||||
}
|
||||
|
||||
// Relasi ke activity logs
|
||||
public function activityLogs(): HasMany
|
||||
{
|
||||
return $this->hasMany(ActivityLog::class);
|
||||
}
|
||||
|
||||
// Relasi ke alerts
|
||||
public function alerts(): HasMany
|
||||
{
|
||||
return $this->hasMany(Alert::class);
|
||||
}
|
||||
|
||||
// Scope untuk device online
|
||||
public function scopeOnline($query)
|
||||
{
|
||||
return $query->where('status', 'online');
|
||||
}
|
||||
|
||||
// Scope untuk device aktif
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
// Accessor untuk status badge color
|
||||
public function getStatusColorAttribute()
|
||||
{
|
||||
return match($this->status) {
|
||||
'online' => 'green',
|
||||
'offline' => 'red',
|
||||
'maintenance' => 'yellow',
|
||||
default => 'gray'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DoorAccessReading extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id',
|
||||
'door_opened',
|
||||
'is_authorized_access',
|
||||
'access_type',
|
||||
'access_method',
|
||||
'user_id_card',
|
||||
'duration_seconds',
|
||||
'is_suspicious',
|
||||
'door_location',
|
||||
'is_forced_entry',
|
||||
'metadata',
|
||||
'door_opened_at',
|
||||
'door_closed_at',
|
||||
'recorded_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'door_opened' => 'boolean',
|
||||
'is_authorized_access' => 'boolean',
|
||||
'is_suspicious' => 'boolean',
|
||||
'is_forced_entry' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'door_opened_at' => 'datetime',
|
||||
'door_closed_at' => 'datetime',
|
||||
'recorded_at' => 'datetime',
|
||||
'duration_seconds' => 'integer'
|
||||
];
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah waktu dalam jam kerja (authorized time)
|
||||
*/
|
||||
public function checkAuthorizedTime(Carbon $timestamp = null): bool
|
||||
{
|
||||
$time = $timestamp ?? $this->recorded_at ?? now();
|
||||
|
||||
// Jam kerja: Senin-Jumat 07:00-18:00 (lebih fleksibel untuk akses pintu)
|
||||
$workStart = 7; // 07:00
|
||||
$workEnd = 18; // 18:00
|
||||
|
||||
$hour = $time->hour;
|
||||
$isWeekday = $time->isWeekday();
|
||||
|
||||
return $isWeekday && $hour >= $workStart && $hour < $workEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan apakah akses mencurigakan berdasarkan berbagai faktor
|
||||
*/
|
||||
public function determineSuspiciousAccess(): bool
|
||||
{
|
||||
// Akses mencurigakan jika:
|
||||
// 1. Forced entry (paksa masuk)
|
||||
// 2. Akses di luar jam kerja tanpa ID card
|
||||
// 3. Durasi pintu terbuka terlalu lama (>180 detik)
|
||||
// 4. Akses berulang dalam waktu singkat
|
||||
// 5. Akses tanpa ID card di jam kerja
|
||||
|
||||
if ($this->is_forced_entry) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->checkAuthorizedTime() && !$this->user_id_card) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->duration_seconds > 180) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->user_id_card && $this->access_method !== 'emergency') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan tipe akses berdasarkan kondisi
|
||||
*/
|
||||
public function determineAccessType(): string
|
||||
{
|
||||
if ($this->is_forced_entry) {
|
||||
return 'forced_entry';
|
||||
}
|
||||
|
||||
if ($this->access_method === 'emergency') {
|
||||
return 'emergency';
|
||||
}
|
||||
|
||||
if ($this->access_method === 'maintenance') {
|
||||
return 'maintenance';
|
||||
}
|
||||
|
||||
if ($this->user_id_card && $this->checkAuthorizedTime()) {
|
||||
return 'authorized';
|
||||
}
|
||||
|
||||
if (!$this->checkAuthorizedTime()) {
|
||||
return 'after_hours';
|
||||
}
|
||||
|
||||
return 'unauthorized';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung durasi pintu terbuka jika ada door_opened_at dan door_closed_at
|
||||
*/
|
||||
public function calculateDuration(): int
|
||||
{
|
||||
if ($this->door_opened_at && $this->door_closed_at) {
|
||||
return $this->door_opened_at->diffInSeconds($this->door_closed_at);
|
||||
}
|
||||
|
||||
return $this->duration_seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah ID card valid (simulasi - bisa diintegrasikan dengan sistem HR)
|
||||
*/
|
||||
public function isValidIdCard(): bool
|
||||
{
|
||||
if (!$this->user_id_card) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simulasi validasi ID card
|
||||
// Format: EMP-XXXX (4 digit angka)
|
||||
return preg_match('/^EMP-\d{4}$/', $this->user_id_card);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority level berdasarkan tipe akses
|
||||
*/
|
||||
public function getPriorityLevel(): string
|
||||
{
|
||||
return match($this->access_type) {
|
||||
'forced_entry' => 'critical',
|
||||
'unauthorized' => 'high',
|
||||
'after_hours' => 'high',
|
||||
'emergency' => 'medium',
|
||||
'maintenance' => 'low',
|
||||
'authorized' => 'info',
|
||||
default => 'medium'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk pintu terbuka
|
||||
*/
|
||||
public function scopeDoorOpened($query)
|
||||
{
|
||||
return $query->where('door_opened', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk akses mencurigakan
|
||||
*/
|
||||
public function scopeSuspicious($query)
|
||||
{
|
||||
return $query->where('is_suspicious', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk akses tidak sah
|
||||
*/
|
||||
public function scopeUnauthorized($query)
|
||||
{
|
||||
return $query->where('is_authorized_access', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk tipe akses tertentu
|
||||
*/
|
||||
public function scopeAccessType($query, $type)
|
||||
{
|
||||
return $query->where('access_type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk lokasi pintu tertentu
|
||||
*/
|
||||
public function scopeDoorLocation($query, $location)
|
||||
{
|
||||
return $query->where('door_location', $location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk forced entry
|
||||
*/
|
||||
public function scopeForcedEntry($query)
|
||||
{
|
||||
return $query->where('is_forced_entry', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access summary untuk dashboard
|
||||
*/
|
||||
public function getAccessSummary(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'door_opened' => $this->door_opened,
|
||||
'access_type' => $this->access_type,
|
||||
'is_suspicious' => $this->is_suspicious,
|
||||
'user_id_card' => $this->user_id_card,
|
||||
'duration' => $this->duration_seconds,
|
||||
'location' => $this->door_location,
|
||||
'priority' => $this->getPriorityLevel(),
|
||||
'timestamp' => $this->recorded_at->format('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DoorReading extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id',
|
||||
'door_open',
|
||||
'is_authorized_access',
|
||||
'is_forced_entry',
|
||||
'access_type',
|
||||
'door_location',
|
||||
'open_duration_seconds',
|
||||
'proper_closure',
|
||||
'access_card_data',
|
||||
'metadata',
|
||||
'door_opened_at',
|
||||
'door_closed_at',
|
||||
'recorded_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'door_open' => 'boolean',
|
||||
'is_authorized_access' => 'boolean',
|
||||
'is_forced_entry' => 'boolean',
|
||||
'proper_closure' => 'boolean',
|
||||
'access_card_data' => 'array',
|
||||
'metadata' => 'array',
|
||||
'door_opened_at' => 'datetime',
|
||||
'door_closed_at' => 'datetime',
|
||||
'recorded_at' => 'datetime',
|
||||
'open_duration_seconds' => 'integer'
|
||||
];
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah akses dalam jam kerja yang sah
|
||||
*/
|
||||
public function checkAuthorizedAccess(Carbon $timestamp = null): bool
|
||||
{
|
||||
$time = $timestamp ?? $this->recorded_at ?? now();
|
||||
|
||||
// Jam kerja: Senin-Jumat 07:00-18:00 (lebih fleksibel untuk maintenance)
|
||||
$workStart = 7; // 07:00
|
||||
$workEnd = 18; // 18:00
|
||||
|
||||
$hour = $time->hour;
|
||||
$isWeekday = $time->isWeekday();
|
||||
|
||||
return $isWeekday && $hour >= $workStart && $hour < $workEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan apakah pembukaan paksa berdasarkan durasi dan kondisi
|
||||
*/
|
||||
public function detectForcedEntry(): bool
|
||||
{
|
||||
// Indikator pembukaan paksa:
|
||||
// 1. Dibuka di luar jam kerja tanpa kartu akses
|
||||
// 2. Durasi terbuka sangat singkat (<5 detik) - kemungkinan dipaksa
|
||||
// 3. Tidak ditutup dengan benar
|
||||
// 4. Tidak ada data kartu akses di luar jam kerja
|
||||
|
||||
if (!$this->is_authorized_access && !$this->access_card_data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->open_duration_seconds > 0 && $this->open_duration_seconds < 5) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->proper_closure && !$this->is_authorized_access) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan tipe akses berdasarkan kondisi
|
||||
*/
|
||||
public function determineAccessType(): string
|
||||
{
|
||||
if ($this->is_forced_entry) {
|
||||
return 'forced';
|
||||
}
|
||||
|
||||
if (!$this->is_authorized_access) {
|
||||
return 'unauthorized';
|
||||
}
|
||||
|
||||
// Cek apakah maintenance berdasarkan durasi dan waktu
|
||||
if ($this->open_duration_seconds > 300 && $this->is_authorized_access) {
|
||||
return 'maintenance';
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung durasi terbuka jika ada door_opened_at dan door_closed_at
|
||||
*/
|
||||
public function calculateOpenDuration(): int
|
||||
{
|
||||
if ($this->door_opened_at && $this->door_closed_at) {
|
||||
return $this->door_opened_at->diffInSeconds($this->door_closed_at);
|
||||
}
|
||||
|
||||
return $this->open_duration_seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah pintu masih terbuka (belum ditutup)
|
||||
*/
|
||||
public function isCurrentlyOpen(): bool
|
||||
{
|
||||
return $this->door_open && !$this->door_closed_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get security risk level
|
||||
*/
|
||||
public function getSecurityRiskLevel(): string
|
||||
{
|
||||
if ($this->is_forced_entry) {
|
||||
return 'critical';
|
||||
}
|
||||
|
||||
if (!$this->is_authorized_access) {
|
||||
return 'high';
|
||||
}
|
||||
|
||||
if ($this->open_duration_seconds > 600) { // Terbuka > 10 menit
|
||||
return 'medium';
|
||||
}
|
||||
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk pintu terbuka
|
||||
*/
|
||||
public function scopeOpen($query)
|
||||
{
|
||||
return $query->where('door_open', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk akses tidak sah
|
||||
*/
|
||||
public function scopeUnauthorized($query)
|
||||
{
|
||||
return $query->where('is_authorized_access', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk pembukaan paksa
|
||||
*/
|
||||
public function scopeForcedEntry($query)
|
||||
{
|
||||
return $query->where('is_forced_entry', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk tipe akses tertentu
|
||||
*/
|
||||
public function scopeAccessType($query, $type)
|
||||
{
|
||||
return $query->where('access_type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk lokasi pintu tertentu
|
||||
*/
|
||||
public function scopeDoorLocation($query, $location)
|
||||
{
|
||||
return $query->where('door_location', $location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk pintu yang masih terbuka
|
||||
*/
|
||||
public function scopeCurrentlyOpen($query)
|
||||
{
|
||||
return $query->where('door_open', true)->whereNull('door_closed_at');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class LoRaMessage extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id',
|
||||
'node_id',
|
||||
'gateway_id',
|
||||
'direction',
|
||||
'message_type',
|
||||
'payload',
|
||||
'parsed_data',
|
||||
'rssi',
|
||||
'snr',
|
||||
'spreading_factor',
|
||||
'frequency',
|
||||
'bandwidth',
|
||||
'is_processed',
|
||||
'is_acknowledged',
|
||||
'status',
|
||||
'error_message',
|
||||
'metadata',
|
||||
'transmitted_at',
|
||||
'received_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'parsed_data' => 'array',
|
||||
'metadata' => 'array',
|
||||
'is_processed' => 'boolean',
|
||||
'is_acknowledged' => 'boolean',
|
||||
'transmitted_at' => 'datetime',
|
||||
'received_at' => 'datetime',
|
||||
'rssi' => 'float',
|
||||
'snr' => 'float',
|
||||
'frequency' => 'float'
|
||||
];
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse LoRa payload berdasarkan message type
|
||||
*/
|
||||
public function parsePayload(): array
|
||||
{
|
||||
try {
|
||||
switch ($this->message_type) {
|
||||
case 'sensor_data':
|
||||
return $this->parseSensorData();
|
||||
case 'heartbeat':
|
||||
return $this->parseHeartbeat();
|
||||
case 'command':
|
||||
return $this->parseCommand();
|
||||
case 'ack':
|
||||
return $this->parseAcknowledgment();
|
||||
case 'config':
|
||||
return $this->parseConfig();
|
||||
default:
|
||||
return $this->parseGeneric();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("LoRa payload parsing failed: " . $e->getMessage(), [
|
||||
'message_id' => $this->id,
|
||||
'payload' => $this->payload
|
||||
]);
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse sensor data payload
|
||||
* Format: "SENSOR|VIBRATION|1.5,2.3,1.8|2.0" atau "SENSOR|PIR|1|85|120|front" atau "SENSOR|DOOR|1|EMP-1234|45|keycard"
|
||||
*/
|
||||
private function parseSensorData(): array
|
||||
{
|
||||
$parts = explode('|', $this->payload);
|
||||
|
||||
if (count($parts) < 3) {
|
||||
throw new \Exception('Invalid sensor data format');
|
||||
}
|
||||
|
||||
$sensorType = strtoupper($parts[1]);
|
||||
$data = ['sensor_type' => $sensorType];
|
||||
|
||||
switch ($sensorType) {
|
||||
case 'VIBRATION':
|
||||
// Format: SENSOR|VIBRATION|x,y,z|threshold
|
||||
if (count($parts) >= 4) {
|
||||
$axes = explode(',', $parts[2]);
|
||||
$data = array_merge($data, [
|
||||
'x_axis' => (float)($axes[0] ?? 0),
|
||||
'y_axis' => (float)($axes[1] ?? 0),
|
||||
'z_axis' => (float)($axes[2] ?? 0),
|
||||
'threshold' => (float)($parts[3] ?? 2.0)
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PIR':
|
||||
// Format: SENSOR|PIR|detected|intensity|duration|zone
|
||||
if (count($parts) >= 6) {
|
||||
$data = array_merge($data, [
|
||||
'motion_detected' => (bool)$parts[2],
|
||||
'motion_intensity' => (int)$parts[3],
|
||||
'duration_seconds' => (int)$parts[4],
|
||||
'detection_zone' => $parts[5]
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DOOR':
|
||||
// Format: SENSOR|DOOR|opened|id_card|duration|method|location
|
||||
if (count($parts) >= 7) {
|
||||
$data = array_merge($data, [
|
||||
'door_opened' => (bool)$parts[2],
|
||||
'user_id_card' => $parts[3] !== 'NULL' ? $parts[3] : null,
|
||||
'duration_seconds' => (int)$parts[4],
|
||||
'access_method' => $parts[5],
|
||||
'door_location' => $parts[6]
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$data['raw_data'] = array_slice($parts, 2);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse heartbeat payload
|
||||
* Format: "HEARTBEAT|battery_level|signal_strength|uptime"
|
||||
*/
|
||||
private function parseHeartbeat(): array
|
||||
{
|
||||
$parts = explode('|', $this->payload);
|
||||
|
||||
return [
|
||||
'message_type' => 'heartbeat',
|
||||
'battery_level' => isset($parts[1]) ? (int)$parts[1] : null,
|
||||
'signal_strength' => isset($parts[2]) ? (int)$parts[2] : null,
|
||||
'uptime_seconds' => isset($parts[3]) ? (int)$parts[3] : null,
|
||||
'timestamp' => now()->toISOString()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command payload
|
||||
* Format: "COMMAND|action|parameters"
|
||||
*/
|
||||
private function parseCommand(): array
|
||||
{
|
||||
$parts = explode('|', $this->payload);
|
||||
|
||||
return [
|
||||
'message_type' => 'command',
|
||||
'action' => $parts[1] ?? 'unknown',
|
||||
'parameters' => isset($parts[2]) ? explode(',', $parts[2]) : [],
|
||||
'command_id' => $parts[3] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse acknowledgment payload
|
||||
* Format: "ACK|command_id|status|message"
|
||||
*/
|
||||
private function parseAcknowledgment(): array
|
||||
{
|
||||
$parts = explode('|', $this->payload);
|
||||
|
||||
return [
|
||||
'message_type' => 'ack',
|
||||
'command_id' => $parts[1] ?? null,
|
||||
'ack_status' => $parts[2] ?? 'unknown',
|
||||
'ack_message' => $parts[3] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse config payload
|
||||
* Format: "CONFIG|parameter|value"
|
||||
*/
|
||||
private function parseConfig(): array
|
||||
{
|
||||
$parts = explode('|', $this->payload);
|
||||
|
||||
return [
|
||||
'message_type' => 'config',
|
||||
'parameter' => $parts[1] ?? null,
|
||||
'value' => $parts[2] ?? null,
|
||||
'config_id' => $parts[3] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse generic payload
|
||||
*/
|
||||
private function parseGeneric(): array
|
||||
{
|
||||
return [
|
||||
'message_type' => 'generic',
|
||||
'raw_payload' => $this->payload,
|
||||
'parts' => explode('|', $this->payload)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate command payload untuk dikirim ke LoRa node
|
||||
*/
|
||||
public static function generateCommandPayload(string $action, array $parameters = [], string $commandId = null): string
|
||||
{
|
||||
$commandId = $commandId ?? uniqid('CMD_');
|
||||
$paramStr = implode(',', $parameters);
|
||||
|
||||
return "COMMAND|{$action}|{$paramStr}|{$commandId}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate config payload untuk konfigurasi node
|
||||
*/
|
||||
public static function generateConfigPayload(string $parameter, $value, string $configId = null): string
|
||||
{
|
||||
$configId = $configId ?? uniqid('CFG_');
|
||||
|
||||
return "CONFIG|{$parameter}|{$value}|{$configId}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek kualitas sinyal LoRa
|
||||
*/
|
||||
public function getSignalQuality(): string
|
||||
{
|
||||
if ($this->rssi === null) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// RSSI thresholds (dBm)
|
||||
if ($this->rssi >= -70) {
|
||||
return 'excellent';
|
||||
} elseif ($this->rssi >= -85) {
|
||||
return 'good';
|
||||
} elseif ($this->rssi >= -100) {
|
||||
return 'fair';
|
||||
} else {
|
||||
return 'poor';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate jarak berdasarkan RSSI (rough calculation)
|
||||
*/
|
||||
public function estimateDistance(): ?float
|
||||
{
|
||||
if ($this->rssi === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simplified path loss calculation
|
||||
// Distance (km) = 10^((Tx_Power - RSSI - 32.44 - 20*log10(frequency)) / 20)
|
||||
$txPower = 14; // Typical LoRa TX power in dBm
|
||||
$frequency = $this->frequency ?? 868.0; // MHz
|
||||
|
||||
$pathLoss = $txPower - $this->rssi - 32.44 - (20 * log10($frequency));
|
||||
$distance = pow(10, $pathLoss / 20);
|
||||
|
||||
return round($distance, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk inbound messages
|
||||
*/
|
||||
public function scopeInbound($query)
|
||||
{
|
||||
return $query->where('direction', 'inbound');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk outbound messages
|
||||
*/
|
||||
public function scopeOutbound($query)
|
||||
{
|
||||
return $query->where('direction', 'outbound');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk unprocessed messages
|
||||
*/
|
||||
public function scopeUnprocessed($query)
|
||||
{
|
||||
return $query->where('is_processed', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk message type tertentu
|
||||
*/
|
||||
public function scopeMessageType($query, $type)
|
||||
{
|
||||
return $query->where('message_type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk node tertentu
|
||||
*/
|
||||
public function scopeFromNode($query, $nodeId)
|
||||
{
|
||||
return $query->where('node_id', $nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk gateway tertentu
|
||||
*/
|
||||
public function scopeFromGateway($query, $gatewayId)
|
||||
{
|
||||
return $query->where('gateway_id', $gatewayId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark message as processed
|
||||
*/
|
||||
public function markAsProcessed(bool $success = true, string $errorMessage = null): void
|
||||
{
|
||||
$this->update([
|
||||
'is_processed' => true,
|
||||
'status' => $success ? 'processed' : 'failed',
|
||||
'error_message' => $errorMessage
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark outbound message as acknowledged
|
||||
*/
|
||||
public function markAsAcknowledged(): void
|
||||
{
|
||||
$this->update([
|
||||
'is_acknowledged' => true,
|
||||
'status' => 'acknowledged'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get message summary untuk dashboard
|
||||
*/
|
||||
public function getMessageSummary(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'node_id' => $this->node_id,
|
||||
'direction' => $this->direction,
|
||||
'message_type' => $this->message_type,
|
||||
'signal_quality' => $this->getSignalQuality(),
|
||||
'estimated_distance' => $this->estimateDistance(),
|
||||
'is_processed' => $this->is_processed,
|
||||
'status' => $this->status,
|
||||
'received_at' => $this->received_at?->format('Y-m-d H:i:s'),
|
||||
'transmitted_at' => $this->transmitted_at?->format('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class PirReading extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id',
|
||||
'motion_detected',
|
||||
'motion_intensity',
|
||||
'duration_seconds',
|
||||
'is_authorized_time',
|
||||
'is_suspicious',
|
||||
'motion_type',
|
||||
'detection_zone',
|
||||
'metadata',
|
||||
'motion_start',
|
||||
'motion_end',
|
||||
'recorded_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'motion_detected' => 'boolean',
|
||||
'is_authorized_time' => 'boolean',
|
||||
'is_suspicious' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'motion_start' => 'datetime',
|
||||
'motion_end' => 'datetime',
|
||||
'recorded_at' => 'datetime',
|
||||
'motion_intensity' => 'integer',
|
||||
'duration_seconds' => 'integer'
|
||||
];
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah waktu dalam jam kerja (authorized time)
|
||||
*/
|
||||
public function checkAuthorizedTime(Carbon $timestamp = null): bool
|
||||
{
|
||||
$time = $timestamp ?? $this->recorded_at ?? now();
|
||||
|
||||
// Jam kerja: Senin-Jumat 08:00-17:00
|
||||
$workStart = 8; // 08:00
|
||||
$workEnd = 17; // 17:00
|
||||
|
||||
$hour = $time->hour;
|
||||
$isWeekday = $time->isWeekday();
|
||||
|
||||
return $isWeekday && $hour >= $workStart && $hour < $workEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan apakah gerakan mencurigakan
|
||||
*/
|
||||
public function determineSuspiciousMotion(): bool
|
||||
{
|
||||
// Gerakan mencurigakan jika:
|
||||
// 1. Di luar jam kerja
|
||||
// 2. Intensitas tinggi (>70) di luar jam kerja
|
||||
// 3. Durasi sangat lama (>300 detik) di luar jam kerja
|
||||
// 4. Gerakan berulang dalam waktu singkat
|
||||
|
||||
if (!$this->is_authorized_time) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->is_authorized_time && $this->motion_intensity > 70) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->is_authorized_time && $this->duration_seconds > 300) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan tipe gerakan berdasarkan kondisi
|
||||
*/
|
||||
public function determineMotionType(): string
|
||||
{
|
||||
if (!$this->motion_detected) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
if (!$this->is_authorized_time) {
|
||||
return 'unauthorized';
|
||||
}
|
||||
|
||||
if ($this->is_suspicious) {
|
||||
return 'suspicious';
|
||||
}
|
||||
|
||||
if ($this->motion_intensity > 80 || $this->duration_seconds > 600) {
|
||||
return 'suspicious';
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung durasi gerakan jika ada motion_start dan motion_end
|
||||
*/
|
||||
public function calculateDuration(): int
|
||||
{
|
||||
if ($this->motion_start && $this->motion_end) {
|
||||
return $this->motion_start->diffInSeconds($this->motion_end);
|
||||
}
|
||||
|
||||
return $this->duration_seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk gerakan terdeteksi
|
||||
*/
|
||||
public function scopeMotionDetected($query)
|
||||
{
|
||||
return $query->where('motion_detected', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk gerakan mencurigakan
|
||||
*/
|
||||
public function scopeSuspicious($query)
|
||||
{
|
||||
return $query->where('is_suspicious', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk gerakan di luar jam kerja
|
||||
*/
|
||||
public function scopeUnauthorizedTime($query)
|
||||
{
|
||||
return $query->where('is_authorized_time', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk tipe gerakan tertentu
|
||||
*/
|
||||
public function scopeMotionType($query, $type)
|
||||
{
|
||||
return $query->where('motion_type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk zona deteksi tertentu
|
||||
*/
|
||||
public function scopeDetectionZone($query, $zone)
|
||||
{
|
||||
return $query->where('detection_zone', $zone);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ReedSwitchReading extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id',
|
||||
'door_open',
|
||||
'is_authorized',
|
||||
'is_forced_entry',
|
||||
'access_method',
|
||||
'door_status',
|
||||
'open_duration_seconds',
|
||||
'access_level',
|
||||
'door_location',
|
||||
'metadata',
|
||||
'door_opened_at',
|
||||
'door_closed_at',
|
||||
'recorded_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'door_open' => 'boolean',
|
||||
'is_authorized' => 'boolean',
|
||||
'is_forced_entry' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'door_opened_at' => 'datetime',
|
||||
'door_closed_at' => 'datetime',
|
||||
'recorded_at' => 'datetime',
|
||||
'open_duration_seconds' => 'integer'
|
||||
];
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah akses dalam jam kerja yang diizinkan
|
||||
*/
|
||||
public function checkAuthorizedTime(Carbon $timestamp = null): bool
|
||||
{
|
||||
$time = $timestamp ?? $this->recorded_at ?? now();
|
||||
|
||||
// Jam kerja: Senin-Jumat 07:00-18:00 (lebih fleksibel untuk akses pintu)
|
||||
$workStart = 7; // 07:00
|
||||
$workEnd = 18; // 18:00
|
||||
|
||||
$hour = $time->hour;
|
||||
$isWeekday = $time->isWeekday();
|
||||
|
||||
return $isWeekday && $hour >= $workStart && $hour < $workEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan apakah akses tidak sah berdasarkan berbagai faktor
|
||||
*/
|
||||
public function determineUnauthorizedAccess(): bool
|
||||
{
|
||||
// Akses tidak sah jika:
|
||||
// 1. Di luar jam kerja
|
||||
// 2. Pembukaan paksa (forced entry)
|
||||
// 3. Durasi terbuka terlalu lama (>1800 detik = 30 menit)
|
||||
// 4. Metode akses tidak dikenal
|
||||
|
||||
if (!$this->checkAuthorizedTime()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->is_forced_entry) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->open_duration_seconds > 1800) { // 30 menit
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->access_method === 'unknown' || $this->access_method === 'force') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan level akses berdasarkan kondisi
|
||||
*/
|
||||
public function determineAccessLevel(): string
|
||||
{
|
||||
if ($this->is_forced_entry) {
|
||||
return 'emergency';
|
||||
}
|
||||
|
||||
if (!$this->checkAuthorizedTime()) {
|
||||
return 'unauthorized';
|
||||
}
|
||||
|
||||
if ($this->open_duration_seconds > 1800 || $this->access_method === 'unknown') {
|
||||
return 'suspicious';
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan status pintu berdasarkan kondisi
|
||||
*/
|
||||
public function determineDoorStatus(): string
|
||||
{
|
||||
if ($this->is_forced_entry) {
|
||||
return 'forced';
|
||||
}
|
||||
|
||||
if (!$this->door_open) {
|
||||
return 'closed';
|
||||
}
|
||||
|
||||
if ($this->open_duration_seconds > 300) { // 5 menit
|
||||
return 'ajar'; // Pintu terbuka terlalu lama
|
||||
}
|
||||
|
||||
return 'open';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung durasi pintu terbuka jika ada door_opened_at dan door_closed_at
|
||||
*/
|
||||
public function calculateOpenDuration(): int
|
||||
{
|
||||
if ($this->door_opened_at && $this->door_closed_at) {
|
||||
return $this->door_opened_at->diffInSeconds($this->door_closed_at);
|
||||
}
|
||||
|
||||
if ($this->door_opened_at && $this->door_open) {
|
||||
// Pintu masih terbuka, hitung dari sekarang
|
||||
return $this->door_opened_at->diffInSeconds(now());
|
||||
}
|
||||
|
||||
return $this->open_duration_seconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah ini pembukaan paksa berdasarkan pola
|
||||
*/
|
||||
public function detectForcedEntry(): bool
|
||||
{
|
||||
// Deteksi pembukaan paksa berdasarkan:
|
||||
// 1. Tidak ada metode akses yang valid
|
||||
// 2. Pembukaan di luar jam kerja dengan durasi pendek (hit and run)
|
||||
// 3. Multiple attempts dalam waktu singkat
|
||||
|
||||
if ($this->access_method === 'force' || $this->access_method === 'unknown') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->checkAuthorizedTime() && $this->open_duration_seconds < 30) {
|
||||
return true; // Pembukaan cepat di luar jam kerja
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk pintu terbuka
|
||||
*/
|
||||
public function scopeDoorOpen($query)
|
||||
{
|
||||
return $query->where('door_open', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk akses tidak sah
|
||||
*/
|
||||
public function scopeUnauthorized($query)
|
||||
{
|
||||
return $query->where('is_authorized', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk pembukaan paksa
|
||||
*/
|
||||
public function scopeForcedEntry($query)
|
||||
{
|
||||
return $query->where('is_forced_entry', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk level akses tertentu
|
||||
*/
|
||||
public function scopeAccessLevel($query, $level)
|
||||
{
|
||||
return $query->where('access_level', $level);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk lokasi pintu tertentu
|
||||
*/
|
||||
public function scopeDoorLocation($query, $location)
|
||||
{
|
||||
return $query->where('door_location', $location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk status pintu tertentu
|
||||
*/
|
||||
public function scopeDoorStatus($query, $status)
|
||||
{
|
||||
return $query->where('door_status', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk metode akses tertentu
|
||||
*/
|
||||
public function scopeAccessMethod($query, $method)
|
||||
{
|
||||
return $query->where('access_method', $method);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Sensor extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id',
|
||||
'name',
|
||||
'type',
|
||||
'pin_number',
|
||||
'status',
|
||||
'threshold_min',
|
||||
'threshold_max',
|
||||
'unit',
|
||||
'sampling_rate',
|
||||
'calibration_data',
|
||||
'description',
|
||||
'is_active'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'calibration_data' => 'array',
|
||||
'threshold_min' => 'decimal:2',
|
||||
'threshold_max' => 'decimal:2',
|
||||
'is_active' => 'boolean',
|
||||
'sampling_rate' => 'integer'
|
||||
];
|
||||
|
||||
// Relasi ke device
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
// Relasi ke sensor readings
|
||||
public function sensorReadings(): HasMany
|
||||
{
|
||||
return $this->hasMany(SensorReading::class);
|
||||
}
|
||||
|
||||
// Relasi ke activity logs
|
||||
public function activityLogs(): HasMany
|
||||
{
|
||||
return $this->hasMany(ActivityLog::class);
|
||||
}
|
||||
|
||||
// Relasi ke alerts
|
||||
public function alerts(): HasMany
|
||||
{
|
||||
return $this->hasMany(Alert::class);
|
||||
}
|
||||
|
||||
// Scope untuk sensor aktif
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'active')->where('is_active', true);
|
||||
}
|
||||
|
||||
// Scope berdasarkan tipe sensor
|
||||
public function scopeOfType($query, $type)
|
||||
{
|
||||
return $query->where('type', $type);
|
||||
}
|
||||
|
||||
// Accessor untuk status color
|
||||
public function getStatusColorAttribute()
|
||||
{
|
||||
return match($this->status) {
|
||||
'active' => 'green',
|
||||
'inactive' => 'red',
|
||||
'error' => 'red',
|
||||
default => 'gray'
|
||||
};
|
||||
}
|
||||
|
||||
// Accessor untuk type display name
|
||||
public function getTypeDisplayAttribute()
|
||||
{
|
||||
return match($this->type) {
|
||||
'pir' => 'PIR Motion Sensor',
|
||||
'vibration' => 'Vibration Sensor (SW-420)',
|
||||
'reed_switch' => 'Reed Switch',
|
||||
'temperature' => 'Temperature Sensor',
|
||||
'humidity' => 'Humidity Sensor',
|
||||
default => ucfirst($this->type)
|
||||
};
|
||||
}
|
||||
|
||||
// Method untuk mendapatkan reading terakhir
|
||||
public function getLatestReading()
|
||||
{
|
||||
return $this->sensorReadings()->latest('reading_time')->first();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class SensorReading extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'sensor_id',
|
||||
'device_id',
|
||||
'value',
|
||||
'raw_value',
|
||||
'status',
|
||||
'battery_level',
|
||||
'signal_strength',
|
||||
'metadata',
|
||||
'reading_time',
|
||||
'is_processed'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array',
|
||||
'value' => 'decimal:4',
|
||||
'battery_level' => 'decimal:2',
|
||||
'signal_strength' => 'integer',
|
||||
'reading_time' => 'datetime',
|
||||
'is_processed' => 'boolean'
|
||||
];
|
||||
|
||||
// Relasi ke sensor
|
||||
public function sensor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Sensor::class);
|
||||
}
|
||||
|
||||
// Relasi ke device
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
// Scope untuk reading hari ini
|
||||
public function scopeToday($query)
|
||||
{
|
||||
return $query->whereDate('reading_time', today());
|
||||
}
|
||||
|
||||
// Scope untuk reading dalam rentang waktu
|
||||
public function scopeBetweenDates($query, $startDate, $endDate)
|
||||
{
|
||||
return $query->whereBetween('reading_time', [$startDate, $endDate]);
|
||||
}
|
||||
|
||||
// Scope berdasarkan status
|
||||
public function scopeByStatus($query, $status)
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
|
||||
// Accessor untuk status color
|
||||
public function getStatusColorAttribute()
|
||||
{
|
||||
return match($this->status) {
|
||||
'normal' => 'green',
|
||||
'warning' => 'yellow',
|
||||
'critical' => 'red',
|
||||
'error' => 'red',
|
||||
default => 'gray'
|
||||
};
|
||||
}
|
||||
|
||||
// Accessor untuk formatted value
|
||||
public function getFormattedValueAttribute()
|
||||
{
|
||||
$unit = $this->sensor->unit ?? '';
|
||||
return $this->value . ($unit ? ' ' . $unit : '');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class VibrationReading extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id',
|
||||
'x_axis',
|
||||
'y_axis',
|
||||
'z_axis',
|
||||
'magnitude',
|
||||
'is_abnormal',
|
||||
'threshold',
|
||||
'status',
|
||||
'metadata',
|
||||
'recorded_at'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_abnormal' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'recorded_at' => 'datetime',
|
||||
'x_axis' => 'float',
|
||||
'y_axis' => 'float',
|
||||
'z_axis' => 'float',
|
||||
'magnitude' => 'float',
|
||||
'threshold' => 'float'
|
||||
];
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung magnitude getaran dari sumbu X, Y, Z
|
||||
*/
|
||||
public function calculateMagnitude(): float
|
||||
{
|
||||
return sqrt(pow($this->x_axis, 2) + pow($this->y_axis, 2) + pow($this->z_axis, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cek apakah getaran abnormal berdasarkan threshold
|
||||
*/
|
||||
public function checkAbnormal(): bool
|
||||
{
|
||||
$magnitude = $this->calculateMagnitude();
|
||||
return $magnitude > $this->threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tentukan status berdasarkan magnitude
|
||||
*/
|
||||
public function determineStatus(): string
|
||||
{
|
||||
$magnitude = $this->calculateMagnitude();
|
||||
|
||||
if ($magnitude <= $this->threshold) {
|
||||
return 'normal';
|
||||
} elseif ($magnitude <= $this->threshold * 1.5) {
|
||||
return 'warning';
|
||||
} else {
|
||||
return 'critical';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk getaran abnormal
|
||||
*/
|
||||
public function scopeAbnormal($query)
|
||||
{
|
||||
return $query->where('is_abnormal', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope untuk status tertentu
|
||||
*/
|
||||
public function scopeStatus($query, $status)
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,556 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\LoRaMessage;
|
||||
use App\Models\VibrationReading;
|
||||
use App\Models\PirReading;
|
||||
use App\Models\DoorAccessReading;
|
||||
use App\Models\Device;
|
||||
use App\Services\NotificationService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class LoRaProcessingService
|
||||
{
|
||||
protected $notificationService;
|
||||
|
||||
public function __construct(NotificationService $notificationService)
|
||||
{
|
||||
$this->notificationService = $notificationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process inbound LoRa message
|
||||
*/
|
||||
public function processInboundMessage(LoRaMessage $message): array
|
||||
{
|
||||
try {
|
||||
// Parse payload
|
||||
$parsedData = $message->parsePayload();
|
||||
|
||||
// Update message dengan parsed data
|
||||
$message->update([
|
||||
'parsed_data' => $parsedData
|
||||
]);
|
||||
|
||||
$result = ['success' => false, 'action' => 'none'];
|
||||
|
||||
// Process berdasarkan message type
|
||||
switch ($message->message_type) {
|
||||
case 'sensor_data':
|
||||
$result = $this->processSensorData($message, $parsedData);
|
||||
break;
|
||||
|
||||
case 'heartbeat':
|
||||
$result = $this->processHeartbeat($message, $parsedData);
|
||||
break;
|
||||
|
||||
case 'ack':
|
||||
$result = $this->processAcknowledgment($message, $parsedData);
|
||||
break;
|
||||
|
||||
default:
|
||||
$result = ['success' => true, 'action' => 'logged'];
|
||||
}
|
||||
|
||||
// Mark message as processed
|
||||
$message->markAsProcessed($result['success'], $result['error'] ?? null);
|
||||
|
||||
Log::info("LoRa message processed", [
|
||||
'message_id' => $message->id,
|
||||
'node_id' => $message->node_id,
|
||||
'message_type' => $message->message_type,
|
||||
'result' => $result
|
||||
]);
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$message->markAsProcessed(false, $e->getMessage());
|
||||
|
||||
Log::error("LoRa message processing failed", [
|
||||
'message_id' => $message->id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process sensor data dari LoRa message
|
||||
*/
|
||||
private function processSensorData(LoRaMessage $message, array $parsedData): array
|
||||
{
|
||||
if (!isset($parsedData['sensor_type'])) {
|
||||
return ['success' => false, 'error' => 'Missing sensor type'];
|
||||
}
|
||||
|
||||
$sensorType = strtoupper($parsedData['sensor_type']);
|
||||
|
||||
switch ($sensorType) {
|
||||
case 'VIBRATION':
|
||||
return $this->processVibrationData($message, $parsedData);
|
||||
|
||||
case 'PIR':
|
||||
return $this->processPirData($message, $parsedData);
|
||||
|
||||
case 'DOOR':
|
||||
return $this->processDoorAccessData($message, $parsedData);
|
||||
|
||||
default:
|
||||
return ['success' => false, 'error' => 'Unknown sensor type: ' . $sensorType];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process vibration sensor data
|
||||
*/
|
||||
private function processVibrationData(LoRaMessage $message, array $data): array
|
||||
{
|
||||
try {
|
||||
// Hitung magnitude
|
||||
$magnitude = sqrt(
|
||||
pow($data['x_axis'] ?? 0, 2) +
|
||||
pow($data['y_axis'] ?? 0, 2) +
|
||||
pow($data['z_axis'] ?? 0, 2)
|
||||
);
|
||||
|
||||
$threshold = $data['threshold'] ?? 2.0;
|
||||
$isAbnormal = $magnitude > $threshold;
|
||||
|
||||
$status = 'normal';
|
||||
if ($magnitude > $threshold * 1.5) {
|
||||
$status = 'critical';
|
||||
} elseif ($magnitude > $threshold) {
|
||||
$status = 'warning';
|
||||
}
|
||||
|
||||
// Simpan ke VibrationReading
|
||||
$vibrationReading = VibrationReading::create([
|
||||
'device_id' => $message->device_id,
|
||||
'x_axis' => $data['x_axis'] ?? 0,
|
||||
'y_axis' => $data['y_axis'] ?? 0,
|
||||
'z_axis' => $data['z_axis'] ?? 0,
|
||||
'magnitude' => $magnitude,
|
||||
'is_abnormal' => $isAbnormal,
|
||||
'threshold' => $threshold,
|
||||
'status' => $status,
|
||||
'metadata' => [
|
||||
'lora_message_id' => $message->id,
|
||||
'node_id' => $message->node_id,
|
||||
'rssi' => $message->rssi,
|
||||
'snr' => $message->snr
|
||||
],
|
||||
'recorded_at' => $message->received_at ?? now()
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'action' => 'vibration_data_saved',
|
||||
'vibration_reading_id' => $vibrationReading->id,
|
||||
'magnitude' => $magnitude,
|
||||
'status' => $status,
|
||||
'is_abnormal' => $isAbnormal
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return ['success' => false, 'error' => 'Vibration processing failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process PIR sensor data
|
||||
*/
|
||||
private function processPirData(LoRaMessage $message, array $data): array
|
||||
{
|
||||
try {
|
||||
$isAuthorizedTime = $this->checkAuthorizedTime();
|
||||
$isSuspicious = $this->determineSuspiciousPirMotion($data, $isAuthorizedTime);
|
||||
|
||||
$motionType = 'normal';
|
||||
if (!$isAuthorizedTime) {
|
||||
$motionType = 'unauthorized';
|
||||
} elseif ($isSuspicious) {
|
||||
$motionType = 'suspicious';
|
||||
}
|
||||
|
||||
// Simpan ke PirReading
|
||||
$pirReading = PirReading::create([
|
||||
'device_id' => $message->device_id,
|
||||
'motion_detected' => $data['motion_detected'] ?? false,
|
||||
'motion_intensity' => $data['motion_intensity'] ?? 0,
|
||||
'duration_seconds' => $data['duration_seconds'] ?? 0,
|
||||
'is_authorized_time' => $isAuthorizedTime,
|
||||
'is_suspicious' => $isSuspicious,
|
||||
'motion_type' => $motionType,
|
||||
'detection_zone' => $data['detection_zone'] ?? 'center',
|
||||
'metadata' => [
|
||||
'lora_message_id' => $message->id,
|
||||
'node_id' => $message->node_id,
|
||||
'rssi' => $message->rssi,
|
||||
'snr' => $message->snr
|
||||
],
|
||||
'recorded_at' => $message->received_at ?? now()
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'action' => 'pir_data_saved',
|
||||
'pir_reading_id' => $pirReading->id,
|
||||
'motion_type' => $motionType,
|
||||
'is_suspicious' => $isSuspicious
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return ['success' => false, 'error' => 'PIR processing failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process door access sensor data
|
||||
*/
|
||||
private function processDoorAccessData(LoRaMessage $message, array $data): array
|
||||
{
|
||||
try {
|
||||
$isAuthorizedTime = $this->checkAuthorizedTime();
|
||||
$isAuthorizedAccess = $this->determineAuthorizedDoorAccess($data, $isAuthorizedTime);
|
||||
$isSuspicious = $this->determineSuspiciousDoorAccess($data, $isAuthorizedAccess);
|
||||
|
||||
$accessType = $this->determineDoorAccessType($data, $isAuthorizedAccess, $isAuthorizedTime);
|
||||
|
||||
// Simpan ke DoorAccessReading
|
||||
$doorReading = DoorAccessReading::create([
|
||||
'device_id' => $message->device_id,
|
||||
'door_opened' => $data['door_opened'] ?? false,
|
||||
'is_authorized_access' => $isAuthorizedAccess,
|
||||
'access_type' => $accessType,
|
||||
'access_method' => $data['access_method'] ?? 'unknown',
|
||||
'user_id_card' => $data['user_id_card'] ?? null,
|
||||
'duration_seconds' => $data['duration_seconds'] ?? 0,
|
||||
'is_suspicious' => $isSuspicious,
|
||||
'door_location' => $data['door_location'] ?? 'main_entrance',
|
||||
'is_forced_entry' => ($data['access_method'] ?? '') === 'force',
|
||||
'metadata' => [
|
||||
'lora_message_id' => $message->id,
|
||||
'node_id' => $message->node_id,
|
||||
'rssi' => $message->rssi,
|
||||
'snr' => $message->snr
|
||||
],
|
||||
'recorded_at' => $message->received_at ?? now()
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'action' => 'door_access_data_saved',
|
||||
'door_reading_id' => $doorReading->id,
|
||||
'access_type' => $accessType,
|
||||
'is_suspicious' => $isSuspicious
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return ['success' => false, 'error' => 'Door access processing failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process heartbeat message
|
||||
*/
|
||||
private function processHeartbeat(LoRaMessage $message, array $data): array
|
||||
{
|
||||
try {
|
||||
// Update device last seen
|
||||
if ($message->device) {
|
||||
$message->device->update([
|
||||
'last_seen' => $message->received_at ?? now(),
|
||||
'status' => 'online'
|
||||
]);
|
||||
}
|
||||
|
||||
// Check battery level untuk alert
|
||||
$batteryLevel = $data['battery_level'] ?? null;
|
||||
if ($batteryLevel !== null && $batteryLevel < 20) {
|
||||
$this->sendLowBatteryAlert($message, $batteryLevel);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'action' => 'heartbeat_processed',
|
||||
'battery_level' => $batteryLevel,
|
||||
'signal_strength' => $data['signal_strength'] ?? null,
|
||||
'uptime' => $data['uptime_seconds'] ?? null
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return ['success' => false, 'error' => 'Heartbeat processing failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process acknowledgment message
|
||||
*/
|
||||
private function processAcknowledgment(LoRaMessage $message, array $data): array
|
||||
{
|
||||
try {
|
||||
$commandId = $data['command_id'] ?? null;
|
||||
|
||||
if ($commandId) {
|
||||
// Cari outbound message yang sesuai
|
||||
$outboundMessage = LoRaMessage::where('direction', 'outbound')
|
||||
->where('metadata->command_id', $commandId)
|
||||
->first();
|
||||
|
||||
if ($outboundMessage) {
|
||||
$outboundMessage->markAsAcknowledged();
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'action' => 'acknowledgment_processed',
|
||||
'command_id' => $commandId,
|
||||
'ack_status' => $data['ack_status'] ?? 'unknown'
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return ['success' => false, 'error' => 'ACK processing failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmit message ke LoRa gateway
|
||||
*/
|
||||
public function transmitMessage(LoRaMessage $message): array
|
||||
{
|
||||
try {
|
||||
// Implementasi tergantung jenis LoRa gateway yang digunakan
|
||||
// Contoh untuk HTTP-based gateway
|
||||
|
||||
$gatewayUrl = config('lora.gateway_url', 'http://localhost:8080/lora/send');
|
||||
|
||||
$payload = [
|
||||
'node_id' => $message->node_id,
|
||||
'payload' => $message->payload,
|
||||
'spreading_factor' => $message->spreading_factor ?? 7,
|
||||
'frequency' => $message->frequency ?? 868.1,
|
||||
'tx_power' => 14
|
||||
];
|
||||
|
||||
// Kirim ke gateway (simulasi)
|
||||
Log::info("LoRa message would be transmitted", [
|
||||
'gateway_url' => $gatewayUrl,
|
||||
'payload' => $payload
|
||||
]);
|
||||
|
||||
// TODO: Implementasi actual transmission
|
||||
/*
|
||||
$response = Http::timeout(10)->post($gatewayUrl, $payload);
|
||||
|
||||
if ($response->successful()) {
|
||||
$message->update(['status' => 'transmitted']);
|
||||
return ['success' => true, 'gateway_response' => $response->json()];
|
||||
} else {
|
||||
$message->update(['status' => 'failed']);
|
||||
return ['success' => false, 'error' => 'Gateway transmission failed'];
|
||||
}
|
||||
*/
|
||||
|
||||
// Simulasi sukses
|
||||
$message->update(['status' => 'transmitted']);
|
||||
return ['success' => true, 'simulated' => true];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$message->update(['status' => 'failed', 'error_message' => $e->getMessage()]);
|
||||
|
||||
Log::error("LoRa transmission failed", [
|
||||
'message_id' => $message->id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send low battery alert
|
||||
*/
|
||||
private function sendLowBatteryAlert(LoRaMessage $message, int $batteryLevel): void
|
||||
{
|
||||
try {
|
||||
Log::warning("Low battery alert", [
|
||||
'node_id' => $message->node_id,
|
||||
'battery_level' => $batteryLevel,
|
||||
'rssi' => $message->rssi
|
||||
]);
|
||||
|
||||
// TODO: Implementasi notification untuk low battery
|
||||
// $this->notificationService->sendLowBatteryAlert($message, $batteryLevel);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to send low battery alert: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check authorized time (jam kerja)
|
||||
*/
|
||||
private function checkAuthorizedTime(): bool
|
||||
{
|
||||
$hour = now()->hour;
|
||||
return $hour >= 8 && $hour < 17;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine suspicious PIR motion
|
||||
*/
|
||||
private function determineSuspiciousPirMotion(array $data, bool $isAuthorizedTime): bool
|
||||
{
|
||||
if (!$isAuthorizedTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$intensity = $data['motion_intensity'] ?? 0;
|
||||
$duration = $data['duration_seconds'] ?? 0;
|
||||
|
||||
return $intensity > 80 || $duration > 300;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine authorized door access
|
||||
*/
|
||||
private function determineAuthorizedDoorAccess(array $data, bool $isAuthorizedTime): bool
|
||||
{
|
||||
$method = $data['access_method'] ?? 'unknown';
|
||||
$idCard = $data['user_id_card'] ?? null;
|
||||
|
||||
if ($method === 'force') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($method === 'emergency') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($method === 'keycard' && $idCard && preg_match('/^EMP-\d{4}$/', $idCard)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine suspicious door access
|
||||
*/
|
||||
private function determineSuspiciousDoorAccess(array $data, bool $isAuthorizedAccess): bool
|
||||
{
|
||||
if (!$isAuthorizedAccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$duration = $data['duration_seconds'] ?? 0;
|
||||
return $duration > 180;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine door access type
|
||||
*/
|
||||
private function determineDoorAccessType(array $data, bool $isAuthorizedAccess, bool $isAuthorizedTime): string
|
||||
{
|
||||
$method = $data['access_method'] ?? 'unknown';
|
||||
|
||||
if ($method === 'force') {
|
||||
return 'forced_entry';
|
||||
}
|
||||
|
||||
if ($method === 'emergency') {
|
||||
return 'emergency';
|
||||
}
|
||||
|
||||
if ($method === 'maintenance') {
|
||||
return 'maintenance';
|
||||
}
|
||||
|
||||
if ($isAuthorizedAccess && $isAuthorizedTime) {
|
||||
return 'authorized';
|
||||
}
|
||||
|
||||
if (!$isAuthorizedTime) {
|
||||
return 'after_hours';
|
||||
}
|
||||
|
||||
return 'unauthorized';
|
||||
}
|
||||
|
||||
/**
|
||||
* Send command to LoRa node
|
||||
*/
|
||||
public function sendCommandToNode(string $nodeId, string $action, array $parameters = []): array
|
||||
{
|
||||
try {
|
||||
$commandId = uniqid('CMD_');
|
||||
$payload = LoRaMessage::generateCommandPayload($action, $parameters, $commandId);
|
||||
|
||||
$message = LoRaMessage::create([
|
||||
'node_id' => $nodeId,
|
||||
'gateway_id' => 'GATEWAY_001',
|
||||
'direction' => 'outbound',
|
||||
'message_type' => 'command',
|
||||
'payload' => $payload,
|
||||
'status' => 'pending',
|
||||
'metadata' => [
|
||||
'command_id' => $commandId,
|
||||
'action' => $action,
|
||||
'parameters' => $parameters
|
||||
],
|
||||
'transmitted_at' => now()
|
||||
]);
|
||||
|
||||
$result = $this->transmitMessage($message);
|
||||
|
||||
return array_merge($result, [
|
||||
'message_id' => $message->id,
|
||||
'command_id' => $commandId
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send configuration to LoRa node
|
||||
*/
|
||||
public function sendConfigToNode(string $nodeId, string $parameter, $value): array
|
||||
{
|
||||
try {
|
||||
$configId = uniqid('CFG_');
|
||||
$payload = LoRaMessage::generateConfigPayload($parameter, $value, $configId);
|
||||
|
||||
$message = LoRaMessage::create([
|
||||
'node_id' => $nodeId,
|
||||
'gateway_id' => 'GATEWAY_001',
|
||||
'direction' => 'outbound',
|
||||
'message_type' => 'config',
|
||||
'payload' => $payload,
|
||||
'status' => 'pending',
|
||||
'metadata' => [
|
||||
'config_id' => $configId,
|
||||
'parameter' => $parameter,
|
||||
'value' => $value
|
||||
],
|
||||
'transmitted_at' => now()
|
||||
]);
|
||||
|
||||
$result = $this->transmitMessage($message);
|
||||
|
||||
return array_merge($result, [
|
||||
'message_id' => $message->id,
|
||||
'config_id' => $configId
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,374 @@
|
|||
/**
|
||||
* ============================================
|
||||
* Smart Rack Security System - ESP32
|
||||
* PIR + Reed Switch + SW420 + LoRa + Laravel API
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <SPI.h>
|
||||
#include <LoRa.h>
|
||||
|
||||
// ============================================
|
||||
// WIFI
|
||||
// ============================================
|
||||
const char* ssid = "gege";
|
||||
const char* password = "biasaaja";
|
||||
|
||||
// GANTI DENGAN IP LARAVEL KAMU
|
||||
const char* API_BASE = "http://10.185.214.166:8000/api";
|
||||
|
||||
// ============================================
|
||||
// DEVICE
|
||||
// ============================================
|
||||
const int DEVICE_ID = 1;
|
||||
const String NODE_ID = "LORA_001";
|
||||
|
||||
// ============================================
|
||||
// PIN SENSOR
|
||||
// ============================================
|
||||
#define PIR_PIN 33
|
||||
#define REED_PIN 32
|
||||
#define SW420_PIN 34
|
||||
#define BUZZER 25
|
||||
#define LED_MERAH 13
|
||||
#define LED_HIJAU 4
|
||||
|
||||
// ============================================
|
||||
// PIN LORA SX1278 RA-02
|
||||
// ============================================
|
||||
#define LORA_SS 27
|
||||
#define LORA_RST 14
|
||||
#define LORA_DIO0 26
|
||||
|
||||
// ============================================
|
||||
// VARIABEL
|
||||
// ============================================
|
||||
int pirState;
|
||||
int reedState;
|
||||
int vibrationState;
|
||||
int lastVibration = LOW;
|
||||
bool motionActive = false;
|
||||
bool doorOpen = false;
|
||||
unsigned long motionStartTime = 0;
|
||||
unsigned long doorOpenTime = 0;
|
||||
unsigned long lastHeartbeat = 0;
|
||||
const unsigned long HEARTBEAT_INTERVAL = 300000;
|
||||
|
||||
// ============================================
|
||||
// LED
|
||||
// ============================================
|
||||
void setLED(bool bahaya) {
|
||||
digitalWrite(LED_MERAH, bahaya ? HIGH : LOW);
|
||||
digitalWrite(LED_HIJAU, bahaya ? LOW : HIGH);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BUZZER
|
||||
// ============================================
|
||||
void buzzerPIR() {
|
||||
digitalWrite(BUZZER, HIGH);
|
||||
delay(300);
|
||||
digitalWrite(BUZZER, LOW);
|
||||
}
|
||||
|
||||
void buzzerREED() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
digitalWrite(BUZZER, HIGH);
|
||||
delay(100);
|
||||
digitalWrite(BUZZER, LOW);
|
||||
delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
void buzzerGETAR() {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
digitalWrite(BUZZER, HIGH);
|
||||
delay(80);
|
||||
digitalWrite(BUZZER, LOW);
|
||||
delay(80);
|
||||
}
|
||||
}
|
||||
|
||||
void buzzerStartup() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
digitalWrite(BUZZER, HIGH);
|
||||
delay(200);
|
||||
digitalWrite(BUZZER, LOW);
|
||||
delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WIFI
|
||||
// ============================================
|
||||
void setupWifi() {
|
||||
Serial.println("Menghubungkan WiFi: " + String(ssid));
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, password);
|
||||
|
||||
int attempt = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && attempt < 30) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
attempt++;
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("");
|
||||
Serial.println("WiFi Connected!");
|
||||
Serial.println("IP: " + WiFi.localIP().toString());
|
||||
} else {
|
||||
Serial.println("");
|
||||
Serial.println("WiFi gagal terhubung. Cek SSID/Password.");
|
||||
Serial.println("Pastikan hotspot 2.4GHz dan sudah aktif.");
|
||||
}
|
||||
}
|
||||
|
||||
void checkWifi() {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("WiFi putus, reconnecting...");
|
||||
WiFi.disconnect();
|
||||
delay(1000);
|
||||
WiFi.begin(ssid, password);
|
||||
|
||||
int attempt = 0;
|
||||
while (WiFi.status() != WL_CONNECTED && attempt < 20) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
attempt++;
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("\nWiFi terhubung kembali! IP: " + WiFi.localIP().toString());
|
||||
} else {
|
||||
Serial.println("\nGagal reconnect. Tunggu 10 detik...");
|
||||
delay(10000); // tunggu lebih lama sebelum coba lagi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HTTP POST
|
||||
// ============================================
|
||||
bool httpPost(String endpoint, String jsonBody) {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
Serial.println("WiFi disconnected, skip kirim data");
|
||||
return false;
|
||||
}
|
||||
|
||||
HTTPClient http;
|
||||
String url = String(API_BASE) + endpoint;
|
||||
|
||||
http.begin(url);
|
||||
http.addHeader("Content-Type", "application/json");
|
||||
http.addHeader("Accept", "application/json");
|
||||
http.setTimeout(8000);
|
||||
|
||||
int httpCode = http.POST(jsonBody);
|
||||
|
||||
if (httpCode > 0) {
|
||||
String response = http.getString();
|
||||
Serial.println("HTTP " + String(httpCode) + " → " + endpoint);
|
||||
if (httpCode == 201 || httpCode == 200) {
|
||||
Serial.println("✓ Data tersimpan");
|
||||
http.end();
|
||||
return true;
|
||||
} else {
|
||||
Serial.println("✗ Response: " + response.substring(0, 150));
|
||||
}
|
||||
} else {
|
||||
Serial.println("✗ HTTP Error: " + String(httpCode));
|
||||
Serial.println(" URL: " + url);
|
||||
Serial.println(" Cek: server jalan? IP benar? Firewall?");
|
||||
}
|
||||
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// KIRIM PIR
|
||||
// ============================================
|
||||
void kirimDataPIR(bool detected, int intensity, int duration, String zone) {
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["device_id"] = DEVICE_ID;
|
||||
doc["motion_detected"] = detected;
|
||||
doc["motion_intensity"] = intensity;
|
||||
doc["duration_seconds"] = duration;
|
||||
doc["detection_zone"] = zone;
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
Serial.println("Kirim PIR → intensity=" + String(intensity) + " duration=" + String(duration) + "s");
|
||||
httpPost("/pir/data", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// KIRIM GETARAN
|
||||
// ============================================
|
||||
void kirimDataGetaran(float x, float y, float z) {
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["device_id"] = DEVICE_ID;
|
||||
doc["x_axis"] = x;
|
||||
doc["y_axis"] = y;
|
||||
doc["z_axis"] = z;
|
||||
doc["threshold"] = 2.0;
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
float mag = sqrt(x*x + y*y + z*z);
|
||||
Serial.println("Kirim Getaran → magnitude=" + String(mag, 2));
|
||||
httpPost("/vibration/data", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// KIRIM REED SWITCH
|
||||
// ============================================
|
||||
void kirimDataReedSwitch(bool opened, int duration) {
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["device_id"] = DEVICE_ID;
|
||||
doc["door_opened"] = opened;
|
||||
doc["duration_seconds"] = duration;
|
||||
doc["access_method"] = "manual";
|
||||
doc["door_location"] = "rack";
|
||||
doc["is_forced_entry"] = false;
|
||||
|
||||
String body;
|
||||
serializeJson(doc, body);
|
||||
Serial.println("Kirim Reed Switch → opened=" + String(opened) + " duration=" + String(duration) + "s");
|
||||
httpPost("/door-access/data", body);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HEARTBEAT
|
||||
// ============================================
|
||||
void kirimHeartbeat() {
|
||||
String payload = "HEARTBEAT|" + NODE_ID;
|
||||
LoRa.beginPacket();
|
||||
LoRa.print(payload);
|
||||
LoRa.endPacket();
|
||||
Serial.println("Heartbeat terkirim. Uptime: " + String(millis()/1000) + "s");
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SETUP
|
||||
// ============================================
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
// SENSOR
|
||||
pinMode(PIR_PIN, INPUT);
|
||||
pinMode(REED_PIN, INPUT_PULLUP); // PULLUP untuk reed switch
|
||||
pinMode(SW420_PIN, INPUT);
|
||||
|
||||
// OUTPUT
|
||||
pinMode(BUZZER, OUTPUT);
|
||||
pinMode(LED_MERAH, OUTPUT);
|
||||
pinMode(LED_HIJAU, OUTPUT);
|
||||
|
||||
digitalWrite(BUZZER, LOW);
|
||||
setLED(false); // LED hijau menyala default
|
||||
|
||||
Serial.println("=================================");
|
||||
Serial.println(" SMART RACK SECURITY SYSTEM");
|
||||
Serial.println("=================================");
|
||||
|
||||
// WIFI
|
||||
setupWifi();
|
||||
|
||||
// LORA
|
||||
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
|
||||
if (!LoRa.begin(433E6)) {
|
||||
Serial.println("LoRa gagal! Lanjut tanpa LoRa.");
|
||||
} else {
|
||||
LoRa.setTxPower(17);
|
||||
Serial.println("LoRa OK");
|
||||
}
|
||||
|
||||
buzzerStartup();
|
||||
Serial.println("System Ready!");
|
||||
Serial.println("API: " + String(API_BASE));
|
||||
Serial.println("Device ID: " + String(DEVICE_ID));
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LOOP
|
||||
// ============================================
|
||||
void loop() {
|
||||
checkWifi();
|
||||
|
||||
pirState = digitalRead(PIR_PIN);
|
||||
reedState = digitalRead(REED_PIN); // LOW = tertutup, HIGH = terbuka
|
||||
vibrationState = digitalRead(SW420_PIN);
|
||||
|
||||
// =========================================
|
||||
// PIR - Deteksi Gerakan
|
||||
// =========================================
|
||||
if (pirState == HIGH && !motionActive) {
|
||||
motionActive = true;
|
||||
motionStartTime = millis();
|
||||
Serial.println(">>> PIR: Gerakan terdeteksi!");
|
||||
buzzerPIR();
|
||||
setLED(true);
|
||||
}
|
||||
else if (pirState == LOW && motionActive) {
|
||||
motionActive = false;
|
||||
int duration = (millis() - motionStartTime) / 1000;
|
||||
int intensity = random(60, 90);
|
||||
kirimDataPIR(true, intensity, duration, "center");
|
||||
Serial.println(">>> PIR: Gerakan selesai, durasi=" + String(duration) + "s");
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// REED SWITCH - Status Rak
|
||||
// =========================================
|
||||
if (reedState == HIGH && !doorOpen) {
|
||||
doorOpen = true;
|
||||
doorOpenTime = millis();
|
||||
Serial.println(">>> Reed Switch: Rak terbuka!");
|
||||
buzzerREED();
|
||||
setLED(true);
|
||||
}
|
||||
else if (reedState == LOW && doorOpen) {
|
||||
doorOpen = false;
|
||||
int duration = (millis() - doorOpenTime) / 1000;
|
||||
kirimDataReedSwitch(true, duration);
|
||||
Serial.println(">>> Reed Switch: Rak tertutup, durasi=" + String(duration) + "s");
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// SW-420 - Deteksi Getaran
|
||||
// =========================================
|
||||
if (vibrationState == HIGH && lastVibration == LOW) {
|
||||
Serial.println(">>> SW-420: Getaran terdeteksi!");
|
||||
float x = random(-300, 300) / 100.0;
|
||||
float y = random(-300, 300) / 100.0;
|
||||
float z = random(80, 120) / 100.0;
|
||||
buzzerGETAR();
|
||||
setLED(true);
|
||||
kirimDataGetaran(x, y, z);
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// Kembali normal jika tidak ada bahaya
|
||||
// =========================================
|
||||
if (!motionActive && !doorOpen && vibrationState == LOW) {
|
||||
setLED(false);
|
||||
}
|
||||
|
||||
lastVibration = vibrationState;
|
||||
|
||||
// =========================================
|
||||
// HEARTBEAT setiap 5 menit
|
||||
// =========================================
|
||||
if (millis() - lastHeartbeat >= HEARTBEAT_INTERVAL) {
|
||||
kirimHeartbeat();
|
||||
lastHeartbeat = millis();
|
||||
}
|
||||
|
||||
delay(300);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
];
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^11.5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane",
|
||||
| "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||
| "deferred", "background", "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'deferred' => [
|
||||
'driver' => 'deferred',
|
||||
],
|
||||
|
||||
'background' => [
|
||||
'driver' => 'background',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'connections' => [
|
||||
'database',
|
||||
'deferred',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_API_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "memcached",
|
||||
| "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain without subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
];
|
||||
|
|
@ -0,0 +1,652 @@
|
|||
-- =====================================================
|
||||
-- SMART RACK SECURITY SYSTEM - MySQL Database Schema
|
||||
-- =====================================================
|
||||
-- Generated: 2026-04-29
|
||||
-- Compatible with: MySQL 8.0+, MariaDB 10.3+
|
||||
-- =====================================================
|
||||
|
||||
-- Create database
|
||||
CREATE DATABASE IF NOT EXISTS smart_rack_security
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE smart_rack_security;
|
||||
|
||||
-- =====================================================
|
||||
-- 1. USERS TABLE
|
||||
-- =====================================================
|
||||
CREATE TABLE users (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
email_verified_at TIMESTAMP NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin', 'operator', 'viewer') DEFAULT 'viewer',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
last_login_at TIMESTAMP NULL,
|
||||
remember_token VARCHAR(100) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_email (email),
|
||||
INDEX idx_role_active (role, is_active)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 2. DEVICES TABLE (Arduino/LoRa Nodes)
|
||||
-- =====================================================
|
||||
CREATE TABLE devices (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
device_id VARCHAR(50) UNIQUE NOT NULL COMMENT 'Node ID seperti LORA_001',
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type ENUM('lora_node', 'gateway', 'sensor_hub') DEFAULT 'lora_node',
|
||||
location VARCHAR(255) NULL COMMENT 'Lokasi fisik device',
|
||||
ip_address VARCHAR(45) NULL,
|
||||
mac_address VARCHAR(17) NULL,
|
||||
firmware_version VARCHAR(50) NULL,
|
||||
battery_level DECIMAL(5,2) DEFAULT 100.00 COMMENT 'Battery percentage',
|
||||
signal_strength INT DEFAULT 0 COMMENT 'Signal strength in dBm',
|
||||
is_online BOOLEAN DEFAULT FALSE,
|
||||
last_seen_at TIMESTAMP NULL,
|
||||
configuration JSON NULL COMMENT 'Device specific config',
|
||||
metadata JSON NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_device_id (device_id),
|
||||
INDEX idx_type_online (type, is_online),
|
||||
INDEX idx_last_seen (last_seen_at)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 3. SENSORS TABLE
|
||||
-- =====================================================
|
||||
CREATE TABLE sensors (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
device_id BIGINT UNSIGNED NOT NULL,
|
||||
sensor_type ENUM('pir', 'reed_switch', 'vibration', 'temperature', 'humidity', 'door_access') NOT NULL,
|
||||
sensor_name VARCHAR(255) NOT NULL,
|
||||
pin_number INT NULL COMMENT 'GPIO pin number',
|
||||
unit VARCHAR(20) NULL COMMENT 'Measurement unit',
|
||||
min_value DECIMAL(10,4) NULL,
|
||||
max_value DECIMAL(10,4) NULL,
|
||||
threshold DECIMAL(10,4) NULL COMMENT 'Alert threshold',
|
||||
calibration_offset DECIMAL(10,4) DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
metadata JSON NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE,
|
||||
INDEX idx_device_sensor (device_id, sensor_type),
|
||||
INDEX idx_sensor_type_active (sensor_type, is_active)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 4. PIR READINGS TABLE (Motion Detection)
|
||||
-- =====================================================
|
||||
CREATE TABLE pir_readings (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
device_id BIGINT UNSIGNED NOT NULL,
|
||||
motion_detected BOOLEAN DEFAULT FALSE COMMENT 'Gerakan terdeteksi',
|
||||
motion_intensity INT DEFAULT 0 COMMENT 'Intensitas gerakan (0-100)',
|
||||
duration_seconds INT DEFAULT 0 COMMENT 'Durasi gerakan dalam detik',
|
||||
is_authorized_time BOOLEAN DEFAULT TRUE COMMENT 'Apakah dalam jam kerja',
|
||||
is_suspicious BOOLEAN DEFAULT FALSE COMMENT 'Gerakan mencurigakan',
|
||||
motion_type ENUM('normal', 'suspicious', 'unauthorized') DEFAULT 'normal',
|
||||
detection_zone VARCHAR(50) NULL COMMENT 'Area deteksi (front, back, side, center)',
|
||||
metadata JSON NULL COMMENT 'Data tambahan sensor',
|
||||
motion_start TIMESTAMP NULL COMMENT 'Waktu mulai gerakan',
|
||||
motion_end TIMESTAMP NULL COMMENT 'Waktu selesai gerakan',
|
||||
recorded_at TIMESTAMP NOT NULL COMMENT 'Waktu pembacaan sensor',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE,
|
||||
INDEX idx_device_recorded (device_id, recorded_at),
|
||||
INDEX idx_motion_suspicious (motion_detected, is_suspicious),
|
||||
INDEX idx_authorized_type (is_authorized_time, motion_type),
|
||||
INDEX idx_recorded_at (recorded_at)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 5. DOOR READINGS TABLE (Reed Switch)
|
||||
-- =====================================================
|
||||
CREATE TABLE door_readings (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
device_id BIGINT UNSIGNED NOT NULL,
|
||||
door_open BOOLEAN DEFAULT FALSE COMMENT 'Status pintu terbuka',
|
||||
is_authorized_access BOOLEAN DEFAULT TRUE COMMENT 'Akses yang sah',
|
||||
is_forced_entry BOOLEAN DEFAULT FALSE COMMENT 'Pembukaan paksa',
|
||||
access_type ENUM('normal', 'unauthorized', 'forced', 'maintenance') DEFAULT 'normal',
|
||||
door_location VARCHAR(100) NULL COMMENT 'front_panel, back_panel, side_door, main_door',
|
||||
open_duration_seconds INT DEFAULT 0 COMMENT 'Durasi terbuka dalam detik',
|
||||
proper_closure BOOLEAN DEFAULT TRUE COMMENT 'Apakah ditutup dengan benar',
|
||||
access_card_data JSON NULL COMMENT 'Data kartu akses jika ada',
|
||||
metadata JSON NULL COMMENT 'Data tambahan sensor',
|
||||
door_opened_at TIMESTAMP NULL COMMENT 'Waktu pintu dibuka',
|
||||
door_closed_at TIMESTAMP NULL COMMENT 'Waktu pintu ditutup',
|
||||
recorded_at TIMESTAMP NOT NULL COMMENT 'Waktu pembacaan sensor',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE,
|
||||
INDEX idx_device_recorded (device_id, recorded_at),
|
||||
INDEX idx_door_authorized (door_open, is_authorized_access),
|
||||
INDEX idx_access_location (access_type, door_location),
|
||||
INDEX idx_forced_entry (is_forced_entry, proper_closure),
|
||||
INDEX idx_recorded_at (recorded_at)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 6. VIBRATION READINGS TABLE (SW-420 Sensor)
|
||||
-- =====================================================
|
||||
CREATE TABLE vibration_readings (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
device_id BIGINT UNSIGNED NOT NULL,
|
||||
x_axis DECIMAL(8,4) NOT NULL COMMENT 'Getaran sumbu X',
|
||||
y_axis DECIMAL(8,4) NOT NULL COMMENT 'Getaran sumbu Y',
|
||||
z_axis DECIMAL(8,4) NOT NULL COMMENT 'Getaran sumbu Z',
|
||||
magnitude DECIMAL(8,4) GENERATED ALWAYS AS (SQRT(POW(x_axis,2) + POW(y_axis,2) + POW(z_axis,2))) STORED COMMENT 'Total magnitude getaran',
|
||||
is_abnormal BOOLEAN DEFAULT FALSE COMMENT 'Status getaran abnormal',
|
||||
threshold DECIMAL(8,4) DEFAULT 2.0000 COMMENT 'Batas normal getaran',
|
||||
status ENUM('normal', 'warning', 'critical') DEFAULT 'normal',
|
||||
metadata JSON NULL COMMENT 'Data tambahan sensor',
|
||||
recorded_at TIMESTAMP NOT NULL COMMENT 'Waktu pembacaan sensor',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE,
|
||||
INDEX idx_device_recorded (device_id, recorded_at),
|
||||
INDEX idx_abnormal_status (is_abnormal, status),
|
||||
INDEX idx_magnitude (magnitude),
|
||||
INDEX idx_recorded_at (recorded_at)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 7. DOOR ACCESS READINGS TABLE (Detailed Access Log)
|
||||
-- =====================================================
|
||||
CREATE TABLE door_access_readings (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
device_id BIGINT UNSIGNED NOT NULL,
|
||||
door_opened BOOLEAN DEFAULT FALSE,
|
||||
user_id_card VARCHAR(100) NULL COMMENT 'ID Card atau badge number',
|
||||
duration_seconds INT DEFAULT 0,
|
||||
access_method ENUM('keycard', 'manual', 'forced', 'maintenance') DEFAULT 'manual',
|
||||
door_location VARCHAR(100) NULL,
|
||||
is_authorized BOOLEAN DEFAULT TRUE,
|
||||
access_granted_by VARCHAR(100) NULL COMMENT 'Who granted access',
|
||||
metadata JSON NULL,
|
||||
recorded_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE,
|
||||
INDEX idx_device_recorded (device_id, recorded_at),
|
||||
INDEX idx_user_card (user_id_card),
|
||||
INDEX idx_access_method (access_method, is_authorized),
|
||||
INDEX idx_recorded_at (recorded_at)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 8. LORA MESSAGES TABLE (Communication Log)
|
||||
-- =====================================================
|
||||
CREATE TABLE lora_messages (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
node_id VARCHAR(50) NOT NULL COMMENT 'Source node ID',
|
||||
gateway_id VARCHAR(50) NULL COMMENT 'Gateway yang menerima',
|
||||
message_type ENUM('sensor_data', 'heartbeat', 'command', 'ack', 'error') NOT NULL,
|
||||
raw_payload TEXT NOT NULL COMMENT 'Raw LoRa message',
|
||||
parsed_data JSON NULL COMMENT 'Parsed message data',
|
||||
rssi INT NULL COMMENT 'Signal strength',
|
||||
snr DECIMAL(5,2) NULL COMMENT 'Signal to noise ratio',
|
||||
frequency DECIMAL(10,2) NULL COMMENT 'Frequency in Hz',
|
||||
is_processed BOOLEAN DEFAULT FALSE,
|
||||
processing_error TEXT NULL,
|
||||
received_at TIMESTAMP NOT NULL,
|
||||
processed_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_node_received (node_id, received_at),
|
||||
INDEX idx_message_type (message_type, is_processed),
|
||||
INDEX idx_received_at (received_at)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 9. SENSOR READINGS TABLE (General Sensor Data)
|
||||
-- =====================================================
|
||||
CREATE TABLE sensor_readings (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
sensor_id BIGINT UNSIGNED NOT NULL,
|
||||
device_id BIGINT UNSIGNED NOT NULL,
|
||||
value DECIMAL(15,4) NOT NULL COMMENT 'Processed sensor value',
|
||||
raw_value VARCHAR(255) NULL COMMENT 'Raw sensor reading',
|
||||
status ENUM('normal', 'warning', 'critical', 'error') DEFAULT 'normal',
|
||||
battery_level DECIMAL(5,2) NULL COMMENT 'Device battery level',
|
||||
signal_strength INT NULL COMMENT 'Signal strength in dBm',
|
||||
metadata JSON NULL,
|
||||
reading_time TIMESTAMP NOT NULL,
|
||||
is_processed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (sensor_id) REFERENCES sensors(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE,
|
||||
INDEX idx_sensor_reading_time (sensor_id, reading_time),
|
||||
INDEX idx_device_reading_time (device_id, reading_time),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_reading_time (reading_time)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 10. ALERTS TABLE (System Alerts & Notifications)
|
||||
-- =====================================================
|
||||
CREATE TABLE alerts (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
device_id BIGINT UNSIGNED NULL,
|
||||
alert_type ENUM('motion', 'door_access', 'vibration', 'system', 'security') NOT NULL,
|
||||
severity ENUM('low', 'medium', 'high', 'critical') DEFAULT 'medium',
|
||||
title VARCHAR(255) NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
source_table VARCHAR(50) NULL COMMENT 'Table yang memicu alert',
|
||||
source_id BIGINT UNSIGNED NULL COMMENT 'ID record yang memicu alert',
|
||||
is_acknowledged BOOLEAN DEFAULT FALSE,
|
||||
acknowledged_by BIGINT UNSIGNED NULL,
|
||||
acknowledged_at TIMESTAMP NULL,
|
||||
metadata JSON NULL,
|
||||
triggered_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (acknowledged_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
INDEX idx_device_triggered (device_id, triggered_at),
|
||||
INDEX idx_type_severity (alert_type, severity),
|
||||
INDEX idx_acknowledged (is_acknowledged, acknowledged_at),
|
||||
INDEX idx_triggered_at (triggered_at)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 11. ACTIVITY LOGS TABLE (System Activity)
|
||||
-- =====================================================
|
||||
CREATE TABLE activity_logs (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NULL,
|
||||
device_id BIGINT UNSIGNED NULL,
|
||||
action VARCHAR(100) NOT NULL COMMENT 'Action performed',
|
||||
description TEXT NULL,
|
||||
ip_address VARCHAR(45) NULL,
|
||||
user_agent TEXT NULL,
|
||||
metadata JSON NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE SET NULL,
|
||||
INDEX idx_user_created (user_id, created_at),
|
||||
INDEX idx_device_created (device_id, created_at),
|
||||
INDEX idx_action (action),
|
||||
INDEX idx_created_at (created_at)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 12. SYSTEM SETTINGS TABLE
|
||||
-- =====================================================
|
||||
CREATE TABLE system_settings (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
setting_key VARCHAR(100) UNIQUE NOT NULL,
|
||||
setting_value TEXT NULL,
|
||||
setting_type ENUM('string', 'integer', 'float', 'boolean', 'json') DEFAULT 'string',
|
||||
description TEXT NULL,
|
||||
is_public BOOLEAN DEFAULT FALSE COMMENT 'Can be accessed by non-admin users',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_key_public (setting_key, is_public)
|
||||
);
|
||||
-- =====
|
||||
================================================
|
||||
-- SAMPLE DATA INSERTION
|
||||
-- =====================================================
|
||||
|
||||
-- Insert default admin user
|
||||
INSERT INTO users (name, email, password, role, is_active) VALUES
|
||||
('System Admin', 'admin@smartrack.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin', 1),
|
||||
('Operator', 'operator@smartrack.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'operator', 1);
|
||||
|
||||
-- Insert sample devices
|
||||
INSERT INTO devices (device_id, name, type, location, battery_level, is_online) VALUES
|
||||
('LORA_001', 'Smart Rack Node 1', 'lora_node', 'Server Room A - Rack 1', 85.50, 1),
|
||||
('LORA_002', 'Smart Rack Node 2', 'lora_node', 'Server Room A - Rack 2', 92.30, 1),
|
||||
('GATEWAY_001', 'LoRa Gateway Main', 'gateway', 'Server Room A - Central', 100.00, 1);
|
||||
|
||||
-- Insert sensors for each device
|
||||
INSERT INTO sensors (device_id, sensor_type, sensor_name, pin_number, unit, threshold, is_active) VALUES
|
||||
-- Device LORA_001 sensors
|
||||
(1, 'pir', 'PIR Motion Sensor', 33, 'boolean', 1, 1),
|
||||
(1, 'reed_switch', 'Door Reed Switch', 32, 'boolean', 1, 1),
|
||||
(1, 'vibration', 'Vibration Sensor SW-420', 34, 'g-force', 2.0, 1),
|
||||
-- Device LORA_002 sensors
|
||||
(2, 'pir', 'PIR Motion Sensor', 33, 'boolean', 1, 1),
|
||||
(2, 'reed_switch', 'Door Reed Switch', 32, 'boolean', 1, 1),
|
||||
(2, 'vibration', 'Vibration Sensor SW-420', 34, 'g-force', 2.5, 1);
|
||||
|
||||
-- Insert system settings
|
||||
INSERT INTO system_settings (setting_key, setting_value, setting_type, description, is_public) VALUES
|
||||
('system_name', 'Smart Rack Security System', 'string', 'System display name', 1),
|
||||
('alert_email', 'alerts@smartrack.com', 'string', 'Email for system alerts', 0),
|
||||
('work_hours_start', '08:00', 'string', 'Work hours start time', 1),
|
||||
('work_hours_end', '17:00', 'string', 'Work hours end time', 1),
|
||||
('vibration_threshold_default', '2.0', 'float', 'Default vibration threshold', 1),
|
||||
('heartbeat_interval', '300', 'integer', 'Heartbeat interval in seconds', 1),
|
||||
('lora_frequency', '433000000', 'integer', 'LoRa frequency in Hz', 0),
|
||||
('max_door_open_duration', '600', 'integer', 'Max door open duration in seconds', 1);
|
||||
|
||||
-- =====================================================
|
||||
-- TRIGGERS FOR AUTOMATIC CALCULATIONS
|
||||
-- =====================================================
|
||||
|
||||
-- Trigger untuk update magnitude pada vibration_readings
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER tr_vibration_magnitude_update
|
||||
BEFORE UPDATE ON vibration_readings
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
-- Magnitude sudah dihitung otomatis dengan GENERATED ALWAYS AS
|
||||
-- Update status berdasarkan magnitude vs threshold
|
||||
IF (SQRT(POW(NEW.x_axis,2) + POW(NEW.y_axis,2) + POW(NEW.z_axis,2))) > NEW.threshold THEN
|
||||
SET NEW.is_abnormal = TRUE;
|
||||
IF (SQRT(POW(NEW.x_axis,2) + POW(NEW.y_axis,2) + POW(NEW.z_axis,2))) > (NEW.threshold * 1.5) THEN
|
||||
SET NEW.status = 'critical';
|
||||
ELSE
|
||||
SET NEW.status = 'warning';
|
||||
END IF;
|
||||
ELSE
|
||||
SET NEW.is_abnormal = FALSE;
|
||||
SET NEW.status = 'normal';
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
-- Trigger untuk insert vibration_readings
|
||||
CREATE TRIGGER tr_vibration_magnitude_insert
|
||||
BEFORE INSERT ON vibration_readings
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
-- Update status berdasarkan magnitude vs threshold
|
||||
IF (SQRT(POW(NEW.x_axis,2) + POW(NEW.y_axis,2) + POW(NEW.z_axis,2))) > NEW.threshold THEN
|
||||
SET NEW.is_abnormal = TRUE;
|
||||
IF (SQRT(POW(NEW.x_axis,2) + POW(NEW.y_axis,2) + POW(NEW.z_axis,2))) > (NEW.threshold * 1.5) THEN
|
||||
SET NEW.status = 'critical';
|
||||
ELSE
|
||||
SET NEW.status = 'warning';
|
||||
END IF;
|
||||
ELSE
|
||||
SET NEW.is_abnormal = FALSE;
|
||||
SET NEW.status = 'normal';
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
-- Trigger untuk auto-create alerts pada data abnormal
|
||||
CREATE TRIGGER tr_create_vibration_alert
|
||||
AFTER INSERT ON vibration_readings
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF NEW.is_abnormal = TRUE THEN
|
||||
INSERT INTO alerts (
|
||||
device_id,
|
||||
alert_type,
|
||||
severity,
|
||||
title,
|
||||
message,
|
||||
source_table,
|
||||
source_id,
|
||||
triggered_at
|
||||
) VALUES (
|
||||
NEW.device_id,
|
||||
'vibration',
|
||||
CASE
|
||||
WHEN NEW.status = 'critical' THEN 'critical'
|
||||
WHEN NEW.status = 'warning' THEN 'high'
|
||||
ELSE 'medium'
|
||||
END,
|
||||
CONCAT('Abnormal Vibration Detected - ', NEW.status),
|
||||
CONCAT('Vibration magnitude ', NEW.magnitude, ' exceeds threshold ', NEW.threshold, ' on device ', (SELECT device_id FROM devices WHERE id = NEW.device_id)),
|
||||
'vibration_readings',
|
||||
NEW.id,
|
||||
NEW.recorded_at
|
||||
);
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
-- Trigger untuk PIR suspicious motion alerts
|
||||
CREATE TRIGGER tr_create_pir_alert
|
||||
AFTER INSERT ON pir_readings
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF NEW.is_suspicious = TRUE OR NEW.is_authorized_time = FALSE THEN
|
||||
INSERT INTO alerts (
|
||||
device_id,
|
||||
alert_type,
|
||||
severity,
|
||||
title,
|
||||
message,
|
||||
source_table,
|
||||
source_id,
|
||||
triggered_at
|
||||
) VALUES (
|
||||
NEW.device_id,
|
||||
'motion',
|
||||
CASE
|
||||
WHEN NEW.is_suspicious = TRUE THEN 'high'
|
||||
WHEN NEW.is_authorized_time = FALSE THEN 'medium'
|
||||
ELSE 'low'
|
||||
END,
|
||||
CONCAT('Motion Alert - ', NEW.motion_type),
|
||||
CONCAT('Motion detected with intensity ', NEW.motion_intensity, ' for ', NEW.duration_seconds, ' seconds in zone ', COALESCE(NEW.detection_zone, 'unknown')),
|
||||
'pir_readings',
|
||||
NEW.id,
|
||||
NEW.recorded_at
|
||||
);
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
-- Trigger untuk door access alerts
|
||||
CREATE TRIGGER tr_create_door_alert
|
||||
AFTER INSERT ON door_readings
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF NEW.is_forced_entry = TRUE OR NEW.is_authorized_access = FALSE THEN
|
||||
INSERT INTO alerts (
|
||||
device_id,
|
||||
alert_type,
|
||||
severity,
|
||||
title,
|
||||
message,
|
||||
source_table,
|
||||
source_id,
|
||||
triggered_at
|
||||
) VALUES (
|
||||
NEW.device_id,
|
||||
'door_access',
|
||||
CASE
|
||||
WHEN NEW.is_forced_entry = TRUE THEN 'critical'
|
||||
WHEN NEW.is_authorized_access = FALSE THEN 'high'
|
||||
ELSE 'medium'
|
||||
END,
|
||||
CONCAT('Door Access Alert - ', NEW.access_type),
|
||||
CONCAT('Door ', NEW.door_location, ' accessed via ', NEW.access_type, ' for ', NEW.open_duration_seconds, ' seconds'),
|
||||
'door_readings',
|
||||
NEW.id,
|
||||
NEW.recorded_at
|
||||
);
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- =====================================================
|
||||
-- VIEWS FOR EASY DATA ACCESS
|
||||
-- =====================================================
|
||||
|
||||
-- View untuk dashboard summary
|
||||
CREATE VIEW dashboard_summary AS
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM devices WHERE is_online = TRUE) as online_devices,
|
||||
(SELECT COUNT(*) FROM devices WHERE is_online = FALSE) as offline_devices,
|
||||
(SELECT COUNT(*) FROM alerts WHERE is_acknowledged = FALSE AND triggered_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)) as unacknowledged_alerts_24h,
|
||||
(SELECT COUNT(*) FROM pir_readings WHERE motion_detected = TRUE AND recorded_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)) as motions_24h,
|
||||
(SELECT COUNT(*) FROM door_readings WHERE door_open = TRUE AND recorded_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)) as door_accesses_24h,
|
||||
(SELECT COUNT(*) FROM vibration_readings WHERE is_abnormal = TRUE AND recorded_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)) as abnormal_vibrations_24h;
|
||||
|
||||
-- View untuk device status
|
||||
CREATE VIEW device_status AS
|
||||
SELECT
|
||||
d.id,
|
||||
d.device_id,
|
||||
d.name,
|
||||
d.type,
|
||||
d.location,
|
||||
d.battery_level,
|
||||
d.signal_strength,
|
||||
d.is_online,
|
||||
d.last_seen_at,
|
||||
COUNT(s.id) as sensor_count,
|
||||
COUNT(CASE WHEN s.is_active = TRUE THEN 1 END) as active_sensors,
|
||||
(SELECT COUNT(*) FROM alerts WHERE device_id = d.id AND is_acknowledged = FALSE) as unacknowledged_alerts
|
||||
FROM devices d
|
||||
LEFT JOIN sensors s ON d.id = s.device_id
|
||||
GROUP BY d.id;
|
||||
|
||||
-- View untuk recent activities
|
||||
CREATE VIEW recent_activities AS
|
||||
SELECT
|
||||
'motion' as activity_type,
|
||||
d.device_id,
|
||||
d.name as device_name,
|
||||
d.location,
|
||||
CONCAT('Motion detected - Intensity: ', p.motion_intensity, ', Duration: ', p.duration_seconds, 's') as description,
|
||||
p.recorded_at as activity_time
|
||||
FROM pir_readings p
|
||||
JOIN devices d ON p.device_id = d.id
|
||||
WHERE p.motion_detected = TRUE AND p.recorded_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'door_access' as activity_type,
|
||||
d.device_id,
|
||||
d.name as device_name,
|
||||
d.location,
|
||||
CONCAT('Door ', dr.door_location, ' - ', dr.access_type, ' access for ', dr.open_duration_seconds, 's') as description,
|
||||
dr.recorded_at as activity_time
|
||||
FROM door_readings dr
|
||||
JOIN devices d ON dr.device_id = d.id
|
||||
WHERE dr.door_open = TRUE AND dr.recorded_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'vibration' as activity_type,
|
||||
d.device_id,
|
||||
d.name as device_name,
|
||||
d.location,
|
||||
CONCAT('Vibration - Magnitude: ', ROUND(v.magnitude, 2), ' (', v.status, ')') as description,
|
||||
v.recorded_at as activity_time
|
||||
FROM vibration_readings v
|
||||
JOIN devices d ON v.device_id = d.id
|
||||
WHERE v.is_abnormal = TRUE AND v.recorded_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||
|
||||
ORDER BY activity_time DESC
|
||||
LIMIT 50;
|
||||
|
||||
-- =====================================================
|
||||
-- STORED PROCEDURES
|
||||
-- =====================================================
|
||||
|
||||
-- Procedure untuk cleanup old data
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE CleanupOldData(IN days_to_keep INT)
|
||||
BEGIN
|
||||
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
RESIGNAL;
|
||||
END;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
-- Delete old sensor readings (keep last X days)
|
||||
DELETE FROM sensor_readings WHERE reading_time < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||
DELETE FROM pir_readings WHERE recorded_at < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||
DELETE FROM door_readings WHERE recorded_at < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||
DELETE FROM vibration_readings WHERE recorded_at < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||
DELETE FROM door_access_readings WHERE recorded_at < DATE_SUB(NOW(), INTERVAL days_to_keep DAY);
|
||||
|
||||
-- Delete old LoRa messages (keep last 30 days max)
|
||||
DELETE FROM lora_messages WHERE received_at < DATE_SUB(NOW(), INTERVAL LEAST(days_to_keep, 30) DAY);
|
||||
|
||||
-- Delete old acknowledged alerts (keep last 90 days max)
|
||||
DELETE FROM alerts WHERE is_acknowledged = TRUE AND acknowledged_at < DATE_SUB(NOW(), INTERVAL LEAST(days_to_keep, 90) DAY);
|
||||
|
||||
-- Delete old activity logs (keep last 180 days max)
|
||||
DELETE FROM activity_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL LEAST(days_to_keep, 180) DAY);
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT CONCAT('Cleanup completed. Removed data older than ', days_to_keep, ' days.') as result;
|
||||
END$$
|
||||
|
||||
-- Procedure untuk device health check
|
||||
CREATE PROCEDURE DeviceHealthCheck()
|
||||
BEGIN
|
||||
SELECT
|
||||
d.device_id,
|
||||
d.name,
|
||||
d.battery_level,
|
||||
d.signal_strength,
|
||||
d.is_online,
|
||||
d.last_seen_at,
|
||||
CASE
|
||||
WHEN d.last_seen_at < DATE_SUB(NOW(), INTERVAL 10 MINUTE) THEN 'OFFLINE'
|
||||
WHEN d.battery_level < 20 THEN 'LOW_BATTERY'
|
||||
WHEN d.signal_strength < -100 THEN 'WEAK_SIGNAL'
|
||||
ELSE 'HEALTHY'
|
||||
END as health_status,
|
||||
TIMESTAMPDIFF(MINUTE, d.last_seen_at, NOW()) as minutes_since_last_seen
|
||||
FROM devices d
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN d.last_seen_at < DATE_SUB(NOW(), INTERVAL 10 MINUTE) THEN 1
|
||||
WHEN d.battery_level < 20 THEN 2
|
||||
WHEN d.signal_strength < -100 THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
d.device_id;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- =====================================================
|
||||
-- INDEXES FOR PERFORMANCE OPTIMIZATION
|
||||
-- =====================================================
|
||||
|
||||
-- Additional composite indexes for common queries
|
||||
CREATE INDEX idx_pir_device_time_motion ON pir_readings(device_id, recorded_at, motion_detected);
|
||||
CREATE INDEX idx_door_device_time_open ON door_readings(device_id, recorded_at, door_open);
|
||||
CREATE INDEX idx_vibration_device_time_abnormal ON vibration_readings(device_id, recorded_at, is_abnormal);
|
||||
CREATE INDEX idx_alerts_device_time_ack ON alerts(device_id, triggered_at, is_acknowledged);
|
||||
|
||||
-- Indexes for time-based queries
|
||||
CREATE INDEX idx_pir_recorded_at_desc ON pir_readings(recorded_at DESC);
|
||||
CREATE INDEX idx_door_recorded_at_desc ON door_readings(recorded_at DESC);
|
||||
CREATE INDEX idx_vibration_recorded_at_desc ON vibration_readings(recorded_at DESC);
|
||||
CREATE INDEX idx_alerts_triggered_at_desc ON alerts(triggered_at DESC);
|
||||
|
||||
-- =====================================================
|
||||
-- COMPLETION MESSAGE
|
||||
-- =====================================================
|
||||
SELECT '✅ Smart Rack Security Database Schema Created Successfully!' as status,
|
||||
'Database: smart_rack_security' as database_name,
|
||||
'12 Tables Created' as tables_count,
|
||||
'3 Views Created' as views_count,
|
||||
'2 Stored Procedures Created' as procedures_count,
|
||||
'6 Triggers Created' as triggers_count;
|
||||
|
|
@ -0,0 +1 @@
|
|||
*.sqlite*
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('devices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name'); // Nama device (Device Rak A, Device Rak B, dll)
|
||||
$table->string('device_id')->unique(); // ID unik device untuk LoRa
|
||||
$table->string('location'); // Lokasi device (Rak A, Rak B, dll)
|
||||
$table->enum('type', ['sensor_node', 'gateway', 'server']); // Tipe device
|
||||
$table->enum('status', ['online', 'offline', 'maintenance'])->default('offline'); // Status device
|
||||
$table->string('ip_address')->nullable(); // IP address jika ada
|
||||
$table->string('mac_address')->nullable(); // MAC address
|
||||
$table->integer('signal_strength')->nullable(); // Kekuatan sinyal (0-100)
|
||||
$table->timestamp('last_seen')->nullable(); // Terakhir kali device terdeteksi
|
||||
$table->json('configuration')->nullable(); // Konfigurasi device dalam JSON
|
||||
$table->text('description')->nullable(); // Deskripsi device
|
||||
$table->boolean('is_active')->default(true); // Apakah device aktif
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('devices');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sensors', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained()->onDelete('cascade'); // Relasi ke tabel devices
|
||||
$table->string('name'); // Nama sensor (PIR, SW-420, Reed Switch)
|
||||
$table->enum('type', ['pir', 'vibration', 'reed_switch', 'temperature', 'humidity']); // Tipe sensor
|
||||
$table->string('pin_number')->nullable(); // Pin GPIO yang digunakan
|
||||
$table->enum('status', ['active', 'inactive', 'error'])->default('active'); // Status sensor
|
||||
$table->decimal('threshold_min', 8, 2)->nullable(); // Nilai minimum threshold
|
||||
$table->decimal('threshold_max', 8, 2)->nullable(); // Nilai maksimum threshold
|
||||
$table->string('unit')->nullable(); // Unit pengukuran (%, °C, V, dll)
|
||||
$table->integer('sampling_rate')->default(1000); // Rate sampling dalam ms
|
||||
$table->json('calibration_data')->nullable(); // Data kalibrasi sensor
|
||||
$table->text('description')->nullable(); // Deskripsi sensor
|
||||
$table->boolean('is_active')->default(true); // Apakah sensor aktif
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sensors');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sensor_readings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('sensor_id')->constrained()->onDelete('cascade'); // Relasi ke tabel sensors
|
||||
$table->foreignId('device_id')->constrained()->onDelete('cascade'); // Relasi ke tabel devices
|
||||
$table->decimal('value', 10, 4); // Nilai pembacaan sensor
|
||||
$table->string('raw_value')->nullable(); // Nilai mentah dari sensor
|
||||
$table->enum('status', ['normal', 'warning', 'critical', 'error'])->default('normal'); // Status pembacaan
|
||||
$table->decimal('battery_level', 5, 2)->nullable(); // Level baterai device (%)
|
||||
$table->integer('signal_strength')->nullable(); // Kekuatan sinyal saat pembacaan
|
||||
$table->json('metadata')->nullable(); // Data tambahan dalam JSON
|
||||
$table->timestamp('reading_time'); // Waktu pembacaan sensor
|
||||
$table->boolean('is_processed')->default(false); // Apakah data sudah diproses
|
||||
$table->timestamps();
|
||||
|
||||
// Index untuk performa query
|
||||
$table->index(['sensor_id', 'reading_time']);
|
||||
$table->index(['device_id', 'reading_time']);
|
||||
$table->index('reading_time');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sensor_readings');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('activity_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->nullable()->constrained()->onDelete('set null'); // Device yang terkait
|
||||
$table->foreignId('sensor_id')->nullable()->constrained()->onDelete('set null'); // Sensor yang terkait
|
||||
$table->string('event_type'); // Tipe kejadian (motion_detected, door_opened, vibration_detected, dll)
|
||||
$table->enum('severity', ['info', 'warning', 'critical', 'error'])->default('info'); // Tingkat keparahan
|
||||
$table->string('title'); // Judul aktivitas
|
||||
$table->text('description'); // Deskripsi detail aktivitas
|
||||
$table->json('event_data')->nullable(); // Data kejadian dalam JSON
|
||||
$table->string('location')->nullable(); // Lokasi kejadian
|
||||
$table->string('user_agent')->nullable(); // User agent jika dari web
|
||||
$table->string('ip_address')->nullable(); // IP address sumber
|
||||
$table->timestamp('event_time'); // Waktu kejadian
|
||||
$table->boolean('is_acknowledged')->default(false); // Apakah sudah diakui
|
||||
$table->timestamp('acknowledged_at')->nullable(); // Waktu diakui
|
||||
$table->string('acknowledged_by')->nullable(); // Siapa yang mengakui
|
||||
$table->timestamps();
|
||||
|
||||
// Index untuk performa query
|
||||
$table->index(['event_type', 'event_time']);
|
||||
$table->index(['severity', 'event_time']);
|
||||
$table->index('event_time');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('activity_logs');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('alerts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->nullable()->constrained()->onDelete('set null'); // Device yang memicu alert
|
||||
$table->foreignId('sensor_id')->nullable()->constrained()->onDelete('set null'); // Sensor yang memicu alert
|
||||
$table->foreignId('activity_log_id')->nullable()->constrained()->onDelete('set null'); // Relasi ke activity log
|
||||
$table->string('alert_type'); // Tipe alert (security_breach, sensor_offline, low_battery, dll)
|
||||
$table->enum('priority', ['low', 'medium', 'high', 'critical'])->default('medium'); // Prioritas alert
|
||||
$table->enum('status', ['active', 'acknowledged', 'resolved', 'dismissed'])->default('active'); // Status alert
|
||||
$table->string('title'); // Judul alert
|
||||
$table->text('message'); // Pesan alert
|
||||
$table->json('alert_data')->nullable(); // Data alert dalam JSON
|
||||
$table->string('location')->nullable(); // Lokasi alert
|
||||
$table->timestamp('triggered_at'); // Waktu alert dipicu
|
||||
$table->timestamp('acknowledged_at')->nullable(); // Waktu alert diakui
|
||||
$table->timestamp('resolved_at')->nullable(); // Waktu alert diselesaikan
|
||||
$table->string('acknowledged_by')->nullable(); // Siapa yang mengakui
|
||||
$table->string('resolved_by')->nullable(); // Siapa yang menyelesaikan
|
||||
$table->text('resolution_notes')->nullable(); // Catatan penyelesaian
|
||||
$table->boolean('is_sent_notification')->default(false); // Apakah notifikasi sudah dikirim
|
||||
$table->json('notification_channels')->nullable(); // Channel notifikasi yang digunakan
|
||||
$table->timestamps();
|
||||
|
||||
// Index untuk performa query
|
||||
$table->index(['alert_type', 'status']);
|
||||
$table->index(['priority', 'triggered_at']);
|
||||
$table->index(['status', 'triggered_at']);
|
||||
$table->index('triggered_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('alerts');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('vibration_readings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained()->onDelete('cascade');
|
||||
$table->float('x_axis', 8, 4); // Getaran sumbu X
|
||||
$table->float('y_axis', 8, 4); // Getaran sumbu Y
|
||||
$table->float('z_axis', 8, 4); // Getaran sumbu Z
|
||||
$table->float('magnitude', 8, 4); // Total magnitude getaran
|
||||
$table->boolean('is_abnormal')->default(false); // Status getaran abnormal
|
||||
$table->float('threshold', 8, 4)->default(2.0); // Batas normal getaran
|
||||
$table->string('status')->default('normal'); // normal, warning, critical
|
||||
$table->json('metadata')->nullable(); // Data tambahan sensor
|
||||
$table->timestamp('recorded_at'); // Waktu pembacaan sensor
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['device_id', 'recorded_at']);
|
||||
$table->index(['is_abnormal', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('vibration_readings');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('pir_readings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained()->onDelete('cascade');
|
||||
$table->boolean('motion_detected')->default(false); // Gerakan terdeteksi
|
||||
$table->integer('motion_intensity')->default(0); // Intensitas gerakan (0-100)
|
||||
$table->integer('duration_seconds')->default(0); // Durasi gerakan dalam detik
|
||||
$table->boolean('is_authorized_time')->default(true); // Apakah dalam jam kerja
|
||||
$table->boolean('is_suspicious')->default(false); // Gerakan mencurigakan
|
||||
$table->string('motion_type')->default('normal'); // normal, suspicious, unauthorized
|
||||
$table->string('detection_zone')->nullable(); // Area deteksi (front, back, side)
|
||||
$table->json('metadata')->nullable(); // Data tambahan sensor
|
||||
$table->timestamp('motion_start')->nullable(); // Waktu mulai gerakan
|
||||
$table->timestamp('motion_end')->nullable(); // Waktu selesai gerakan
|
||||
$table->timestamp('recorded_at'); // Waktu pembacaan sensor
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['device_id', 'recorded_at']);
|
||||
$table->index(['motion_detected', 'is_suspicious']);
|
||||
$table->index(['is_authorized_time', 'motion_type']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('pir_readings');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('reed_switch_readings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained()->onDelete('cascade');
|
||||
$table->boolean('door_open')->default(false); // Status pintu (true=buka, false=tutup)
|
||||
$table->boolean('is_authorized')->default(true); // Apakah pembukaan sah
|
||||
$table->boolean('is_forced_entry')->default(false); // Pembukaan paksa
|
||||
$table->string('access_method')->nullable(); // Metode akses (key, card, force, unknown)
|
||||
$table->string('door_status')->default('closed'); // closed, open, ajar, forced
|
||||
$table->integer('open_duration_seconds')->default(0); // Durasi pintu terbuka
|
||||
$table->string('access_level')->default('normal'); // normal, suspicious, unauthorized, emergency
|
||||
$table->string('door_location')->nullable(); // Lokasi pintu (front, back, side, main)
|
||||
$table->json('metadata')->nullable(); // Data tambahan sensor
|
||||
$table->timestamp('door_opened_at')->nullable(); // Waktu pintu dibuka
|
||||
$table->timestamp('door_closed_at')->nullable(); // Waktu pintu ditutup
|
||||
$table->timestamp('recorded_at'); // Waktu pembacaan sensor
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['device_id', 'recorded_at']);
|
||||
$table->index(['door_open', 'is_authorized']);
|
||||
$table->index(['access_level', 'door_status']);
|
||||
$table->index(['is_forced_entry', 'door_location']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('reed_switch_readings');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('door_readings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained()->onDelete('cascade');
|
||||
$table->boolean('door_open')->default(false); // Status pintu terbuka
|
||||
$table->boolean('is_authorized_access')->default(true); // Akses yang sah
|
||||
$table->boolean('is_forced_entry')->default(false); // Pembukaan paksa
|
||||
$table->string('access_type')->default('normal'); // normal, unauthorized, forced, maintenance
|
||||
$table->string('door_location')->nullable(); // front_panel, back_panel, side_door, main_door
|
||||
$table->integer('open_duration_seconds')->default(0); // Durasi terbuka dalam detik
|
||||
$table->boolean('proper_closure')->default(true); // Apakah ditutup dengan benar
|
||||
$table->json('access_card_data')->nullable(); // Data kartu akses jika ada
|
||||
$table->json('metadata')->nullable(); // Data tambahan sensor
|
||||
$table->timestamp('door_opened_at')->nullable(); // Waktu pintu dibuka
|
||||
$table->timestamp('door_closed_at')->nullable(); // Waktu pintu ditutup
|
||||
$table->timestamp('recorded_at'); // Waktu pembacaan sensor
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['device_id', 'recorded_at']);
|
||||
$table->index(['door_open', 'is_authorized_access']);
|
||||
$table->index(['access_type', 'door_location']);
|
||||
$table->index(['is_forced_entry', 'proper_closure']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('door_readings');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('door_access_readings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained()->onDelete('cascade');
|
||||
$table->boolean('door_opened')->default(false); // Status pintu terbuka
|
||||
$table->boolean('is_authorized_access')->default(false); // Akses resmi/tidak
|
||||
$table->string('access_type')->default('unknown'); // authorized, unauthorized, maintenance, emergency
|
||||
$table->string('access_method')->nullable(); // keycard, manual, force, unknown
|
||||
$table->string('user_id_card')->nullable(); // ID card yang digunakan (jika ada)
|
||||
$table->integer('duration_seconds')->default(0); // Durasi pintu terbuka
|
||||
$table->boolean('is_suspicious')->default(false); // Akses mencurigakan
|
||||
$table->string('door_location')->nullable(); // front_door, back_door, side_door
|
||||
$table->boolean('is_forced_entry')->default(false); // Paksa masuk
|
||||
$table->json('metadata')->nullable(); // Data tambahan sensor
|
||||
$table->timestamp('door_opened_at')->nullable(); // Waktu pintu dibuka
|
||||
$table->timestamp('door_closed_at')->nullable(); // Waktu pintu ditutup
|
||||
$table->timestamp('recorded_at'); // Waktu pembacaan sensor
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['device_id', 'recorded_at']);
|
||||
$table->index(['door_opened', 'is_suspicious']);
|
||||
$table->index(['is_authorized_access', 'access_type']);
|
||||
$table->index(['user_id_card', 'access_method']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('door_access_readings');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('lo_ra_messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->nullable()->constrained()->onDelete('set null');
|
||||
$table->string('node_id', 50); // LoRa node identifier (e.g., LORA_001)
|
||||
$table->string('gateway_id', 50)->nullable(); // LoRa gateway identifier
|
||||
$table->enum('direction', ['inbound', 'outbound']); // Message direction
|
||||
$table->enum('message_type', ['sensor_data', 'command', 'heartbeat', 'ack', 'config']); // Message type
|
||||
$table->text('payload'); // Raw LoRa message payload
|
||||
$table->json('parsed_data')->nullable(); // Parsed sensor data
|
||||
$table->float('rssi', 8, 2)->nullable(); // Received Signal Strength Indicator
|
||||
$table->float('snr', 8, 2)->nullable(); // Signal-to-Noise Ratio
|
||||
$table->integer('spreading_factor')->nullable(); // LoRa spreading factor (7-12)
|
||||
$table->float('frequency', 10, 6)->nullable(); // Frequency in MHz
|
||||
$table->integer('bandwidth')->nullable(); // Bandwidth in Hz
|
||||
$table->boolean('is_processed')->default(false); // Message processing status
|
||||
$table->boolean('is_acknowledged')->default(false); // ACK status for outbound messages
|
||||
$table->string('status')->default('received'); // received, processed, failed, acknowledged
|
||||
$table->text('error_message')->nullable(); // Error details if processing failed
|
||||
$table->json('metadata')->nullable(); // Additional LoRa parameters
|
||||
$table->timestamp('transmitted_at')->nullable(); // When message was sent (for outbound)
|
||||
$table->timestamp('received_at')->nullable(); // When message was received (for inbound)
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['node_id', 'created_at']);
|
||||
$table->index(['direction', 'message_type']);
|
||||
$table->index(['is_processed', 'status']);
|
||||
$table->index(['gateway_id', 'received_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('lo_ra_messages');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Buat admin hanya jika belum ada
|
||||
if (!\App\Models\User::where('email', 'admin@smartrack.com')->exists()) {
|
||||
User::factory()->create([
|
||||
'name' => 'Admin Smart Rack',
|
||||
'email' => 'admin@smartrack.com',
|
||||
'password' => 'admin123',
|
||||
]);
|
||||
}
|
||||
|
||||
// Seed Smart Rack Security data
|
||||
$this->call([
|
||||
DeviceSeeder::class,
|
||||
SensorSeeder::class,
|
||||
SampleDataSeeder::class,
|
||||
]);
|
||||
|
||||
$this->command->info('Smart Rack Security database seeded successfully!');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DeviceSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$devices = [
|
||||
[
|
||||
'name' => 'Device Rak A',
|
||||
'device_id' => 'RACK_A_001',
|
||||
'location' => 'Rak A - Lantai 1',
|
||||
'type' => 'sensor_node',
|
||||
'status' => 'online',
|
||||
'ip_address' => '192.168.1.101',
|
||||
'mac_address' => '00:1B:44:11:3A:B7',
|
||||
'signal_strength' => 95,
|
||||
'last_seen' => Carbon::now()->subSeconds(5),
|
||||
'configuration' => json_encode([
|
||||
'lora_frequency' => '868MHz',
|
||||
'transmission_power' => '14dBm',
|
||||
'data_rate' => 'SF7BW125',
|
||||
'battery_type' => 'Li-ion 3.7V'
|
||||
]),
|
||||
'description' => 'Node sensor utama untuk monitoring rak dengan sensor PIR, getaran, dan reed switch',
|
||||
'is_active' => true,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
[
|
||||
'name' => 'LoRa Gateway',
|
||||
'device_id' => 'GATEWAY_001',
|
||||
'location' => 'Server Room',
|
||||
'type' => 'gateway',
|
||||
'status' => 'online',
|
||||
'ip_address' => '192.168.1.100',
|
||||
'mac_address' => '00:1B:44:11:3A:B9',
|
||||
'signal_strength' => 100,
|
||||
'last_seen' => Carbon::now()->subSeconds(1),
|
||||
'configuration' => json_encode([
|
||||
'lora_frequency' => '868MHz',
|
||||
'channels' => 8,
|
||||
'max_devices' => 100,
|
||||
'range' => '5km'
|
||||
]),
|
||||
'description' => 'Gateway LoRa untuk menerima data dari semua sensor node',
|
||||
'is_active' => true,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
[
|
||||
'name' => 'Server Monitoring',
|
||||
'device_id' => 'SERVER_001',
|
||||
'location' => 'Server Room',
|
||||
'type' => 'server',
|
||||
'status' => 'online',
|
||||
'ip_address' => '192.168.1.10',
|
||||
'mac_address' => '00:1B:44:11:3A:BA',
|
||||
'signal_strength' => 100,
|
||||
'last_seen' => Carbon::now(),
|
||||
'configuration' => json_encode([
|
||||
'os' => 'Ubuntu 22.04 LTS',
|
||||
'cpu' => 'Intel Core i5-8400',
|
||||
'ram' => '16GB DDR4',
|
||||
'storage' => '500GB SSD'
|
||||
]),
|
||||
'description' => 'Server utama untuk processing dan storage data monitoring',
|
||||
'is_active' => true,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
];
|
||||
|
||||
DB::table('devices')->insert($devices);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class SampleDataSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// Cek apakah data sudah ada, kalau sudah skip
|
||||
if (\App\Models\Sensor::count() === 0) {
|
||||
return;
|
||||
}
|
||||
// Sample Sensor Readings
|
||||
$sensorReadings = [
|
||||
// PIR Sensor readings (normal state)
|
||||
[
|
||||
'sensor_id' => 1, // PIR A
|
||||
'device_id' => 1,
|
||||
'value' => 0,
|
||||
'raw_value' => '0',
|
||||
'status' => 'normal',
|
||||
'battery_level' => 85.5,
|
||||
'signal_strength' => 95,
|
||||
'metadata' => json_encode(['temperature' => 25.3, 'humidity' => 60]),
|
||||
'reading_time' => Carbon::now()->subMinutes(1),
|
||||
'is_processed' => true,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
// Vibration sensor readings (normal state)
|
||||
[
|
||||
'sensor_id' => 2, // Vibration A
|
||||
'device_id' => 1,
|
||||
'value' => 45,
|
||||
'raw_value' => '45',
|
||||
'status' => 'normal',
|
||||
'battery_level' => 85.5,
|
||||
'signal_strength' => 95,
|
||||
'metadata' => json_encode(['baseline' => 40, 'peak' => 50]),
|
||||
'reading_time' => Carbon::now()->subMinutes(1),
|
||||
'is_processed' => true,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
// Reed switch readings (closed state)
|
||||
[
|
||||
'sensor_id' => 3, // Reed Switch A
|
||||
'device_id' => 1,
|
||||
'value' => 0,
|
||||
'raw_value' => '0',
|
||||
'status' => 'normal',
|
||||
'battery_level' => 85.5,
|
||||
'signal_strength' => 95,
|
||||
'metadata' => json_encode(['door_state' => 'closed']),
|
||||
'reading_time' => Carbon::now()->subMinutes(1),
|
||||
'is_processed' => true,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
];
|
||||
|
||||
// Sample Activity Logs
|
||||
$activityLogs = [
|
||||
[
|
||||
'device_id' => 1,
|
||||
'sensor_id' => 1,
|
||||
'event_type' => 'motion_detected',
|
||||
'severity' => 'warning',
|
||||
'title' => 'Gerakan Terdeteksi',
|
||||
'description' => 'Sensor PIR mendeteksi gerakan manusia di area rak A pada pukul 12:10',
|
||||
'event_data' => json_encode([
|
||||
'sensor_value' => 1,
|
||||
'duration' => '5 seconds',
|
||||
'confidence' => 'high'
|
||||
]),
|
||||
'location' => 'Rak A - Lantai 1',
|
||||
'user_agent' => null,
|
||||
'ip_address' => '192.168.1.101',
|
||||
'event_time' => Carbon::now()->subHours(2)->subMinutes(10),
|
||||
'is_acknowledged' => false,
|
||||
'acknowledged_at' => null,
|
||||
'acknowledged_by' => null,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
[
|
||||
'device_id' => 1,
|
||||
'sensor_id' => 3,
|
||||
'event_type' => 'door_opened',
|
||||
'severity' => 'info',
|
||||
'title' => 'Rak Dibuka',
|
||||
'description' => 'Reed switch mendeteksi rak A dibuka pada pukul 12:15',
|
||||
'event_data' => json_encode([
|
||||
'sensor_value' => 1,
|
||||
'previous_state' => 'closed',
|
||||
'current_state' => 'opened'
|
||||
]),
|
||||
'location' => 'Rak A - Lantai 1',
|
||||
'user_agent' => null,
|
||||
'ip_address' => '192.168.1.101',
|
||||
'event_time' => Carbon::now()->subHours(2)->subMinutes(5),
|
||||
'is_acknowledged' => true,
|
||||
'acknowledged_at' => Carbon::now()->subHours(2),
|
||||
'acknowledged_by' => 'admin',
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
[
|
||||
'device_id' => 1,
|
||||
'sensor_id' => 2,
|
||||
'event_type' => 'system_normal',
|
||||
'severity' => 'info',
|
||||
'title' => 'Sistem Normal',
|
||||
'description' => 'Semua sensor kembali ke kondisi normal pada pukil 12:20',
|
||||
'event_data' => json_encode([
|
||||
'all_sensors_status' => 'normal',
|
||||
'system_health' => 'good'
|
||||
]),
|
||||
'location' => 'Rak A - Lantai 1',
|
||||
'user_agent' => null,
|
||||
'ip_address' => '192.168.1.101',
|
||||
'event_time' => Carbon::now()->subHours(2),
|
||||
'is_acknowledged' => true,
|
||||
'acknowledged_at' => Carbon::now()->subHours(1)->subMinutes(30),
|
||||
'acknowledged_by' => 'admin',
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
];
|
||||
|
||||
// Sample Alerts
|
||||
$alerts = [
|
||||
[
|
||||
'device_id' => 1,
|
||||
'sensor_id' => 1,
|
||||
'activity_log_id' => 1,
|
||||
'alert_type' => 'security_breach',
|
||||
'priority' => 'high',
|
||||
'status' => 'acknowledged',
|
||||
'title' => 'Aktivitas Mencurigakan Terdeteksi',
|
||||
'message' => 'Sensor PIR mendeteksi gerakan di area rak A di luar jam operasional',
|
||||
'alert_data' => json_encode([
|
||||
'detection_time' => '12:10:00',
|
||||
'sensor_confidence' => 'high',
|
||||
'recommended_action' => 'Check CCTV footage'
|
||||
]),
|
||||
'location' => 'Rak A - Lantai 1',
|
||||
'triggered_at' => Carbon::now()->subHours(2)->subMinutes(10),
|
||||
'acknowledged_at' => Carbon::now()->subHours(2)->subMinutes(5),
|
||||
'resolved_at' => null,
|
||||
'acknowledged_by' => 'admin',
|
||||
'resolved_by' => null,
|
||||
'resolution_notes' => null,
|
||||
'is_sent_notification' => true,
|
||||
'notification_channels' => json_encode(['email', 'dashboard']),
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
DB::table('sensor_readings')->insert($sensorReadings);
|
||||
DB::table('activity_logs')->insert($activityLogs);
|
||||
DB::table('alerts')->insert($alerts);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class SensorSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$sensors = [
|
||||
// Sensors untuk Device Rak A (device_id = 1)
|
||||
[
|
||||
'device_id' => 1,
|
||||
'name' => 'PIR Motion Sensor',
|
||||
'type' => 'pir',
|
||||
'pin_number' => 'GPIO2',
|
||||
'status' => 'active',
|
||||
'threshold_min' => 0,
|
||||
'threshold_max' => 1,
|
||||
'unit' => 'boolean',
|
||||
'sampling_rate' => 500,
|
||||
'calibration_data' => json_encode([
|
||||
'sensitivity' => 'high',
|
||||
'detection_range' => '7m',
|
||||
'detection_angle' => '120°'
|
||||
]),
|
||||
'description' => 'Sensor PIR untuk mendeteksi gerakan manusia di area rak',
|
||||
'is_active' => true,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
[
|
||||
'device_id' => 1,
|
||||
'name' => 'Vibration Sensor SW-420',
|
||||
'type' => 'vibration',
|
||||
'pin_number' => 'GPIO3',
|
||||
'status' => 'active',
|
||||
'threshold_min' => 0,
|
||||
'threshold_max' => 1024,
|
||||
'unit' => 'analog',
|
||||
'sampling_rate' => 100,
|
||||
'calibration_data' => json_encode([
|
||||
'sensitivity' => 'medium',
|
||||
'trigger_threshold' => 512,
|
||||
'debounce_time' => '50ms'
|
||||
]),
|
||||
'description' => 'Sensor getaran SW-420 untuk mendeteksi guncangan pada rak',
|
||||
'is_active' => true,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
[
|
||||
'device_id' => 1,
|
||||
'name' => 'Reed Switch',
|
||||
'type' => 'reed_switch',
|
||||
'pin_number' => 'GPIO4',
|
||||
'status' => 'active',
|
||||
'threshold_min' => 0,
|
||||
'threshold_max' => 1,
|
||||
'unit' => 'boolean',
|
||||
'sampling_rate' => 1000,
|
||||
'calibration_data' => json_encode([
|
||||
'magnet_distance' => '2cm',
|
||||
'switch_type' => 'normally_open'
|
||||
]),
|
||||
'description' => 'Reed switch untuk mendeteksi status buka/tutup rak',
|
||||
'is_active' => true,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
],
|
||||
];
|
||||
|
||||
DB::table('sensors')->insert($sensors);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
[phases.setup]
|
||||
nixPkgs = ["php82", "php82Extensions.pdo", "php82Extensions.pdo_mysql", "php82Extensions.mbstring", "php82Extensions.xml", "php82Extensions.curl", "php82Extensions.zip", "php82Extensions.gd", "php82Extensions.tokenizer", "php82Extensions.bcmath", "composer"]
|
||||
|
||||
[phases.install]
|
||||
cmds = ["composer install --no-dev --optimize-autoloader"]
|
||||
|
||||
[phases.build]
|
||||
cmds = [
|
||||
"php artisan config:cache",
|
||||
"php artisan route:cache",
|
||||
"php artisan view:cache"
|
||||
]
|
||||
|
||||
[start]
|
||||
cmd = "php artisan migrate --force && php artisan db:seed --force && php -S 0.0.0.0:$PORT -t public"
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"$schema": "https://www.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"axios": "^1.11.0",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^7.0.7"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow:
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
<?php
|
||||
|
||||
// Quick test untuk semua 3 sensor Smart Rack Security System
|
||||
echo "🧪 Quick Smart Rack Security System Test\n";
|
||||
echo "========================================\n\n";
|
||||
|
||||
// Test Vibration Calculations
|
||||
echo "📊 VIBRATION SENSOR TEST:\n";
|
||||
echo "-------------------------\n";
|
||||
|
||||
// Simulasi data normal
|
||||
echo "✅ Normal Vibration:\n";
|
||||
$normal = [
|
||||
'x_axis' => 0.5,
|
||||
'y_axis' => 0.8,
|
||||
'z_axis' => 0.6,
|
||||
'magnitude' => sqrt(0.5*0.5 + 0.8*0.8 + 0.6*0.6),
|
||||
'threshold' => 2.0
|
||||
];
|
||||
echo "Magnitude: " . round($normal['magnitude'], 2) . " (Status: NORMAL)\n";
|
||||
|
||||
// Simulasi data warning
|
||||
echo "⚠️ Warning Vibration:\n";
|
||||
$warning = [
|
||||
'x_axis' => 1.8,
|
||||
'y_axis' => 2.2,
|
||||
'z_axis' => 1.5,
|
||||
'magnitude' => sqrt(1.8*1.8 + 2.2*2.2 + 1.5*1.5),
|
||||
'threshold' => 2.0
|
||||
];
|
||||
echo "Magnitude: " . round($warning['magnitude'], 2) . " (Status: WARNING)\n";
|
||||
|
||||
// Simulasi data critical
|
||||
echo "🚨 Critical Vibration:\n";
|
||||
$critical = [
|
||||
'x_axis' => 2.8,
|
||||
'y_axis' => 3.5,
|
||||
'z_axis' => 2.9,
|
||||
'magnitude' => sqrt(2.8*2.8 + 3.5*3.5 + 2.9*2.9),
|
||||
'threshold' => 2.0
|
||||
];
|
||||
echo "Magnitude: " . round($critical['magnitude'], 2) . " (Status: CRITICAL - ALERT!)\n\n";
|
||||
|
||||
// Test PIR Motion Detection
|
||||
echo "👁️ PIR MOTION SENSOR TEST:\n";
|
||||
echo "---------------------------\n";
|
||||
|
||||
// Function to check working hours
|
||||
function isWorkingHours($hour) {
|
||||
return ($hour >= 8 && $hour < 17);
|
||||
}
|
||||
|
||||
// Function to determine motion type
|
||||
function getMotionType($intensity, $duration, $isWorkingTime) {
|
||||
if (!$isWorkingTime) {
|
||||
return 'unauthorized';
|
||||
}
|
||||
|
||||
if ($intensity > 80 || $duration > 300) {
|
||||
return 'suspicious';
|
||||
}
|
||||
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
// Test scenarios
|
||||
$pirTests = [
|
||||
[
|
||||
'time' => 10, // 10:00 AM
|
||||
'intensity' => 60,
|
||||
'duration' => 45,
|
||||
'zone' => 'front',
|
||||
'description' => 'Normal office access'
|
||||
],
|
||||
[
|
||||
'time' => 14, // 2:00 PM
|
||||
'intensity' => 85,
|
||||
'duration' => 350,
|
||||
'zone' => 'back',
|
||||
'description' => 'Suspicious high activity'
|
||||
],
|
||||
[
|
||||
'time' => 22, // 10:00 PM
|
||||
'intensity' => 70,
|
||||
'duration' => 120,
|
||||
'zone' => 'front',
|
||||
'description' => 'After hours access'
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($pirTests as $test) {
|
||||
$isWorking = isWorkingHours($test['time']);
|
||||
$motionType = getMotionType($test['intensity'], $test['duration'], $isWorking);
|
||||
$workStatus = $isWorking ? 'Working Hours' : 'After Hours';
|
||||
|
||||
$alertIcon = match($motionType) {
|
||||
'normal' => '✅',
|
||||
'suspicious' => '⚠️ ',
|
||||
'unauthorized' => '🚨',
|
||||
default => '❓'
|
||||
};
|
||||
|
||||
echo "{$alertIcon} {$test['description']}:\n";
|
||||
echo " Time: {$test['time']}:00 ({$workStatus})\n";
|
||||
echo " Intensity: {$test['intensity']}%, Duration: {$test['duration']}s\n";
|
||||
echo " Zone: {$test['zone']}, Type: {$motionType}\n";
|
||||
|
||||
if ($motionType !== 'normal') {
|
||||
echo " 🔔 ALERT TRIGGERED!\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// Test Door Access (Reed Switch)
|
||||
echo "🚪 DOOR ACCESS (REED SWITCH) TEST:\n";
|
||||
echo "----------------------------------\n";
|
||||
|
||||
// Function to check door access authorization
|
||||
function checkDoorAccess($time, $idCard, $method, $duration) {
|
||||
$isWorkingTime = ($time >= 7 && $time < 18);
|
||||
$hasValidId = !empty($idCard) && preg_match('/^EMP-\d{4}$/', $idCard);
|
||||
|
||||
if ($method === 'force') {
|
||||
return 'forced_entry';
|
||||
}
|
||||
|
||||
if ($method === 'emergency') {
|
||||
return 'emergency';
|
||||
}
|
||||
|
||||
if ($method === 'maintenance') {
|
||||
return 'maintenance';
|
||||
}
|
||||
|
||||
if ($hasValidId && $isWorkingTime) {
|
||||
return 'authorized';
|
||||
}
|
||||
|
||||
if (!$isWorkingTime) {
|
||||
return 'after_hours';
|
||||
}
|
||||
|
||||
return 'unauthorized';
|
||||
}
|
||||
|
||||
// Door access test scenarios
|
||||
$doorTests = [
|
||||
[
|
||||
'time' => 9, // 9:00 AM
|
||||
'id_card' => 'EMP-1234',
|
||||
'method' => 'keycard',
|
||||
'duration' => 30,
|
||||
'location' => 'front_door',
|
||||
'description' => 'Normal employee access'
|
||||
],
|
||||
[
|
||||
'time' => 14, // 2:00 PM
|
||||
'id_card' => null,
|
||||
'method' => 'manual',
|
||||
'duration' => 60,
|
||||
'location' => 'back_door',
|
||||
'description' => 'No ID card access'
|
||||
],
|
||||
[
|
||||
'time' => 22, // 10:00 PM
|
||||
'id_card' => null,
|
||||
'method' => 'force',
|
||||
'duration' => 180,
|
||||
'location' => 'front_door',
|
||||
'description' => 'Forced entry at night'
|
||||
],
|
||||
[
|
||||
'time' => 23, // 11:00 PM
|
||||
'id_card' => null,
|
||||
'method' => 'emergency',
|
||||
'duration' => 45,
|
||||
'location' => 'main_entrance',
|
||||
'description' => 'Emergency access'
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($doorTests as $test) {
|
||||
$accessType = checkDoorAccess($test['time'], $test['id_card'], $test['method'], $test['duration']);
|
||||
$workStatus = ($test['time'] >= 7 && $test['time'] < 18) ? 'Working Hours' : 'After Hours';
|
||||
$idInfo = $test['id_card'] ? $test['id_card'] : 'No ID';
|
||||
|
||||
$alertIcon = match($accessType) {
|
||||
'authorized' => '✅',
|
||||
'maintenance' => '🔧',
|
||||
'emergency' => '🆘',
|
||||
'after_hours' => '🌙',
|
||||
'unauthorized' => '⚠️ ',
|
||||
'forced_entry' => '🚨',
|
||||
default => '❓'
|
||||
};
|
||||
|
||||
echo "{$alertIcon} {$test['description']}:\n";
|
||||
echo " Time: {$test['time']}:00 ({$workStatus})\n";
|
||||
echo " ID Card: {$idInfo}, Method: {$test['method']}\n";
|
||||
echo " Duration: {$test['duration']}s, Location: {$test['location']}\n";
|
||||
echo " Access Type: {$accessType}\n";
|
||||
|
||||
if (in_array($accessType, ['unauthorized', 'forced_entry', 'after_hours'])) {
|
||||
echo " 🔔 ALERT TRIGGERED!\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
echo "📡 API ENDPOINTS READY:\n";
|
||||
echo "=======================\n";
|
||||
echo "Vibration API: http://localhost:8000/api/vibration/data\n";
|
||||
echo "PIR API: http://localhost:8000/api/pir/data\n";
|
||||
echo "Door Access API: http://localhost:8000/api/door-access/data\n";
|
||||
echo "LoRa API: http://localhost:8000/api/lora/receive\n";
|
||||
echo "\n";
|
||||
echo "Statistics APIs:\n";
|
||||
echo "Vibration Stats: http://localhost:8000/api/vibration/statistics\n";
|
||||
echo "PIR Stats: http://localhost:8000/api/pir/statistics\n";
|
||||
echo "Door Access Stats: http://localhost:8000/api/door-access/statistics\n";
|
||||
echo "LoRa Stats: http://localhost:8000/api/lora/statistics\n\n";
|
||||
|
||||
echo "🧪 TEST COMMANDS:\n";
|
||||
echo "=================\n";
|
||||
echo "php test_vibration_api.php # Test vibration sensor\n";
|
||||
echo "php test_pir_api.php # Test PIR motion sensor\n";
|
||||
echo "php test_door_access_api.php # Test door access (reed switch)\n";
|
||||
echo "php test_lora_api.php # Test LoRa communication\n\n";
|
||||
|
||||
echo "🔔 NOTIFICATION TEST COMMANDS:\n";
|
||||
echo "==============================\n";
|
||||
echo "curl -X POST http://localhost:8000/api/test-notification\n";
|
||||
echo "curl -X POST http://localhost:8000/api/test-pir-notification\n";
|
||||
echo "curl -X POST http://localhost:8000/api/test-door-access-notification\n\n";
|
||||
|
||||
echo "📡 LORA COMMAND EXAMPLES:\n";
|
||||
echo "=========================\n";
|
||||
echo "# Send LoRa sensor data:\n";
|
||||
echo "curl -X POST http://localhost:8000/api/lora/receive \\\n";
|
||||
echo " -H \"Content-Type: application/json\" \\\n";
|
||||
echo " -d '{\"node_id\":\"LORA_001\",\"payload\":\"SENSOR|VIBRATION|2.5,3.2,1.8|2.0\",\"rssi\":-82.5}'\n\n";
|
||||
echo "# Send command to LoRa node:\n";
|
||||
echo "curl -X POST http://localhost:8000/api/lora/send-command \\\n";
|
||||
echo " -H \"Content-Type: application/json\" \\\n";
|
||||
echo " -d '{\"node_id\":\"LORA_001\",\"action\":\"set_threshold\",\"parameters\":[\"2.5\"]}'\n\n";
|
||||
|
||||
echo "✅ Smart Rack Security System Ready!\n";
|
||||
echo "🔔 All 4 systems will automatically send notifications for abnormal conditions:\n";
|
||||
echo " 📊 Vibration: Detects abnormal shaking/movement\n";
|
||||
echo " 👁️ PIR Motion: Detects suspicious movement patterns\n";
|
||||
echo " 🚪 Door Access: Detects unauthorized door access\n";
|
||||
echo " 📡 LoRa: Long-range communication for remote sensors\n";
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import './bootstrap';
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
|
@ -0,0 +1,782 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - Smart Rack Security</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/feather-icons"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.font-display { font-family: 'Space Grotesk', sans-serif; }
|
||||
.glass { backdrop-filter: blur(16px); background: rgba(255, 255, 255, 0.9); border: 1px solid rgba(255, 255, 255, 0.2); }
|
||||
.animate-pulse-slow { animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
||||
.animate-bounce-slow { animation: bounce 2s infinite; }
|
||||
|
||||
/* LED & Buzzer Animations */
|
||||
@keyframes led-blink {
|
||||
0%, 100% { opacity: 1; box-shadow: 0 0 12px 4px currentColor; }
|
||||
50% { opacity: 0.3; box-shadow: none; }
|
||||
}
|
||||
@keyframes buzzer-wave {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
100% { transform: scale(2.5); opacity: 0; }
|
||||
}
|
||||
.led-on-red {
|
||||
animation: led-blink 0.6s ease-in-out infinite;
|
||||
color: #ef4444;
|
||||
}
|
||||
.led-on-green {
|
||||
box-shadow: 0 0 12px 4px #22c55e;
|
||||
color: #22c55e;
|
||||
}
|
||||
.led-off {
|
||||
color: #6b7280;
|
||||
opacity: 0.4;
|
||||
}
|
||||
.buzzer-active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #ef4444;
|
||||
animation: buzzer-wave 1s ease-out infinite;
|
||||
}
|
||||
.buzzer-active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #ef4444;
|
||||
animation: buzzer-wave 1s ease-out 0.4s infinite;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 font-sans overflow-x-hidden">
|
||||
|
||||
<div class="flex h-screen overflow-hidden">
|
||||
|
||||
<!-- OVERLAY untuk mobile -->
|
||||
<div id="sidebar-overlay" class="fixed inset-0 bg-black/50 z-30 hidden md:hidden" onclick="toggleSidebar()"></div>
|
||||
|
||||
<!-- SIDEBAR -->
|
||||
<aside id="sidebar" class="fixed md:static z-40 top-0 left-0 h-full w-72 bg-gradient-to-b from-slate-900 via-gray-900 to-slate-800 text-gray-200 shadow-2xl p-6 transform -translate-x-full md:translate-x-0 transition-all duration-300 border-r border-gray-700/50 overflow-y-auto">
|
||||
|
||||
<!-- LOGO -->
|
||||
<div class="flex items-center gap-4 mb-12 p-3 bg-gradient-to-r from-indigo-600/20 to-purple-600/20 rounded-2xl border border-indigo-500/20">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<i data-feather="shield" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-display font-bold text-white leading-none">Smart Rack</h2>
|
||||
<p class="text-sm text-indigo-300">Security System</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MENU -->
|
||||
<nav class="space-y-3">
|
||||
<a href="/" class="group flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-gradient-to-r hover:from-slate-700/50 hover:to-slate-600/50 hover:text-white transition-all duration-300 text-gray-300">
|
||||
<div class="w-10 h-10 bg-gray-700/50 group-hover:bg-indigo-500/20 rounded-lg flex items-center justify-center transition-all duration-300">
|
||||
<i data-feather="arrow-left" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<span class="font-medium">Home</span>
|
||||
</a>
|
||||
|
||||
<a href="/dashboard" class="group flex items-center gap-4 px-4 py-3 rounded-xl bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center">
|
||||
<i data-feather="home" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<span class="font-medium">Dashboard</span>
|
||||
</a>
|
||||
|
||||
<a href="/monitoring" class="group flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-gradient-to-r hover:from-slate-700/50 hover:to-slate-600/50 hover:text-white transition-all duration-300 text-gray-300">
|
||||
<div class="w-10 h-10 bg-gray-700/50 group-hover:bg-blue-500/20 rounded-lg flex items-center justify-center transition-all duration-300">
|
||||
<i data-feather="activity" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<span class="font-medium">Monitoring Sensor</span>
|
||||
</a>
|
||||
|
||||
<a href="/log" class="group flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-gradient-to-r hover:from-slate-700/50 hover:to-slate-600/50 hover:text-white transition-all duration-300 text-gray-300">
|
||||
<div class="w-10 h-10 bg-gray-700/50 group-hover:bg-green-500/20 rounded-lg flex items-center justify-center transition-all duration-300">
|
||||
<i data-feather="file-text" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<span class="font-medium">Log Aktivitas</span>
|
||||
</a>
|
||||
|
||||
<a href="/device" class="group flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-gradient-to-r hover:from-slate-700/50 hover:to-slate-600/50 hover:text-white transition-all duration-300 text-gray-300">
|
||||
<div class="w-10 h-10 bg-gray-700/50 group-hover:bg-purple-500/20 rounded-lg flex items-center justify-center transition-all duration-300">
|
||||
<i data-feather="cpu" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<span class="font-medium">Status Device</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- DIVIDER -->
|
||||
<div class="border-t border-gray-700/50 my-8"></div>
|
||||
|
||||
<!-- USER -->
|
||||
<div class="flex items-center gap-4 p-4 bg-gradient-to-r from-gray-800/50 to-slate-700/50 rounded-2xl border border-gray-700/30">
|
||||
<div class="relative">
|
||||
<img src="https://i.pravatar.cc/48" class="w-12 h-12 rounded-xl shadow-lg">
|
||||
<div class="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-slate-800"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold text-white">Admin</p>
|
||||
<p class="text-sm text-gray-400">System Operator</p>
|
||||
</div>
|
||||
|
||||
<a href="{{ route('logout') }}" class="w-10 h-10 bg-red-500/20 hover:bg-red-500/30 rounded-lg flex items-center justify-center text-red-400 hover:text-red-300 transition-all duration-300" title="Logout">
|
||||
<i data-feather="log-out" class="w-5 h-5"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<!-- MAIN -->
|
||||
<div class="flex-1 flex flex-col">
|
||||
|
||||
<!-- TOPBAR -->
|
||||
<header class="glass border-b border-white/20 flex justify-between items-center px-4 md:px-8 py-4 md:py-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Hamburger button mobile -->
|
||||
<button onclick="toggleSidebar()" class="md:hidden w-10 h-10 bg-gray-100 rounded-xl flex items-center justify-center text-gray-600 hover:bg-gray-200 transition-all">
|
||||
<i data-feather="menu" class="w-5 h-5"></i>
|
||||
</button>
|
||||
<div>
|
||||
<h1 class="text-lg md:text-2xl font-display font-bold text-gray-800">Dashboard Monitoring</h1>
|
||||
<p class="text-gray-600 mt-0.5 text-xs md:text-sm hidden sm:block">Sistem keamanan rak berbasis IoT</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 md:gap-6">
|
||||
<div class="flex items-center gap-2 bg-gradient-to-r from-green-500/10 to-emerald-500/10 px-3 py-2 rounded-xl border border-green-200">
|
||||
<div class="w-2 h-2 md:w-3 md:h-3 bg-green-500 rounded-full animate-pulse-slow"></div>
|
||||
<span class="text-green-700 font-semibold text-xs md:text-sm hidden sm:inline">Sistem Online</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative">
|
||||
<img src="https://i.pravatar.cc/44" class="w-9 h-9 md:w-11 md:h-11 rounded-xl shadow-lg">
|
||||
<div class="absolute -bottom-1 -right-1 w-3 h-3 md:w-4 md:h-4 bg-green-500 rounded-full border-2 border-white"></div>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<span class="text-gray-800 font-semibold text-sm">Admin</span>
|
||||
<p class="text-gray-500 text-xs">Online</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- CONTENT -->
|
||||
<main class="p-4 md:p-8 space-y-6 md:space-y-10 overflow-y-auto">
|
||||
|
||||
<div class="mb-4 md:mb-8">
|
||||
<h2 class="text-2xl md:text-3xl font-display font-bold text-gray-800 mb-1 md:mb-2">Ringkasan Sistem</h2>
|
||||
<p class="text-gray-600 text-sm md:text-base">Overview monitoring keamanan rak secara realtime</p>
|
||||
</div>
|
||||
|
||||
<!-- STATISTICS -->
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-6">
|
||||
|
||||
<div class="group bg-white p-4 md:p-8 rounded-2xl md:rounded-3xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-indigo-500/5 to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="w-10 h-10 md:w-16 md:h-16 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-3 md:mb-4">
|
||||
<i data-feather="layers" class="w-5 h-5 md:w-8 md:h-8 text-white"></i>
|
||||
</div>
|
||||
<p class="text-gray-600 font-medium mb-1 md:mb-2 text-xs md:text-base">Total Sensor</p>
|
||||
<h3 class="text-2xl md:text-4xl font-display font-bold text-indigo-600">{{ $totalSensor }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group bg-white p-4 md:p-8 rounded-2xl md:rounded-3xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-green-500/5 to-emerald-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="w-10 h-10 md:w-16 md:h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-3 md:mb-4">
|
||||
<i data-feather="check-circle" class="w-5 h-5 md:w-8 md:h-8 text-white"></i>
|
||||
</div>
|
||||
<p class="text-gray-600 font-medium mb-1 md:mb-2 text-xs md:text-base">Sensor Aktif</p>
|
||||
<h3 class="text-2xl md:text-4xl font-display font-bold text-green-600">{{ $sensorAktif }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group bg-white p-4 md:p-8 rounded-2xl md:rounded-3xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-red-500/5 to-pink-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="w-10 h-10 md:w-16 md:h-16 bg-gradient-to-br from-red-500 to-pink-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-3 md:mb-4">
|
||||
<i data-feather="alert-triangle" class="w-5 h-5 md:w-8 md:h-8 text-white"></i>
|
||||
</div>
|
||||
<p class="text-gray-600 font-medium mb-1 md:mb-2 text-xs md:text-base">Peringatan</p>
|
||||
<h3 class="text-2xl md:text-4xl font-display font-bold text-red-600">{{ $peringatanHariIni }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group bg-white p-4 md:p-8 rounded-2xl md:rounded-3xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-500/5 to-blue-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="w-10 h-10 md:w-16 md:h-16 bg-gradient-to-br from-purple-500 to-blue-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-3 md:mb-4">
|
||||
<i data-feather="shield-check" class="w-5 h-5 md:w-8 md:h-8 text-white"></i>
|
||||
</div>
|
||||
<p class="text-gray-600 font-medium mb-1 md:mb-2 text-xs md:text-base">Status</p>
|
||||
<h3 class="text-xl md:text-3xl font-display font-bold text-purple-600">{{ $alertAktif > 0 ? 'Waspada' : 'Normal' }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ALERT REALTIME -->
|
||||
@if($alertTerbaru->count() > 0 || $pirAktif || $vibAktif || $reedAktif)
|
||||
<div class="bg-gradient-to-br from-red-50 to-orange-50 rounded-3xl shadow-lg p-8 border border-red-200">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-red-500 to-orange-600 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="alert-triangle" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-red-800">⚠ Alert Aktif</h3>
|
||||
<p class="text-red-600 text-sm">{{ $alertTerbaru->count() }} peringatan memerlukan perhatian</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/log" class="text-red-600 hover:text-red-800 text-sm font-semibold flex items-center gap-1">
|
||||
Lihat semua <i data-feather="arrow-right" class="w-4 h-4"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Deteksi sensor aktif saat ini -->
|
||||
@if($pirAktif || $vibAktif || $reedAktif)
|
||||
<div class="mb-4 p-4 bg-red-500 rounded-2xl text-white flex items-center gap-3">
|
||||
<div class="w-3 h-3 bg-white rounded-full animate-pulse flex-shrink-0"></div>
|
||||
<p class="font-semibold text-sm">
|
||||
DETEKSI AKTIF SEKARANG:
|
||||
@if($pirAktif) <span class="bg-white/20 px-2 py-0.5 rounded-lg ml-1">PIR</span> @endif
|
||||
@if($vibAktif) <span class="bg-white/20 px-2 py-0.5 rounded-lg ml-1">SW-420</span> @endif
|
||||
@if($reedAktif) <span class="bg-white/20 px-2 py-0.5 rounded-lg ml-1">Reed Switch</span> @endif
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Daftar alert -->
|
||||
<div class="space-y-3">
|
||||
@foreach($alertTerbaru as $alert)
|
||||
@php
|
||||
$priorityColor = match($alert->priority) {
|
||||
'critical' => 'bg-red-100 border-red-300 text-red-800',
|
||||
'high' => 'bg-orange-100 border-orange-300 text-orange-800',
|
||||
'medium' => 'bg-yellow-100 border-yellow-300 text-yellow-800',
|
||||
default => 'bg-blue-100 border-blue-300 text-blue-800',
|
||||
};
|
||||
$dotColor = match($alert->priority) {
|
||||
'critical' => 'bg-red-500',
|
||||
'high' => 'bg-orange-500',
|
||||
'medium' => 'bg-yellow-500',
|
||||
default => 'bg-blue-500',
|
||||
};
|
||||
@endphp
|
||||
<div class="flex items-start gap-4 p-4 {{ $priorityColor }} rounded-2xl border">
|
||||
<div class="w-3 h-3 {{ $dotColor }} rounded-full mt-1 flex-shrink-0 animate-pulse"></div>
|
||||
<div class="flex-1">
|
||||
<p class="font-semibold text-sm">{{ $alert->title }}</p>
|
||||
<p class="text-xs mt-0.5 opacity-80">{{ $alert->message }}</p>
|
||||
<p class="text-xs mt-1 opacity-60">
|
||||
{{ $alert->device?->name ?? 'Sistem' }} ·
|
||||
{{ $alert->triggered_at?->diffForHumans() ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs font-bold uppercase px-2 py-1 bg-white/50 rounded-lg flex-shrink-0">
|
||||
{{ $alert->priority }}
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- BUZZER & LED STATUS -->
|
||||
<div class="bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg p-8 border border-gray-100">
|
||||
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-red-500 to-orange-600 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="bell" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">Status Buzzer & LED Lokal</h3>
|
||||
<p class="text-gray-600 text-sm">Indikator perangkat keras saat sensor terdeteksi</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-400">
|
||||
<div id="polling-dot" class="w-2 h-2 bg-green-400 rounded-full animate-pulse-slow"></div>
|
||||
<span id="last-update-text">Memuat...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ALERT BANNER -->
|
||||
<div id="alert-banner" class="hidden mb-6 p-4 bg-gradient-to-r from-red-500 to-orange-500 rounded-2xl text-white flex items-center gap-4 shadow-lg">
|
||||
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<i data-feather="alert-triangle" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-bold text-sm">⚠ PERINGATAN AKTIF</p>
|
||||
<p id="alert-banner-text" class="text-red-100 text-xs mt-0.5">Sensor mendeteksi aktivitas mencurigakan</p>
|
||||
</div>
|
||||
<button onclick="dismissAlert()" class="w-8 h-8 bg-white/20 hover:bg-white/30 rounded-lg flex items-center justify-center transition-all">
|
||||
<i data-feather="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
|
||||
<!-- BUZZER -->
|
||||
<div class="bg-gradient-to-br from-gray-50 to-slate-100 rounded-2xl p-6 border border-gray-200">
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-5">Buzzer</p>
|
||||
<div class="flex items-center gap-6">
|
||||
<!-- Buzzer Icon -->
|
||||
<div class="relative flex items-center justify-center w-20 h-20 flex-shrink-0">
|
||||
<div id="buzzer-icon" class="relative w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center transition-all duration-300">
|
||||
<i data-feather="volume-x" class="w-7 h-7 text-gray-400" id="buzzer-icon-inner"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div id="buzzer-status-dot" class="w-4 h-4 rounded-full bg-gray-300 transition-all duration-300"></div>
|
||||
<p id="buzzer-status-text" class="text-xl font-display font-bold text-gray-400">Tidak Aktif</p>
|
||||
</div>
|
||||
<p id="buzzer-trigger-text" class="text-gray-500 text-sm">Menunggu deteksi sensor...</p>
|
||||
<div class="mt-3 flex flex-wrap gap-2" id="buzzer-triggers">
|
||||
<span class="text-xs px-2 py-1 bg-gray-200 text-gray-500 rounded-lg">PIR</span>
|
||||
<span class="text-xs px-2 py-1 bg-gray-200 text-gray-500 rounded-lg">SW-420</span>
|
||||
<span class="text-xs px-2 py-1 bg-gray-200 text-gray-500 rounded-lg">Reed Switch</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LED -->
|
||||
<div class="bg-gradient-to-br from-gray-50 to-slate-100 rounded-2xl p-6 border border-gray-200">
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-5">LED Indikator</p>
|
||||
<div class="flex items-center gap-6">
|
||||
<!-- LED Icon -->
|
||||
<div class="flex flex-col items-center gap-3 flex-shrink-0">
|
||||
<!-- LED Merah -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div id="led-red" class="w-8 h-8 rounded-full bg-gray-200 led-off transition-all duration-300 flex items-center justify-center">
|
||||
<div class="w-4 h-4 rounded-full bg-gray-400"></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500 font-medium">MERAH</span>
|
||||
</div>
|
||||
<!-- LED Hijau -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div id="led-green" class="w-8 h-8 rounded-full bg-green-100 transition-all duration-300 flex items-center justify-center" style="box-shadow: 0 0 10px 3px #22c55e;">
|
||||
<div class="w-4 h-4 rounded-full bg-green-500"></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500 font-medium">HIJAU</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<p id="led-status-text" class="text-xl font-display font-bold text-green-600">Aman</p>
|
||||
</div>
|
||||
<p id="led-detail-text" class="text-gray-500 text-sm">LED hijau menyala — sistem normal</p>
|
||||
<div class="mt-3 p-2 bg-white rounded-xl border border-gray-100">
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
<div class="w-3 h-3 rounded-full bg-green-400"></div>
|
||||
<span>Hijau = Aman | </span>
|
||||
<div class="w-3 h-3 rounded-full bg-red-400"></div>
|
||||
<span>Merah = Bahaya</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SENSOR TRIGGER STATUS -->
|
||||
<div class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
|
||||
<!-- PIR -->
|
||||
<div id="card-pir" class="flex items-center gap-4 p-4 rounded-2xl border-2 border-gray-100 bg-white transition-all duration-500">
|
||||
<div id="icon-pir" class="w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center transition-all duration-300">
|
||||
<i data-feather="eye" class="w-6 h-6 text-gray-400"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-700 text-sm">Sensor PIR</p>
|
||||
<p id="status-pir" class="text-xs text-gray-400 mt-0.5">Tidak ada gerakan</p>
|
||||
</div>
|
||||
<div id="dot-pir" class="ml-auto w-3 h-3 rounded-full bg-gray-300"></div>
|
||||
</div>
|
||||
|
||||
<!-- SW-420 -->
|
||||
<div id="card-sw420" class="flex items-center gap-4 p-4 rounded-2xl border-2 border-gray-100 bg-white transition-all duration-500">
|
||||
<div id="icon-sw420" class="w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center transition-all duration-300">
|
||||
<i data-feather="zap" class="w-6 h-6 text-gray-400"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-700 text-sm">Sensor SW-420</p>
|
||||
<p id="status-sw420" class="text-xs text-gray-400 mt-0.5">Tidak ada getaran</p>
|
||||
</div>
|
||||
<div id="dot-sw420" class="ml-auto w-3 h-3 rounded-full bg-gray-300"></div>
|
||||
</div>
|
||||
|
||||
<!-- Reed Switch -->
|
||||
<div id="card-reed" class="flex items-center gap-4 p-4 rounded-2xl border-2 border-gray-100 bg-white transition-all duration-500">
|
||||
<div id="icon-reed" class="w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center transition-all duration-300">
|
||||
<i data-feather="unlock" class="w-6 h-6 text-gray-400"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-700 text-sm">Reed Switch</p>
|
||||
<p id="status-reed" class="text-xs text-gray-400 mt-0.5">Pintu tertutup</p>
|
||||
</div>
|
||||
<div id="dot-reed" class="ml-auto w-3 h-3 rounded-full bg-gray-300"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- STATUS -->
|
||||
<div class="bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg p-8 border border-gray-100">
|
||||
|
||||
<div class="flex items-center gap-3 mb-8">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="activity" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">Status Sistem</h3>
|
||||
<p class="text-gray-600">Kondisi operasional saat ini</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
<div class="group bg-gradient-to-br from-green-50 to-emerald-50 p-6 rounded-2xl border border-green-200 hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-green-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i data-feather="check" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-green-800">Semua sensor aktif</p>
|
||||
<p class="text-green-600 text-sm">Monitoring berjalan normal</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group bg-gradient-to-br from-blue-50 to-indigo-50 p-6 rounded-2xl border border-blue-200 hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i data-feather="radio" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-blue-800">Komunikasi LoRa stabil</p>
|
||||
<p class="text-blue-600 text-sm">Koneksi optimal</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group bg-gradient-to-br from-yellow-50 to-orange-50 p-6 rounded-2xl border border-yellow-200 hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-yellow-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i data-feather="alert-circle" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-yellow-800">Aktivitas terdeteksi hari ini</p>
|
||||
<p class="text-yellow-600 text-sm">2 kejadian tercatat</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group bg-gradient-to-br from-purple-50 to-pink-50 p-6 rounded-2xl border border-purple-200 hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-purple-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i data-feather="clock" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-purple-800">Update terakhir</p>
|
||||
<p class="text-purple-600 text-sm">Update: {{ $lastUpdate ? $lastUpdate->diffForHumans() : 'Belum ada data' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- QUICK ACCESS -->
|
||||
<div class="bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg p-8 border border-gray-100">
|
||||
|
||||
<div class="flex items-center gap-3 mb-8">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="zap" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">Menu Cepat</h3>
|
||||
<p class="text-gray-600">Akses fitur utama sistem</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
|
||||
<a href="/monitoring" class="group bg-gradient-to-br from-blue-500 to-indigo-600 p-8 rounded-2xl text-white text-center font-semibold shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="w-16 h-16 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
|
||||
<i data-feather="activity" class="w-8 h-8"></i>
|
||||
</div>
|
||||
<h4 class="text-lg font-display font-bold mb-2">Monitoring Sensor</h4>
|
||||
<p class="text-blue-100 text-sm">Pantau status sensor realtime</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/log" class="group bg-gradient-to-br from-gray-600 to-slate-700 p-8 rounded-2xl text-white text-center font-semibold shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="w-16 h-16 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
|
||||
<i data-feather="file-text" class="w-8 h-8"></i>
|
||||
</div>
|
||||
<h4 class="text-lg font-display font-bold mb-2">Log Aktivitas</h4>
|
||||
<p class="text-gray-200 text-sm">Riwayat kejadian sistem</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/device" class="group bg-gradient-to-br from-green-500 to-emerald-600 p-8 rounded-2xl text-white text-center font-semibold shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="w-16 h-16 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
|
||||
<i data-feather="cpu" class="w-8 h-8"></i>
|
||||
</div>
|
||||
<h4 class="text-lg font-display font-bold mb-2">Status Device</h4>
|
||||
<p class="text-green-100 text-sm">Kondisi perangkat IoT</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
feather.replace()
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Toggle sidebar mobile
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebar-overlay');
|
||||
const isOpen = !sidebar.classList.contains('-translate-x-full');
|
||||
if (isOpen) {
|
||||
sidebar.classList.add('-translate-x-full');
|
||||
overlay.classList.add('hidden');
|
||||
} else {
|
||||
sidebar.classList.remove('-translate-x-full');
|
||||
overlay.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Auto refresh halaman setiap 30 detik untuk update alert
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 30000);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// =============================================
|
||||
// BUZZER & LED REALTIME POLLING
|
||||
// =============================================
|
||||
|
||||
let alertDismissed = false;
|
||||
let pollingInterval = null;
|
||||
|
||||
// Threshold waktu deteksi dianggap "aktif" (dalam detik)
|
||||
const DETECTION_WINDOW = 30;
|
||||
|
||||
async function fetchSensorStatus() {
|
||||
try {
|
||||
const [pirRes, vibRes, reedRes] = await Promise.all([
|
||||
fetch('/api/pir/readings?limit=1').then(r => r.json()).catch(() => null),
|
||||
fetch('/api/vibration/readings?limit=1').then(r => r.json()).catch(() => null),
|
||||
fetch('/api/door-access/readings?limit=1').then(r => r.json()).catch(() => null),
|
||||
]);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Cek PIR
|
||||
let pirActive = false;
|
||||
if (pirRes?.success && pirRes.data?.length > 0) {
|
||||
const latest = pirRes.data[0];
|
||||
const age = (now - new Date(latest.recorded_at).getTime()) / 1000;
|
||||
pirActive = latest.motion_detected && age <= DETECTION_WINDOW;
|
||||
}
|
||||
|
||||
// Cek Vibration (SW-420)
|
||||
let sw420Active = false;
|
||||
if (vibRes?.success && vibRes.data?.length > 0) {
|
||||
const latest = vibRes.data[0];
|
||||
const age = (now - new Date(latest.recorded_at).getTime()) / 1000;
|
||||
sw420Active = latest.is_abnormal && age <= DETECTION_WINDOW;
|
||||
}
|
||||
|
||||
// Cek Reed Switch
|
||||
let reedActive = false;
|
||||
if (reedRes?.success && reedRes.data?.length > 0) {
|
||||
const latest = reedRes.data[0];
|
||||
const age = (now - new Date(latest.recorded_at).getTime()) / 1000;
|
||||
reedActive = latest.door_open && age <= DETECTION_WINDOW;
|
||||
}
|
||||
|
||||
const anyActive = pirActive || sw420Active || reedActive;
|
||||
|
||||
updateUI(pirActive, sw420Active, reedActive, anyActive);
|
||||
updateLastUpdateText();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Polling error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUI(pirActive, sw420Active, reedActive, anyActive) {
|
||||
// --- SENSOR CARDS ---
|
||||
updateSensorCard('pir', pirActive, 'Gerakan terdeteksi!', 'Tidak ada gerakan');
|
||||
updateSensorCard('sw420', sw420Active, 'Getaran terdeteksi!', 'Tidak ada getaran');
|
||||
updateSensorCard('reed', reedActive, 'Pintu terbuka!', 'Pintu tertutup');
|
||||
|
||||
// --- BUZZER ---
|
||||
const buzzerIcon = document.getElementById('buzzer-icon');
|
||||
const buzzerDot = document.getElementById('buzzer-status-dot');
|
||||
const buzzerText = document.getElementById('buzzer-status-text');
|
||||
const buzzerTrigger = document.getElementById('buzzer-trigger-text');
|
||||
const buzzerIconInner = document.getElementById('buzzer-icon-inner');
|
||||
|
||||
if (anyActive) {
|
||||
buzzerIcon.className = 'relative w-16 h-16 bg-red-500 rounded-full flex items-center justify-center transition-all duration-300 buzzer-active';
|
||||
buzzerDot.className = 'w-4 h-4 rounded-full bg-red-500 animate-pulse transition-all duration-300';
|
||||
buzzerText.className = 'text-xl font-display font-bold text-red-600';
|
||||
buzzerText.textContent = 'BERBUNYI!';
|
||||
buzzerIconInner.setAttribute('data-feather', 'volume-2');
|
||||
|
||||
const triggers = [];
|
||||
if (pirActive) triggers.push('PIR');
|
||||
if (sw420Active) triggers.push('SW-420');
|
||||
if (reedActive) triggers.push('Reed Switch');
|
||||
buzzerTrigger.textContent = 'Dipicu oleh: ' + triggers.join(', ');
|
||||
|
||||
// Update trigger badges
|
||||
document.getElementById('buzzer-triggers').innerHTML = triggers.map(t =>
|
||||
`<span class="text-xs px-2 py-1 bg-red-100 text-red-600 rounded-lg font-semibold animate-pulse">${t}</span>`
|
||||
).join('');
|
||||
} else {
|
||||
buzzerIcon.className = 'relative w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center transition-all duration-300';
|
||||
buzzerDot.className = 'w-4 h-4 rounded-full bg-gray-300 transition-all duration-300';
|
||||
buzzerText.className = 'text-xl font-display font-bold text-gray-400';
|
||||
buzzerText.textContent = 'Tidak Aktif';
|
||||
buzzerIconInner.setAttribute('data-feather', 'volume-x');
|
||||
buzzerTrigger.textContent = 'Menunggu deteksi sensor...';
|
||||
document.getElementById('buzzer-triggers').innerHTML = `
|
||||
<span class="text-xs px-2 py-1 bg-gray-200 text-gray-500 rounded-lg">PIR</span>
|
||||
<span class="text-xs px-2 py-1 bg-gray-200 text-gray-500 rounded-lg">SW-420</span>
|
||||
<span class="text-xs px-2 py-1 bg-gray-200 text-gray-500 rounded-lg">Reed Switch</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// --- LED ---
|
||||
const ledRed = document.getElementById('led-red');
|
||||
const ledGreen = document.getElementById('led-green');
|
||||
const ledStatusText = document.getElementById('led-status-text');
|
||||
const ledDetailText = document.getElementById('led-detail-text');
|
||||
|
||||
if (anyActive) {
|
||||
// LED Merah menyala, hijau mati
|
||||
ledRed.className = 'w-8 h-8 rounded-full bg-red-100 transition-all duration-300 flex items-center justify-center';
|
||||
ledRed.style.boxShadow = '0 0 14px 5px #ef4444';
|
||||
ledRed.innerHTML = '<div class="w-4 h-4 rounded-full bg-red-500 animate-pulse"></div>';
|
||||
|
||||
ledGreen.className = 'w-8 h-8 rounded-full bg-gray-100 transition-all duration-300 flex items-center justify-center';
|
||||
ledGreen.style.boxShadow = 'none';
|
||||
ledGreen.innerHTML = '<div class="w-4 h-4 rounded-full bg-gray-300"></div>';
|
||||
|
||||
ledStatusText.className = 'text-xl font-display font-bold text-red-600';
|
||||
ledStatusText.textContent = 'BAHAYA!';
|
||||
ledDetailText.textContent = 'LED merah menyala — aktivitas mencurigakan terdeteksi';
|
||||
} else {
|
||||
// LED Hijau menyala, merah mati
|
||||
ledRed.className = 'w-8 h-8 rounded-full bg-gray-100 transition-all duration-300 flex items-center justify-center';
|
||||
ledRed.style.boxShadow = 'none';
|
||||
ledRed.innerHTML = '<div class="w-4 h-4 rounded-full bg-gray-300"></div>';
|
||||
|
||||
ledGreen.className = 'w-8 h-8 rounded-full bg-green-100 transition-all duration-300 flex items-center justify-center';
|
||||
ledGreen.style.boxShadow = '0 0 10px 3px #22c55e';
|
||||
ledGreen.innerHTML = '<div class="w-4 h-4 rounded-full bg-green-500"></div>';
|
||||
|
||||
ledStatusText.className = 'text-xl font-display font-bold text-green-600';
|
||||
ledStatusText.textContent = 'Aman';
|
||||
ledDetailText.textContent = 'LED hijau menyala — sistem normal';
|
||||
}
|
||||
|
||||
// --- ALERT BANNER ---
|
||||
const banner = document.getElementById('alert-banner');
|
||||
if (anyActive && !alertDismissed) {
|
||||
const msgs = [];
|
||||
if (pirActive) msgs.push('gerakan (PIR)');
|
||||
if (sw420Active) msgs.push('getaran (SW-420)');
|
||||
if (reedActive) msgs.push('pintu terbuka (Reed Switch)');
|
||||
document.getElementById('alert-banner-text').textContent =
|
||||
'Terdeteksi: ' + msgs.join(', ') + ' — Buzzer & LED merah aktif!';
|
||||
banner.classList.remove('hidden');
|
||||
} else if (!anyActive) {
|
||||
banner.classList.add('hidden');
|
||||
alertDismissed = false;
|
||||
}
|
||||
|
||||
// Re-render feather icons
|
||||
feather.replace();
|
||||
}
|
||||
|
||||
function updateSensorCard(id, active, activeText, inactiveText) {
|
||||
const card = document.getElementById('card-' + id);
|
||||
const icon = document.getElementById('icon-' + id);
|
||||
const status = document.getElementById('status-' + id);
|
||||
const dot = document.getElementById('dot-' + id);
|
||||
|
||||
if (active) {
|
||||
card.className = 'flex items-center gap-4 p-4 rounded-2xl border-2 border-red-300 bg-red-50 transition-all duration-500';
|
||||
icon.className = 'w-12 h-12 bg-red-500 rounded-xl flex items-center justify-center transition-all duration-300';
|
||||
icon.querySelector('i').className = icon.querySelector('i').className.replace('text-gray-400', 'text-white');
|
||||
status.textContent = activeText;
|
||||
status.className = 'text-xs text-red-600 font-semibold mt-0.5';
|
||||
dot.className = 'ml-auto w-3 h-3 rounded-full bg-red-500 animate-pulse';
|
||||
} else {
|
||||
card.className = 'flex items-center gap-4 p-4 rounded-2xl border-2 border-gray-100 bg-white transition-all duration-500';
|
||||
icon.className = 'w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center transition-all duration-300';
|
||||
status.textContent = inactiveText;
|
||||
status.className = 'text-xs text-gray-400 mt-0.5';
|
||||
dot.className = 'ml-auto w-3 h-3 rounded-full bg-gray-300';
|
||||
}
|
||||
}
|
||||
|
||||
function updateLastUpdateText() {
|
||||
const now = new Date();
|
||||
const timeStr = now.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
document.getElementById('last-update-text').textContent = 'Update: ' + timeStr;
|
||||
}
|
||||
|
||||
function dismissAlert() {
|
||||
alertDismissed = true;
|
||||
document.getElementById('alert-banner').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Mulai polling setiap 5 detik
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
fetchSensorStatus();
|
||||
pollingInterval = setInterval(fetchSensorStatus, 5000);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Status Device - Smart Rack Security</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/feather-icons"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.font-display { font-family: 'Space Grotesk', sans-serif; }
|
||||
.glass { backdrop-filter: blur(16px); background: rgba(255, 255, 255, 0.9); border: 1px solid rgba(255, 255, 255, 0.2); }
|
||||
.animate-pulse-slow { animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 min-h-screen p-4 md:p-6 font-sans overflow-x-hidden">
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center shadow-lg">
|
||||
<i data-feather="cpu" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl md:text-4xl font-display font-bold text-gray-800">Status Device IoT</h1>
|
||||
<p class="text-gray-600 mt-0.5 text-sm">Kondisi perangkat dan konektivitas sistem</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 w-full sm:w-auto">
|
||||
<div class="flex items-center gap-2 bg-gradient-to-r from-green-500/10 to-emerald-500/10 px-3 py-2 rounded-xl border border-green-200">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse-slow"></div>
|
||||
<span class="text-green-700 font-semibold text-xs">System Online</span>
|
||||
</div>
|
||||
|
||||
<a href="/dashboard" class="ml-auto sm:ml-0 group bg-gradient-to-r from-indigo-600 to-purple-600 text-white px-4 py-2 md:px-6 md:py-3 rounded-xl font-semibold hover:shadow-lg hover:scale-105 transition-all duration-300 flex items-center gap-2">
|
||||
<i data-feather="arrow-left" class="w-4 h-4 group-hover:-translate-x-1 transition-transform"></i>
|
||||
<span class="text-sm md:text-base">Dashboard</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- DEVICE GRID -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
|
||||
<!-- DEVICE A -->
|
||||
<div class="group bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-500 p-8 border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-green-50 to-emerald-50 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative z-10">
|
||||
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-2xl flex items-center justify-center group-hover:scale-110 group-hover:rotate-6 transition-all duration-500 shadow-lg">
|
||||
<i data-feather="box" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">Device Rak A</h3>
|
||||
<p class="text-gray-600 text-sm">IoT Sensor Node</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end">
|
||||
<span class="bg-gradient-to-r from-green-500/10 to-emerald-500/10 text-green-700 px-4 py-2 rounded-xl text-sm font-semibold border border-green-200">
|
||||
Online
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<p class="text-3xl font-display font-bold text-green-600 mb-2">Terhubung</p>
|
||||
<p class="text-gray-600 leading-relaxed">Device aktif dan mengirim data sensor secara realtime dengan koneksi yang stabil.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-4 border-t border-gray-100">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-500">Signal Strength:</span>
|
||||
<span class="text-green-600 font-semibold">95%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- LORA -->
|
||||
<div class="group bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-500 p-8 border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-blue-50 to-indigo-50 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative z-10">
|
||||
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-2xl flex items-center justify-center group-hover:scale-110 group-hover:rotate-6 transition-all duration-500 shadow-lg">
|
||||
<i data-feather="radio" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">LoRa Gateway</h3>
|
||||
<p class="text-gray-600 text-sm">Communication Hub</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end">
|
||||
<span class="bg-gradient-to-r from-green-500/10 to-emerald-500/10 text-green-700 px-4 py-2 rounded-xl text-sm font-semibold border border-green-200">
|
||||
Aktif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<p class="text-3xl font-display font-bold text-green-600 mb-2">Terhubung</p>
|
||||
<p class="text-gray-600 leading-relaxed">Gateway menerima data dari device sensor dengan jangkauan komunikasi optimal.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-4 border-t border-gray-100">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-500">Range:</span>
|
||||
<span class="text-blue-600 font-semibold">5 km</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SERVER -->
|
||||
<div class="group bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-500 p-8 border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-50 to-pink-50 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative z-10">
|
||||
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-600 rounded-2xl flex items-center justify-center group-hover:scale-110 group-hover:rotate-6 transition-all duration-500 shadow-lg">
|
||||
<i data-feather="server" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">Koneksi Server</h3>
|
||||
<p class="text-gray-600 text-sm">Data Processing</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end">
|
||||
<span class="bg-gradient-to-r from-green-500/10 to-emerald-500/10 text-green-700 px-4 py-2 rounded-xl text-sm font-semibold border border-green-200">
|
||||
Stabil
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<p class="text-3xl font-display font-bold text-green-600 mb-2">Terhubung</p>
|
||||
<p class="text-gray-600 leading-relaxed">Server menerima dan memproses data monitoring secara realtime dengan performa optimal.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-4 border-t border-gray-100">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-gray-500">Uptime:</span>
|
||||
<span class="text-purple-600 font-semibold">99.9%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SYSTEM INFO -->
|
||||
<div class="mt-12 bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg p-8 border border-gray-100">
|
||||
|
||||
<div class="flex items-center gap-3 mb-8">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-emerald-600 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="settings" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">Status Sistem</h3>
|
||||
<p class="text-gray-600">Kondisi operasional perangkat IoT</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
|
||||
<div class="group bg-gradient-to-br from-green-50 to-emerald-50 p-6 rounded-2xl border border-green-200 hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-green-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i data-feather="check-circle" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-green-800">Semua device aktif</p>
|
||||
<p class="text-green-600 text-sm">3/3 perangkat online</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group bg-gradient-to-br from-blue-50 to-indigo-50 p-6 rounded-2xl border border-blue-200 hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i data-feather="radio" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-blue-800">Komunikasi LoRa berjalan baik</p>
|
||||
<p class="text-blue-600 text-sm">Koneksi stabil</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group bg-gradient-to-br from-purple-50 to-pink-50 p-6 rounded-2xl border border-purple-200 hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-purple-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i data-feather="clock" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-purple-800">Update status</p>
|
||||
<p class="text-purple-600 text-sm">5 detik lalu</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
feather.replace()
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Smart Rack Security - Sistem Monitoring IoT</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/feather-icons"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.font-display { font-family: 'Space Grotesk', sans-serif; }
|
||||
.gradient-text { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
.glass { backdrop-filter: blur(16px); background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); }
|
||||
.animate-float { animation: float 6s ease-in-out infinite; }
|
||||
@keyframes float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-20px); } }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 font-sans text-gray-800 overflow-x-hidden">
|
||||
|
||||
<!-- NAVBAR -->
|
||||
<nav class="glass fixed top-0 w-full z-50 border-b border-white/10">
|
||||
<div class="max-w-7xl mx-auto flex justify-between items-center px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<i data-feather="shield" class="w-5 h-5 text-white"></i>
|
||||
</div>
|
||||
<h1 class="text-xl font-display font-bold text-gray-800">Smart Rack Security</h1>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center gap-8">
|
||||
<a href="/home" class="text-gray-700 hover:text-indigo-600 transition-colors font-medium">Home</a>
|
||||
<a href="#fitur" class="text-gray-700 hover:text-indigo-600 transition-colors font-medium">Sensor</a>
|
||||
<a href="#tentang" class="text-gray-700 hover:text-indigo-600 transition-colors font-medium">Tentang</a>
|
||||
<a href="/login" class="bg-gradient-to-r from-indigo-600 to-purple-600 text-white px-6 py-2.5 rounded-xl hover:shadow-lg hover:scale-105 transition-all duration-300 font-medium">
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button class="md:hidden p-2 rounded-lg hover:bg-white/20 transition-colors">
|
||||
<i data-feather="menu" class="w-6 h-6 text-gray-800"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="relative min-h-screen flex items-center justify-center overflow-hidden pt-20">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-indigo-600 via-purple-700 to-blue-800"></div>
|
||||
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.05"%3E%3Ccircle cx="30" cy="30" r="2"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')] opacity-20"></div>
|
||||
|
||||
<div class="relative max-w-7xl mx-auto text-center px-6 z-10">
|
||||
<div class="animate-float mb-8">
|
||||
<div class="w-20 h-20 bg-white/10 backdrop-blur-sm rounded-2xl flex items-center justify-center mx-auto mb-6 border border-white/20">
|
||||
<i data-feather="shield" class="w-10 h-10 text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="text-6xl md:text-7xl lg:text-8xl font-display font-bold text-white leading-tight mb-6">
|
||||
Sistem Monitoring
|
||||
<span class="block gradient-text bg-gradient-to-r from-blue-200 to-purple-200 bg-clip-text text-transparent">
|
||||
Keamanan IoT
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p class="text-xl md:text-2xl text-blue-100 max-w-3xl mx-auto mb-12 leading-relaxed">
|
||||
Platform monitoring keamanan rak berbasis Internet of Things dengan sensor PIR, getaran SW-420, reed switch, dan komunikasi LoRa untuk pengawasan realtime.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-center gap-6 mb-16">
|
||||
<a href="/dashboard" class="group bg-white text-indigo-600 px-8 py-4 rounded-2xl font-semibold hover:shadow-2xl hover:scale-105 transition-all duration-300 flex items-center justify-center gap-3">
|
||||
<span>Masuk Dashboard</span>
|
||||
<i data-feather="arrow-right" class="w-5 h-5 group-hover:translate-x-1 transition-transform"></i>
|
||||
</a>
|
||||
|
||||
<a href="#fitur" class="glass text-white px-8 py-4 rounded-2xl font-semibold hover:bg-white/20 transition-all duration-300 flex items-center justify-center gap-3">
|
||||
<span>Pelajari Sistem</span>
|
||||
<i data-feather="chevron-down" class="w-5 h-5"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Floating Elements -->
|
||||
<div class="absolute top-20 left-10 w-4 h-4 bg-blue-400 rounded-full animate-pulse opacity-60"></div>
|
||||
<div class="absolute top-40 right-20 w-6 h-6 bg-purple-400 rounded-full animate-bounce opacity-40"></div>
|
||||
<div class="absolute bottom-20 left-20 w-3 h-3 bg-indigo-300 rounded-full animate-ping opacity-50"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- STATISTIK -->
|
||||
<section class="py-20 bg-white relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-blue-50 to-indigo-50 opacity-50"></div>
|
||||
|
||||
<div class="relative max-w-7xl mx-auto px-6">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl font-display font-bold text-gray-800 mb-4">Sistem Terdepan</h2>
|
||||
<p class="text-xl text-gray-600">Teknologi monitoring keamanan yang dapat diandalkan</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||
<div class="group text-center p-8 bg-white rounded-2xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 border border-gray-100">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
|
||||
<i data-feather="layers" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-3xl font-display font-bold text-indigo-600 mb-2">4</h3>
|
||||
<p class="text-gray-600 font-medium">Sensor Sistem</p>
|
||||
</div>
|
||||
|
||||
<div class="group text-center p-8 bg-white rounded-2xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 border border-gray-100">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
|
||||
<i data-feather="zap" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-3xl font-display font-bold text-green-600 mb-2">Realtime</h3>
|
||||
<p class="text-gray-600 font-medium">Monitoring</p>
|
||||
</div>
|
||||
|
||||
<div class="group text-center p-8 bg-white rounded-2xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 border border-gray-100">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-cyan-600 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
|
||||
<i data-feather="radio" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-3xl font-display font-bold text-blue-600 mb-2">LoRa</h3>
|
||||
<p class="text-gray-600 font-medium">Komunikasi</p>
|
||||
</div>
|
||||
|
||||
<div class="group text-center p-8 bg-white rounded-2xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 border border-gray-100">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-600 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
|
||||
<i data-feather="clock" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-3xl font-display font-bold text-purple-600 mb-2">6 Jam</h3>
|
||||
<p class="text-gray-600 font-medium">Keamanan</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SENSOR -->
|
||||
<section id="fitur" class="py-24 bg-gradient-to-br from-gray-50 to-blue-50 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="%23f1f5f9" fill-opacity="0.4"%3E%3Cpath d="M20 20c0-5.5-4.5-10-10-10s-10 4.5-10 10 4.5 10 10 10 10-4.5 10-10zm10 0c0-5.5-4.5-10-10-10s-10 4.5-10 10 4.5 10 10 10 10-4.5 10-10z"/%3E%3C/g%3E%3C/svg%3E')] opacity-30"></div>
|
||||
|
||||
<div class="relative max-w-7xl mx-auto px-6">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl md:text-5xl font-display font-bold text-gray-800 mb-4">Sensor Monitoring Sistem</h2>
|
||||
<p class="text-xl text-gray-600 max-w-2xl mx-auto">Teknologi sensor terdepan untuk monitoring keamanan yang komprehensif</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<!-- PIR -->
|
||||
<div class="group bg-white p-8 rounded-3xl shadow-lg hover:shadow-2xl hover:-translate-y-4 transition-all duration-500 text-center border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-indigo-50 to-purple-50 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="w-20 h-20 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:scale-110 group-hover:rotate-6 transition-all duration-500 shadow-lg">
|
||||
<i data-feather="activity" class="w-10 h-10 text-white"></i>
|
||||
</div>
|
||||
<h4 class="font-display font-bold text-xl mb-4 text-gray-800">Sensor PIR</h4>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Mendeteksi pergerakan manusia di sekitar rak untuk mengidentifikasi aktivitas mencurigakan dengan akurasi tinggi.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GETAR -->
|
||||
<div class="group bg-white p-8 rounded-3xl shadow-lg hover:shadow-2xl hover:-translate-y-4 transition-all duration-500 text-center border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-blue-50 to-cyan-50 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="w-20 h-20 bg-gradient-to-br from-blue-500 to-cyan-600 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:scale-110 group-hover:rotate-6 transition-all duration-500 shadow-lg">
|
||||
<i data-feather="zap" class="w-10 h-10 text-white"></i>
|
||||
</div>
|
||||
<h4 class="font-display font-bold text-xl mb-4 text-gray-800">Sensor Getar</h4>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Sensor SW-420 mendeteksi getaran ketika rak digeser, didorong, atau disentuh dengan sensitivitas optimal.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REED -->
|
||||
<div class="group bg-white p-8 rounded-3xl shadow-lg hover:shadow-2xl hover:-translate-y-4 transition-all duration-500 text-center border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-green-50 to-emerald-50 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="w-20 h-20 bg-gradient-to-br from-green-500 to-emerald-600 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:scale-110 group-hover:rotate-6 transition-all duration-500 shadow-lg">
|
||||
<i data-feather="unlock" class="w-10 h-10 text-white"></i>
|
||||
</div>
|
||||
<h4 class="font-display font-bold text-xl mb-4 text-gray-800">Reed Switch</h4>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Mengetahui kondisi rak terbuka atau tertutup secara otomatis dengan respons yang cepat dan akurat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LORA -->
|
||||
<div class="group bg-white p-8 rounded-3xl shadow-lg hover:shadow-2xl hover:-translate-y-4 transition-all duration-500 text-center border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-50 to-pink-50 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="w-20 h-20 bg-gradient-to-br from-purple-500 to-pink-600 rounded-2xl flex items-center justify-center mx-auto mb-6 group-hover:scale-110 group-hover:rotate-6 transition-all duration-500 shadow-lg">
|
||||
<i data-feather="radio" class="w-10 h-10 text-white"></i>
|
||||
</div>
|
||||
<h4 class="font-display font-bold text-xl mb-4 text-gray-800">Komunikasi LoRa</h4>
|
||||
<p class="text-gray-600 leading-relaxed">
|
||||
Mengirim data sensor ke server monitoring dengan jangkauan komunikasi jarak jauh dan konsumsi daya rendah.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TENTANG -->
|
||||
<section id="tentang" class="py-24 bg-white relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-indigo-50 to-purple-50 opacity-30"></div>
|
||||
|
||||
<div class="relative max-w-5xl mx-auto text-center px-6">
|
||||
<div class="mb-16">
|
||||
<h2 class="text-4xl md:text-5xl font-display font-bold text-gray-800 mb-6">Tentang Sistem</h2>
|
||||
<div class="w-24 h-1 bg-gradient-to-r from-indigo-500 to-purple-600 mx-auto rounded-full mb-8"></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/80 backdrop-blur-sm rounded-3xl p-12 shadow-2xl border border-gray-100">
|
||||
<p class="text-xl text-gray-700 leading-relaxed mb-8">
|
||||
Smart Rack Security merupakan sistem keamanan berbasis <span class="font-semibold text-indigo-600">Internet of Things</span> yang
|
||||
dirancang untuk meningkatkan pengawasan barang pada rak penyimpanan dengan teknologi terdepan.
|
||||
</p>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-8 mt-12">
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<i data-feather="cpu" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h4 class="font-display font-semibold text-lg text-gray-800 mb-2">Sensor Terintegrasi</h4>
|
||||
<p class="text-gray-600 text-sm">PIR, SW-420, Reed Switch</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-blue-500 to-cyan-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<i data-feather="zap" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h4 class="font-display font-semibold text-lg text-gray-800 mb-2">Monitoring Realtime</h4>
|
||||
<p class="text-gray-600 text-sm">Pengawasan otomatis saat toko buka</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<i data-feather="radio" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h4 class="font-display font-semibold text-lg text-gray-800 mb-2">Komunikasi LoRa</h4>
|
||||
<p class="text-gray-600 text-sm">Jangkauan jauh, daya rendah</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="py-24 bg-gradient-to-br from-indigo-600 via-purple-700 to-blue-800 text-white text-center relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.05"%3E%3Ccircle cx="30" cy="30" r="2"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')] opacity-20"></div>
|
||||
|
||||
<div class="relative max-w-4xl mx-auto px-6">
|
||||
<div class="mb-8">
|
||||
<div class="w-16 h-16 bg-white/10 backdrop-blur-sm rounded-2xl flex items-center justify-center mx-auto mb-6 border border-white/20">
|
||||
<i data-feather="monitor" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<h2 class="text-4xl md:text-5xl font-display font-bold mb-6">Mulai Monitoring Sekarang</h2>
|
||||
<p class="text-xl text-blue-100 max-w-2xl mx-auto mb-10">
|
||||
Akses dashboard monitoring untuk mengawasi keamanan rak Anda secara realtime dengan teknologi IoT terdepan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row justify-center gap-6">
|
||||
<a href="/dashboard" class="group bg-white text-indigo-600 px-10 py-4 rounded-2xl font-semibold hover:shadow-2xl hover:scale-105 transition-all duration-300 flex items-center justify-center gap-3">
|
||||
<span>Buka Dashboard</span>
|
||||
<i data-feather="arrow-right" class="w-5 h-5 group-hover:translate-x-1 transition-transform"></i>
|
||||
</a>
|
||||
|
||||
<a href="/login" class="glass text-white px-10 py-4 rounded-2xl font-semibold hover:bg-white/20 transition-all duration-300 flex items-center justify-center gap-3">
|
||||
<span>Login Admin</span>
|
||||
<i data-feather="user" class="w-5 h-5"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer class="bg-gradient-to-br from-slate-900 via-gray-900 to-slate-800 text-gray-300 py-16 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="%23ffffff" fill-opacity="0.02"%3E%3Cpath d="M20 20c0-5.5-4.5-10-10-10s-10 4.5-10 10 4.5 10 10 10 10-4.5 10-10zm10 0c0-5.5-4.5-10-10-10s-10 4.5-10 10 4.5 10 10 10 10-4.5 10-10z"/%3E%3C/g%3E%3C/svg%3E')] opacity-30"></div>
|
||||
|
||||
<div class="relative max-w-7xl mx-auto px-6">
|
||||
<div class="text-center mb-12">
|
||||
<div class="flex items-center justify-center gap-3 mb-6">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<i data-feather="shield" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-display font-bold text-white">Smart Rack Security</h3>
|
||||
</div>
|
||||
<p class="text-gray-400 max-w-2xl mx-auto leading-relaxed">
|
||||
Sistem monitoring keamanan rak berbasis IoT dengan teknologi sensor terdepan untuk pengawasan realtime yang dapat diandalkan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-8 mb-12">
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<i data-feather="cpu" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<h4 class="font-semibold text-white mb-2">Teknologi IoT</h4>
|
||||
<p class="text-gray-400 text-sm">Sensor terintegrasi dengan komunikasi LoRa</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-blue-500 to-cyan-600 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<i data-feather="monitor" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<h4 class="font-semibold text-white mb-2">Dashboard Modern</h4>
|
||||
<p class="text-gray-400 text-sm">Interface monitoring yang intuitif</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-emerald-600 rounded-xl flex items-center justify-center mx-auto mb-4">
|
||||
<i data-feather="shield-check" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<h4 class="font-semibold text-white mb-2">Keamanan Saat Toko Buka</h4>
|
||||
<p class="text-gray-400 text-sm">Pengawasan otomatis selama 6 jam operasional</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-700 pt-8 text-center">
|
||||
<p class="text-gray-400 text-sm">
|
||||
© 2026 Smart Rack Security System. Monitoring Keamanan Rak Barang dengan Teknologi IoT.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
feather.replace()
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Log Aktivitas - Smart Rack Security</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/feather-icons"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.font-display { font-family: 'Space Grotesk', sans-serif; }
|
||||
.glass { backdrop-filter: blur(16px); background: rgba(255, 255, 255, 0.9); border: 1px solid rgba(255, 255, 255, 0.2); }
|
||||
.animate-pulse-slow { animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 min-h-screen p-4 md:p-6 font-sans overflow-x-hidden">
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
|
||||
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center shadow-lg">
|
||||
<i data-feather="file-text" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl md:text-4xl font-display font-bold text-gray-800">Log Aktivitas</h1>
|
||||
<p class="text-gray-600 mt-0.5 text-sm">Riwayat kejadian dan aktivitas sensor</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 w-full sm:w-auto">
|
||||
<div class="flex items-center gap-2 bg-gradient-to-r from-blue-500/10 to-indigo-500/10 px-3 py-2 rounded-xl border border-blue-200">
|
||||
<div class="w-2 h-2 bg-blue-500 rounded-full animate-pulse-slow"></div>
|
||||
<span class="text-blue-700 font-semibold text-xs">Live Updates</span>
|
||||
</div>
|
||||
|
||||
<a href="/dashboard" class="ml-auto sm:ml-0 group bg-gradient-to-r from-indigo-600 to-purple-600 text-white px-4 py-2 md:px-6 md:py-3 rounded-xl font-semibold hover:shadow-lg hover:scale-105 transition-all duration-300 flex items-center gap-2">
|
||||
<i data-feather="arrow-left" class="w-4 h-4 group-hover:-translate-x-1 transition-transform"></i>
|
||||
<span class="text-sm md:text-base">Dashboard</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- LOG TABLE -->
|
||||
<div class="bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg overflow-hidden border border-gray-100">
|
||||
|
||||
<div class="p-8 border-b border-gray-100">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="list" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">Riwayat Aktivitas</h3>
|
||||
<p class="text-gray-600">Total {{ $totalLog }} log tercatat</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Statistik ringkas -->
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
<span class="px-3 py-1 bg-blue-100 text-blue-700 rounded-xl text-sm font-semibold">Hari ini: {{ $logHariIni }}</span>
|
||||
<span class="px-3 py-1 bg-yellow-100 text-yellow-700 rounded-xl text-sm font-semibold">Warning: {{ $logWarning }}</span>
|
||||
<span class="px-3 py-1 bg-red-100 text-red-700 rounded-xl text-sm font-semibold">Critical: {{ $logCritical }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto hidden md:block">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gradient-to-r from-slate-50 to-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">Waktu</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">Sensor</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
|
||||
@forelse($logs as $log)
|
||||
@php
|
||||
$severityColor = match($log->severity) {
|
||||
'warning' => ['row' => 'hover:from-yellow-50 hover:to-orange-50', 'icon' => 'bg-yellow-100', 'text' => 'text-yellow-600', 'badge' => 'from-yellow-500/10 to-orange-500/10 text-yellow-700 border-yellow-200', 'dot' => 'bg-yellow-500'],
|
||||
'critical' => ['row' => 'hover:from-red-50 hover:to-pink-50', 'icon' => 'bg-red-100', 'text' => 'text-red-600', 'badge' => 'from-red-500/10 to-pink-500/10 text-red-700 border-red-200', 'dot' => 'bg-red-500'],
|
||||
default => ['row' => 'hover:from-green-50 hover:to-emerald-50', 'icon' => 'bg-green-100', 'text' => 'text-green-600', 'badge' => 'from-green-500/10 to-emerald-500/10 text-green-700 border-green-200', 'dot' => 'bg-green-500'],
|
||||
};
|
||||
$sensorIcon = match($log->sensor?->type ?? '') {
|
||||
'pir' => 'activity',
|
||||
'vibration' => 'zap',
|
||||
'reed_switch' => 'unlock',
|
||||
default => 'cpu',
|
||||
};
|
||||
@endphp
|
||||
<tr class="group hover:bg-gradient-to-r {{ $severityColor['row'] }} transition-all duration-300">
|
||||
<td class="px-6 py-5">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 {{ $severityColor['icon'] }} rounded-xl flex items-center justify-center">
|
||||
<i data-feather="clock" class="w-5 h-5 {{ $severityColor['text'] }}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-800">{{ $log->event_time->format('H:i') }}</p>
|
||||
<p class="text-sm text-gray-500">{{ $log->event_time->format('d M Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-5">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-indigo-100 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="{{ $sensorIcon }}" class="w-5 h-5 text-indigo-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-800">{{ $log->sensor?->name ?? $log->device?->name ?? 'Sistem' }}</p>
|
||||
<p class="text-sm text-gray-500">{{ $log->location ?? '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-5">
|
||||
<span class="inline-flex items-center gap-2 bg-gradient-to-r {{ $severityColor['badge'] }} px-4 py-2 rounded-xl text-sm font-semibold border">
|
||||
<div class="w-2 h-2 {{ $severityColor['dot'] }} rounded-full"></div>
|
||||
{{ $log->event_type_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-5">
|
||||
<p class="text-gray-600 text-sm">{{ $log->description }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-8 py-16 text-center">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<div class="w-16 h-16 bg-gray-100 rounded-2xl flex items-center justify-center">
|
||||
<i data-feather="inbox" class="w-8 h-8 text-gray-400"></i>
|
||||
</div>
|
||||
<p class="text-gray-500 font-medium">Belum ada log aktivitas</p>
|
||||
<p class="text-gray-400 text-sm">Log akan muncul saat sensor mendeteksi aktivitas</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- CARD VIEW untuk mobile -->
|
||||
<div class="md:hidden divide-y divide-gray-100">
|
||||
@forelse($logs as $log)
|
||||
@php
|
||||
$severityColor = match($log->severity) {
|
||||
'warning' => ['bg' => 'bg-yellow-50 border-yellow-200', 'badge' => 'bg-yellow-100 text-yellow-700', 'dot' => 'bg-yellow-500'],
|
||||
'critical' => ['bg' => 'bg-red-50 border-red-200', 'badge' => 'bg-red-100 text-red-700', 'dot' => 'bg-red-500'],
|
||||
default => ['bg' => 'bg-green-50 border-green-200', 'badge' => 'bg-green-100 text-green-700', 'dot' => 'bg-green-500'],
|
||||
};
|
||||
$sensorIcon = match($log->sensor?->type ?? '') {
|
||||
'pir' => 'activity',
|
||||
'vibration' => 'zap',
|
||||
'reed_switch' => 'unlock',
|
||||
default => 'cpu',
|
||||
};
|
||||
@endphp
|
||||
<div class="p-4 {{ $severityColor['bg'] }} border-l-4">
|
||||
<div class="flex items-start justify-between gap-3 mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 {{ $severityColor['dot'] }} rounded-full mt-1 flex-shrink-0"></div>
|
||||
<p class="font-semibold text-gray-800 text-sm">{{ $log->event_type_display }}</p>
|
||||
</div>
|
||||
<span class="text-xs {{ $severityColor['badge'] }} px-2 py-1 rounded-lg font-semibold flex-shrink-0">
|
||||
{{ $log->event_time->format('H:i') }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-600 text-xs mb-1">{{ $log->description }}</p>
|
||||
<p class="text-gray-400 text-xs">
|
||||
{{ $log->sensor?->name ?? $log->device?->name ?? 'Sistem' }} · {{ $log->event_time->format('d M Y') }}
|
||||
</p>
|
||||
</div>
|
||||
@empty
|
||||
<div class="p-8 text-center">
|
||||
<i data-feather="inbox" class="w-8 h-8 text-gray-400 mx-auto mb-2"></i>
|
||||
<p class="text-gray-500 text-sm">Belum ada log aktivitas</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($logs->hasPages())
|
||||
<div class="p-6 border-t border-gray-100">
|
||||
{{ $logs->links() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- STATUS INFO -->
|
||||
<div class="mt-12 bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg p-8 border border-gray-100">
|
||||
|
||||
<div class="flex items-center gap-3 mb-8">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-green-500 to-emerald-600 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="info" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">Informasi Sistem</h3>
|
||||
<p class="text-gray-600">Status operasional logging</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
|
||||
<div class="group bg-gradient-to-br from-green-50 to-emerald-50 p-6 rounded-2xl border border-green-200 hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-green-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i data-feather="check-circle" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-green-800">Sensor aktif dan berjalan normal</p>
|
||||
<p class="text-green-600 text-sm">Semua sistem operasional</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group bg-gradient-to-br from-blue-50 to-indigo-50 p-6 rounded-2xl border border-blue-200 hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i data-feather="radio" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-blue-800">Komunikasi LoRa stabil</p>
|
||||
<p class="text-blue-600 text-sm">Koneksi optimal</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group bg-gradient-to-br from-purple-50 to-pink-50 p-6 rounded-2xl border border-purple-200 hover:shadow-lg transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-purple-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<i data-feather="clock" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-purple-800">Update log terakhir</p>
|
||||
<p class="text-purple-600 text-sm">{{ $logs->first()?->event_time->diffForHumans() ?? 'Belum ada data' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
feather.replace()
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - Smart Rack Security</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/feather-icons"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.font-display { font-family: 'Space Grotesk', sans-serif; }
|
||||
.glass { backdrop-filter: blur(16px); background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); }
|
||||
.animate-float { animation: float 6s ease-in-out infinite; }
|
||||
@keyframes float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-20px); } }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-indigo-600 via-purple-700 to-blue-800 flex items-center justify-center min-h-screen overflow-hidden relative">
|
||||
|
||||
<!-- Background Pattern -->
|
||||
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.05"%3E%3Ccircle cx="30" cy="30" r="2"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')] opacity-20"></div>
|
||||
|
||||
<!-- Floating Elements -->
|
||||
<div class="absolute top-20 left-10 w-4 h-4 bg-blue-400 rounded-full animate-pulse opacity-60"></div>
|
||||
<div class="absolute top-40 right-20 w-6 h-6 bg-purple-400 rounded-full animate-bounce opacity-40"></div>
|
||||
<div class="absolute bottom-20 left-20 w-3 h-3 bg-indigo-300 rounded-full animate-ping opacity-50"></div>
|
||||
<div class="absolute bottom-40 right-10 w-5 h-5 bg-pink-400 rounded-full animate-pulse opacity-30"></div>
|
||||
|
||||
<!-- Login Card -->
|
||||
<div class="relative z-10 w-full max-w-md mx-auto px-6">
|
||||
|
||||
<div class="glass rounded-3xl p-10 shadow-2xl border border-white/20 backdrop-blur-xl">
|
||||
|
||||
<!-- Logo & Title -->
|
||||
<div class="text-center mb-10">
|
||||
<div class="animate-float mb-6">
|
||||
<div class="w-20 h-20 bg-white/10 backdrop-blur-sm rounded-2xl flex items-center justify-center mx-auto border border-white/20">
|
||||
<i data-feather="shield" class="w-10 h-10 text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl font-display font-bold text-white mb-2">Smart Rack Security</h1>
|
||||
<p class="text-blue-100">Sistem Monitoring Keamanan IoT</p>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form action="{{ route('login.process') }}" method="POST" class="space-y-6">
|
||||
@csrf
|
||||
|
||||
<!-- Alert Messages -->
|
||||
@if(session('error'))
|
||||
<div class="bg-red-500/10 border border-red-500/20 text-red-200 px-4 py-3 rounded-2xl text-sm">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('success'))
|
||||
<div class="bg-green-500/10 border border-green-500/20 text-green-200 px-4 py-3 rounded-2xl text-sm">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Demo Info -->
|
||||
<div class="bg-blue-500/10 border border-blue-500/20 text-blue-200 px-4 py-3 rounded-2xl text-sm text-center">
|
||||
<p>Masukkan email dan password akun Anda untuk masuk</p>
|
||||
</div>
|
||||
|
||||
<!-- Username -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-white font-medium">Username</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<i data-feather="user" class="w-5 h-5 text-blue-200"></i>
|
||||
</div>
|
||||
<input type="text" name="username" placeholder="Masukkan email" value="{{ old('username') }}"
|
||||
class="w-full pl-12 pr-4 py-4 bg-white/10 border border-white/20 rounded-2xl text-white placeholder-blue-200 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-white/40 transition-all duration-300 backdrop-blur-sm" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-white font-medium">Password</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<i data-feather="lock" class="w-5 h-5 text-blue-200"></i>
|
||||
</div>
|
||||
<input type="password" name="password" placeholder="Masukkan password"
|
||||
class="w-full pl-12 pr-4 py-4 bg-white/10 border border-white/20 rounded-2xl text-white placeholder-blue-200 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-white/40 transition-all duration-300 backdrop-blur-sm" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="flex items-center">
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input type="checkbox" name="remember" class="w-4 h-4 text-indigo-600 bg-white/10 border-white/20 rounded focus:ring-white/30">
|
||||
<span class="text-blue-100 text-sm">Ingat saya</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Login Button -->
|
||||
<button type="submit" class="group w-full bg-white text-indigo-600 py-4 rounded-2xl font-semibold hover:shadow-2xl hover:scale-105 transition-all duration-300 flex items-center justify-center gap-3">
|
||||
<span>Masuk Dashboard</span>
|
||||
<i data-feather="arrow-right" class="w-5 h-5 group-hover:translate-x-1 transition-transform"></i>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<script>
|
||||
feather.replace();
|
||||
</script>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="text-center mt-8 pt-6 border-t border-white/10 space-y-3">
|
||||
<p class="text-blue-100 text-sm">Sistem Monitoring Rak Barang</p>
|
||||
<p class="text-blue-100 text-sm">Belum punya akun?
|
||||
<a href="/register" class="text-white font-semibold hover:underline">Daftar di sini</a>
|
||||
</p>
|
||||
<a href="/home" class="text-blue-200 hover:text-white text-sm transition-colors flex items-center justify-center gap-2">
|
||||
<i data-feather="arrow-left" class="w-4 h-4"></i>
|
||||
<span>Kembali ke Home</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Monitoring Sensor - Smart Rack Security</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/feather-icons"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.font-display { font-family: 'Space Grotesk', sans-serif; }
|
||||
.glass { backdrop-filter: blur(16px); background: rgba(255, 255, 255, 0.9); border: 1px solid rgba(255, 255, 255, 0.2); }
|
||||
.animate-pulse-slow { animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 min-h-screen p-4 md:p-6 font-sans overflow-x-hidden">
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl flex items-center justify-center shadow-lg">
|
||||
<i data-feather="activity" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl md:text-4xl font-display font-bold text-gray-800">Monitoring Sensor</h1>
|
||||
<p class="text-gray-600 mt-0.5 text-sm">Data realtime dari sensor keamanan rak</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 w-full sm:w-auto">
|
||||
<div class="flex items-center gap-2 bg-gradient-to-r from-green-500/10 to-emerald-500/10 px-3 py-2 rounded-xl border border-green-200">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse-slow"></div>
|
||||
<span class="text-green-700 font-semibold text-xs">Live Monitoring</span>
|
||||
</div>
|
||||
<a href="/dashboard" class="ml-auto sm:ml-0 group bg-gradient-to-r from-indigo-600 to-purple-600 text-white px-4 py-2 md:px-6 md:py-3 rounded-xl font-semibold hover:shadow-lg hover:scale-105 transition-all duration-300 flex items-center gap-2">
|
||||
<i data-feather="arrow-left" class="w-4 h-4 group-hover:-translate-x-1 transition-transform"></i>
|
||||
<span class="text-sm md:text-base">Dashboard</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STATISTIK 24 JAM -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
|
||||
<div class="bg-white rounded-2xl shadow p-6 border border-gray-100 flex items-center gap-4">
|
||||
<div class="w-14 h-14 bg-indigo-100 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="eye" class="w-7 h-7 text-indigo-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">Gerakan Terdeteksi (24 jam)</p>
|
||||
<p class="text-3xl font-display font-bold text-indigo-600">{{ $pirCount24h }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow p-6 border border-gray-100 flex items-center gap-4">
|
||||
<div class="w-14 h-14 bg-yellow-100 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="zap" class="w-7 h-7 text-yellow-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">Getaran Abnormal (24 jam)</p>
|
||||
<p class="text-3xl font-display font-bold text-yellow-600">{{ $vibrationCount24h }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow p-6 border border-gray-100 flex items-center gap-4">
|
||||
<div class="w-14 h-14 bg-purple-100 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="unlock" class="w-7 h-7 text-purple-600"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-500 text-sm">Rak Dibuka (24 jam)</p>
|
||||
<p class="text-3xl font-display font-bold text-purple-600">{{ $reedCount24h }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SENSOR GRID -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-10">
|
||||
|
||||
<!-- PIR -->
|
||||
<div class="group bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg hover:shadow-2xl transition-all duration-500 p-8 border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-indigo-50 to-blue-50 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-indigo-500 to-blue-600 rounded-2xl flex items-center justify-center shadow-lg">
|
||||
<i data-feather="eye" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">Sensor PIR</h3>
|
||||
<p class="text-gray-500 text-sm">Motion Detection</p>
|
||||
</div>
|
||||
</div>
|
||||
@if($pirSensor && $pirSensor->status === 'active')
|
||||
<span class="bg-green-100 text-green-700 px-3 py-1 rounded-xl text-xs font-semibold border border-green-200">Aktif</span>
|
||||
@else
|
||||
<span class="bg-red-100 text-red-700 px-3 py-1 rounded-xl text-xs font-semibold border border-red-200">Tidak Aktif</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($pirLatest)
|
||||
<div class="mb-4">
|
||||
<p class="text-3xl font-display font-bold {{ $pirLatest->motion_detected ? 'text-red-600' : 'text-green-600' }} mb-1">
|
||||
{{ $pirLatest->motion_detected ? '⚠ Gerakan Terdeteksi' : '✓ Tidak Ada Gerakan' }}
|
||||
</p>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Tipe: <span class="font-semibold text-gray-700">{{ ucfirst($pirLatest->motion_type ?? '-') }}</span>
|
||||
| Intensitas: <span class="font-semibold text-gray-700">{{ $pirLatest->motion_intensity ?? 0 }}%</span>
|
||||
</p>
|
||||
<p class="text-gray-500 text-sm mt-1">
|
||||
Zona: <span class="font-semibold text-gray-700">{{ ucfirst($pirLatest->detection_zone ?? '-') }}</span>
|
||||
| Durasi: <span class="font-semibold text-gray-700">{{ $pirLatest->duration_seconds ?? 0 }}s</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="pt-4 border-t border-gray-100 flex justify-between text-sm">
|
||||
<span class="text-gray-500">Update terakhir:</span>
|
||||
<span class="{{ $pirLatest->motion_detected ? 'text-red-600' : 'text-green-600' }} font-semibold">
|
||||
{{ $pirLatest->recorded_at->diffForHumans() }}
|
||||
</span>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-2xl font-display font-bold text-gray-400 mb-2">Belum Ada Data</p>
|
||||
<p class="text-gray-500 text-sm">Menunggu data dari sensor...</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VIBRATION -->
|
||||
<div class="group bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg hover:shadow-2xl transition-all duration-500 p-8 border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-yellow-50 to-orange-50 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-yellow-500 to-orange-600 rounded-2xl flex items-center justify-center shadow-lg">
|
||||
<i data-feather="zap" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">Sensor Getar</h3>
|
||||
<p class="text-gray-500 text-sm">SW-420 Vibration</p>
|
||||
</div>
|
||||
</div>
|
||||
@if($vibrationSensor && $vibrationSensor->status === 'active')
|
||||
<span class="bg-green-100 text-green-700 px-3 py-1 rounded-xl text-xs font-semibold border border-green-200">Aktif</span>
|
||||
@else
|
||||
<span class="bg-red-100 text-red-700 px-3 py-1 rounded-xl text-xs font-semibold border border-red-200">Tidak Aktif</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($vibrationLatest)
|
||||
<div class="mb-4">
|
||||
<p class="text-3xl font-display font-bold {{ $vibrationLatest->is_abnormal ? 'text-red-600' : 'text-green-600' }} mb-1">
|
||||
{{ $vibrationLatest->is_abnormal ? '⚠ Getaran Abnormal' : '✓ Stabil' }}
|
||||
</p>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Status: <span class="font-semibold text-gray-700">{{ ucfirst($vibrationLatest->status ?? '-') }}</span>
|
||||
| Magnitude: <span class="font-semibold text-gray-700">{{ number_format($vibrationLatest->magnitude ?? 0, 2) }}</span>
|
||||
</p>
|
||||
<p class="text-gray-500 text-sm mt-1">
|
||||
X: <span class="font-semibold">{{ number_format($vibrationLatest->x_axis, 2) }}</span>
|
||||
Y: <span class="font-semibold">{{ number_format($vibrationLatest->y_axis, 2) }}</span>
|
||||
Z: <span class="font-semibold">{{ number_format($vibrationLatest->z_axis, 2) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="pt-4 border-t border-gray-100 flex justify-between text-sm">
|
||||
<span class="text-gray-500">Update terakhir:</span>
|
||||
<span class="{{ $vibrationLatest->is_abnormal ? 'text-red-600' : 'text-green-600' }} font-semibold">
|
||||
{{ $vibrationLatest->recorded_at->diffForHumans() }}
|
||||
</span>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-2xl font-display font-bold text-gray-400 mb-2">Belum Ada Data</p>
|
||||
<p class="text-gray-500 text-sm">Menunggu data dari sensor...</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REED SWITCH -->
|
||||
<div class="group bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg hover:shadow-2xl transition-all duration-500 p-8 border border-gray-100 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-50 to-pink-50 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-600 rounded-2xl flex items-center justify-center shadow-lg">
|
||||
<i data-feather="unlock" class="w-8 h-8 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">Reed Switch</h3>
|
||||
<p class="text-gray-500 text-sm">Door/Lock Status</p>
|
||||
</div>
|
||||
</div>
|
||||
@if($reedSensor && $reedSensor->status === 'active')
|
||||
<span class="bg-green-100 text-green-700 px-3 py-1 rounded-xl text-xs font-semibold border border-green-200">Aktif</span>
|
||||
@else
|
||||
<span class="bg-red-100 text-red-700 px-3 py-1 rounded-xl text-xs font-semibold border border-red-200">Tidak Aktif</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($reedLatest)
|
||||
<div class="mb-4">
|
||||
<p class="text-3xl font-display font-bold {{ $reedLatest->door_open ? 'text-red-600' : 'text-green-600' }} mb-1">
|
||||
{{ $reedLatest->door_open ? '⚠ Rak Terbuka' : '✓ Rak Tertutup' }}
|
||||
</p>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Status: <span class="font-semibold text-gray-700">{{ ucfirst($reedLatest->door_status ?? '-') }}</span>
|
||||
| Level: <span class="font-semibold text-gray-700">{{ ucfirst($reedLatest->access_level ?? '-') }}</span>
|
||||
</p>
|
||||
<p class="text-gray-500 text-sm mt-1">
|
||||
Metode: <span class="font-semibold text-gray-700">{{ ucfirst($reedLatest->access_method ?? '-') }}</span>
|
||||
| Durasi: <span class="font-semibold text-gray-700">{{ $reedLatest->open_duration_seconds ?? 0 }}s</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="pt-4 border-t border-gray-100 flex justify-between text-sm">
|
||||
<span class="text-gray-500">Update terakhir:</span>
|
||||
<span class="{{ $reedLatest->door_open ? 'text-red-600' : 'text-green-600' }} font-semibold">
|
||||
{{ $reedLatest->recorded_at->diffForHumans() }}
|
||||
</span>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-2xl font-display font-bold text-gray-400 mb-2">Belum Ada Data</p>
|
||||
<p class="text-gray-500 text-sm">Menunggu data dari sensor...</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- RIWAYAT DATA TERBARU -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-10">
|
||||
|
||||
<!-- Riwayat PIR -->
|
||||
<div class="bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg p-6 border border-gray-100">
|
||||
<div class="flex items-center gap-3 mb-5">
|
||||
<div class="w-10 h-10 bg-indigo-100 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="eye" class="w-5 h-5 text-indigo-600"></i>
|
||||
</div>
|
||||
<h4 class="font-display font-bold text-gray-800">Riwayat PIR</h4>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
@forelse($pirHistory as $pir)
|
||||
<div class="flex items-center justify-between p-3 rounded-xl {{ $pir->motion_detected ? 'bg-red-50 border border-red-100' : 'bg-gray-50 border border-gray-100' }}">
|
||||
<div>
|
||||
<p class="text-sm font-semibold {{ $pir->motion_detected ? 'text-red-700' : 'text-gray-700' }}">
|
||||
{{ $pir->motion_detected ? 'Gerakan' : 'Aman' }}
|
||||
@if($pir->motion_detected) — {{ $pir->motion_intensity }}% @endif
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">{{ $pir->recorded_at->format('d M H:i:s') }}</p>
|
||||
</div>
|
||||
<div class="w-2 h-2 rounded-full {{ $pir->motion_detected ? 'bg-red-500' : 'bg-green-500' }}"></div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-400 text-sm text-center py-4">Belum ada data</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Riwayat Vibration -->
|
||||
<div class="bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg p-6 border border-gray-100">
|
||||
<div class="flex items-center gap-3 mb-5">
|
||||
<div class="w-10 h-10 bg-yellow-100 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="zap" class="w-5 h-5 text-yellow-600"></i>
|
||||
</div>
|
||||
<h4 class="font-display font-bold text-gray-800">Riwayat Getaran</h4>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
@forelse($vibrationHistory as $vib)
|
||||
<div class="flex items-center justify-between p-3 rounded-xl {{ $vib->is_abnormal ? 'bg-red-50 border border-red-100' : 'bg-gray-50 border border-gray-100' }}">
|
||||
<div>
|
||||
<p class="text-sm font-semibold {{ $vib->is_abnormal ? 'text-red-700' : 'text-gray-700' }}">
|
||||
{{ ucfirst($vib->status) }} — {{ number_format($vib->magnitude, 2) }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">{{ $vib->recorded_at->format('d M H:i:s') }}</p>
|
||||
</div>
|
||||
<div class="w-2 h-2 rounded-full {{ $vib->is_abnormal ? 'bg-red-500' : 'bg-green-500' }}"></div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-400 text-sm text-center py-4">Belum ada data</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Riwayat Reed Switch -->
|
||||
<div class="bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg p-6 border border-gray-100">
|
||||
<div class="flex items-center gap-3 mb-5">
|
||||
<div class="w-10 h-10 bg-purple-100 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="unlock" class="w-5 h-5 text-purple-600"></i>
|
||||
</div>
|
||||
<h4 class="font-display font-bold text-gray-800">Riwayat Reed Switch</h4>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
@forelse($reedHistory as $reed)
|
||||
<div class="flex items-center justify-between p-3 rounded-xl {{ $reed->door_open ? 'bg-red-50 border border-red-100' : 'bg-gray-50 border border-gray-100' }}">
|
||||
<div>
|
||||
<p class="text-sm font-semibold {{ $reed->door_open ? 'text-red-700' : 'text-gray-700' }}">
|
||||
{{ $reed->door_open ? 'Terbuka' : 'Tertutup' }} — {{ ucfirst($reed->access_level ?? 'normal') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">{{ $reed->recorded_at->format('d M H:i:s') }}</p>
|
||||
</div>
|
||||
<div class="w-2 h-2 rounded-full {{ $reed->door_open ? 'bg-red-500' : 'bg-green-500' }}"></div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-400 text-sm text-center py-4">Belum ada data</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- STATUS BAR -->
|
||||
<div class="bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg p-8 border border-gray-100">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="w-12 h-12 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="monitor" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-display font-bold text-gray-800">Status Sistem</h3>
|
||||
<p class="text-gray-600">Kondisi operasional monitoring</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="bg-gradient-to-br from-green-50 to-emerald-50 p-6 rounded-2xl border border-green-200">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-green-500 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="check-circle" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-green-800">Sensor aktif</p>
|
||||
<p class="text-green-600 text-sm">{{ \App\Models\Sensor::where('status','active')->count() }}/{{ \App\Models\Sensor::count() }} sensor online</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 p-6 rounded-2xl border border-blue-200">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="radio" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-blue-800">Komunikasi LoRa</p>
|
||||
<p class="text-blue-600 text-sm">Signal: {{ $device?->signal_strength ?? '-' }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-purple-50 to-pink-50 p-6 rounded-2xl border border-purple-200">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 bg-purple-500 rounded-xl flex items-center justify-center">
|
||||
<i data-feather="clock" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-purple-800">Update terakhir</p>
|
||||
<p class="text-purple-600 text-sm">{{ $lastUpdate ? $lastUpdate->diffForHumans() : 'Belum ada data' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
feather.replace();
|
||||
|
||||
// Auto refresh setiap 10 detik
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 10000);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Register - Smart Rack Security</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/feather-icons"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.font-display { font-family: 'Space Grotesk', sans-serif; }
|
||||
.glass { backdrop-filter: blur(16px); background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); }
|
||||
.animate-float { animation: float 6s ease-in-out infinite; }
|
||||
@keyframes float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-20px); } }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-indigo-600 via-purple-700 to-blue-800 flex items-center justify-center min-h-screen overflow-hidden relative py-8">
|
||||
|
||||
<!-- Background Pattern -->
|
||||
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.05"%3E%3Ccircle cx="30" cy="30" r="2"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E')] opacity-20"></div>
|
||||
|
||||
<!-- Floating Elements -->
|
||||
<div class="absolute top-20 left-10 w-4 h-4 bg-blue-400 rounded-full animate-pulse opacity-60"></div>
|
||||
<div class="absolute top-40 right-20 w-6 h-6 bg-purple-400 rounded-full animate-bounce opacity-40"></div>
|
||||
<div class="absolute bottom-20 left-20 w-3 h-3 bg-indigo-300 rounded-full animate-ping opacity-50"></div>
|
||||
<div class="absolute bottom-40 right-10 w-5 h-5 bg-pink-400 rounded-full animate-pulse opacity-30"></div>
|
||||
|
||||
<!-- Register Card -->
|
||||
<div class="relative z-10 w-full max-w-md mx-auto px-6">
|
||||
|
||||
<div class="glass rounded-3xl p-10 shadow-2xl border border-white/20 backdrop-blur-xl">
|
||||
|
||||
<!-- Logo & Title -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="animate-float mb-6">
|
||||
<div class="w-20 h-20 bg-white/10 backdrop-blur-sm rounded-2xl flex items-center justify-center mx-auto border border-white/20">
|
||||
<i data-feather="user-plus" class="w-10 h-10 text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-3xl font-display font-bold text-white mb-2">Buat Akun</h1>
|
||||
<p class="text-blue-100">Daftar untuk akses Smart Rack Security</p>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form action="{{ route('register.process') }}" method="POST" class="space-y-5">
|
||||
@csrf
|
||||
|
||||
<!-- Alert Messages -->
|
||||
@if(session('error'))
|
||||
<div class="bg-red-500/10 border border-red-500/20 text-red-200 px-4 py-3 rounded-2xl text-sm">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="bg-red-500/10 border border-red-500/20 text-red-200 px-4 py-3 rounded-2xl text-sm space-y-1">
|
||||
@foreach($errors->all() as $error)
|
||||
<p>• {{ $error }}</p>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Nama -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-white font-medium">Nama Lengkap</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<i data-feather="user" class="w-5 h-5 text-blue-200"></i>
|
||||
</div>
|
||||
<input type="text" name="name" placeholder="Masukkan nama lengkap" value="{{ old('name') }}"
|
||||
class="w-full pl-12 pr-4 py-4 bg-white/10 border border-white/20 rounded-2xl text-white placeholder-blue-200 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-white/40 transition-all duration-300 backdrop-blur-sm" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-white font-medium">Email</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<i data-feather="mail" class="w-5 h-5 text-blue-200"></i>
|
||||
</div>
|
||||
<input type="email" name="email" placeholder="Masukkan email" value="{{ old('email') }}"
|
||||
class="w-full pl-12 pr-4 py-4 bg-white/10 border border-white/20 rounded-2xl text-white placeholder-blue-200 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-white/40 transition-all duration-300 backdrop-blur-sm" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-white font-medium">Password</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<i data-feather="lock" class="w-5 h-5 text-blue-200"></i>
|
||||
</div>
|
||||
<input type="password" name="password" placeholder="Minimal 8 karakter"
|
||||
class="w-full pl-12 pr-4 py-4 bg-white/10 border border-white/20 rounded-2xl text-white placeholder-blue-200 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-white/40 transition-all duration-300 backdrop-blur-sm" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Konfirmasi Password -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-white font-medium">Konfirmasi Password</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<i data-feather="lock" class="w-5 h-5 text-blue-200"></i>
|
||||
</div>
|
||||
<input type="password" name="password_confirmation" placeholder="Ulangi password"
|
||||
class="w-full pl-12 pr-4 py-4 bg-white/10 border border-white/20 rounded-2xl text-white placeholder-blue-200 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-white/40 transition-all duration-300 backdrop-blur-sm" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register Button -->
|
||||
<button type="submit" class="group w-full bg-white text-indigo-600 py-4 rounded-2xl font-semibold hover:shadow-2xl hover:scale-105 transition-all duration-300 flex items-center justify-center gap-3 mt-2">
|
||||
<span>Daftar Sekarang</span>
|
||||
<i data-feather="arrow-right" class="w-5 h-5 group-hover:translate-x-1 transition-transform"></i>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<script>
|
||||
feather.replace();
|
||||
</script>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="text-center mt-8 pt-6 border-t border-white/10 space-y-3">
|
||||
<p class="text-blue-100 text-sm">Sudah punya akun?</p>
|
||||
<a href="/login" class="inline-flex items-center gap-2 text-white font-semibold bg-white/10 hover:bg-white/20 px-6 py-3 rounded-xl transition-all duration-300">
|
||||
<i data-feather="log-in" class="w-4 h-4"></i>
|
||||
<span>Masuk Sekarang</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Api\VibrationController;
|
||||
use App\Http\Controllers\Api\PirController;
|
||||
use App\Http\Controllers\Api\DoorAccessController;
|
||||
use App\Http\Controllers\Api\LoRaController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is where you can register API routes for your application. These
|
||||
| routes are loaded by the RouteServiceProvider and all of them will
|
||||
| be assigned to the "api" middleware group. Make something great!
|
||||
|
|
||||
*/
|
||||
|
||||
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
|
||||
return $request->user();
|
||||
});
|
||||
|
||||
// Vibration Sensor API Routes
|
||||
Route::prefix('vibration')->group(function () {
|
||||
// Terima data sensor getar dari IoT device
|
||||
Route::post('/data', [VibrationController::class, 'receiveData']);
|
||||
|
||||
// Ambil data vibration terbaru
|
||||
Route::get('/readings', [VibrationController::class, 'getLatestReadings']);
|
||||
|
||||
// Ambil statistik getaran
|
||||
Route::get('/statistics', [VibrationController::class, 'getStatistics']);
|
||||
});
|
||||
|
||||
// PIR Sensor API Routes
|
||||
Route::prefix('pir')->group(function () {
|
||||
// Terima data sensor PIR dari IoT device
|
||||
Route::post('/data', [PirController::class, 'receiveData']);
|
||||
|
||||
// Ambil data PIR terbaru
|
||||
Route::get('/readings', [PirController::class, 'getLatestReadings']);
|
||||
|
||||
// Ambil statistik gerakan
|
||||
Route::get('/statistics', [PirController::class, 'getStatistics']);
|
||||
});
|
||||
|
||||
// Door Access (Reed Switch) API Routes
|
||||
Route::prefix('door-access')->group(function () {
|
||||
// Terima data sensor Reed Switch dari IoT device
|
||||
Route::post('/data', [DoorAccessController::class, 'receiveData']);
|
||||
|
||||
// Ambil data door access terbaru
|
||||
Route::get('/readings', [DoorAccessController::class, 'getLatestReadings']);
|
||||
|
||||
// Ambil statistik door access
|
||||
Route::get('/statistics', [DoorAccessController::class, 'getStatistics']);
|
||||
});
|
||||
|
||||
// LoRa Communication API Routes
|
||||
Route::prefix('lora')->group(function () {
|
||||
// Terima message LoRa dari gateway
|
||||
Route::post('/receive', [LoRaController::class, 'receiveMessage']);
|
||||
|
||||
// Kirim command ke LoRa node
|
||||
Route::post('/send-command', [LoRaController::class, 'sendCommand']);
|
||||
|
||||
// Kirim konfigurasi ke LoRa node
|
||||
Route::post('/send-config', [LoRaController::class, 'sendConfig']);
|
||||
|
||||
// Ambil messages LoRa terbaru
|
||||
Route::get('/messages', [LoRaController::class, 'getMessages']);
|
||||
|
||||
// Ambil statistik LoRa communication
|
||||
Route::get('/statistics', [LoRaController::class, 'getStatistics']);
|
||||
|
||||
// Process unprocessed messages
|
||||
Route::post('/process-messages', [LoRaController::class, 'processUnprocessedMessages']);
|
||||
});
|
||||
|
||||
// Test notification endpoints
|
||||
Route::post('/test-notification', function () {
|
||||
$notificationService = new \App\Services\NotificationService();
|
||||
$result = $notificationService->sendTestNotification();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Test vibration notification sent',
|
||||
'results' => $result
|
||||
]);
|
||||
});
|
||||
|
||||
Route::post('/test-pir-notification', function () {
|
||||
$notificationService = new \App\Services\NotificationService();
|
||||
$result = $notificationService->sendTestPirNotification();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Test PIR notification sent',
|
||||
'results' => $result
|
||||
]);
|
||||
});
|
||||
|
||||
Route::post('/test-door-access-notification', function () {
|
||||
$notificationService = new \App\Services\NotificationService();
|
||||
$result = $notificationService->sendTestDoorAccessNotification();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Test Door Access notification sent',
|
||||
'results' => $result
|
||||
]);
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
Route::get('/', function () {
|
||||
return view('home');
|
||||
})->name('home');
|
||||
|
||||
Route::get('/home', function () {
|
||||
return view('home');
|
||||
})->name('home.page');
|
||||
|
||||
Route::get('/login', function () {
|
||||
return view('login');
|
||||
})->name('login');
|
||||
|
||||
// Halaman Register
|
||||
Route::get('/register', function () {
|
||||
return view('register');
|
||||
})->name('register');
|
||||
|
||||
// Proses Register
|
||||
Route::post('/register', function (\Illuminate\Http\Request $request) {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
], [
|
||||
'name.required' => 'Nama lengkap wajib diisi.',
|
||||
'email.required' => 'Email wajib diisi.',
|
||||
'email.email' => 'Format email tidak valid.',
|
||||
'email.unique' => 'Email sudah terdaftar, gunakan email lain.',
|
||||
'password.required' => 'Password wajib diisi.',
|
||||
'password.min' => 'Password minimal 8 karakter.',
|
||||
'password.confirmed' => 'Konfirmasi password tidak cocok.',
|
||||
]);
|
||||
|
||||
$user = \App\Models\User::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'password' => $request->password,
|
||||
]);
|
||||
|
||||
\Illuminate\Support\Facades\Auth::login($user);
|
||||
|
||||
return redirect()->route('dashboard')->with('success', 'Akun berhasil dibuat! Selamat datang, ' . $user->name . '.');
|
||||
})->name('register.process');
|
||||
|
||||
// Route untuk proses login
|
||||
Route::post('/login', function (Request $request) {
|
||||
$request->validate([
|
||||
'username' => 'required',
|
||||
'password' => 'required',
|
||||
]);
|
||||
|
||||
// Cek kredensial ke tabel users (kolom email = username)
|
||||
$credentials = [
|
||||
'email' => $request->input('username'),
|
||||
'password' => $request->input('password'),
|
||||
];
|
||||
|
||||
if (\Illuminate\Support\Facades\Auth::attempt($credentials, $request->boolean('remember'))) {
|
||||
$request->session()->regenerate();
|
||||
return redirect()->route('dashboard')->with('success', 'Login berhasil!');
|
||||
}
|
||||
|
||||
return back()->with('error', 'Email atau password salah. Silakan coba lagi.')->withInput(['username' => $request->input('username')]);
|
||||
})->name('login.process');
|
||||
|
||||
// Route untuk logout
|
||||
Route::get('/logout', function () {
|
||||
\Illuminate\Support\Facades\Auth::logout();
|
||||
request()->session()->invalidate();
|
||||
request()->session()->regenerateToken();
|
||||
return redirect()->route('home')->with('success', 'Logout berhasil!');
|
||||
})->name('logout');
|
||||
|
||||
// Group routes yang memerlukan login
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/dashboard', function () {
|
||||
$totalSensor = \App\Models\Sensor::where('is_active', true)->count();
|
||||
$sensorAktif = \App\Models\Sensor::where('status', 'active')->count();
|
||||
$peringatanHariIni = \App\Models\Alert::whereDate('triggered_at', today())->count();
|
||||
$alertAktif = \App\Models\Alert::where('status', 'active')->count();
|
||||
$lastUpdate = \App\Models\ActivityLog::latest('event_time')->first()?->event_time;
|
||||
|
||||
// Alert terbaru untuk ditampilkan di dashboard
|
||||
$alertTerbaru = \App\Models\Alert::with('device')
|
||||
->where('status', 'active')
|
||||
->orderBy('triggered_at', 'desc')
|
||||
->limit(5)
|
||||
->get();
|
||||
|
||||
// Deteksi sensor aktif dalam 30 detik terakhir
|
||||
$pirAktif = \App\Models\PirReading::where('recorded_at', '>=', now()->subSeconds(30))
|
||||
->where('motion_detected', true)->exists();
|
||||
$vibAktif = \App\Models\VibrationReading::where('recorded_at', '>=', now()->subSeconds(30))
|
||||
->where('is_abnormal', true)->exists();
|
||||
$reedAktif = \App\Models\ReedSwitchReading::where('recorded_at', '>=', now()->subSeconds(30))
|
||||
->where('door_open', true)->exists();
|
||||
|
||||
return view('dashboard', compact(
|
||||
'totalSensor', 'sensorAktif', 'peringatanHariIni', 'alertAktif',
|
||||
'lastUpdate', 'alertTerbaru', 'pirAktif', 'vibAktif', 'reedAktif'
|
||||
));
|
||||
})->name('dashboard');
|
||||
|
||||
Route::get('/monitoring', function () {
|
||||
// Ambil data terbaru dari tabel spesifik sensor
|
||||
$pirLatest = \App\Models\PirReading::with('device')
|
||||
->orderBy('recorded_at', 'desc')->first();
|
||||
$vibrationLatest = \App\Models\VibrationReading::with('device')
|
||||
->orderBy('recorded_at', 'desc')->first();
|
||||
$reedLatest = \App\Models\ReedSwitchReading::with('device')
|
||||
->orderBy('recorded_at', 'desc')->first();
|
||||
|
||||
// Ambil data sensor dari tabel sensors untuk info nama & status
|
||||
$pirSensor = \App\Models\Sensor::where('type', 'pir')->first();
|
||||
$vibrationSensor = \App\Models\Sensor::where('type', 'vibration')->first();
|
||||
$reedSensor = \App\Models\Sensor::where('type', 'reed_switch')->first();
|
||||
|
||||
// Statistik 24 jam terakhir
|
||||
$pirCount24h = \App\Models\PirReading::where('recorded_at', '>=', now()->subHours(24))
|
||||
->where('motion_detected', true)->count();
|
||||
$vibrationCount24h = \App\Models\VibrationReading::where('recorded_at', '>=', now()->subHours(24))
|
||||
->where('is_abnormal', true)->count();
|
||||
$reedCount24h = \App\Models\ReedSwitchReading::where('recorded_at', '>=', now()->subHours(24))
|
||||
->where('door_open', true)->count();
|
||||
|
||||
// Riwayat 5 data terakhir tiap sensor
|
||||
$pirHistory = \App\Models\PirReading::orderBy('recorded_at', 'desc')->limit(5)->get();
|
||||
$vibrationHistory = \App\Models\VibrationReading::orderBy('recorded_at', 'desc')->limit(5)->get();
|
||||
$reedHistory = \App\Models\ReedSwitchReading::orderBy('recorded_at', 'desc')->limit(5)->get();
|
||||
|
||||
$device = \App\Models\Device::where('status', 'online')->first();
|
||||
$lastUpdate = \App\Models\PirReading::orderBy('recorded_at', 'desc')->first()?->recorded_at;
|
||||
|
||||
return view('monitoring', compact(
|
||||
'pirSensor', 'vibrationSensor', 'reedSensor',
|
||||
'pirLatest', 'vibrationLatest', 'reedLatest',
|
||||
'pirCount24h', 'vibrationCount24h', 'reedCount24h',
|
||||
'pirHistory', 'vibrationHistory', 'reedHistory',
|
||||
'device', 'lastUpdate'
|
||||
));
|
||||
})->name('monitoring');
|
||||
|
||||
Route::get('/log', function () {
|
||||
$logs = \App\Models\ActivityLog::with(['device', 'sensor'])
|
||||
->orderBy('event_time', 'desc')
|
||||
->paginate(20);
|
||||
$totalLog = \App\Models\ActivityLog::count();
|
||||
$logHariIni = \App\Models\ActivityLog::whereDate('event_time', today())->count();
|
||||
$logWarning = \App\Models\ActivityLog::where('severity', 'warning')->count();
|
||||
$logCritical = \App\Models\ActivityLog::where('severity', 'critical')->count();
|
||||
|
||||
return view('log', compact('logs', 'totalLog', 'logHariIni', 'logWarning', 'logCritical'));
|
||||
})->name('log');
|
||||
|
||||
Route::get('/device', function () {
|
||||
return view('device');
|
||||
})->name('device');
|
||||
});
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
/**
|
||||
* Script untuk setup database MySQL Smart Rack Security System
|
||||
* Jalankan setelah membuat database MySQL dengan create_mysql_tables.sql
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// Load Laravel app
|
||||
$app = require_once __DIR__ . '/bootstrap/app.php';
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
echo "🚀 Setting up Smart Rack Security MySQL Database...\n\n";
|
||||
|
||||
try {
|
||||
// Test database connection
|
||||
echo "📡 Testing database connection...\n";
|
||||
DB::connection()->getPdo();
|
||||
echo "✅ Database connection successful!\n\n";
|
||||
|
||||
// Check if tables exist
|
||||
echo "📊 Checking database tables...\n";
|
||||
$tables = [
|
||||
'users', 'devices', 'sensors', 'pir_readings',
|
||||
'door_readings', 'vibration_readings', 'door_access_readings',
|
||||
'lora_messages', 'sensor_readings', 'alerts',
|
||||
'activity_logs', 'system_settings'
|
||||
];
|
||||
|
||||
$existingTables = [];
|
||||
foreach ($tables as $table) {
|
||||
if (Schema::hasTable($table)) {
|
||||
$existingTables[] = $table;
|
||||
echo "✅ Table '{$table}' exists\n";
|
||||
} else {
|
||||
echo "❌ Table '{$table}' missing\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (count($existingTables) === count($tables)) {
|
||||
echo "\n🎉 All tables exist! Database setup is complete.\n\n";
|
||||
|
||||
// Show database statistics
|
||||
echo "📈 Database Statistics:\n";
|
||||
foreach ($tables as $table) {
|
||||
try {
|
||||
$count = DB::table($table)->count();
|
||||
echo " {$table}: {$count} records\n";
|
||||
} catch (Exception $e) {
|
||||
echo " {$table}: Error reading - {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\n📱 Dashboard Summary:\n";
|
||||
try {
|
||||
$summary = DB::select('SELECT * FROM dashboard_summary')[0];
|
||||
echo " Online Devices: {$summary->online_devices}\n";
|
||||
echo " Offline Devices: {$summary->offline_devices}\n";
|
||||
echo " Unacknowledged Alerts (24h): {$summary->unacknowledged_alerts_24h}\n";
|
||||
echo " Motions (24h): {$summary->motions_24h}\n";
|
||||
echo " Door Accesses (24h): {$summary->door_accesses_24h}\n";
|
||||
echo " Abnormal Vibrations (24h): {$summary->abnormal_vibrations_24h}\n";
|
||||
} catch (Exception $e) {
|
||||
echo " Dashboard view not available: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
} else {
|
||||
echo "\n⚠️ Some tables are missing. Please run the create_mysql_tables.sql script first:\n";
|
||||
echo " mysql -u root -p < create_mysql_tables.sql\n";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "❌ Database connection failed: " . $e->getMessage() . "\n";
|
||||
echo "\n🔧 Please check your .env configuration:\n";
|
||||
echo " DB_CONNECTION=mysql\n";
|
||||
echo " DB_HOST=127.0.0.1\n";
|
||||
echo " DB_PORT=3306\n";
|
||||
echo " DB_DATABASE=smart_rack_security\n";
|
||||
echo " DB_USERNAME=root\n";
|
||||
echo " DB_PASSWORD=your_password\n\n";
|
||||
echo "📝 And make sure MySQL server is running and database exists.\n";
|
||||
}
|
||||
|
||||
echo "\n🔗 Next Steps:\n";
|
||||
echo "1. Start your Arduino with the updated code (arduino_fixed_code.ino)\n";
|
||||
echo "2. Test LoRa communication\n";
|
||||
echo "3. Monitor data in the database\n";
|
||||
echo "4. Access Laravel application: php artisan serve\n";
|
||||
?>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
*
|
||||
!private/
|
||||
!public/
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
compiled.php
|
||||
config.php
|
||||
down
|
||||
events.scanned.php
|
||||
maintenance.php
|
||||
routes.php
|
||||
routes.scanned.php
|
||||
schedule-*
|
||||
services.json
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
*
|
||||
!data/
|
||||
!.gitignore
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue