diff --git a/AGENTS.md b/AGENTS.md index e1c2dc4..67dba00 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,7 @@ This repository contains a small ESP8266/ESP32 Arduino project for monitoring wa - `arduino/arduino.ino` contains the Arduino sketch and main application logic. - `server/log.php` accepts measurement POST requests and appends them as JSON Lines. +- `server/functions.inc.php` contains shared PHP helpers, including JSON responses, HTTP requests, and Telegram notifications. - `data/YYYY.jsonl` is created at runtime for logged measurements and should not be treated as source code. - `README.md` gives the high-level project purpose. - `LICENSE` contains licensing information. @@ -29,12 +30,13 @@ arduino-cli upload -p COM3 --fqbn esp32:esp32:esp32 arduino arduino-cli monitor -p COM3 -c baudrate=115200 php -l server/log.php +php -l server/functions.inc.php ``` - `compile` verifies the sketch builds for the selected board. - `upload` flashes the firmware to the connected board. - `monitor` opens the serial console used by debug output. -- `php -l` checks the PHP logging endpoint for syntax errors. +- `php -l` checks the PHP scripts for syntax errors. Adjust `--fqbn` and `COM3` for the actual board and port. @@ -50,6 +52,8 @@ Prefer small functions with one responsibility. Keep hardware pins, URLs, timing For PHP scripts, keep them framework-free unless the project grows. Return JSON responses, use explicit HTTP status codes, and keep generated log files under `data/`. +Keep shared PHP helpers in `server/functions.inc.php`. Use the existing `telegram()` helper for Telegram notifications, and wrap notification calls so delivery failures do not change the HTTP response or stop measurement logging. + ## Hardware Notes For ESP8266 conductive level sensing, use external pull-down resistors from each `PIN_LEVEL1..4` input to `GND`. The common electrode is driven to positive voltage only during measurement. @@ -70,9 +74,12 @@ Before committing firmware changes: Before committing server changes: - Run `php -l server/log.php`. +- Run `php -l server/functions.inc.php` when shared helpers change. - Test that non-POST requests return HTTP 405 with `Allow: POST`. - Test a form POST such as `level1=1&level2=0&level3=0&level4=1`. - Verify that a single compact JSON object is appended to `data/YYYY.jsonl`. +- For notification changes, verify that Telegram messages are sent only when `level1..level4` differ from the previous saved measurement. +- Verify invalid level combinations such as `0100`, `1010`, and `1101` produce the measurement error notification without causing an HTTP error. ## Commit & Pull Request Guidelines diff --git a/server/functions.inc.php b/server/functions.inc.php new file mode 100644 index 0000000..9ffa30a --- /dev/null +++ b/server/functions.inc.php @@ -0,0 +1,61 @@ + $to, 'message' => $message)); + return json_encode($json, true); +} + +function notifyUsers(string $message): void +{ + foreach (['igor', 'veronika'] as $user) { + try { + set_error_handler(static function (int $severity, string $errorMessage): bool { + throw new ErrorException($errorMessage, 0, $severity); + }); + telegram($user, $message); + } catch (Throwable $e) { + error_log('Telegram notification failed for ' . $user . ': ' . $e->getMessage()); + } finally { + restore_error_handler(); + } + } +} diff --git a/server/log.php b/server/log.php index 04799c9..cca29b5 100644 --- a/server/log.php +++ b/server/log.php @@ -2,11 +2,72 @@ declare(strict_types=1); -function respondJson(int $statusCode, array $payload): void +require_once __DIR__.'/functions.inc.php'; + +function loadPreviousRecord(string $filename): ?array { - http_response_code($statusCode); - header('Content-Type: application/json; charset=utf-8'); - echo json_encode($payload) . PHP_EOL; + if (!is_file($filename)) { + return null; + } + + $lines = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if ($lines === false || count($lines) < 2) { + return null; + } + + // The current record has already been appended, so the previous one is the line before it. + $previous = json_decode($lines[count($lines) - 2], true); + if (!is_array($previous)) { + return null; + } + + return $previous; +} + +function levelsChanged(?array $previous, array $current): bool +{ + if ($previous === null) { + return false; + } + + foreach (['level1', 'level2', 'level3', 'level4'] as $name) { + if (!array_key_exists($name, $previous) || (int) $previous[$name] !== (int) $current[$name]) { + return true; + } + } + + return false; +} + +function isValidLevelState(array $levels): bool +{ + $lowerLevelInactive = false; + foreach (['level1', 'level2', 'level3', 'level4'] as $name) { + if ((int) $levels[$name] === 0) { + $lowerLevelInactive = true; + continue; + } + + if ($lowerLevelInactive) { + return false; + } + } + + return true; +} + +function calculateFilledPercent(array $levels): int +{ + $activeLevels = 0; + foreach (['level1', 'level2', 'level3', 'level4'] as $name) { + if ((int) $levels[$name] !== 1) { + break; + } + + $activeLevels++; + } + + return $activeLevels * 25; } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { @@ -83,6 +144,23 @@ if ($written === false) { exit; } +$previousRecord = loadPreviousRecord($filename); +if (levelsChanged($previousRecord, $record)) { + // Notify only when the level state changed compared to the previous saved measurement. + if (!isValidLevelState($record)) { + notifyUsers(sprintf( + 'Chyba merania hladiny kondenzátu klimatizácie (level1=%d, level2=%d, level3=%d, level4=%d)', + $record['level1'], + $record['level2'], + $record['level3'], + $record['level4'] + )); + } else { + $filledPercent = calculateFilledPercent($record); + notifyUsers(sprintf('Nádržka kondenzu klimatizácie: %d%%', $filledPercent)); + } +} + respondJson(200, [ 'success' => true, ]);