<?php
namespace SHServ\Middleware;
use \SHServ\Models\Devices;
use \SHServ\Models\Scripts;
use \SHServ\Helpers\DeviceScriptsHelper;
use \SHServ\Entities\Device;
abstract class ControlScripts {
protected $devices_model;
protected $device_scripts_helper;
protected $modes_context;
abstract public function register_sync_map(): void;
abstract protected function register_events_handlers(): void;
abstract protected function register_regular_scripts(): void;
abstract protected function register_actions_scripts(): void;
public function __construct() {
list($scope_folder, $scope_name) = explode("\\", str_replace("\\Scopes", "", str_replace("SHServ", "", static::class)));
if($scope_folder != "ControlScripts" or (new Scripts()) -> script_state("scope", $scope_name)) {
$this -> register_sync_map();
$this -> register_events_handlers();
$this -> register_regular_scripts();
$this -> register_actions_scripts();
}
}
protected function registry(): ScriptsRegistry {
return app() -> scripts_registry;
}
protected function add_event_handler(String $event_name, callable $handler): void {
events() -> handler("app:{$event_name}", function(Array $params) use ($handler, $event_name) {
try {
$handler($params["device"], $params["data"]);
} catch(\Exception $e) {
logging() -> error('php:ControlScripts', 'Event handler failed', [
'event' => $event_name,
'scope' => static::class,
'message' => $e -> getMessage(),
'trace' => $e -> getTraceAsString(),
]);
}
});
}
protected function devices(): Devices {
if(!$this -> devices_model) {
$this -> devices_model = new Devices();
}
return $this -> devices_model;
}
protected function helper(): DeviceScriptsHelper {
if(!$this -> device_scripts_helper) {
$this -> device_scripts_helper = new DeviceScriptsHelper($this -> devices());
}
return $this -> device_scripts_helper;
}
protected function mode(): \SHServ\Middleware\ModesContext {
if(!$this -> modes_context) {
$this -> modes_context = new \SHServ\Middleware\ModesContext();
}
return $this -> modes_context;
}
protected function get_scope_name(): String {
$ref = new \ReflectionClass(static::class);
return $ref -> getShortName();
}
protected function delay_action(String $timer_alias, String $action_alias, Array $params, int $delay_seconds): bool {
$timers = new \SHServ\Models\Timers();
return $timers -> create_timer($timer_alias, $this -> get_scope_name(), "action", $action_alias, $params, time() + $delay_seconds);
}
protected function delay_event(String $timer_alias, String $event_name, Array $params, int $delay_seconds): bool {
$timers = new \SHServ\Models\Timers();
return $timers -> create_timer($timer_alias, $this -> get_scope_name(), "event", $event_name, $params, time() + $delay_seconds);
}
protected function delay_regular(String $timer_alias, String $regular_alias, int $delay_seconds): bool {
$timers = new \SHServ\Models\Timers();
return $timers -> create_timer($timer_alias, $this -> get_scope_name(), "regular", $regular_alias, [], time() + $delay_seconds);
}
protected function cancel_timer(String $timer_alias): bool {
$timers = new \SHServ\Models\Timers();
return $timers -> cancel_timer($timer_alias, $this -> get_scope_name());
}
protected function add_regular_script(Array $attributes, callable $script): bool {
$reg = $this -> registry();
if(!isset($attributes["alias"])) {
return false;
}
if(isset($reg ->regular[$attributes["alias"]])) {
return false;
}
$ref = new \ReflectionClass(static::class);
$path_info = pathinfo($ref -> getFileName());
$attributes["name"] = $attributes["name"] ?? "unknown";
$attributes["description"] = $attributes["description"] ?? "";
$attributes["classname"] = static::class;
$attributes["path"] = $path_info["dirname"];
$attributes["filename"] = $path_info["basename"];
$attributes["author"] = $attributes["author"] ?? "Unknown author";
$reg ->regular[$attributes["alias"]] = [
"attributes" => $attributes,
"code" => $this -> get_source_code($script),
"script" => $script
];
return true;
}
public static function flush_statics(): void {
app() -> scripts_registry -> flush();
}
public static function get_regular_scripts(): Array {
return app() -> scripts_registry -> getRegularScripts();
}
public static function get_actions_scripts(): Array {
return app() -> scripts_registry -> getActionsScripts();
}
public function add_action_script(Array $attributes, callable $script): bool {
$reg = $this -> registry();
if(!isset($attributes["alias"])) {
return false;
}
if(isset($reg ->actions[$attributes["alias"]])) {
return false;
}
$ref = new \ReflectionClass(static::class);
$path_info = pathinfo($ref -> getFileName());
$attributes["name"] = $attributes["name"] ?? "unknown";
$attributes["description"] = $attributes["description"] ?? "";
$attributes["classname"] = static::class;
$attributes["path"] = $path_info["dirname"];
$attributes["filename"] = $path_info["basename"];
$attributes["author"] = $attributes["author"] ?? "Unknown author";
$reg ->actions[$attributes["alias"]] = [
"attributes" => $attributes,
"code" => $this -> get_source_code($script),
"script" => $script,
"params_schema" => $attributes["params_schema"] ?? null
];
return true;
}
public static function run_action_script(String $alias, Array $params): Array | null {
$reg = app() -> scripts_registry;
if(!isset($reg ->actions[$alias])) {
return null;
}
$scripts_model = new Scripts();
if(!$scripts_model -> script_state("action", $alias)) {
return null;
}
logging() -> info('php:ControlScripts', 'Run action script', ['alias' => $alias]);
$start_time = microtime(true);
$result = $reg ->actions[$alias]["script"]($params);
$exec_time = microtime(true) - $start_time;
$exec_time = round($exec_time, 3);
logging() -> debug('php:ControlScripts', 'Action script finished', ['alias' => $alias, 'exec_time' => $exec_time]);
return [
"result" => $result,
"exec_time" => "{$exec_time} seconds"
];
}
public static function run_regular_script(String $alias): bool {
$reg = app() -> scripts_registry;
if(!isset($reg ->regular[$alias])) {
return false;
}
$scripts_model = new Scripts();
if(!$scripts_model -> script_state("regular", $alias)) {
return false;
}
logging() -> info('php:ControlScripts', 'Run regular script', ['alias' => $alias]);
try {
$reg ->regular[$alias]["script"]();
} catch(\Exception $e) {
logging() -> error('php:ControlScripts', 'Regular script failed', ['alias' => $alias, 'message' => $e -> getMessage()]);
return false;
}
return true;
}
protected function get_source_code($func): String {
$ref_func = new \ReflectionFunction($func);
$file_name = $ref_func -> getFileName();
$start_line = $ref_func -> getStartLine();
$end_line = $ref_func -> getEndLine();
$lines = file($file_name);
$code = implode(
"",
array_slice(
$lines,
$start_line - 1,
$end_line - $start_line + 1
)
);
return $code;
}
protected function add_sync_connection(Array $sync_connection) {
$registry = app() -> scripts_registry;
foreach($registry -> syncMap["connections"] as $existing) {
if($existing === $sync_connection) {
return;
}
}
$registry -> syncMap["connections"][] = $sync_connection;
}
public function sync_map() {
return app() -> scripts_registry -> getSyncMap();
}
// --- Button helpers ---
protected function btn_on_online(String $alias, Array $muted = []): void {
$this -> add_event_handler("button@{$alias}.online", function(Device $btns_block, Array $data) use ($muted) {
$btns_block_api = $btns_block -> device_api();
if($btns_block_api instanceof \SHServ\Tools\DeviceAPI\Button) {
foreach($muted as $ch) {
$btns_block_api -> set_channel_state("mute", $ch);
}
}
$this -> helper() -> sync_btn_channels($this -> sync_map(), $btns_block -> alias);
});
}
protected function set_btns_click_handlers($alias): void {
$self = $this;
$buttons = $this -> helper() -> prepare_sync_map_by_alias($this -> sync_map(), $alias);
foreach($buttons as $btn_channel => $entry) {
if($entry[0]["type"] != "relay") {
continue;
}
$relay_alias = $entry[0]["alias"];
$relay_channel = $entry[0]["channel"];
$this -> add_event_handler("button@{$alias}({$btn_channel}).press", function(Device $btns_block, Array $data) use ($self, $btn_channel, $relay_alias, $relay_channel) {
$btns_block_api = $btns_block -> device_api();
$relay_api = $this -> devices() -> by_alias($relay_alias) -> device_api();
if($relay_api instanceof \SHServ\Tools\DeviceAPI\Relay and $btns_block_api instanceof \SHServ\Tools\DeviceAPI\Button) {
$relay_api -> toggle_channel($relay_channel);
$this -> helper() -> sync_relay_to_btns($this -> sync_map(), $relay_alias);
}
});
}
}
/**
* Автоматическая регистрация обработчиков для блоков кнопок.
* Массив: alias => ["mute" => [каналы]]
* Для каждого alias регистрирует btn_on.online (mute + sync) и click handlers (toggle relay + sync).
*/
protected function auto_button_bindings(Array $config): void {
foreach($config as $alias => $options) {
$muted = $options["mute"] ?? [];
$this -> btn_on_online($alias, $muted);
$this -> set_btns_click_handlers($alias);
}
}
// --- Group helpers ---
protected function group_set_state(Array $targets, bool $state): Array {
return $this -> helper() -> group_set_state($targets, $state, $this -> sync_map());
}
protected function group_toggle(Array $targets): Array {
return $this -> helper() -> group_toggle($targets, $this -> sync_map());
}
/**
* Зарегистрировать action script для группового toggle нескольких реле.
* Pairs: строка (alias) или [alias, channel].
*/
protected function add_group_toggle_action(String $alias, String $name, Array $pairs, Array $extra_attrs = []): bool {
$attrs = array_merge([
"alias" => $alias,
"name" => $name,
], $extra_attrs);
return $this -> add_action_script($attrs, function($params) use ($pairs) {
return $this -> group_toggle($pairs);
});
}
/**
* Зарегистрировать action script для toggle канала реле.
* После toggle автоматически синхронизирует индикаторы кнопок через sync_map.
*/
protected function add_toggle_action(String $alias, String $name, String $relay_alias, int $channel = 0, Array $extra_attrs = []): bool {
$attrs = array_merge([
"alias" => $alias,
"name" => $name,
], $extra_attrs);
return $this -> add_action_script($attrs, function($params) use ($relay_alias, $channel) {
$relay_api = $this -> devices() -> by_alias($relay_alias) -> device_api();
$result = false;
if($relay_api instanceof \SHServ\Tools\DeviceAPI\Relay) {
$result = $relay_api -> toggle_channel($channel);
$this -> helper() -> sync_relay_to_btns($this -> sync_map(), $relay_alias);
}
return ["result" => $result];
});
}
/**
* Зарегистрировать action script для включения (on) канала реле.
* Для многоканальных реле укажите $channel, для одноканальных оставьте null.
*/
protected function add_on_action(String $alias, String $name, String $relay_alias, ?int $channel = null, Array $extra_attrs = []): bool {
$attrs = array_merge([
"alias" => $alias,
"name" => $name,
], $extra_attrs);
return $this -> add_action_script($attrs, function($params) use ($relay_alias, $channel) {
$relay_api = $this -> devices() -> by_alias($relay_alias) -> device_api();
$result = false;
if($relay_api instanceof \SHServ\Tools\DeviceAPI\Relay) {
$result = $channel === null
? $relay_api -> set_state(true)
: $relay_api -> set_channel_state(true, $channel);
$this -> helper() -> sync_relay_to_btns($this -> sync_map(), $relay_alias);
}
return ["result" => $result];
});
}
/**
* Зарегистрировать action script для выключения (off) канала реле.
* Для многоканальных реле укажите $channel, для одноканальных оставьте null.
*/
protected function add_off_action(String $alias, String $name, String $relay_alias, ?int $channel = null, Array $extra_attrs = []): bool {
$attrs = array_merge([
"alias" => $alias,
"name" => $name,
], $extra_attrs);
return $this -> add_action_script($attrs, function($params) use ($relay_alias, $channel) {
$relay_api = $this -> devices() -> by_alias($relay_alias) -> device_api();
$result = false;
if($relay_api instanceof \SHServ\Tools\DeviceAPI\Relay) {
$result = $channel === null
? $relay_api -> set_state(false)
: $relay_api -> set_channel_state(false, $channel);
$this -> helper() -> sync_relay_to_btns($this -> sync_map(), $relay_alias);
}
return ["result" => $result];
});
}
/**
* Зарегистрировать action script для управления люком (open/close).
*/
protected function add_hatch_action(String $alias, String $name, String $hatch_alias, String $operation, int $percent = 100, Array $extra_attrs = []): bool {
$attrs = array_merge([
"alias" => $alias,
"name" => $name,
], $extra_attrs);
return $this -> add_action_script($attrs, function($params) use ($hatch_alias, $operation, $percent) {
$hatch_api = $this -> devices() -> by_alias($hatch_alias) -> device_api();
$result = false;
if($hatch_api instanceof \SHServ\Tools\DeviceAPI\Hatch) {
$result = $operation === "open"
? $hatch_api -> open($percent)
: $hatch_api -> close($percent);
}
return ["result" => $result];
});
}
}