add energy tracking: 15-byte payload, power consumption accumulation

- Firmware: energy.c/h tracks relay ON-time and uptime via k_uptime_get,
  load_watts (default 400W) persistent via Zephyr Settings API
- Payload extended from 7 to 15 bytes (backwards compatible):
  relay ON-time (uint24), uptime (uint24), load in watts (uint16)
- New 'w' downlink for load configuration
- Fix downlink handler: 'i'/'w' commands now checked before combined packet
- Server: g2h_rs_energy_tracking table for lifetime power_consumption_wh,
  reboot detection via uptime vs real elapsed time
- Decoder/encoder and docs updated
This commit is contained in:
Jochen Gojowsky 2026-02-19 17:17:30 +01:00
parent aefdc7b05c
commit 0287921b38
10 changed files with 777 additions and 64 deletions

View File

@ -3,61 +3,83 @@
## Uplink Payload (Device → Network Server)
**Port:** 2
**Length:** 7 bytes
**Length:** 7 Bytes (ohne Energietracking) oder 15 Bytes (mit Energietracking)
**Format:** Binary
| Byte | Field | Type | Unit | Description |
|------|-------|------|------|-------------|
| 0-1 | Room Temperature | int16_t | 0.01°C | SHT4x sensor reading (big-endian) |
| 2-3 | Floor Temperature | int16_t | 0.01°C | MLX90614 IR sensor reading (big-endian) * |
| 4-5 | Humidity | int16_t | 0.01% | SHT4x sensor reading (big-endian) |
| 6 | Relay State | uint8_t | - | Current relay status (0=OFF, 1=ON) |
|-------|------------------|---------|----------|-------------|
| 0-1 | Room Temperature | int16 | 0.01°C | SHT4x sensor (big-endian) |
| 2-3 | Floor Temperature| int16 | 0.01°C | MLX90614 IR sensor (big-endian) * |
| 4-5 | Humidity | int16 | 0.01% | SHT4x sensor (big-endian) |
| 6 | Relay State | uint8 | - | 0=OFF, 1=ON |
| **7-9** | **Relay ON-Time** | **uint24** | **Sekunden** | **Kumulative Einschaltdauer seit Boot** |
| **10-12** | **Uptime** | **uint24** | **Sekunden** | **Gerätelaufzeit seit Boot** |
| **13-14** | **Load** | **uint16** | **Watt** | **Konfigurierte Heizlast** |
**Example Uplink:**
- `08 34 0A 28 15 E0 01` = 21.00°C room, 26.00°C floor, 56.00% humidity, relay ON
**Hinweis zur Abwärtskompatibilität:** Geräte ohne Energietracking senden 7 Bytes. Der Decoder erkennt die Version anhand von `bytes.length`.
**Beispiel (15 Bytes):**
- `08 34 0A 28 15 E0 01 00 00 F0 00 01 2C 01 90`
= 21.00°C Raum, 26.00°C Boden, 56.00% Feuchte, Relay ON, 240s ON-Zeit, 300s Uptime, 400W Last
## Downlink Payload (Network Server → Device)
**Port:** Any
**Format:** Binary
### Combined Configuration Packet
### Kombiniertes Konfigurationspaket
**Length:** 3 bytes
**Length:** 3 Bytes
**Format:** `[heating_enable][room_temp_threshold][floor_temp_threshold]`
| Byte | Field | Type | Range | Description |
|------|-------|------|-------|-------------|
| 0 | Heating Enable | uint8_t | 0-1 | Heating control logic (0=DISABLED, 1=ENABLED) |
| 1 | Room Temperature Threshold | uint8_t | 0-255 | Target room temperature in °C |
| 2 | Floor Temperature Threshold | uint8_t | 0-255 | Target floor temperature in °C |
|------|--------------------------|-------|-------|-------------|
| 0 | Heating Enable | uint8 | 0-1 | 0=DISABLED, 1=ENABLED |
| 1 | Room Temperature Threshold | uint8 | 0-255 | Raumtemperatur-Schwellwert in °C |
| 2 | Floor Temperature Threshold | uint8 | 0-255 | Bodentemperatur-Schwellwert in °C |
**Examples:**
- `01 16 19` = Enable heating, room 22°C, floor 25°C
- `00 14 18` = Disable heating, room 20°C, floor 24°C
**Beispiele:**
- `01 16 19` = Heizung AN, Raum 22°C, Boden 25°C
- `00 14 18` = Heizung AUS
### Send Interval Command
### Sendeintervall
**Length:** 2 bytes
**Length:** 2 Bytes
**Format:** `'i'[interval_minutes]`
| Byte | Field | Type | Range | Description |
|------|-------|------|-------|-------------|
| 0 | Command | ASCII | 'i' (0x69) | Send interval command identifier |
| 1 | Interval | uint8_t | 1-255 | Send interval in minutes |
|------|----------|-------|-------|-------------|
| 0 | Command | ASCII | 0x69 | Befehlskennung 'i' |
| 1 | Interval | uint8 | 1-255 | Sendeintervall in Minuten |
**Examples:**
- `69 05` = Set send interval to 5 minutes
- `69 0A` = Set send interval to 10 minutes
- `69 0F` = Set send interval to 15 minutes
- `69 3C` = Set send interval to 60 minutes
**Beispiele:**
- `69 05` = 5 Minuten
- `69 0A` = 10 Minuten
- `69 3C` = 60 Minuten
## Data Conversion
### Last-Konfiguration
- **Temperature:** `value = (int16_t)(temperature * 100)`
- **Humidity:** `value = (int16_t)(humidity * 100)`
- **Decoding:** `actual_value = received_value / 100.0`
**Length:** 3 Bytes
**Format:** `'w'[watts_high][watts_low]`
## Special Cases
| Byte | Field | Type | Range | Description |
|------|-------------|-------|--------|-------------|
| 0 | Command | ASCII | 0x77 | Befehlskennung 'w' |
| 1-2 | Load Watts | uint16 | 1-65535 | Heizlast in Watt (big-endian), persistent gespeichert |
**\* Floor Temperature Sensor:** If the MLX90614 sensor is not available or not responding, the floor temperature will be transmitted as `-273.15°C` (hex: `95 5D`).
**Beispiele:**
- `77 01 90` = 400W (0x0190)
- `77 03 E8` = 1000W (0x03E8)
## Datenkonvertierung
- **Temperatur:** `raw = (int16)(temperature * 100)``actual = raw / 100.0`
- **Luftfeuchtigkeit:** `raw = (int16)(humidity * 100)``actual = raw / 100.0`
- **ON-Zeit / Uptime:** uint24 big-endian, Sekunden seit Boot (wird bei Reboot zurückgesetzt)
- **Last:** uint16 big-endian, Watt
## Sonderfälle
**\* Bodentemperatursensor:** Wenn der MLX90614 nicht verfügbar ist, wird `-273.15°C` (hex: `95 5D`) übertragen.
**Reboot-Erkennung (Portal):** Wenn `relay_on_time` im aktuellen Paket kleiner ist als im vorherigen, wurde das Gerät neu gestartet.

