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"]);
+ }
+}