embedded_raumsenor_lorawan/debug_G2H-RS.php

543 lines
21 KiB
PHP

<?php
include("./connect_iot.php");
$methode = $_SERVER['REQUEST_METHOD'];
$debug = true;
$file = 'log_g2h_rs_'.date("Y-m-d").'.txt';
$seqNumber = getseqno();
$inputstream = file_get_contents('php://input');
if($debug) {
file_put_contents($file, $inputstream, FILE_APPEND | LOCK_EX);
}
$daten = json_decode($inputstream, true);
switch ($methode) {
case 'GET':
$response = array(
'status' => 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['electricity'] = round($powerConsumptionWh / 1000, 6);
// 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'])) {
// power_consumption in data_formatted nachträglich eintragen
if($powerConsumptionWh !== null) {
$df = json_decode($row['data_formatted'], true);
if($df !== null) {
$df['electricity'] = round($powerConsumptionWh / 1000, 6);
$newDf = mysqli_real_escape_string($iotconnection, json_encode($df));
mysqli_query($iotconnection,
"UPDATE device_messages SET data_formatted = '$newDf' WHERE id = '{$row['id']}'");
$row['data_formatted'] = $newDf;
}
}
$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;
}
?>