View File

@ -42,6 +42,15 @@ Alle Befehle als Base64 in ChirpStack eingeben:
| Heizung AUS | `00 14 18` | `ABQa` | Heizung komplett deaktiviert |
| Heizung AN, 20°C Raum, 24°C Boden | `01 14 18` | `ARQa` | Automatik mit niedrigeren Schwellwerten |
### Heizlast konfigurieren (3 Bytes)
**Format**: `'w'[watts_high][watts_low]`
| Beschreibung | Hex | Base64 | Funktion |
|--------------|-----|--------|----------|
| 400W (Standard) | `77 01 90` | `dwGQ` | Typische Fußbodenheizung |
| 1000W | `77 03 E8` | `dwPo` | Stärkere Heizung |
| 2000W | `77 07 D0` | `dw/Q` | Hochleistungsheizung |
### Sendeintervall ändern (2 Bytes)
**Format**: `'i'[minuten]`
@ -58,14 +67,17 @@ Alle Befehle als Base64 in ChirpStack eingeben:
## Datenformat (Uplink)
Alle 10 Minuten wird ein 7-Byte Datenpaket gesendet:
Alle 10 Minuten wird ein 15-Byte Datenpaket gesendet (abwärtskompatibel zu 7-Byte):
| Byte | Inhalt | Format |
|------|--------|--------|
|-------|-------------------|--------|
| 0-1 | Raumtemperatur | int16 × 100 (z.B. 2134 = 21.34°C) |
| 2-3 | Bodentemperatur | int16 × 100 |
| 4-5 | Luftfeuchtigkeit | int16 × 100 (z.B. 4520 = 45.20%) |
| 6 | Relais-Status | 0 = AUS, 1 = AN |
| 7-9 | Relay ON-Zeit | uint24, Sekunden seit Boot |
| 10-12 | Uptime | uint24, Sekunden seit Boot |
| 13-14 | Heizlast | uint16, Watt |
## Betriebsmodi

