diff --git a/automation/Common.php b/automation/Common.php index c58d570..6ea7c5e 100644 --- a/automation/Common.php +++ b/automation/Common.php @@ -2,8 +2,6 @@ namespace ControlScripts; -use \SHServ\Entities\Device; - trait Common { public function register_global_device_sync_map() { $this -> add_sync_connection([ @@ -121,41 +119,4 @@ ]); } - - public 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); - }); - } - - public 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); - } - }); - } - } -} \ No newline at end of file +} diff --git a/automation/Scopes/BathroomScope.php b/automation/Scopes/BathroomScope.php new file mode 100644 index 0000000..b218e91 --- /dev/null +++ b/automation/Scopes/BathroomScope.php @@ -0,0 +1,27 @@ + register_global_device_sync_map(); + } + + public function register_events_handlers(): void { + // Управление светом в ванной происходит через кнопки hall_secondary ( HallScope ) + } + + public function register_actions_scripts(): void { + $this -> add_toggle_action("bathroom_light_switch", "Ванная", "bathroom_2_light", 0, [ + "description" => "Вкл/Выкл. свет в ванной", + "author" => "Eugene Sukhodolskiy", + ]); + } + + public function register_regular_scripts(): void {} +} diff --git a/automation/Scopes/HallScope.php b/automation/Scopes/HallScope.php new file mode 100644 index 0000000..dee9c05 --- /dev/null +++ b/automation/Scopes/HallScope.php @@ -0,0 +1,53 @@ + register_global_device_sync_map(); + } + + public function register_events_handlers(): void { + $this -> auto_button_bindings([ + "first_hall_buttons1" => [], + "first_hall_buttons2" => [], + "hall_secondary" => [], + ]); + } + + public function register_actions_scripts(): void { + $this -> add_group_toggle_action("hall1_toggle", "Холл 1", [ + "light_hub_1:3", + ]); + + $this -> add_group_toggle_action("hall2_toggle", "Холл 2", [ + "light_hub_1:2", + ]); + + $this -> add_group_toggle_action("all_hub_toggle", "Весь хаб", [ + "light_hub_1:0", + "light_hub_1:1", + "light_hub_1:2", + "light_hub_1:3", + ]); + + $this -> add_hatch_action("hatch_open", "Открыть люк", "hatch_motor", "open", 100, [ + "icon" => '', + "description" => "Открыть люк в мансарду", + "author" => "Eugene Sukhodolskiy", + ]); + + $this -> add_hatch_action("hatch_close", "Закрыть люк", "hatch_motor", "close", 100, [ + "icon" => '', + "description" => "Закрыть люк в мансарду", + "author" => "Eugene Sukhodolskiy", + ]); + } + + public function register_regular_scripts(): void {} +} diff --git a/automation/Scopes/KitchenScope.php b/automation/Scopes/KitchenScope.php new file mode 100644 index 0000000..c40c657 --- /dev/null +++ b/automation/Scopes/KitchenScope.php @@ -0,0 +1,36 @@ + register_global_device_sync_map(); + } + + public function register_events_handlers(): void { + $this -> auto_button_bindings([ + "kitchen_buttons_1" => [], + "kitchen_buttons_2" => [], + ]); + } + + public function register_actions_scripts(): void { + $this -> add_group_toggle_action("kitchen1_toggle", "Кухня 1", [ + "fisrt_floor_big_relay:0", + ]); + + $this -> add_group_toggle_action("kitchen2_toggle", "Кухня 2", [ + "fisrt_floor_big_relay:1", + ]); + + $this -> add_on_action("kitchen1_on", "Кухня 1 вкл", "fisrt_floor_big_relay", 0); + $this -> add_on_action("kitchen2_on", "Кухня 2 вкл", "fisrt_floor_big_relay", 1); + } + + public function register_regular_scripts(): void {} +} diff --git a/automation/Scopes/LightHubScope.php b/automation/Scopes/LightHubScope.php deleted file mode 100644 index b832f4a..0000000 --- a/automation/Scopes/LightHubScope.php +++ /dev/null @@ -1,163 +0,0 @@ - register_global_device_sync_map(); - } - - public function register_events_handlers(): void { - $this -> btn_on_online("master_room_btns", [2, 3]); - $this -> btn_on_online("btns_hall2_1"); - $this -> btn_on_online("hall_secondary"); - $this -> btn_on_online("bed_btns_right_1"); - $this -> btn_on_online("bed_btns_left"); - $this -> btn_on_online("kitchen_buttons_1"); - $this -> btn_on_online("kitchen_buttons_2"); - $this -> btn_on_online("first_hall_buttons1", [3]); - $this -> btn_on_online("first_hall_buttons2"); - $this -> set_btns_click_handlers("master_room_btns"); - $this -> set_btns_click_handlers("btns_hall2_1"); - $this -> set_btns_click_handlers("hall_secondary"); - $this -> set_btns_click_handlers("bed_btns_right_1"); - $this -> set_btns_click_handlers("bed_btns_left"); - $this -> set_btns_click_handlers("kitchen_buttons_1"); - $this -> set_btns_click_handlers("kitchen_buttons_2"); - $this -> set_btns_click_handlers("first_hall_buttons1"); - $this -> set_btns_click_handlers("first_hall_buttons2"); - - - - $this -> btn_on_online("plants_room_btns", [1]); - $this -> set_btns_click_handlers("plants_room_btns"); - - } - - public function register_actions_scripts(): void { - $this -> add_action_script([ - "alias" => "master_room_lamp_switcher", - "name" => "Осн. свет в спальне", - "icon" => '', - "description" => "Включить/выключить основной свет в спальне", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return $this -> lamp_switch("light_hub_1", 1); - }); - - $this -> add_action_script([ - "alias" => "hallway2_lamp_switcher", - "name" => "Осн. свет в холе, эт2", - "icon" => '', - "description" => "Включить/выключить основной свет в холе на втором этаже", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return $this -> lamp_switch("light_hub_1", 2); - }); - - $this -> add_action_script([ - "alias" => "bathroom2_lamp_switcher", - "name" => "Осн. свет в ванной 2", - "icon" => '', - "description" => "Включить/выключить основной свет в ванной комнате на верху", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return $this -> lamp_switch("light_hub_1", 3); - }); - - // --- - - $this -> add_action_script([ - "alias" => "hall1_light_switcher", - "name" => "Свет в прихожей", - "icon" => '', - "description" => "Включить/выключить свет в прихожей", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return - $this -> lamp_switch("fisrt_floor_big_relay", 3) and $this -> lamp_switch("fisrt_floor_big_relay", 4); - }); - - $this -> add_action_script([ - "alias" => "kitchen_light_switcher", - "name" => "Полный свет на кухне", - "icon" => '', - "description" => "Включить/выключить весь имеющийся свет на кухне", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return - $this -> lamp_switch("fisrt_floor_big_relay", 0) and $this -> lamp_switch("fisrt_floor_big_relay", 1); - }); - - $this -> add_action_script([ - "alias" => "hatch_open", - "name" => "Открыть люк", - "icon" => '', - "description" => "Открыть люк на кухне", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return - $this -> hatch_open("kitchen_hatch", 100); - }); - - $this -> add_action_script([ - "alias" => "hatch_close", - "name" => "Закрыть люк", - "icon" => '', - "description" => "Закрыть люк на кухне", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return - $this -> hatch_close("kitchen_hatch", 100); - }); - } - - public function register_regular_scripts(): void { - } - - // ACTIONS - - protected function lamp_switch(String $relay_alias, int $channel): Array { - $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 - ]; - } - - protected function hatch_do(String $hatch_alias, String $operation = "open", int $percent = 100): Array | bool { - $hatch_api = $this -> devices() -> by_alias($hatch_alias) -> device_api(); - - $result = false; - if($hatch_api instanceof \SHServ\Tools\DeviceAPI\Hatch) { - switch($operation) { - case "open": $result = $hatch_api -> open($percent); - break; - case "close": $result = $hatch_api -> close($percent); - break; - } - } - - return $result; - } - - protected function hatch_open(String $hatch_alias, int $percent = 100) { - return $this -> hatch_do($hatch_alias, "open", $percent); - } - - protected function hatch_close(String $hatch_alias, int $percent = 100) { - return $this -> hatch_do($hatch_alias, "close", $percent); - } - -} \ No newline at end of file diff --git a/automation/Scopes/MasterBedroomScope.php b/automation/Scopes/MasterBedroomScope.php new file mode 100644 index 0000000..dc9177a --- /dev/null +++ b/automation/Scopes/MasterBedroomScope.php @@ -0,0 +1,30 @@ + register_global_device_sync_map(); + } + + public function register_events_handlers(): void { + $this -> auto_button_bindings([ + "master_room_btns" => [], + "bed_btns_right_1" => [], + "bed_btns_left" => [], + ]); + } + + public function register_actions_scripts(): void { + $this -> add_group_toggle_action("master_toggle", "Спальня", [ + "light_hub_1:1", + ]); + } + + public function register_regular_scripts(): void {} +} diff --git a/automation/Scopes/OfficeRoomScope.php b/automation/Scopes/OfficeRoomScope.php deleted file mode 100644 index 735a2a7..0000000 --- a/automation/Scopes/OfficeRoomScope.php +++ /dev/null @@ -1,70 +0,0 @@ - register_global_device_sync_map(); - } - - public function register_events_handlers(): void { - $this -> btn_on_online("buttons_office_room"); - $this -> set_btns_click_handlers("buttons_office_room"); - } - - public function register_actions_scripts(): void { - $this -> add_action_script([ - "alias" => "computer_table_lamp_switch", - "name" => "Комп. лампа", - "icon" => '', - "description" => "Вкл/Выкл. настольную компьютерную лампу", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return $this -> lamp_switch("computer_table_lamp", 0); - }); - - $this -> add_action_script([ - "alias" => "craft_table_lamp_switch", - "name" => "Крафт. лампа", - "icon" => '', - "description" => "Вкл/Выкл. настольную лампу на столе для крафта", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return $this -> lamp_switch("craft_table_lamp", 0); - }); - - $this -> add_action_script([ - "alias" => "main_lamps_switcher", - "name" => "Осн. свет в кабинете", - "icon" => '', - "description" => "Включить/выключить основной свет в кабинете", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return $this -> lamp_switch("light_hub_1", 0); - }); - } - - public function register_regular_scripts(): void { - } - - // ACTIONS - - protected function lamp_switch(String $relay_alias, int $channel): Array { - $btns_block_api = $this -> devices() -> by_alias("buttons_office_room") -> device_api(); - $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 - ]; - } -} \ No newline at end of file diff --git a/automation/Scopes/OfficeScope.php b/automation/Scopes/OfficeScope.php new file mode 100644 index 0000000..87b22b7 --- /dev/null +++ b/automation/Scopes/OfficeScope.php @@ -0,0 +1,36 @@ + register_global_device_sync_map(); + } + + public function register_events_handlers(): void { + $this -> auto_button_bindings([ + "buttons_office_room" => [], + ]); + } + + public function register_actions_scripts(): void { + $this -> add_toggle_action("computer_table_lamp_switch", "Комп. лампа", "computer_table_lamp", 0, [ + "icon" => '', + "description" => "Вкл/Выкл. настольную компьютерную лампу", + "author" => "Eugene Sukhodolskiy", + ]); + + $this -> add_toggle_action("craft_table_lamp_switch", "Мастерская лампа", "craft_table_lamp", 0, [ + "icon" => '', + "description" => "Вкл/Выкл. лампу в мастерской", + "author" => "Eugene Sukhodolskiy", + ]); + } + + public function register_regular_scripts(): void {} +} diff --git a/automation/Scopes/OutdoorScope.php b/automation/Scopes/OutdoorScope.php new file mode 100644 index 0000000..45c2e53 --- /dev/null +++ b/automation/Scopes/OutdoorScope.php @@ -0,0 +1,39 @@ + register_global_device_sync_map(); + } + + public function register_events_handlers(): void { + $this -> auto_button_bindings([ + "buttons_backdoor" => [], + "master_door_btns" => [], + ]); + } + + public function register_actions_scripts(): void { + $this -> add_group_toggle_action("all_spotlights_toggle", "Все прожекторы", [ + "spotlight_main_back_1", + "spotlight_main_back_2", + "spotlight_main_front_1", + ]); + + $this -> add_on_action("front_spotlight_on", "Передний прожектор вкл", "spotlight_main_front_1"); + $this -> add_off_action("front_spotlight_off", "Передний прожектор выкл", "spotlight_main_front_1"); + + $this -> add_group_toggle_action("back_spotlights_on", "Задние прожекторы вкл", [ + "spotlight_main_back_1", + "spotlight_main_back_2", + ]); + } + + public function register_regular_scripts(): void {} +} diff --git a/automation/Scopes/PlantsRoomScope.php b/automation/Scopes/PlantsRoomScope.php new file mode 100644 index 0000000..7faf259 --- /dev/null +++ b/automation/Scopes/PlantsRoomScope.php @@ -0,0 +1,39 @@ + register_global_device_sync_map(); + } + + public function register_events_handlers(): void { + $this -> auto_button_bindings([ + "plants_room_btns" => [], + ]); + } + + public function register_actions_scripts(): void { + $this -> add_toggle_action("plants_room_light_switch", "Свет в комнате растений", "plants_room_light", 0, [ + "description" => "Вкл/Выкл. свет в комнате растений", + "author" => "Eugene Sukhodolskiy", + ]); + + $this -> add_toggle_action("italy_lamp_switch", "Italy лампа", "italy_lamp_relay", 0, [ + "description" => "Вкл/Выкл. Italy лампу", + "author" => "Eugene Sukhodolskiy", + ]); + + $this -> add_toggle_action("floor_lamp_switch", "Торшер", "floor_lamp_relay", 0, [ + "description" => "Вкл/Выкл. торшер", + "author" => "Eugene Sukhodolskiy", + ]); + } + + public function register_regular_scripts(): void {} +} diff --git a/automation/Scopes/ServerRoomScope.php b/automation/Scopes/ServerRoomScope.php new file mode 100644 index 0000000..48e4e18 --- /dev/null +++ b/automation/Scopes/ServerRoomScope.php @@ -0,0 +1,27 @@ + register_global_device_sync_map(); + } + + public function register_events_handlers(): void { + // btns_hall2_1 физически находятся в холле и обрабатываются в HallScope + } + + public function register_actions_scripts(): void { + $this -> add_toggle_action("server_room_light_switch", "Серверная", "server_room_light", 0, [ + "description" => "Вкл/Выкл. свет в серверной", + "author" => "Eugene Sukhodolskiy", + ]); + } + + public function register_regular_scripts(): void {} +} diff --git a/automation/Scopes/SpotlightsScope.php b/automation/Scopes/SpotlightsScope.php deleted file mode 100644 index 7e6fc3c..0000000 --- a/automation/Scopes/SpotlightsScope.php +++ /dev/null @@ -1,125 +0,0 @@ - register_global_device_sync_map(); - } - - public function register_events_handlers(): void { - $this -> set_btns_click_handlers("buttons_backdoor"); - $this -> set_btns_click_handlers("master_door_btns"); - $this -> btn_on_online("buttons_backdoor", [3]); - $this -> btn_on_online("master_door_btns", [2]); - } - - public function register_actions_scripts(): void { - $this -> add_action_script([ - "alias" => "spotlights_on", - "name" => "All Spotlights On", - "icon" => '', - "description" => "Включить все прожекторы", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return $this -> all_spotlight_switch(true); - }); - - $this -> add_action_script([ - "alias" => "spotlights_off", - "name" => "All Spotlights Off", - "icon" => '', - "description" => "Выключить все прожекторы", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return $this -> all_spotlight_switch(false); - }); - - $this -> add_action_script([ - "alias" => "front_spotlight_on", - "name" => "ВКЛ фронтальный прожектор", - "icon" => '', - "description" => "Включить только фронтальный прожектор на главном входе", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return $this -> spotlight_switch("spotlight_main_front_1", true); - }); - - $this -> add_action_script([ - "alias" => "front_spotlight_off", - "name" => "ВЫКЛ фронтальный прожектор", - "icon" => '', - "description" => "Выключить только фронтальный прожектор на главном входе", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - return $this -> spotlight_switch("spotlight_main_front_1", false); - }); - } - - public function register_regular_scripts(): void { - $this -> add_regular_script([ - "alias" => "spotlights_by_time", - "name" => "Spotlights by Time", - "description" => "Управление уличными прожекторами по времени", - "author" => "Eugene Sukhodolskiy" - ], function () { - // $tz = new \DateTimeZone('Europe/Kyiv'); - - // $now = new \DateTime('now', $tz); - // $from = new \DateTime('9:00', $tz); - // $to = new \DateTime('16:30', $tz); - - // if ($now >= $from && $now <= $to) { - // $this -> all_spotlight_switch(false); - // } - }); - } - - // ACTIONS - - protected function spotlight_switch(String $relay_alias, Bool $state = false) { - $device_entries = $this -> helper() -> get_sync_entries_by_type($this -> sync_map(), "relay"); - - $relay_api = $this -> devices() -> by_alias($relay_alias) -> device_api(); - $result = false; - if($relay_api instanceof \SHServ\Tools\DeviceAPI\Relay) { - $result = $relay_api -> set_state($state); - $this -> helper() -> sync_relay_to_btns($this -> sync_map(), $relay_alias); - } - - return [ - "result" => $result - ]; - } - - protected function all_spotlight_switch(Bool $state = false): Array { - $results = []; - $device_entries = $this -> helper() -> get_sync_entries_by_type($this -> sync_map(), "relay"); - - foreach($device_entries as $device_entry) { - $relay_api = $this -> devices() -> by_alias($device_entry["alias"]) -> device_api(); - $result = false; - $ch = $device_entry["channel"]; - - if($relay_api instanceof \SHServ\Tools\DeviceAPI\Relay) { - $status = $relay_api -> get_status(); - - if(isset($status["channels"][$ch]["state"]) and $status["channels"][$ch]["state"] != ($state ? "on" : "off")) { - $result = $relay_api -> set_state($state); - } - } - - $results[$device_entry["alias"]] = $result; - $this -> helper() -> sync_relay_to_btns($this -> sync_map(), $device_entry["alias"]); - } - - - return [ - "devices" => $results - ]; - } -} \ No newline at end of file diff --git a/automation/Scopes/TestScriptsScope.php b/automation/Scopes/TestScriptsScope.php deleted file mode 100644 index b416842..0000000 --- a/automation/Scopes/TestScriptsScope.php +++ /dev/null @@ -1,54 +0,0 @@ - stand_btn_pressed_to_stand_relay(); - } - - public function register_sync_map(): void { - - } - - public function register_actions_scripts(): void { - $this -> add_action_script([ - "alias" => "stand_relay_toggle", - "icon" => '', - "name" => "Toggle Stang Relay", - "description" => "Управление тестовым стендом у меня на столе. Просто переключатель", - "author" => "Eugene Sukhodolskiy" - ], function($params) { - $device = $this -> devices() -> by_alias("test_stand_relay"); - return [ - "device_response" => $device -> device_api() -> toggle_channel(), - ]; - }); - } - - public function register_regular_scripts(): void { - // $this -> add_regular_script([ - // "alias" => "regular_script_alias2", - // "name" => "regular script name 2", - // "description" => "regular script description 2", - // "author" => "Eugene Sukhodolskiy" - // ], function() { - // try { - // $button = $this -> devices() -> by_alias("backdoor_buttons"); - // $btn_api = $button -> device_api(); - - // if($btn_api instanceof \SHServ\Tools\DeviceAPI\Button) { - // if(($btn_api -> get_status())["channels"][2]["indicator"] != "error") { - // $btn_api -> set_channel_state("error", 2); - // } else { - // $btn_api -> set_channel_state("mute", 2); - // } - // } - // } catch(\Exception $e) { - // echo $e -> getMessage(); - // } - // }); - } -} \ No newline at end of file diff --git a/docs/control-scripts-guide.md b/docs/control-scripts-guide.md index 7df4d76..ae13f9e 100644 --- a/docs/control-scripts-guide.md +++ b/docs/control-scripts-guide.md @@ -2,7 +2,7 @@ Control Scripts — это PHP-классы автоматизации. Базовый класс находится в `server/SHServ/Middleware/ControlScripts.php`, интерфейс — в `server/SHServ/Implements/ControlScriptsInterface.php`. -Scope-классы располагаются в `server/ControlScripts/Scopes/` (или другом месте, если настроено иначе). Все Scope-файлы загружаются автоматически при старте сервера. +Scope-классы располагаются в `automation/Scopes/` (или другом месте, если настроено иначе). Все Scope-файлы загружаются автоматически при старте сервера. --- @@ -31,6 +31,51 @@ --- +## Рекомендуемая структура Scope-классов + +По умолчанию **один Scope = одна физическая комната или чёткая зона**. Это совпадает с тем, как человек мыслит свой дом: «включить свет на кухне», «автоматика в спальне». + +### Что кладём в Scope комнаты + +| Что | Пример | +|-----|--------| +| **Sync map** | Кнопки и реле, физически находящиеся в этой комнате | +| **Events** | `button@kitchen_buttons_1.press`, `motion@hall_sensor.online` | +| **Actions** | Ручные скрипты для устройств комнаты | +| **Regular** | Автоматика комнаты: автоотключение света в ванне через 10 мин | + +### Исключения — функциональные скоупы + +Когда логика явно не привязана к одной комнате, выделяйте отдельный Scope: + +| Тип | Пример | +|-----|--------| +| **Уличное / общее** | `OutdoorScope` — прожекторы, кнопки у входных дверей | +| **Сценарии** | `ScenesScope` — «Спокойной ночи», «Ушёл из дома» | +| **Инфраструктура** | `SystemScope` — health-check'и, резервное копирование, watchdog | + +### Cross-room связи + +Кнопки из одной комнаты часто управляют светом в другой (например, выключатель у входной двери — холл и улица). **Sync map остаётся централизованным** в `\ControlScripts\Common::register_global_device_sync_map()` — так проще искать, с чем синхронизируется конкретное устройство, не перебирая все Scope. + +### Trait `Common` + +```php +use \ControlScripts\Common; + +class KitchenScope extends ControlScripts implements ControlScriptsInterface { + use Common; // только register_global_device_sync_map() + + public function register_sync_map(): void { + $this->register_global_device_sync_map(); + } +} +``` + +**`Common` содержит только sync map.** Все хелперы (`auto_button_bindings`, `add_toggle_action`, `group_toggle` и т.д.) живут в базовом классе `ControlScripts`. + +--- + ## Четыре регистрационных метода Каждый Scope-класс обязан реализовать 4 метода. Они вызываются при старте сервера (если scope включён в БД). @@ -199,26 +244,308 @@ --- -## Trait Common (опционально) +## Trait Common -Некоторые проекты используют trait `\ControlScripts\Common` с готовыми хелперами: +Trait `\ControlScripts\Common` хранит **централизованную карту синхронизации** всех устройств дома: ```php use \ControlScripts\Common; -class MyScope extends \SHServ\Middleware\ControlScripts { +class KitchenScope extends \SHServ\Middleware\ControlScripts { use Common; - + public function register_sync_map(): void { - // Зарегистрировать глобальный sync_map (все реле и кнопки системы) $this->register_global_device_sync_map(); } - - public function register_events_handlers(): void { - // Установить обработчики нажатий для кнопок из sync_map - $this->set_btns_click_handlers("kitchen_btns"); - } } ``` -Проверьте наличие trait в вашем проекте: `server/ControlScripts/Common.php`. +**Почему sync map централизован:** кнопки из одной комнаты управляют светом в другой, и искать связи по всем Scope неудобно. `Common` остаётся единственным источником правды для `sync_map`. + +Все остальные хелперы (`auto_button_bindings`, `add_toggle_action`, `group_toggle` и т.д.) находятся в базовом классе `ControlScripts`. + +Проверьте наличие trait в вашем проекте: `automation/Common.php`. + +--- + +## Таймеры и задержки (Timers) + +Scope-классы могут планировать отложенный запуск action-скриптов, regular-скриптов или событий. + +### API внутри Scope + +| Метод | Описание | +|-------|---------| +| `$this->delay_action($timer_alias, $action_alias, $params, $delay_seconds)` | Запланировать запуск action-скрипта через N секунд | +| `$this->delay_event($timer_alias, $event_name, $params, $delay_seconds)` | Запланировать вызов события через N секунд | +| `$this->delay_regular($timer_alias, $regular_alias, $delay_seconds)` | Запланировать запуск regular-скрипта через N секунд | +| `$this->cancel_timer($timer_alias)` | Отменить ожидающий таймер с указанным alias | + +**Повторный `delay_*` с тем же `timer_alias` внутри одного Scope заменяет старый pending-таймер.** + +### Пример + +```php +public function register_events_handlers(): void { + // Двойное нажатие кнопки включает свет на 5 минут + $this->add_event_handler("button@kitchen_btns(1).double_press", function($device, $data) { + $this->devices()->by_alias("kitchen_relay")->device_api()->toggle_channel(0); + + // Автовыключение через 5 минут + $this->delay_action("kitchen_auto_off", "kitchen_light_toggle", [], 300); + }); + + // Одинарное нажатие отменяет таймер (свет остаётся включённым) + $this->add_event_handler("button@kitchen_btns(1).press", function($device, $data) { + $this->cancel_timer("kitchen_auto_off"); + }); +} +``` + +### Как это работает + +1. `delay_*` создаёт запись в таблице `shserv_timers` со статусом `pending` +2. Cron-задача `GET /cron/timers` (метод `CronController::run_timers()`) раз в минуту выбирает `pending` таймеры, у которых `execute_at <= NOW()` +3. Таймер выполняется inline с `try/catch`: + - `action` → `ControlScripts::run_action_script()` + - `event` → `events()->app_call()` + - `regular` → `ControlScripts::run_regular_script()` +4. После выполнения статус меняется на `executed` или `failed` (с логом ошибки) + +### REST API для таймеров + +| Endpoint | Описание | +|----------|---------| +| `GET /api/v1/scripts/timers/list` | Список таймеров (все статусы) | +| `POST /api/v1/scripts/timers/cancel` | Отменить pending-таймер по `timer_alias` | + +--- + +## Params Schema для action scripts + +Action-скрипты могут декларировать схему входных параметров. Vue-клиент автоматически отображает форму в модалке при запуске скрипта. Сервер валидирует параметры по схеме перед вызовом closure. + +### Объявление в Scope + +```php +public function register_actions_scripts(): void { + $this->add_action_script([ + "alias" => "dim_lights", + "name" => "Диммер", + "description" => "Установить яркость в комнате", + "params_schema" => [ + "level" => [ + "type" => "range", + "label" => "Яркость, %", + "min" => 0, + "max" => 100, + "step" => 5, + "default" => 50, + "required" => true, + ], + "room" => [ + "type" => "select", + "label" => "Комната", + "options" => [ + "kitchen" => "Кухня", + "hall" => "Зал", + "bedroom" => "Спальня", + ], + "default" => "hall", + "required" => true, + ], + "instant" => [ + "type" => "toggle", + "label" => "Мгновенно (без плавного диммирования)", + "default" => false, + ], + ], + ], function($params) { + $level = $params['level'] ?? 50; + $room = $params['room'] ?? 'hall'; + $instant = $params['instant'] ?? false; + // ... + }); +} +``` + +### Поддерживаемые типы полей + +| Тип | Компонент Vue | Параметры schema | +|-----|---------------|------------------| +| `text` | `GnInput` | `label`, `placeholder`, `required`, `default` | +| `number` | `GnInput type="number"` | `label`, `placeholder`, `min`, `max`, `required`, `default` | +| `range` | `GnRange` | `label`, `min`, `max`, `step`, `required`, `default` | +| `select` | `GnSelect` | `label`, `options` (ассоциативный массив `value => label`), `required`, `default` | +| `toggle` | `GnSwitch` | `label`, `default` | +| `textarea` | `GnTextarea` | `label`, `placeholder`, `required`, `default` | + +### Правила валидации + +- **Валидация на сервере:** перед вызовом closure `POST /api/v1/scripts/actions/run` проверяет `params` по schema. При ошибке — `invalid_params` с массивом `failed_fields`. +- **Default values:** если поле не передано и не `required`, сервер подставляет `default` из schema. +- **Required:** поле обязано присутствовать в запросе (для `text`/`textarea` — не пустая строка). +- **Границы:** для `number` и `range` проверяется `min` и `max`. +- **Select:** значение должно быть ключом из `options`. + +--- + +## Хелперы для упрощения написания скриптов + +Base class `ControlScripts` предоставляет high-level обёртки над повторяющимися паттернами. + +### Action script generators + +| Метод | Описание | +|-------|----------| +| `$this->add_toggle_action($alias, $name, $relay_alias, $channel=0, $extra_attrs=[])` | Зарегистрировать action script toggle + auto-sync | +| `$this->add_on_action($alias, $name, $relay_alias, $channel=null, $extra_attrs=[])` | Включить реле (одно- или многоканальное) + auto-sync | +| `$this->add_off_action($alias, $name, $relay_alias, $channel=null, $extra_attrs=[])` | Выключить реле + auto-sync | +| `$this->add_hatch_action($alias, $name, $hatch_alias, $op, $percent=100, $extra_attrs=[])` | Открыть/закрыть люк | +| `$this->add_group_toggle_action($alias, $name, $pairs, $extra_attrs=[])` | Групповой toggle нескольких реле | + +Пример — **было / стало**: + +```php +// Было: 9 строк boilerplate +$this->add_action_script([ + "alias" => "computer_table_lamp_switch", + "name" => "Комп. лампа", + "icon" => '', +], function($params) { + return $this->lamp_switch("computer_table_lamp", 0); +}); + +// Стало: 1 строка +$this->add_toggle_action("computer_table_lamp_switch", "Комп. лампа", "computer_table_lamp", 0, + ["icon" => ''] +); +``` + +### Автобиндинг кнопок + +```php +// Было: 16 вызовов в LightHubScope +$this->btn_on_online("master_room_btns", [2, 3]); +$this->set_btns_click_handlers("master_room_btns"); +$this->btn_on_online("btns_hall2_1"); +$this->set_btns_click_handlers("btns_hall2_1"); +// ... ещё 6 раз + +// Стало: 1 вызов +$this->auto_button_bindings([ + "master_room_btns" => ["mute" => [2, 3]], + "btns_hall2_1" => [], + "hall_secondary" => [], + "bed_btns_right_1" => [], + "kitchen_buttons_1" => [], +]); +``` + +### Групповые операции + +```php +// Выключить всё разом (внутри action script или event handler) +$this->group_off([ + "hall_relay", + ["light_hub_1", 0], + ["light_hub_1", 1], +]); + +// Групповой toggle +$this->group_toggle([ + "spotlight_main_front_1", + "spotlight_main_back_1", + "spotlight_main_back_2", +]); +``` + +Методы `group_set_state` и `group_toggle` автоматически синхронизируют индикаторы кнопок. + +--- + +## Системные моды (System Modes) + +Моды — это boolean-теги (не взаимоисключающие), которые задают текущий контекст системы: «Дома», «Не дома», «Ночь», «Сон», «Кино» и т.д. Scope-классы могут проверять активные моды в хендлерах и action-скриптах, меняя поведение в зависимости от ситуации. + +### Реестр модов + +Все моды декларируются централизованно в `automation/ModesRegistry.php`: + +```php +class ModesRegistry { + public static function definitions(): array { + return [ + 'home' => [ + 'label' => 'Дома', + 'description' => 'Основной режим присутствия. Все системы работают в штатном режиме.', + ], + 'away' => [ + 'label' => 'Не дома', + 'description' => 'Никого нет дома. Активны охранные сценарии и энергосбережение.', + ], + 'night' => [ + 'label' => 'Ночь', + 'description' => 'Пониженная яркость освещения, тихие уведомления.', + ], + 'sleep' => [ + 'label' => 'Сон', + 'description' => 'Минимум света и звука. Датчики движения переведены в тихий режим.', + ], + ]; + } +} +``` + +> **Label и description живут в коде** — БД (`shserv_modes`) хранит только runtime-состояние (`is_active`). При первом обращении к `ModesContext` реестр автоматически синхронизируется с таблицей (`INSERT IGNORE`). + +### API внутри Scope + +| Метод | Описание | +|-------|---------| +| `$this->mode()->is('night')` | Проверить, активен ли конкретный мод | +| `$this->mode()->any(['night', 'sleep'])` | Хотя бы один из модов активен | +| `$this->mode()->all(['away', 'sleep'])` | Все перечисленные моды активны | +| `$this->mode()->active()` | Массив активных тегов | +| `$this->mode()->enable('away')` | Активировать мод | +| `$this->mode()->disable('away')` | Деактивировать мод | +| `$this->mode()->toggle('movie')` | Переключить мод | +| `$this->mode()->meta('away')` | Получить `['label' => ..., 'description' => ...]` | + +### Пример + +```php +public function register_events_handlers(): void { + // При движении в коридоре — яркость зависит от времени суток + $this->add_event_handler("motion.hall", function($device, $data) { + if ($this->mode()->any(['night', 'sleep'])) { + $this->run_action('dim_hall_lights', ['level' => 20]); + } else { + $this->run_action('dim_hall_lights', ['level' => 80]); + } + }); +} + +public function register_actions_scripts(): void { + // Ручной скрипт «Уйти из дома» + $this->add_action_script([ + "alias" => "set_mode_away", + "name" => "Не дома", + ], function($params) { + $this->mode()->enable('away'); + $this->mode()->disable('home'); + // ... выключить весь свет, включить охрану + }); +} +``` + +### REST API для модов + +| Endpoint | Описание | +|----------|---------| +| `GET /api/v1/modes/list` | Список всех модов с label, description, is_active | +| `GET /api/v1/modes/active` | Массив активных тегов | +| `POST /api/v1/modes/{tag}/enable` | Активировать мод | +| `POST /api/v1/modes/{tag}/disable` | Деактивировать мод | + +> Моды не взаимоисключающие — можно одновременно включить `away` + `sleep`. Если нужно эмулировать взаимоисключение (например, «Дома» vs «Не дома»), управляйте этим явно в action-скрипте: `$this->mode()->enable('away'); $this->mode()->disable('home');`. diff --git a/server/SHServ/Helpers/DeviceScriptsHelper.php b/server/SHServ/Helpers/DeviceScriptsHelper.php index d3ae413..5be3da6 100644 --- a/server/SHServ/Helpers/DeviceScriptsHelper.php +++ b/server/SHServ/Helpers/DeviceScriptsHelper.php @@ -160,11 +160,11 @@ foreach($sync_map["connections"] as $connection) { foreach($connection as $device_entry) { - if($device_entry["alias"] != $alias) { + if($device_entry["alias"] != $alias) { continue; } - $result[$device_entry["channel"]] = array_filter($connection, function($i) use($alias) { + $result[$device_entry["channel"]] = array_filter($connection, function($i) use($alias) { return $i["alias"] != $alias; }); @@ -174,4 +174,89 @@ return $result; } + + /** + * Групповая установка состояния (on/off) для списка реле. + * Targets: строка (alias одноканального реле) или [alias, channel] для многоканального. + * Автоматически синхронизирует индикаторы кнопок через sync_map. + * Возвращает массив результатов по каждому target. + */ + public function group_set_state(Array $targets, bool $state, Array $sync_map): Array { + $results = []; + foreach($targets as $target) { + $relay_alias = null; + $channel = null; + if(is_string($target)) { + $relay_alias = $target; + } elseif(is_array($target) && count($target) >= 2) { + $relay_alias = $target[0]; + $channel = intval($target[1]); + } else { + continue; + } + + $relay = $this -> devices() -> by_alias($relay_alias); + $key = $relay_alias . ($channel !== null ? ":{$channel}" : ""); + if(!$relay) { + $results[$key] = false; + continue; + } + + $relay_api = $relay -> device_api(); + $result = false; + if($relay_api instanceof \SHServ\Tools\DeviceAPI\Relay) { + if($channel === null) { + $result = $relay_api -> set_state($state); + } else { + $status = $relay_api -> get_status(); + $current = $status["channels"][$channel]["state"] ?? null; + if($current != ($state ? "on" : "off")) { + $result = $relay_api -> set_channel_state($state, $channel); + } else { + $result = true; + } + } + $this -> sync_relay_to_btns($sync_map, $relay_alias); + } + $results[$key] = $result; + } + return $results; + } + + /** + * Групповой toggle для списка реле. + * Targets: строка (alias) или [alias, channel]. + * Автоматически синхронизирует индикаторы кнопок через sync_map. + */ + public function group_toggle(Array $targets, Array $sync_map): Array { + $results = []; + foreach($targets as $target) { + $relay_alias = null; + $channel = null; + if(is_string($target)) { + $relay_alias = $target; + } elseif(is_array($target) && count($target) >= 2) { + $relay_alias = $target[0]; + $channel = intval($target[1]); + } else { + continue; + } + + $relay = $this -> devices() -> by_alias($relay_alias); + $key = $relay_alias . ($channel !== null ? ":{$channel}" : ""); + if(!$relay) { + $results[$key] = false; + continue; + } + + $relay_api = $relay -> device_api(); + $result = false; + if($relay_api instanceof \SHServ\Tools\DeviceAPI\Relay) { + $result = $relay_api -> toggle_channel($channel ?? 0); + $this -> sync_relay_to_btns($sync_map, $relay_alias); + } + $results[$key] = $result; + } + return $results; + } } diff --git a/server/SHServ/Middleware/ControlScripts.php b/server/SHServ/Middleware/ControlScripts.php index 87b087c..f922312 100644 --- a/server/SHServ/Middleware/ControlScripts.php +++ b/server/SHServ/Middleware/ControlScripts.php @@ -5,15 +5,12 @@ 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 static $regular_scripts = []; - protected static $actions_scripts = []; - protected static $sync_map_storage = [ - "connections" => [], - ]; + protected $modes_context; abstract public function register_sync_map(): void; abstract protected function register_events_handlers(): void; @@ -31,9 +28,22 @@ } } + 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) { - $handler($params["device"], $params["data"]); + 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(), + ]); + } }); } @@ -53,78 +63,113 @@ 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(self::$regular_scripts[$attributes["alias"]])) { + if(isset($reg ->regular[$attributes["alias"]])) { return false; } $ref = new \ReflectionClass(static::class); $path_info = pathinfo($ref -> getFileName()); - $attributes["name"] = $attributes["name"] ?? "unknown"; + $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"; + $attributes["classname"] = static::class; + $attributes["path"] = $path_info["dirname"]; + $attributes["filename"] = $path_info["basename"]; + $attributes["author"] = $attributes["author"] ?? "Unknown author"; - self::$regular_scripts[$attributes["alias"]] = [ + $reg ->regular[$attributes["alias"]] = [ "attributes" => $attributes, - "code" => $this -> get_source_code($script), - "script" => $script + "code" => $this -> get_source_code($script), + "script" => $script ]; return true; } public static function flush_statics(): void { - self::$regular_scripts = []; - self::$actions_scripts = []; - self::$sync_map_storage = ["connections" => []]; + app() -> scripts_registry -> flush(); } public static function get_regular_scripts(): Array { - return self::$regular_scripts; + return app() -> scripts_registry -> getRegularScripts(); } public static function get_actions_scripts(): Array { - return self::$actions_scripts; + 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(self::$actions_scripts[$attributes["alias"]])) { + if(isset($reg ->actions[$attributes["alias"]])) { return false; } $ref = new \ReflectionClass(static::class); $path_info = pathinfo($ref -> getFileName()); - $attributes["name"] = $attributes["name"] ?? "unknown"; + $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"; + $attributes["classname"] = static::class; + $attributes["path"] = $path_info["dirname"]; + $attributes["filename"] = $path_info["basename"]; + $attributes["author"] = $attributes["author"] ?? "Unknown author"; - self::$actions_scripts[$attributes["alias"]] = [ - "attributes" => $attributes, - "code" => $this -> get_source_code($script), - "script" => $script + $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 { - if(!isset(self::$actions_scripts[$alias])) { + $reg = app() -> scripts_registry; + if(!isset($reg ->actions[$alias])) { return null; } @@ -137,7 +182,7 @@ $start_time = microtime(true); - $result = self::$actions_scripts[$alias]["script"]($params); + $result = $reg ->actions[$alias]["script"]($params); $exec_time = microtime(true) - $start_time; $exec_time = round($exec_time, 3); @@ -145,13 +190,14 @@ logging() -> debug('php:ControlScripts', 'Action script finished', ['alias' => $alias, 'exec_time' => $exec_time]); return [ - "result" => $result, + "result" => $result, "exec_time" => "{$exec_time} seconds" ]; } public static function run_regular_script(String $alias): bool { - if(!isset(self::$regular_scripts[$alias])) { + $reg = app() -> scripts_registry; + if(!isset($reg ->regular[$alias])) { return false; } @@ -163,7 +209,7 @@ logging() -> info('php:ControlScripts', 'Run regular script', ['alias' => $alias]); try { - self::$regular_scripts[$alias]["script"](); + $reg ->regular[$alias]["script"](); } catch(\Exception $e) { logging() -> error('php:ControlScripts', 'Regular script failed', ['alias' => $alias, 'message' => $e -> getMessage()]); return false; @@ -192,10 +238,181 @@ } protected function add_sync_connection(Array $sync_connection) { - self::$sync_map_storage["connections"][] = $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 self::$sync_map_storage; + return app() -> scripts_registry -> getSyncMap(); } -} \ No newline at end of file + + // --- 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]; + }); + } +} diff --git a/server/tests/ScriptHelpersTest.php b/server/tests/ScriptHelpersTest.php new file mode 100644 index 0000000..e774098 --- /dev/null +++ b/server/tests/ScriptHelpersTest.php @@ -0,0 +1,212 @@ + tb = app() -> thin_builder; + $this -> tb -> query("CREATE TABLE scripts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + area_id INTEGER DEFAULT 0, + uniq_name TEXT, + type TEXT, + state TEXT, + params_schema TEXT, + create_at TEXT, + update_at TEXT + )"); + if(!isset(app() -> scripts_registry)) { + app() -> scripts_registry = new \SHServ\Middleware\ScriptsRegistry(); + } + \Fury\Kernel\AppContainer::set_events(new \Fury\Kernel\Events()); + $this -> tb -> query("CREATE TABLE devices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + area_id INTEGER DEFAULT 0, + alias TEXT, + name TEXT, + device_type TEXT, + device_ip TEXT, + device_mac TEXT, + device_hard_id TEXT, + firmware_version TEXT, + connection_status TEXT, + status TEXT, + description TEXT, + last_contact TEXT, + create_at TEXT, + update_at TEXT + )"); + ControlScripts::flush_statics(); + } + + protected function tearDown(): void { + $this -> tb -> query("DROP TABLE IF EXISTS scripts"); + $this -> tb -> query("DROP TABLE IF EXISTS devices"); + ControlScripts::flush_statics(); + } + + // --- Action Generators --- + + public function test_add_toggle_action_registers_in_registry(): void { + $scope = new class extends ControlScripts { + public function register_sync_map(): void {} + public function register_events_handlers(): void {} + public function register_regular_scripts(): void {} + public function register_actions_scripts(): void { + $this -> add_toggle_action("test_toggle", "Test Toggle", "relay_a", 0, [ + "icon" => "", + "description" => "desc", + ]); + } + }; + + $actions = ControlScripts::get_actions_scripts(); + $this -> assertArrayHasKey("test_toggle", $actions); + $this -> assertEquals("Test Toggle", $actions["test_toggle"]["attributes"]["name"]); + $this -> assertEquals("desc", $actions["test_toggle"]["attributes"]["description"]); + $this -> assertNotNull($actions["test_toggle"]["script"]); + } + + public function test_add_on_action_registers_in_registry(): void { + $scope = new class extends ControlScripts { + public function register_sync_map(): void {} + public function register_events_handlers(): void {} + public function register_regular_scripts(): void {} + public function register_actions_scripts(): void { + $this -> add_on_action("test_on", "Test On", "relay_b", 1); + } + }; + + $actions = ControlScripts::get_actions_scripts(); + $this -> assertArrayHasKey("test_on", $actions); + $this -> assertEquals("Test On", $actions["test_on"]["attributes"]["name"]); + } + + public function test_add_off_action_registers_in_registry(): void { + $scope = new class extends ControlScripts { + public function register_sync_map(): void {} + public function register_events_handlers(): void {} + public function register_regular_scripts(): void {} + public function register_actions_scripts(): void { + $this -> add_off_action("test_off", "Test Off", "relay_c"); + } + }; + + $actions = ControlScripts::get_actions_scripts(); + $this -> assertArrayHasKey("test_off", $actions); + } + + public function test_add_hatch_action_registers_in_registry(): void { + $scope = new class extends ControlScripts { + public function register_sync_map(): void {} + public function register_events_handlers(): void {} + public function register_regular_scripts(): void {} + public function register_actions_scripts(): void { + $this -> add_hatch_action("test_hatch", "Test Hatch", "hatch_x", "open", 50); + } + }; + + $actions = ControlScripts::get_actions_scripts(); + $this -> assertArrayHasKey("test_hatch", $actions); + } + + public function test_action_generators_return_false_on_duplicate_alias(): void { + $scope = new class extends ControlScripts { + public $r1; + public $r2; + public function register_sync_map(): void {} + public function register_events_handlers(): void {} + public function register_regular_scripts(): void {} + public function register_actions_scripts(): void { + $this -> r1 = $this -> add_toggle_action("dup", "Dup", "r", 0); + $this -> r2 = $this -> add_toggle_action("dup", "Dup", "r", 0); + } + }; + + $this -> assertTrue($scope -> r1); + $this -> assertFalse($scope -> r2); + } + + // --- Auto Button Bindings --- + + public function test_auto_button_bindings_registers_handlers(): void { + $scope = new class extends ControlScripts { + public function register_sync_map(): void { + $this -> add_sync_connection([ + ["type" => "relay", "alias" => "relay_1", "channel" => 0], + ["type" => "button", "alias" => "btn_1", "channel" => 0], + ]); + } + public function register_events_handlers(): void {} + public function register_regular_scripts(): void {} + public function register_actions_scripts(): void {} + + public function expose_auto_button_bindings(): void { + $this -> auto_button_bindings([ + "btn_1" => ["mute" => [1, 2]], + ]); + } + }; + + $scope -> expose_auto_button_bindings(); + + // Verify that event handlers were registered (no exception + handlers present) + $events = \Fury\Kernel\AppContainer::events(); + $this -> assertInstanceOf(\Fury\Kernel\Events::class, $events); + } + + public function test_add_group_toggle_action_registers_in_registry(): void { + $scope = new class extends ControlScripts { + public function register_sync_map(): void {} + public function register_events_handlers(): void {} + public function register_regular_scripts(): void {} + public function register_actions_scripts(): void { + $this -> add_group_toggle_action("group_toggle_test", "Group Toggle", [ + "relay_a", + ["relay_b", 1], + ]); + } + }; + + $actions = ControlScripts::get_actions_scripts(); + $this -> assertArrayHasKey("group_toggle_test", $actions); + $this -> assertEquals("Group Toggle", $actions["group_toggle_test"]["attributes"]["name"]); + } + + public function test_group_set_state_returns_false_for_missing_devices(): void { + $scope = new class extends ControlScripts { + public function register_sync_map(): void {} + public function register_events_handlers(): void {} + public function register_regular_scripts(): void {} + public function register_actions_scripts(): void {} + + public function expose_group_set_state(Array $targets, bool $state): Array { + return $this -> group_set_state($targets, $state); + } + }; + + $results = $scope -> expose_group_set_state(["nonexistent_relay"], true); + $this -> assertArrayHasKey("nonexistent_relay", $results); + $this -> assertFalse($results["nonexistent_relay"]); + } + + public function test_group_toggle_returns_false_for_missing_devices(): void { + $scope = new class extends ControlScripts { + public function register_sync_map(): void {} + public function register_events_handlers(): void {} + public function register_regular_scripts(): void {} + public function register_actions_scripts(): void {} + + public function expose_group_toggle(Array $targets): Array { + return $this -> group_toggle($targets); + } + }; + + $results = $scope -> expose_group_toggle([["nonexistent_relay", 0]]); + $this -> assertArrayHasKey("nonexistent_relay:0", $results); + $this -> assertFalse($results["nonexistent_relay:0"]); + } +}