serialNo = $serialNo; $this->payload = (array) $payload; } /** * Execute the job. */ public function handle(): void { $machine = Machine::withoutGlobalScopes()->where('serial_no', $this->serialNo)->first(); if (!$machine) { Log::warning("MQTT Heartbeat: Machine not found", ['serial_no' => $this->serialNo]); return; } // 更新機台狀態 Log::debug("ProcessHeartbeat: Attempting to set status to online for {$this->serialNo}"); $updateData = [ 'last_heartbeat_at' => now(), 'status' => 'online', ]; // === 狀態異動觸發 (Redis 快取,讓 MQTT 也支援日誌) === $cacheKey = "machine:{$this->serialNo}:state"; $oldState = \Illuminate\Support\Facades\Cache::get($cacheKey); // 1. 偵測狀態由離線轉在線 (寫入日誌) if ($machine->status === 'offline') { \App\Jobs\Machine\ProcessStateLog::dispatch( $machine->id, $machine->company_id, "Machine is online", 'info', [] ); } $newState = $oldState ?? []; // 2. 韌體版本比對 (有改才傳,有改才記) if (isset($this->payload['firmware_version'])) { $fv = (string) $this->payload['firmware_version']; // 優先從快取取舊版本,若無則從資料庫取 $oldVersion = $oldState['firmware_version'] ?? $machine->firmware_version; if ((string)$oldVersion !== $fv) { \App\Jobs\Machine\ProcessStateLog::dispatch( $machine->id, $machine->company_id, "Firmware updated to :version", 'info', ['version' => $fv, 'old' => $oldVersion ?? 'Unknown'] ); } $updateData['firmware_version'] = $fv; $newState['firmware_version'] = $fv; } // 2. 溫度更新 (只要有變動就紀錄日誌,並進行溫度監控告警判定) if (isset($this->payload['temperature'])) { $temp = (int) $this->payload['temperature']; Log::debug("ProcessHeartbeat: Temperature reported for {$this->serialNo}: {$temp}"); $updateData['temperature'] = $temp; // 讀取最後一次記錄日誌的溫度與時間 $tempLogCacheKey = "machine:{$this->serialNo}:last_temp_log"; $lastLogEntry = \Illuminate\Support\Facades\Cache::get($tempLogCacheKey); // 統一邏輯:優先從快取取值,若無則從資料庫取 $oldTemp = isset($lastLogEntry['value']) ? (int)$lastLogEntry['value'] : $machine->temperature; if ($temp !== (int)$oldTemp) { Log::debug("ProcessHeartbeat: Temperature changed from {$oldTemp} to {$temp}. Triggering ordinary log."); \App\Jobs\Machine\ProcessStateLog::dispatch( $machine->id, $machine->company_id, "Temperature reported: :temp°C", 'info', ['temp' => $temp], 'status' // 普通狀態日誌,不發送 Discord ); } // 無論是否有變動,都更新/延長快取壽命,確保快取資料存在 \Illuminate\Support\Facades\Cache::put($tempLogCacheKey, [ 'value' => $temp, 'at' => now()->toDateTimeString() ], 604800); // === 溫度自適應告警判定 === $tempAlertEnabled = false; $machineSettings = $machine->settings ?? []; $company = $machine->company; $companySettings = $company ? ($company->settings ?? []) : []; // 1. 決定是否啟用溫度監控 $enabledSetting = $machineSettings['temp_alert_enabled'] ?? 'inherit'; if ($enabledSetting === 'enabled') { $tempAlertEnabled = true; } elseif ($enabledSetting === 'disabled') { $tempAlertEnabled = false; } else { // inherit 或是沒設定,繼承公司設定 $companyEnabled = $companySettings['discord_notify_enabled'] ?? false; $companyTypes = $companySettings['discord_notify_types'] ?? []; if ($companyEnabled && in_array('temperature', $companyTypes)) { $tempAlertEnabled = true; } } if ($tempAlertEnabled) { $upperLimit = null; $lowerLimit = null; // 優先度 1: 機台自訂 if (isset($machineSettings['temp_upper_limit']) && $machineSettings['temp_upper_limit'] !== '') { $upperLimit = (int)$machineSettings['temp_upper_limit']; } if (isset($machineSettings['temp_lower_limit']) && $machineSettings['temp_lower_limit'] !== '') { $lowerLimit = (int)$machineSettings['temp_lower_limit']; } // 優先度 2: 型號預設 $model = $machine->machineModel; if ($model) { $modelSettings = $model->settings ?? []; if ($upperLimit === null && isset($modelSettings['temp_upper_limit']) && $modelSettings['temp_upper_limit'] !== '') { $upperLimit = (int)$modelSettings['temp_upper_limit']; } if ($lowerLimit === null && isset($modelSettings['temp_lower_limit']) && $modelSettings['temp_lower_limit'] !== '') { $lowerLimit = (int)$modelSettings['temp_lower_limit']; } } // 優先度 3: 公司預設 if ($upperLimit === null && isset($companySettings['temp_upper_limit']) && $companySettings['temp_upper_limit'] !== '') { $upperLimit = (int)$companySettings['temp_upper_limit']; } if ($lowerLimit === null && isset($companySettings['temp_lower_limit']) && $companySettings['temp_lower_limit'] !== '') { $lowerLimit = (int)$companySettings['temp_lower_limit']; } // 全域預設值 fallback if ($upperLimit === null) { $upperLimit = 40; } if ($lowerLimit === null) { $lowerLimit = 0; } $activeKey = "machine:{$this->serialNo}:temp_alert_active"; $cooldownKey = "machine:{$this->serialNo}:temp_alert_cooldown"; $activeStatus = \Illuminate\Support\Facades\Cache::get($activeKey); if ($temp > $upperLimit) { // 若從低溫直接跳到高溫,先恢復低溫 if ($activeStatus === 'low') { \App\Jobs\Machine\ProcessStateLog::dispatch( $machine->id, $machine->company_id, "Temperature restored: :temp°C", 'info', ['temp' => $temp], 'temperature' ); \Illuminate\Support\Facades\Cache::forget($cooldownKey); } $hasCooldown = \Illuminate\Support\Facades\Cache::has($cooldownKey); if ($activeStatus !== 'high' || !$hasCooldown) { \App\Jobs\Machine\ProcessStateLog::dispatch( $machine->id, $machine->company_id, "Temperature too high: :temp°C", 'error', ['temp' => $temp, 'limit' => $upperLimit], 'temperature' ); \Illuminate\Support\Facades\Cache::put($cooldownKey, true, 3600); \Illuminate\Support\Facades\Cache::put($activeKey, 'high', 86400); } } elseif ($temp < $lowerLimit) { // 若從高溫直接跳到低溫,先恢復高溫 if ($activeStatus === 'high') { \App\Jobs\Machine\ProcessStateLog::dispatch( $machine->id, $machine->company_id, "Temperature restored: :temp°C", 'info', ['temp' => $temp], 'temperature' ); \Illuminate\Support\Facades\Cache::forget($cooldownKey); } $hasCooldown = \Illuminate\Support\Facades\Cache::has($cooldownKey); if ($activeStatus !== 'low' || !$hasCooldown) { \App\Jobs\Machine\ProcessStateLog::dispatch( $machine->id, $machine->company_id, "Temperature too low: :temp°C", 'error', ['temp' => $temp, 'limit' => $lowerLimit], 'temperature' ); \Illuminate\Support\Facades\Cache::put($cooldownKey, true, 3600); \Illuminate\Support\Facades\Cache::put($activeKey, 'low', 86400); } } else { // 溫度正常 if ($activeStatus) { \App\Jobs\Machine\ProcessStateLog::dispatch( $machine->id, $machine->company_id, "Temperature restored: :temp°C", 'info', ['temp' => $temp], 'temperature' ); \Illuminate\Support\Facades\Cache::forget($activeKey); \Illuminate\Support\Facades\Cache::forget($cooldownKey); } } } } // 更新快取 \Illuminate\Support\Facades\Cache::put($cacheKey, $newState, 86400); $machine->update($updateData); } }