33
chirpstack3_decoder.js Normal file
View File

@ -0,0 +1,33 @@
// Decode decodes an array of bytes into an object.
// - fPort contains the LoRaWAN fPort number
// - bytes is an array of bytes, e.g. [225, 230, 255, 0]
// - variables contains the device variables e.g. {"calibration": "3.5"}
// The function must return an object, e.g. {"temperature": 22.5}
function Decode(fPort, bytes, variables) {
// Hilfsfunktion: 16-Bit signed Big Endian lesen
function readInt16BE(b0, b1) {
var val = (b0 << 8) | b1;
if (val & 0x8000) val = val - 0x10000;
return val;
}
if (bytes.length < 7) {
return { error: "invalid payload length" };
}
var d = {};
d.heating = readInt16BE(bytes[0], bytes[1]) / 100.0;
d.heatingFloor = readInt16BE(bytes[2], bytes[3]) / 100.0;
d.airHumidity = readInt16BE(bytes[4], bytes[5]) / 100.0;
d.switch = bytes[6];
if (bytes.length >= 15) {
d.relayOnTime = (bytes[7] << 16) | (bytes[8] << 8) | bytes[9];
d.uptime = (bytes[10] << 16) | (bytes[11] << 8) | bytes[12];
d.loadWatts = (bytes[13] << 8) | bytes[14];
}
return d;
}

View File

