mqttService = $mqttService; $this->mqttConfig = config('mqtt'); } protected function initializeMqttSubscriptions() { try { // Subscribe ke topik status response $this->mqttService->subscribe( $this->mqttConfig['topics']['responses']['status'], function (string $topic, string $message) { $this->handleStatusResponse($message); } ); // Subscribe ke topik acknowledgment $this->mqttService->subscribe( $this->mqttConfig['topics']['responses']['ack'], function (string $topic, string $message) { $this->handleAckResponse($message); } ); // Tambahkan subscribe untuk topik bell ring $this->mqttService->subscribe( $this->mqttConfig['topics']['responses']['bell_ring'], function (string $topic, string $message) { $this->handleBellRing($message); } ); } catch (\Exception $e) { Log::error('Failed to initialize MQTT subscriptions: ' . $e->getMessage()); } } protected function handleStatusResponse(string $message) { try { $data = json_decode($message, true); // Validasi payload if (!is_array($data)) { Log::error('Invalid status data format'); return; } // Pastikan semua kunci penting ada $requiredKeys = ['rtc', 'dfplayer', 'rtc_time', 'last_communication', 'last_sync']; foreach ($requiredKeys as $key) { if (!array_key_exists($key, $data)) { Log::error("Missing required key in status data: {$key}"); return; } } // Simpan data ke database Status::updateOrCreate( ['id' => 1], [ 'rtc' => $data['rtc'] ?? false, 'dfplayer' => $data['dfplayer'] ?? false, 'rtc_time' => $data['rtc_time'] ?? null, 'last_communication' => Carbon::createFromTimestamp($data['last_communication'] ?? 0), 'last_sync' => Carbon::createFromTimestamp($data['last_sync'] ?? 0) ] ); } catch (\Exception $e) { Log::error('Error handling status response: ' . $e->getMessage()); } } protected function handleAckResponse(string $message) { try { $data = json_decode($message, true); if (isset($data['action'])) { $action = $data['action']; $message = $data['message'] ?? ''; if ($action === 'sync_ack') { Status::updateOrCreate( ['id' => 1], ['last_sync' => Carbon::now()] ); Log::info('Schedule sync acknowledged: ' . $message); } elseif ($action === 'ring_ack') { Log::info('Bell ring acknowledged: ' . $message); } } } catch (\Exception $e) { Log::error('Error handling ack response: ' . $e->getMessage()); } } public function index(Request $request) { try { // Ambil data utama terlepas dari koneksi MQTT $query = JadwalBel::query(); if ($request->filled('hari')) { $query->where('hari', $request->hari); } if ($request->filled('search')) { $query->where(function($q) use ($request) { $q->where('hari', 'like', '%'.$request->search.'%') ->orWhere('file_number', 'like', '%'.$request->search.'%'); }); } $schedules = $query->orderBy('hari')->orderBy('waktu')->paginate(10); $status = Status::firstOrCreate(['id' => 1]); // Jadwal hari ini $todaySchedules = JadwalBel::where('hari', Carbon::now()->isoFormat('dddd')) ->orderBy('waktu') ->get(); // Jadwal berikutnya $nextSchedule = JadwalBel::where('hari', Carbon::now()->isoFormat('dddd')) ->where('waktu', '>', Carbon::now()->format('H:i:s')) ->orderBy('waktu') ->first(); // Cek koneksi MQTT tanpa menghentikan eksekusi jika error try { $mqttStatus = $this->mqttService->isConnected() ? 'Connected' : 'Disconnected'; } catch (\Exception $e) { $mqttStatus = 'Disconnected'; Log::error('MQTT check failed: ' . $e->getMessage()); } return view('admin.bel.index', [ 'schedules' => $schedules, 'todaySchedules' => $todaySchedules, 'nextSchedule' => $nextSchedule, 'status' => $status, 'mqttStatus' => $mqttStatus ]); } catch (\Exception $e) { Log::error('Error in index method: ' . $e->getMessage()); return back()->with('error', 'Terjadi kesalahan saat memuat data jadwal'); } } public function create() { return view('admin.bel.create', [ 'days' => JadwalBel::DAYS, 'default_file' => '0001' ]); } public function store(Request $request) { $validated = $this->validateSchedule($request); try { $schedule = JadwalBel::create($validated); $this->syncSchedules(); $this->logActivity('Jadwal dibuat', $schedule); return redirect() ->route('bel.index') ->with([ 'success' => 'Jadwal berhasil ditambahkan', 'scroll_to' => 'schedule-'.$schedule->id ]); } catch (\Exception $e) { Log::error('Gagal menambah jadwal: ' . $e->getMessage()); return back() ->withInput() ->with('error', 'Gagal menambah jadwal: ' . $e->getMessage()); } } public function edit($id) { $schedule = JadwalBel::findOrFail($id); return view('admin.bel.edit', [ 'schedule' => $schedule, 'days' => JadwalBel::DAYS ]); } public function update(Request $request, $id) { $validated = $this->validateSchedule($request); $schedule = JadwalBel::findOrFail($id); try { $schedule->update($validated); $this->syncSchedules(); $this->logActivity('Jadwal diperbarui', $schedule); return redirect() ->route('bel.index') ->with([ 'success' => 'Jadwal berhasil diperbarui', 'scroll_to' => 'schedule-'.$schedule->id ]); } catch (\Exception $e) { Log::error('Gagal update jadwal ID '.$schedule->id.': ' . $e->getMessage()); return back() ->withInput() ->with('error', 'Gagal memperbarui jadwal: ' . $e->getMessage()); } } public function destroy($id) { $schedule = JadwalBel::findOrFail($id); try { $schedule->delete(); $this->syncSchedules(); $this->logActivity('Jadwal dihapus', $schedule); return redirect() ->route('bel.index') ->with('success', 'Jadwal berhasil dihapus'); } catch (\Exception $e) { Log::error('Gagal hapus jadwal ID '.$schedule->id.': ' . $e->getMessage()); return back() ->with('error', 'Gagal menghapus jadwal: ' . $e->getMessage()); } } public function deleteAll() { try { JadwalBel::truncate(); $this->syncSchedules(); return redirect() ->route('bel.index') ->with('success', 'Semua jadwal berhasil dihapus'); } catch (\Exception $e) { Log::error('Gagal hapus semua jadwal: ' . $e->getMessage()); return back() ->with('error', 'Gagal menghapus semua jadwal: ' . $e->getMessage()); } } public function toggleStatus($id) { try { $schedule = JadwalBel::findOrFail($id); $newStatus = !$schedule->is_active; $schedule->is_active = $newStatus; $schedule->save(); // Return JSON response for AJAX requests if (request()->wantsJson()) { return response()->json([ 'success' => true, 'message' => 'Status jadwal berhasil diubah', 'is_active' => $newStatus ]); } // Fallback for non-AJAX requests return redirect()->back()->with('success', 'Status jadwal berhasil diubah'); } catch (\Exception $e) { if (request()->wantsJson()) { return response()->json([ 'success' => false, 'message' => 'Gagal mengubah status: ' . $e->getMessage() ], 500); } return redirect()->back()->with('error', 'Gagal mengubah status'); } } public function activateAll() { try { JadwalBel::query()->update(['is_active' => true]); $this->syncSchedules(); return response()->json([ 'success' => true, 'message' => 'Semua jadwal diaktifkan' ]); } catch (\Exception $e) { Log::error('Gagal aktifkan semua jadwal: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Gagal mengaktifkan semua jadwal' ], 500); } } public function deactivateAll() { try { JadwalBel::query()->update(['is_active' => false]); $this->syncSchedules(); return response()->json([ 'success' => true, 'message' => 'Semua jadwal dinonaktifkan' ]); } catch (\Exception $e) { Log::error('Gagal nonaktifkan semua jadwal: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Gagal menonaktifkan semua jadwal' ], 500); } } public function ring(Request $request) { $validated = $request->validate([ 'file_number' => 'required|string|size:4', 'repeat' => 'sometimes|integer|min:1|max:10', 'volume' => 'sometimes|integer|min:0|max:30' ]); try { // Catat ke history terlebih dahulu BellHistory::create([ 'hari' => Carbon::now()->isoFormat('dddd'), 'waktu' => Carbon::now()->format('H:i:s'), 'file_number' => $validated['file_number'], 'trigger_type' => BellHistory::TRIGGER_MANUAL, 'ring_time' => Carbon::now(), 'volume' => $validated['volume'] ?? 15, 'repeat' => $validated['repeat'] ?? 1 ]); // Kirim perintah ke MQTT $message = json_encode([ 'action' => 'ring', 'timestamp' => Carbon::now()->toDateTimeString(), 'file_number' => $validated['file_number'], 'repeat' => $validated['repeat'] ?? 1, 'volume' => $validated['volume'] ?? 15, 'trigger_type' => BellHistory::TRIGGER_MANUAL ]); $this->mqttService->publish( $this->mqttConfig['topics']['commands']['ring'], $message, 1 ); return response()->json([ 'success' => true, 'message' => 'Perintah bel berhasil dikirim', 'data' => [ 'file_number' => $validated['file_number'], 'timestamp' => Carbon::now()->toDateTimeString() ] ]); } catch (\Exception $e) { Log::error('Gagal mengirim bel manual: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Gagal mengirim perintah bel: ' . $e->getMessage() ], 500); } } public function status() { try { $message = json_encode([ 'action' => 'get_status', 'timestamp' => Carbon::now()->toDateTimeString() ]); $this->mqttService->publish( $this->mqttConfig['topics']['commands']['status'], $message, 1 ); $status = Status::firstOrCreate(['id' => 1]); return response()->json([ 'success' => true, 'message' => 'Permintaan status terkirim', 'data' => [ 'rtc' => $status->rtc, 'dfplayer' => $status->dfplayer, 'rtc_time' => $status->rtc_time, 'last_communication' => $status->last_communication, 'last_sync' => $status->last_sync, 'mqtt_status' => $this->mqttService->isConnected(), 'status' => $status->status ?? 'unknown' // Default value jika kolom kosong ] ]); } catch (\Exception $e) { Log::error('Gagal meminta status: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Gagal meminta status perangkat: ' . $e->getMessage() ], 500); } } protected function syncJadwalToEsp($schedules) { try { $message = json_encode([ 'action' => 'sync', 'timestamp' => Carbon::now()->toDateTimeString(), 'schedules' => $schedules ]); $this->mqttService->publish( $this->mqttConfig['topics']['commands']['sync'], $message, 1 ); Log::info("Sync schedules sent to MQTT", ['count' => count($schedules)]); } catch (\Exception $e) { Log::error('Error syncing schedules to MQTT: ' . $e->getMessage()); } } public function syncSchedule() { try { $schedules = JadwalBel::active() ->get() ->map(function ($item) { return [ 'hari' => $item->hari, 'waktu' => Carbon::parse($item->waktu)->format('H:i'), // Pastikan format waktu sesuai 'file_number' => $item->file_number ]; }); $message = json_encode([ 'action' => 'sync', 'timestamp' => Carbon::now()->toDateTimeString(), 'schedules' => $schedules ]); Log::info("Sync message sent to MQTT", ['message' => $message]); // Debugging $this->mqttService->publish( $this->mqttConfig['topics']['commands']['sync'], $message, 1 ); Status::updateOrCreate( ['id' => 1], ['last_sync' => Carbon::now()] ); return response()->json([ 'success' => true, 'message' => 'Jadwal berhasil disinkronisasi', 'data' => [ 'count' => $schedules->count(), 'last_sync' => Carbon::now()->toDateTimeString() ] ]); } catch (\Exception $e) { Log::error('Gagal sync jadwal: ' . $e->getMessage()); return response()->json([ 'success' => false, 'message' => 'Gagal menyinkronisasi jadwal: ' . $e->getMessage() ], 500); } } protected function syncSchedules() { try { $schedules = JadwalBel::active() ->get() ->map(function ($item) { return [ 'hari' => $item->hari, 'waktu' => Carbon::parse($item->waktu)->format('H:i:s'), 'file_number' => $item->file_number ]; }); $this->syncJadwalToEsp($schedules); Log::info("Auto sync: " . count($schedules) . " jadwal"); } catch (\Exception $e) { Log::error('Gagal auto sync: ' . $e->getMessage()); } } public function getNextSchedule() { $now = now(); // Mapping for English to Indonesian day names $dayMap = [ 'Monday' => 'Senin', 'Tuesday' => 'Selasa', 'Wednesday' => 'Rabu', 'Thursday' => 'Kamis', 'Friday' => 'Jumat', 'Saturday' => 'Sabtu', 'Sunday' => 'Minggu' ]; $currentDayEnglish = $now->format('l'); // Get English day name (e.g. "Saturday") $currentDay = $dayMap[$currentDayEnglish] ?? $currentDayEnglish; // Convert to Indonesian $currentTime = $now->format('H:i:s'); // Correct day order (Monday-Sunday) $dayOrder = [ 'Senin' => 1, 'Selasa' => 2, 'Rabu' => 3, 'Kamis' => 4, 'Jumat' => 5, 'Sabtu' => 6, 'Minggu' => 7 ]; // 1. First try to find today's upcoming ACTIVE schedules $todaysSchedule = JadwalBel::where('is_active', true) ->where('hari', $currentDay) ->where('waktu', '>', $currentTime) ->orderBy('waktu') ->first(); if ($todaysSchedule) { return $this->formatScheduleResponse($todaysSchedule); } // 2. If no more today, find the next ACTIVE schedule in the week $allSchedules = JadwalBel::where('is_active', true) ->orderByRaw("FIELD(hari, 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu')") ->orderBy('waktu') ->get(); $currentDayValue = $dayOrder[$currentDay] ?? 0; $closestSchedule = null; $minDayDiff = 8; // More than 7 days foreach ($allSchedules as $schedule) { $scheduleDayValue = $dayOrder[$schedule->hari] ?? 0; // Calculate days difference $dayDiff = ($scheduleDayValue - $currentDayValue + 7) % 7; // If same day but time passed, add 7 days if ($dayDiff === 0 && $schedule->waktu <= $currentTime) { $dayDiff = 7; } // Find schedule with smallest day difference if ($dayDiff < $minDayDiff) { $minDayDiff = $dayDiff; $closestSchedule = $schedule; } } if ($closestSchedule) { return $this->formatScheduleResponse($closestSchedule); } return response()->json([ 'success' => false, 'message' => 'Tidak ada jadwal aktif yang akan datang' ]); } private function formatScheduleResponse($schedule) { return response()->json([ 'success' => true, 'next_schedule' => [ 'hari' => $schedule->hari, 'time' => $schedule->waktu, 'file_number' => $schedule->file_number, 'is_active' => $schedule->is_active ] ]); } protected function validateSchedule(Request $request) { return $request->validate([ 'hari' => 'required|in:' . implode(',', JadwalBel::DAYS), 'waktu' => 'required|date_format:H:i', 'file_number' => 'required|string|size:4', 'is_active' => 'sometimes|boolean' ]); } protected function logActivity($action, JadwalBel $schedule) { Log::info("{$action} - ID: {$schedule->id}, Hari: {$schedule->hari}, Waktu: {$schedule->waktu}, File: {$schedule->file_number}"); } protected function logMqttActivity($action, $message) { $this->mqttService->publish( $this->mqttConfig['topics']['system']['logs'], json_encode([ 'action' => $action, 'message' => $message, 'timestamp' => Carbon::now()->toDateTimeString() ]), 0 ); } protected function handleBellRing(string $message) { try { $data = json_decode($message, true); // Validasi data yang diterima if (!isset($data['hari']) || !isset($data['waktu']) || !isset($data['file_number']) || !isset($data['trigger_type'])) { Log::error('Invalid bell ring data format'); return; } // Simpan ke history BellHistory::create([ 'hari' => $data['hari'], 'waktu' => $data['waktu'], 'file_number' => $data['file_number'], 'trigger_type' => $data['trigger_type'], 'ring_time' => Carbon::now(), 'volume' => $data['volume'] ?? 15, // Default volume 15 'repeat' => $data['repeat'] ?? 1 // Default repeat 1x ]); Log::info('Bell ring recorded to history', ['data' => $data]); } catch (\Exception $e) { Log::error('Error handling bell ring: ' . $e->getMessage()); } } public function history(Request $request) { try { $query = BellHistory::query()->latest('ring_time'); if ($request->filled('date')) { $query->whereDate('ring_time', $request->date); } if ($request->filled('search')) { $query->where(function($q) use ($request) { $q->where('hari', 'like', '%'.$request->search.'%') ->orWhere('file_number', 'like', '%'.$request->search.'%') ->orWhere('trigger_type', 'like', '%'.$request->search.'%'); }); } $histories = $query->paginate(15); return view('admin.bel.history', [ 'histories' => $histories ]); } catch (\Exception $e) { Log::error('Error fetching history: ' . $e->getMessage()); return back()->with('error', 'Gagal memuat riwayat bel'); } } }