Newer
Older
smart-home-server / server / SHServ / Controllers / CronController.php
<?php

namespace SHServ\Controllers;

use \SHServ\Models\Devices;
use \SHServ\Tools\DeviceScanner;
use \SHServ\Middleware\ControlScripts;
use \SHServ\Models\Scripts;

class CronController extends \SHServ\Middleware\Controller {
	protected function ensure_localhost_only() {
		$remote_addr = $_SERVER['REMOTE_ADDR'] ?? '';
		if(!in_array($remote_addr, ['127.0.0.1', '::1'])) {
			http_response_code(403);
			header('Content-Type: application/json');
			echo json_encode([
				"status" => false,
				"error_alias" => "forbidden",
				"msg" => "Forbidden: local access only"
			]);
			exit;
		}
	}

	protected function createDeviceScanner(): DeviceScanner {
		return new DeviceScanner();
	}

	public function run_regular_cron_scripts() {
		$this -> ensure_localhost_only();

		set_time_limit(300);

		$lockFile = sys_get_temp_dir() . "/shserv-regular-scripts.lock";
		$fp = fopen($lockFile, "c");
		if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
			if ($fp) {
				fclose($fp);
			}
			logging() -> warn('php:Cron', 'Regular scripts skipped: another instance is running');
			return;
		}

		try {
			$scripts_model = new Scripts();
			$regular_scripts = ControlScripts::get_regular_scripts();

			foreach($regular_scripts as $alias => $script) {
				if(!$scripts_model -> script_state("regular", $alias)) {
					continue;
				}

				logging() -> info('php:Cron', 'Running regular script', ['alias' => $alias]);
				$result = ControlScripts::run_regular_script($alias);
				logging() -> info('php:Cron', 'Regular script finished', ['alias' => $alias, 'result' => $result]);
			}
		} catch(\Exception $e) {
			logging() -> error('php:Cron', 'Regular scripts batch failed', ['message' => $e -> getMessage()]);
		} finally {
			flock($fp, LOCK_UN);
			fclose($fp);
		}
	}

	public function run_timers() {
		$this -> ensure_localhost_only();

		set_time_limit(300);

		$lockFile = sys_get_temp_dir() . "/shserv-timers.lock";
		$fp = fopen($lockFile, "c");
		if (!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
			if ($fp) {
				fclose($fp);
			}
			logging() -> warn('php:Cron', 'Timers skipped: another instance is running');
			return;
		}

		try {
			$timers_model = new \SHServ\Models\Timers();
			$pending = $timers_model -> get_pending_timers();

			foreach($pending as $timer) {
				try {
					switch($timer["target_type"]) {
						case "action":
							ControlScripts::run_action_script($timer["target_alias"], json_decode($timer["params"], true) ?: []);
							break;
						case "event":
							events() -> app_call($timer["target_alias"], json_decode($timer["params"], true) ?: []);
							break;
						case "regular":
							ControlScripts::run_regular_script($timer["target_alias"]);
							break;
					}

					$timers_model -> mark_status((int)$timer["id"], "executed");
					logging() -> info('php:Cron', 'Timer executed', [
						'timer_alias'  => $timer["timer_alias"],
						'target_type'  => $timer["target_type"],
						'target_alias' => $timer["target_alias"]
					]);
				} catch(\Exception $e) {
					$timers_model -> mark_status((int)$timer["id"], "failed", $e -> getMessage());
					logging() -> error('php:Cron', 'Timer failed', [
						'timer_alias' => $timer["timer_alias"],
						'error'       => $e -> getMessage()
					]);
				}
			}
		} catch(\Exception $e) {
			logging() -> error('php:Cron', 'Timers batch failed', ['message' => $e -> getMessage()]);
		} finally {
			flock($fp, LOCK_UN);
			fclose($fp);
		}
	}

	public function status_update_scanning() {
		$this -> ensure_localhost_only();

		$threshold = FCONF["device_offline_threshold"] ?? 300;
		$now = time();

		$devices_model = new Devices();
		$active_devices = $devices_model -> get_device_list();

		// 1. Помечаем оффлайн устройства с устаревшим last_contact
		foreach($active_devices as $device) {
			$last_contact_ts = strtotime($device -> last_contact);
			if($last_contact_ts === false) {
				continue;
			}

			$seconds_since_contact = $now - $last_contact_ts;

			if($seconds_since_contact > $threshold) {
				if($device -> connection_status != "lost") {
					$device -> connection_status = "lost";
					$device -> update();
				}
			}
		}

		// 2. Сканируем сеть — восстанавливаем lost и обновляем IP при роуминге
		$device_scanner = $this -> createDeviceScanner();
		$found_devices = $device_scanner -> scan_range(FCONF["device_ip_range"][0], FCONF["device_ip_range"][1]);

		$devices_model -> reconcile_scan_results($found_devices);
	}
}