@ -26,6 +26,16 @@ function Encode(fPort, obj, variables) {
return bytes;
}
// Load configuration format (3 bytes): 'w' + watts (2 bytes big-endian)
if (obj.load_watts !== undefined) {
var w = parseInt(obj.load_watts);
bytes[0] = 0x77; // ASCII 'w'
bytes[1] = (w >> 8) & 0xFF;
bytes[2] = w & 0xFF;
return bytes;
}
return null;
}
@ -35,3 +45,6 @@ function Encode(fPort, obj, variables) {
// Send interval: {"send_interval": 5}
// Result: [0x69, 0x05]
// Load configuration: {"load_watts": 400}
// Result: [0x77, 0x01, 0x90]

530
debug_G2H-RS.php Normal file
View File

@ -0,0 +1,530 @@
<?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['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;
}
?>

62
src/energy.c Normal file
View File

@ -0,0 +1,62 @@
#include <zephyr/kernel.h>
#include <zephyr/settings/settings.h>
#include <energy.h>
static int64_t relay_on_timestamp = 0;
static uint32_t relay_on_time_seconds = 0;
static bool relay_currently_on = false;
static uint16_t load_watts = 400;
static int energy_set(const char *key, size_t len, settings_read_cb read_cb, void *cb_arg)
{
if (strcmp(key, "load_watts") == 0) {
read_cb(cb_arg, &load_watts, sizeof(load_watts));
}
return 0;
}
SETTINGS_STATIC_HANDLER_DEFINE(energy, "energy", NULL, energy_set, NULL, NULL);
void energy_init(void)
{
settings_load_subtree("energy");
}
void energy_update_relay_state(bool new_state)
{
int64_t now = k_uptime_get();
if (relay_currently_on && !new_state) {
relay_on_time_seconds += (uint32_t)((now - relay_on_timestamp) / 1000);
}
if (new_state && !relay_currently_on) {
relay_on_timestamp = now;
}
relay_currently_on = new_state;
}
uint32_t energy_get_on_time_seconds(void)
{
if (relay_currently_on) {
int64_t now = k_uptime_get();
return relay_on_time_seconds + (uint32_t)((now - relay_on_timestamp) / 1000);
}
return relay_on_time_seconds;
}
uint32_t energy_get_uptime_seconds(void)
{
return (uint32_t)(k_uptime_get() / 1000);
}
uint16_t energy_get_load_watts(void)
{
return load_watts;
}
void energy_set_load_watts(uint16_t watts)
{
load_watts = watts;
settings_save_one("energy/load_watts", &load_watts, sizeof(load_watts));
}

11
src/energy.h Normal file
View File

@ -0,0 +1,11 @@
#pragma once
#include <stdint.h>
#include <stdbool.h>
void energy_init(void);
void energy_update_relay_state(bool state);
uint32_t energy_get_on_time_seconds(void);
uint32_t energy_get_uptime_seconds(void);
uint16_t energy_get_load_watts(void);
void energy_set_load_watts(uint16_t watts);

View File

@ -6,6 +6,7 @@
#include <lora/lorawan.h>
#include <gpio.h>
#include <energy.h>
#define SETTINGS_KEY "LORAWAN_DEV_NONCE"
@ -41,12 +42,32 @@ static void dl_callback(uint8_t port, uint8_t flags, int16_t rssi, int8_t snr, u
LOG_HEXDUMP_INF(hex_data, len, "Payload: ");
}
/* Send interval command: 'i' + interval_minutes (1 byte)
* Format: ['i'][interval_minutes]
* Example: 'i' + 5 = 5 minutes
*/
if (len >= 2 && hex_data[0] == 'i')
{
uint8_t new_interval = hex_data[1];
send_interval_minutes = new_interval;
LOG_INF("Send interval updated to %d minutes", send_interval_minutes);
}
/* Load configuration: 'w' + watts (2 bytes big-endian)
* Format: ['w'][watts_high][watts_low]
* Example: 0x77 0x01 0x90 = 400W
*/
else if (len >= 3 && hex_data[0] == 'w')
{
uint16_t new_load = ((uint16_t)hex_data[1] << 8) | hex_data[2];
energy_set_load_watts(new_load);
LOG_INF("Load updated to %d W", new_load);
}
/* Combined packet format: [heating_enable][temp1_threshold][temp2_threshold]
* Byte 0: Heating control (0=DISABLED, 1=ENABLED)
* Byte 1: Room temperature threshold (°C)
* Byte 2: Floor temperature threshold (°C)
*/
if (len >= 3)
else if (len >= 3)
{
uint8_t heating_enable = hex_data[0];
uint8_t room_temp_threshold = hex_data[1];
@ -86,16 +107,6 @@ static void dl_callback(uint8_t port, uint8_t flags, int16_t rssi, int8_t snr, u
LOG_INF("Heating control logic enabled with new thresholds");
}
}
/* Send interval command: 'i' + interval_minutes (1 byte)
* Format: ['i'][interval_minutes]
* Example: 'i' + 5 = 5 minutes
*/
else if (len >= 2 && hex_data[0] == 'i')
{
uint8_t new_interval = hex_data[1];
send_interval_minutes = new_interval;
LOG_INF("Send interval updated to %d minutes", send_interval_minutes);
}
}
struct lorawan_downlink_cb downlink_cb = {
@ -342,13 +353,12 @@ void send_data_packet(char *data, uint8_t data_len)
}
}
void create_data_package(char *buffer, float temp_room_mlx, float temp_floor, float humidity, uint8_t relais_state)
void create_data_package(char *buffer, float temp_room_mlx, float temp_floor, float humidity, uint8_t relais_state, uint32_t on_time, uint32_t uptime, uint16_t load_watts)
{
int16_t temp = (int16_t)(temp_room_mlx * 100);
int16_t floor_temp = (int16_t)(temp_floor * 100);
int16_t hum = (int16_t)(humidity * 100);
// Packen der Daten in das Byte-Array
buffer[0] = (temp >> 8) & 0xFF;
buffer[1] = temp & 0xFF;
buffer[2] = (floor_temp >> 8) & 0xFF;
@ -356,4 +366,18 @@ void create_data_package(char *buffer, float temp_room_mlx, float temp_floor, fl
buffer[4] = (hum >> 8) & 0xFF;
buffer[5] = hum & 0xFF;
buffer[6] = relais_state;
/* Relay ON-time (uint24, seconds since boot) */
buffer[7] = (on_time >> 16) & 0xFF;
buffer[8] = (on_time >> 8) & 0xFF;
buffer[9] = on_time & 0xFF;
/* Uptime (uint24, seconds since boot) */
buffer[10] = (uptime >> 16) & 0xFF;
buffer[11] = (uptime >> 8) & 0xFF;
buffer[12] = uptime & 0xFF;
/* Load (uint16, Watt) */
buffer[13] = (load_watts >> 8) & 0xFF;
buffer[14] = load_watts & 0xFF;
}

View File

@ -29,4 +29,4 @@ void join_network_abp();
void send_data_packet(char *data, uint8_t data_len);
void create_data_package(char *package, float temp_room_mlx, float temp_floor, float humidity, uint8_t relais_state);
void create_data_package(char *package, float temp_room_mlx, float temp_floor, float humidity, uint8_t relais_state, uint32_t on_time, uint32_t uptime, uint16_t load_watts);

View File

@ -11,6 +11,7 @@
#include <sensors/mlx90614.h>
#include <sensors/sht4x.h>
#include <gpio.h>
#include <energy.h>
LOG_MODULE_REGISTER(g2h_heat_main);
@ -31,6 +32,7 @@ int main(void)
init_mlx90614();
init_sht4x();
init_lorawan();
energy_init();
// join_network_otaa();
join_network_abp();
@ -86,22 +88,26 @@ int main(void)
// Heat OFF if ANY temperature reaches its threshold
if (temp_below_threshold && floor_below_threshold)
{
relais_state = 1; // Heat on - both temps below thresholds
relais_state = 1;
}
else
{
relais_state = 0; // Heat off - at least one threshold reached
relais_state = 0;
}
energy_update_relay_state(relais_state);
set_relais_state(relais_state);
}
printk("relais: %d (heating_enabled: %d, temp_th: %d°C, floor_th: %d°C)\n",
relais_state, heating_logic_enabled, temperature_threshold, floor_temp_threshold);
/* Pack and send values */
char data[7];
create_data_package(data, temp, floor_temp, humidity, relais_state);
LOG_HEXDUMP_DBG(data, 7, "data");
send_data_packet(data, 7);
char data[15];
create_data_package(data, temp, floor_temp, humidity, relais_state,
energy_get_on_time_seconds(),
energy_get_uptime_seconds(),
energy_get_load_watts());
LOG_HEXDUMP_DBG(data, 15, "data");
send_data_packet(data, 15);
/* Delay until next cycle */
k_sleep(K_MINUTES(send_interval_minutes));