0, 'status_message' => 'Methode GET nicht erlaubt.' ); break; case 'POST': $daten = json_decode(file_get_contents('php://input'), true); if($debug) { file_put_contents($file, "\n\nProcessing POST request\n", FILE_APPEND | LOCK_EX); } // Skip downlink confirmations - only process uplink messages with sensor data if(!isset($daten['objectJSON']) && !isset($daten['object'])) { if($debug) { file_put_contents($file, "Skipping - no sensor data (likely downlink confirmation)\n", FILE_APPEND | LOCK_EX); } $response = array( 'status' => 200, 'status_message' => 'downlink confirmation processed' ); break; } // Initialize variables $heating = ''; $heatingFloor = ''; $airHumidity = ''; $switch = ''; // Extract data from decoded payload $mesg = array(); if(isset($daten['object'])) { $mesg = $daten['object']; } elseif(isset($daten['objectJSON'])) { $mesg = json_decode($daten['objectJSON'], true); } if(!empty($mesg)) { if(isset($mesg['heating'])) { $heating = $mesg['heating']; } if(isset($mesg['heatingFloor'])) { $heatingFloor = $mesg['heatingFloor']; } if(isset($mesg['airHumidity'])) { $airHumidity = $mesg['airHumidity']; } if(isset($mesg['switch'])) { $switch = $mesg['switch']; } } // Energy tracking fields (nur bei 15-Byte Payload vorhanden) $relayOnTime = isset($mesg['relayOnTime']) ? (int)$mesg['relayOnTime'] : null; $uptime = isset($mesg['uptime']) ? (int)$mesg['uptime'] : null; $loadWatts = isset($mesg['loadWatts']) ? (int)$mesg['loadWatts'] : null; // Verbrauchsberechnung via DB-Tabelle g2h_rs_energy_tracking $powerConsumptionWh = null; if($relayOnTime !== null && $uptime !== null && $loadWatts !== null) { $dbiot2 = new iotdbObj(); $conn2 = $dbiot2->getConnstring_iot(); $devId = mysqli_real_escape_string($conn2, $daten['deviceName']); $row = mysqli_fetch_assoc(mysqli_query($conn2, "SELECT relay_on_time, power_consumption_wh, UNIX_TIMESTAMP(updated_at) AS last_ts FROM g2h_rs_energy_tracking WHERE device_id = '$devId'")); $prevOnTime = $row ? (int)$row['relay_on_time'] : 0; $prevWh = $row ? (float)$row['power_consumption_wh'] : 0.0; $elapsed = $row ? (time() - (int)$row['last_ts']) : 0; // Reboot-Erkennung: Uptime kleiner als vergangene Realzeit (60s Toleranz) $reboot = ($row && $uptime < $elapsed - 60); if($reboot) { $delta = $relayOnTime; // Counter läuft seit Neustart von 0 } else { $delta = max(0, $relayOnTime - $prevOnTime); } $powerConsumptionWh = round($prevWh + ($delta * $loadWatts) / 3600.0, 4); mysqli_query($conn2, "INSERT INTO g2h_rs_energy_tracking (device_id, relay_on_time, power_consumption_wh) VALUES ('$devId', $relayOnTime, $powerConsumptionWh) ON DUPLICATE KEY UPDATE relay_on_time = $relayOnTime, power_consumption_wh = $powerConsumptionWh, updated_at = CURRENT_TIMESTAMP"); } // Extract time information $time = 'TIME:'; if(isset($daten['rxInfo']['0']['time'])) { $time .= date("U", strtotime($daten['rxInfo']['0']['time'])); } elseif(isset($daten['rxInfo']['time'])) { $time .= date("U", strtotime($daten['rxInfo']['time'])); } // Extract location information $computedLocation = array(); if(isset($daten['rxInfo']['0']['location'])) { $location = $daten['rxInfo']['0']['location']; if(isset($location['latitude']) && isset($location['longitude'])) { $computedLocation['lat'] = (float)$location['latitude']; $computedLocation['lng'] = (float)$location['longitude']; $computedLocation['radius'] = 20; $computedLocation['source'] = 2; $computedLocation['status'] = 1; } } elseif(isset($daten['rxInfo']['location'])) { $location = $daten['rxInfo']['location']; if(isset($location['latitude']) && isset($location['longitude'])) { $computedLocation['lat'] = (float)$location['latitude']; $computedLocation['lng'] = (float)$location['longitude']; $computedLocation['radius'] = 2000; $computedLocation['source'] = 2; $computedLocation['status'] = 1; } } $response = array( 'status' => 400, 'status_message' => 'success' ); // Build JSON payload for IoT portal $json = array(); $json['id'] = $daten['deviceName']; if(isset($daten['importtime'])) { $json['time'] = $daten['importtime']; } else { $json['time'] = time(); } $json['computedLocation'] = $computedLocation; $json['data'] = "G2H-RS-HTGS-LoRA"; // Map G2H-RS sensor data to IoT portal format if($heating !== '') { $json['heating'] = $heating; } if($heatingFloor !== '') { $json['heatingFloor'] = $heatingFloor; } if($airHumidity !== '') { $json['airHumidity'] = $airHumidity; } if($switch !== '') { $json['switch'] = $switch; } if($relayOnTime !== null) $json['relayOnTime'] = $relayOnTime; if($uptime !== null) $json['uptime'] = $uptime; if($loadWatts !== null) $json['loadWatts'] = $loadWatts; if($powerConsumptionWh !== null) $json['power_consumption'] = $powerConsumptionWh; // Handle sequence number - use fCnt from ChirpStack if available $seqNumber_dev = 1; // Default if(isset($daten['fCnt']) && $daten['fCnt'] != '') { // Use LoRaWAN frame counter as sequence number $seqNumber_dev = (int)$daten['fCnt']; } elseif(isset($mesg['seqno'])) { // Use sequence number from decoded payload if available $seqNumber_dev = (int)$mesg['seqno']; } else { // Fallback to internal counter if(isset($seqNumber[$daten['deviceName']])) { $seqNumber_dev = $seqNumber[$daten['deviceName']]; $seqNumber_dev++; } else { $seqNumber_dev = isset($seqNumber['1']) ? $seqNumber['1'] : 1; $seqNumber_dev++; } $seqNumber[$daten['deviceName']] = $seqNumber_dev; } $json['seqNumber'] = (int)$seqNumber_dev; $json['lqi'] = "Good"; $json['decoderClass'] = "G2HRoomSensor"; $json['countrycode'] = "276"; // Skip sending if sequence number is 1 (initialization) if($json['seqNumber'] == '1') { if($debug) { file_put_contents($file, "\n\nSkipping send - seqNumber is 1\n", FILE_APPEND | LOCK_EX); file_put_contents($file, json_encode($json), FILE_APPEND | LOCK_EX); } } else { // Send to IoT portal $json_encoded = json_encode($json); if($debug) { file_put_contents($file, "\n\nSending to IoT portal:\n", FILE_APPEND | LOCK_EX); file_put_contents($file, $json_encoded, FILE_APPEND | LOCK_EX); } $url = 'https://iot.satspeed.com/api/public/message'; $headers = array( 'Accept: application/json', 'Content-Type: application/json' ); $curl = curl_init(); curl_setopt($curl, CURLOPT_POST, true); curl_setopt($curl, CURLOPT_POSTFIELDS, $json_encoded); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); $response = curl_exec($curl); $code = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); if($debug) { file_put_contents($file, "\n\nResponse code: " . $code . "\n", FILE_APPEND | LOCK_EX); file_put_contents($file, "Response: " . $response . "\n", FILE_APPEND | LOCK_EX); } // AUTO-DOWNLINK: Send settings-based downlink after successful uplink processing if($code == 200) { sendAutomaticDownlink($daten['deviceName'], $daten['devEUI'], $file, $debug); } } break; case 'PUT': $response = array( 'status' => 0, 'status_message' => 'Methode PUT nicht erlaubt.' ); break; case 'DELETE': $response = array( 'status' => 0, 'status_message' => 'Methode DELETE nicht erlaubt.' ); break; } header('Content-Type: application/json'); echo json_encode(["ok" => true]); // Database operations - store message data sleep(1); $dbiot = new iotdbObj(); $iotconnection = $dbiot->getConnstring_iot(); // Update last device message table if(isset($daten['deviceName'])) { $sql2 = "SELECT * FROM device_messages WHERE device_id = '".$daten['deviceName']."' ORDER BY id DESC LIMIT 1"; $result2 = mysqli_query($iotconnection, $sql2); $row = mysqli_fetch_assoc($result2); if($row && isset($row['id'])) { $sql3 = "UPDATE last_device_message SET id = '".$row['id']."', time = '".$row['time']."', data = '".$row['data']."', seqNumber = '".$row['seqNumber']."', created_at = '".$row['created_at']."', updated_at = '".$row['updated_at']."', atlas_location = '".$row['atlas_location']."', data_formatted = '".$row['data_formatted']."', lqi = '".$row['lqi']."', decoderClass = '".$row['decoderClass']."' WHERE device_id = '".$row['device_id']."'"; mysqli_query($iotconnection, $sql3); } } // === AUTO-DOWNLINK FUNCTIONS === function sendAutomaticDownlink($deviceName, $devEUI, $logFile, $debug) { if($debug) { file_put_contents($logFile, "\n\n=== AUTO-DOWNLINK ===\n", FILE_APPEND | LOCK_EX); file_put_contents($logFile, "Device: $deviceName, DevEUI: $devEUI\n", FILE_APPEND | LOCK_EX); } // Convert Base64 DevEUI to Hex if needed $hexDevEUI = $devEUI; if(!ctype_xdigit($devEUI)) { // DevEUI is Base64, decode to binary then to hex $binaryDevEUI = base64_decode($devEUI); $hexDevEUI = bin2hex($binaryDevEUI); if($debug) { file_put_contents($logFile, "Converted DevEUI: $devEUI -> $hexDevEUI\n", FILE_APPEND | LOCK_EX); } } // Get current settings from database $settings = getG2HSettings($deviceName, $debug, $logFile); if(empty($settings)) { if($debug) { file_put_contents($logFile, "No settings found for device - skipping downlink\n", FILE_APPEND | LOCK_EX); } return; } // Collect all settings for combined packet $combinedSettings = [ 'heating_enable' => true, // Default: heating enabled 'room_temp' => 20, // Default room temperature 'floor_temp' => 22 // Default floor temperature ]; foreach($settings as $setting) { switch($setting['type']) { case 'r1': // Relais control $combinedSettings['heating_enable'] = ($setting['value'] == '1'); break; case 't1': // Room temperature $combinedSettings['room_temp'] = intval($setting['value']); break; case 't2': // Floor temperature $combinedSettings['floor_temp'] = intval($setting['value']); break; } } // Send combined downlink packet $jsonPayload = json_encode($combinedSettings); if($debug) { file_put_contents($logFile, "Preparing combined downlink packet\n", FILE_APPEND | LOCK_EX); file_put_contents($logFile, "Combined settings: " . $jsonPayload . "\n", FILE_APPEND | LOCK_EX); file_put_contents($logFile, "heating_enable: " . ($combinedSettings['heating_enable'] ? 'true' : 'false') . "\n", FILE_APPEND | LOCK_EX); file_put_contents($logFile, "room_temp: " . $combinedSettings['room_temp'] . "\n", FILE_APPEND | LOCK_EX); file_put_contents($logFile, "floor_temp: " . $combinedSettings['floor_temp'] . "\n", FILE_APPEND | LOCK_EX); } sendChirpStackJsonDownlink($hexDevEUI, $jsonPayload, $logFile, $debug); } function getG2HSettings($deviceName, $debug, $logFile) { $dbiot = new iotdbObj(); $iotconnection = $dbiot->getConnstring_iot(); // Query for G2H-RS settings based on DB analysis (without settings_area for flexibility): // - Relais: name='active' AND settings_type=32 // - Raumtemperatur: name='fixValue' AND settings_type=32 // - Bodentemperatur: name='fixValue' AND settings_type=31 $sql = "SELECT sf.value, sf.settings_type, sf.name, s.subject_id FROM settings_fields sf, settings s WHERE s.subject_id = '$deviceName' AND s.user_type = 'user' AND s.id = sf.settings AND ( (sf.name = 'active' AND sf.settings_type = 32) OR (sf.name = 'fixValue' AND sf.settings_type = 32) OR (sf.name = 'fixValue' AND sf.settings_type = 31) )"; if($debug) { file_put_contents($logFile, "SQL: $sql\n", FILE_APPEND | LOCK_EX); } $result = mysqli_query($iotconnection, $sql); $settings = array(); while($row = mysqli_fetch_assoc($result)) { $setting = array(); if($row['name'] == 'active' && $row['settings_type'] == '32') { $setting['type'] = 'r1'; // Relais control $setting['value'] = ($row['value'] == 'true' || $row['value'] == '1') ? '1' : '0'; } elseif($row['name'] == 'fixValue' && $row['settings_type'] == '32') { $setting['type'] = 't1'; // Room temperature $setting['value'] = $row['value']; } elseif($row['name'] == 'fixValue' && $row['settings_type'] == '31') { $setting['type'] = 't2'; // Floor temperature $setting['value'] = $row['value']; } if(!empty($setting)) { $settings[] = $setting; if($debug) { file_put_contents($logFile, "Found setting: {$setting['type']} = {$setting['value']}\n", FILE_APPEND | LOCK_EX); } } } return $settings; } function clearChirpStackDeviceQueue($devEUI, $logFile = null, $debug = false) { $CS_URL = 'http://172.17.13.42:8080'; $TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcGlfa2V5X2lkIjoiMDcxM2MyNzUtMWIyNy00YzQ4LTlkYjQtNjc3MDg1NTgzN2M2IiwiYXVkIjoiYXMiLCJpc3MiOiJhcyIsIm5iZiI6MTc1NTk1OTgxOSwic3ViIjoiYXBpX2tleSJ9.sFDgV0Sk5LvXD1KAq50pXP2dmzf6qVtwJP8hPkriHpY'; $url = "{$CS_URL}/api/devices/{$devEUI}/queue"; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => "DELETE", CURLOPT_HTTPHEADER => [ "Grpc-Metadata-Authorization: Bearer {$TOKEN}", "Content-Type: application/json" ], CURLOPT_TIMEOUT => 15, CURLOPT_FAILONERROR => false, ]); $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); if($debug && $logFile) { file_put_contents($logFile, "Queue cleared for device {$devEUI}, HTTP {$httpCode}: {$response}\n", FILE_APPEND | LOCK_EX); } return $httpCode === 200; } function sendChirpStackJsonDownlink($devEUI, $jsonPayload, $logFile, $debug) { $CS_URL = 'http://172.17.13.42:8080'; $TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcGlfa2V5X2lkIjoiMDcxM2MyNzUtMWIyNy00YzQ4LTlkYjQtNjc3MDg1NTgzN2M2IiwiYXVkIjoiYXMiLCJpc3MiOiJhcyIsIm5iZiI6MTc1NTk1OTgxOSwic3ViIjoiYXBpX2tleSJ9.sFDgV0Sk5LvXD1KAq50pXP2dmzf6qVtwJP8hPkriHpY'; // Clear the queue first to prevent queue overflows // Wait 5 seconds to ensure old packets had opportunity to be sent during downlink windows sleep(5); clearChirpStackDeviceQueue($devEUI, $logFile, $debug); // ChirpStack v3 format: we need to encode manually and send as base64 data $jsonObject = json_decode($jsonPayload, true); // Manual encoding like the ChirpStack encoder does $bytes = []; $bytes[0] = $jsonObject['heating_enable'] ? 1 : 0; // Byte 0: heating enable $bytes[1] = intval($jsonObject['room_temp']); // Byte 1: room temp $bytes[2] = intval($jsonObject['floor_temp']); // Byte 2: floor temp $binaryPayload = pack('C*', ...$bytes); $base64Payload = base64_encode($binaryPayload); $queueItem = [ 'deviceQueueItem' => [ 'confirmed' => false, 'fPort' => 2, 'data' => $base64Payload, // Base64 data for ChirpStack v3 ], ]; $url = "{$CS_URL}/api/devices/{$devEUI}/queue"; $headers = [ 'Accept: application/json', 'Content-Type: application/json', 'Grpc-Metadata-Authorization: Bearer ' . $TOKEN, ]; if($debug) { file_put_contents($logFile, "Sending JSON downlink: $jsonPayload\n", FILE_APPEND | LOCK_EX); } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => json_encode($queueItem, JSON_UNESCAPED_SLASHES), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_FAILONERROR => false, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if($debug) { file_put_contents($logFile, "Binary Downlink HTTP $httpCode: $response\n", FILE_APPEND | LOCK_EX); file_put_contents($logFile, "Encoded bytes: " . bin2hex($binaryPayload) . " (Base64: $base64Payload)\n", FILE_APPEND | LOCK_EX); file_put_contents($logFile, "Full API Payload: " . json_encode($queueItem, JSON_UNESCAPED_SLASHES) . "\n", FILE_APPEND | LOCK_EX); } } // Save sequence number changes setarray($seqNumber); function getseqno() { $testarray = unserialize(file_get_contents('/var/www/iot-dev/web/seqno.txt')); return ($testarray); } function setarray($seqno) { file_put_contents('/var/www/iot-dev/web/seqno.txt', serialize($seqno)); return; } ?>