Работа со сценариями

Все устройства в системе intraHouse могут существовать и без сценариев: их можно включать/выключать и смотреть за изменением их состояния. Но только сценарии сделают ваш дом по настоящему «умным». 

Концепция сценариев

Сценарий системы — это набор действий, определяемых пользователем в рамках проекта.
Для каждого проекта можно создать свои сценарии или загрузить уже имеющиеся.
Фактически каждый сценарий — это файл на JavaScript (.js).
Файлы храняться в папке проекта: /var/lib/intrahouse-c/projects/<имя проекта>/scenes/script
При создании сценария определяется его ID — это и есть имя файла.

Пользовательский сценарий динамически создается на базе объекта script и наследует встроенные свойства и методы, которые позволяют выполнять различные действия, работать с таймерами, отслеживать события устройств, писать в журнал и БД, передавать сообщения по каналам связи.
Созданные пользователем функции и переменные становятся методами и свойствами объекта сценария.

Запуск сценария

Чтобы сценарий выполнился, его нужно запустить. Сценарий может быть запущен разными способами:

  • по событиям устройств
  • по расписанию
  • интерактивно (из интерфейса по кнопке, командой sms, командой API,…)
  • из другого сценария

Каждый сценарий должен иметь метод start, определенный пользователем, который запускается при запуске сценария.

Классы устройств

Классы устройств в системе:

  • SensorD — дискретный датчик (датчики движения, протечки, пожарный, геркон,…)
  • SensorA — аналоговый датчик (датчики температуры, влажности, давления, освещенности,…)
  • ActorD — дискретный актуатор (светильник, вентилятор …)
  • ActorA — аналоговый актуатор (диммер …)
  • Meter — счетчик

Структура сценария

Рассмотрим структуру простого скрипта на примере:

/**
* @name Батарея по датчику температуры
* @desc Сценарий включает батарею при понижении температуры и отключает при достижении уставки
* @version 4 
*/

const dt = Device("SensorA", 'Датчик температуры'); 
const bat = Device("ActorD", 'Батарея');

startOnChange([dt, bat], (dt.value < dt.setpoint) && bat.isOff() || (dt.value > dt.setpoint) && bat.isOn());

script({
  start() {
     this.info(bat.fullName + ' будет '+(bat.isOff()? ' ВКЛ ' : ' ВЫКЛ'));
     bat.toggle();
  }
});    

Скрипт сценария состоит из 4 разделов, два из которых — первый и последний — является обязательными.

Раздел 1
Начинается сценарий с многострочного комментария, @name — обязательно, остальное — опционально.

/**
 @name - короткое название, которое будет появляться при выборе сценария в списках
 @desc - комментарий
 @version - версия API. Новая версия = 4, если <3 или не  определено - старое API 
*/

name — наименование скрипта
desc — комментарий (опционально)

Раздел 2
Далее — объявление устройств, участвующих в сценарии.
Любое устройство, которое будет задействовано, нужно объявить.
Это пример мультисценария, поэтому объявление имеет вид:

const dt = Device("SensorA", 'Датчик температуры'); 
const bat = Device("ActorD", 'Батарея');

Если сценарий не связан с конкретными устройствами, например, выполняет только групповые операции, то этот раздел не нужен.

Раздел 3
Затем можно указать, какие устройства являются триггерами сценария: startOnChange([], условное выражение)
Первый параметр — устройство или массив устройств из тех, что были объявлены как Device
Второй параметр — опциональное условное выражение, которое проверяется до того как сценарий запустится
Если триггеры не объявлены (startOnChange отсутствует) — сценарий по событиям запускаться не будет.

startOnChange([dt, bat], (dt.value < dt.setpoint) && bat.isOff() || (dt.value > dt.setpoint) && bat.isOn()));

Раздел 4
Код сценария:

script({
  start() {
     this.info(bat.fullName + ' будет '+(bat.isOff()? ' ВКЛ ' : ' ВЫКЛ'));
     bat.toggle();
  }
});

script создает экземпляр сценария на базе типового объекта script
В результате новый сценарий наследует встроенные функции через this (информирование, запись в журнал, таймеры и т д )
А как параметр мы ему передаем объект с пользовательскими методами (функциями). start() — обязательная функция.
Метод start() вызывается, когда движок запускает сценарий и тот становится активным.

Если внутри сценария просто последовательность действий, он отработает и сразу завершится (станет не активным)
При следующем запуске он опять начнется со start(). Большая часть сценариев работает именно так и 99,9% времени находятся в неактивном состоянии.

Но если сценарий взводит таймеры и/или устанавливает слушателя событий устройства, то он остается активным, следит за своими таймерами и устройствами «изнутри», выполняет действия как реакцию на отслеживаемые события.
Чтобы завершить такой сценарий, нужно выполнить команду this.exit().
При завершении сценария все его таймеры и слушатели удаляются. При следующем запуске опять выполнится start()

Примеры

Попробуем написать сценарий с помощью редактора кода и отследить жизненный цикл сценария.
Редактирование сценариев выполняется в разделе «Сценарии», запуск и контроль выполнения — в разделе «Рабочие сценарии». Можно открыть две вкладки и переключаться между ними.

Hello World как сценарий intraHouse

/**
* @name Hello World
* @desc Тестовый сценарий
* @version 4 
*/

script ({
  start() {
    this.log('Hello, World!');
    this.info('telegram', 'admin', 'Hello, World!');
    this.info('email', 'admin', 'Hello, World!');
  }
});

Этот сценарий гарантированно запишет в журнал.
Если установлены плагины email и telegram и введены адреса для информирования учетной записи admin,
то сообщение уйдет еще по двум каналам. Если же нет — в журнале будут сообщения об ошибке, но ничего фатального не произойдет.

Для тестового запуска воспользуемся операцией «Запустить сценарий» в разделе «Рабочие сценарии».
После запуска вы увидите, что в строке нашего сценария появились Время запуска и Время останова.
Они совпадают, потому что сценарий запустился и сразу завершился — никаких асинхронных действий мы пока не выполняли. Время активности сценария было чрезвычайно мало.

Hello World со световым сопровождением

Информирование, конечно, хорошо. Но уже хочется что-то повключать.
Добавим в приветствие включение светильника на 10 сек.

Нам нужно: 
1. Определить Device — лампу для включения
2. В start добавить команду включения: lamp.on()
3. Взвести таймер на 10 сек и определить метод ‘onTimer’, который запустится, когда таймер досчитает
4. Добавить метод onTimer в наш объект script (называть новые методы можно как угодно, избегая, конечно, использование названий встроенных методов)

/**
* @name Hello World and Blink
* @desc Тестовый сценарий
* @version 4 
*/

const lamp = Device("LAMP1");

script ({
  start() {
    lamp.on();
    this.log('Hello, World!');
    this.startTimer('T1', 10, 'next'); 
    // Обратите внимание, здесь мы не определяем callback напрямую,
    // а просто сообщаем движку имя метода, который нужно будет запустить
  },

  next() {
     lamp.off();
  }  
});

Если сразу после запуска обновить табличку «Рабочие сценарии», вы увидите зеленую галочку слева от сценария.  Это означает, что сценарий активен. Также поменялось Время запуска.
Через 10 сек лампа выключится и сценарий завершится (зеленая галочка должна пропасть).
Почему? Потому что делать сценарию больше нечего — активных таймеров не осталось, и сценарий завершился

Hello World с динамической иллюминацией

Усложняем задачу. Пусть лампа приветственно мигает несколько раз (например, 10).
Для этого: 
1. Добавим переменную для хранения текущего состояния лампы
2. Добавим переменную для хранения счетчика миганий
3. Будем взводить таймер при включении на 3 сек, при выключении — на 0,4 сек

/**
* @name Hello World and Blinking
* @desc Тестовый сценарий
* @version 4 
*/

const lamp = Device("LAMP1");

script ({            
  count:0,  // Просто добавляем объекту свойство - переменную.
  lampState:0, //Внутри методов обращаться к нему нужно через this

  start() {
    this.count = 0; // В таких переменных данные сохраняются и между запусками. 
                    //Иногда это нужно, в данном случае считаем с нуля. 
    this.lampState = lamp.value; // фиксируем, в каком состоянии находится лампа
                                 // при запуске
    this.log('Hello, World!');
    this.next(); // Запускаем функцию, которая также будет работать по таймеру
  },

  next() { 
    if (this.lampState) {
       lamp.off(); // Даем команду на выключение. 
                   // В следующей строке лампа возможно еще не выключится, 
                   // если мы работаем с физическим устр-вом.
                   // Когда  выключится и пришлет новое состояние - зависит от железа
                   // и от плагина.
       this.lampState = 0; // Поэтому здесь ставим ожидаемое состояние. 

       if (this.count <= 10) {
         this.startTimer('T1', 0.4, 'next'); 
       } else { 
         // Выходим после выключения, 10 раз уже мигнули
        this.exit();
       }    
     } else {
        lamp.on();
        this.lampState = 1;
        this.count += 1; // считаем включения
        this.startTimer('T1', 3, 'next'); 
     }
  }
});

Здесь мы постоянно перевзводим таймер, поэтому сценарий сам не завершается.
Для выхода используем this.exit().

Hello World с динамической иллюминацией и ручным управлением

Что-то это приветствие стало слишком утомительным. Добавим возможность отключить иллюминацию, выключив лампу вручную.
То есть сценарий должен понять, что нам надоело, и аккуратно завершиться.

Для этого
1. Добавим слушателя событий лампы: this.addListener(lamp, ‘onLamp’)
2. Добавим метод onLamp в наш объект script

/**
* @name Hello World and Blinking and Manual
* @desc Тестовый сценарий
* @version 4 
*/

const lamp = Device("LAMP1");

script ({
  lampState:0, // Просто добавляем объекту свойства - переменные             
  count:0,  // Внутри методов обращаться к ним нужно через this

  start() {
    this.count = 0; // В таких переменных данные сохраняются и между запусками.
                    // Иногда это нужно, в данном случае считаем с нуля 
    this.lampState = lamp.value; // фиксируем, в каком состоянии находится лампа
                                // при запуске
    this.log('Hello, World!');
    this.addListener(lamp, 'onLamp');
    this.next(); // Запускаем функцию, которая также будет работать по таймеру
  },

  next() { 
    if (this.lampState) {
       this.lampState = 0; // Здесь ставим ожидаемое состояние     
       lamp.off(); // Даем команду на выключение.
                   // В следующей строке лампа возможно еще не выключится,
                   // если мы работаем с физическим устр-вом
                   // Когда  выключится и пришлет новое состояние - зависит от железа 
                   // и от плагина

       if (this.count <= 10) {
         this.startTimer('T1', 0.4, 'next'); 
       } else { 
         // Выходим после выключения, 10 раз уже мигнули
        this.exit();
       }    
     } else {
        this.lampState = 1;     
        lamp.on();
        this.count += 1; // считаем включения
        this.startTimer('T1', 3, 'next'); 
     }
  },

  onLamp() {
    if (this.lampState != lamp.value) {
      // Было внешнее переключение - завершим сценарий
      this.exit();
    }
  }
});

Этот сценарий имеет два асинхронно выполняемых обработчика — onTimer и onLamp.
Мы явно определяем, что нужно завершить сценарий, выполнив команду this.exit() в одном из них.
При завершении слушатель и таймер автоматически удалятся.

Сценарии по событиям устройств

Выше в примерах показаны сценарии, которые можно запустить интерактивно — с кнопки в интерфейсе либо в ответ на входящее сообщение (sms, …). Можно также добавить пункт в расписание и запускать сценарий по времени.
Все эти варианты возможны, так как у нас получился «интерактивный» сценарий — в нем мы не привязываемся к событиям устройств для запуска сценария.

Другой вариант — сценарии, которые запускаются по событиям устройств.
Для этого нам нужно определить устройства — триггеры и, опционально, условия, при которых сценарий должен запуститься.

1. Устройства-триггеры, запускающие сценарий
В начале сценария мы всегда объявляем устройства, которые используются сценарием.
Укажем, что событие датчика движения должно привести к запуску сценария, добавив startOnChange

/**
* @name Свет по датчику движения
* @desc Довольно тупой сценарий, который включает и выключает светильник синхронно с датчиком движения.
* @version 4 
*/
const lamp = Device("LAMP1");
const motion = Device("SMOTION1");

startOnChange(motion);

script({
  start() {
    if (motion.isOn()) {
      lamp.on();
    } else {
      lamp.off();
    }
  }
});

Такой сценарий уже не нужно запускать интерактивно, он работает по событиям датчика движения: startOnChange(motion)

Триггеров может быть несколько, тогда в startOnChange пропишем массив:

/**
* @name Свет по двум датчикам движения
* @desc Включает при сработке одного из датчиков, выключает, если оба сброшены
* @version 4 
*/
const lamp = Device("LAMP1");
const motion1 = Device("SMOTION1");
const motion2 = Device("SMOTION2");

startOnChange([motion1,motion2]);

script({
  start() {
    if (motion1.isOn() || motion2.isOn()) lamp.on();
    if (motion1.isOff() && motion2.isOff()) lamp.off();
  }
});

2. Добавление проверки условия при запуске сценария
Посмотрим на сценарий ниже:

/**
* @name Управление вентилятором по датчику влажности
* @desc Повысилась влажность - включаем вентилятор, понизилась - выключаем
* @version 4 
*/
const vent = Device("VENT1");
const hum = Device("SHUMIDITY1");

startOnChange(hum);

script({
  start() {
    if (hum.value > 60) {
      vent.on();
    } else if (hum.value < 58) {
      vent.off();
    }
  }
});

Если посмотреть на количество запусков сценария — оно будет оооочень большим (при каждом изменении значения датчика влажности).
Хотя продуктивно он сработал (реально включил — выключил) в сотни раз меньше.

Для того, чтобы отсекать такие холостые запуски, в startOnChange можно добавить условие.
Тогда сценарий будет запускаться только если условие истинно.

Условие очевидно — влажность высокая и вентилятор не работает, ИЛИ влажность нормальная и вентилятор работает: (hum.value > 60)&&vent.isOff() || (hum.value < 58)&&vent.isOn())

/**
* @name Управление вентилятором по датчику влажности
* @desc Повысилась влажность - включаем вентилятор, понизилась - выключаем
* @version 4 
*/
const vent = Device("VENT1");
const hum = Device("SHUMIDITY1");

startOnChange([hum], (hum.value > 60)&&vent.isOff() || (hum.value < 58)&&vent.isOn());

script({
  // И если попали сюда - мы уже знаем, что сделать
  start() {
    vent.toggle();
  }
});

Конечно, условие можно проверять и внутри сценария. Предварительное условие — это всего лишь оптимизация.

Синхронный характер сценариев и ограничения

Следует помнить, что сценарии не представляют собой модули, постоянно работающие и самостоятельно контролирующие свое выполнение. Если действительно требуется сценарий такого типа, нужно создать аддон — модуль, запускаемый как дочерний процесс в отдельном потоке.

В отличие от аддона, сценарий — это объект, работающий под управлением движка сценариев в основном потоке сервера.

Такой подход накладывает некоторые ограничения: каждая функция скрипта должна выполняться синхронно и быстро.
Нельзя использовать setTimeout, setInterval, process.nextTick, callback-и и другие асинхронные возможности.
Организацию асинхронной работы берет на себя движок сценариев.

Cоздать новый сценарий

Сценарий создается интерактивно в Project Manager (PM).
Можно воспользоваться графическим редактором или написать сценарий на JavaScript в редакторе кода.
После сохранения или редактирования сценарий сразу становится рабочим, никаких перезагрузок не требуется.

Примечание: При необходимости можно напрямую загрузить файл(ы) сценария(ев) в папку проекта scenes/script.  Добавленные таким образом сценарии будут доступны после перезагрузки сервера.

Добавить сценарий

Добавить сценарий можно в разделе Сценарии/Список сценариев:

Предлагается три варианта сценариев:

  • Новый сценарий
  • Новый мульти-сценарий
  • Новая блок-схема

После выбора варианта сценария, в всплывающем окне необходимо ввести имя сценария, название и комментарий (опционально). Затем нажать кнопку «Сохранить».

Редактировать сценарий

1. Для редактирования обычных сценариев и мульти-сценариев нажать кнопку настройки сценариев и выбрать «Скрипт»:

Открывается окно редактора скрипта. При создании нового сценария в этом окне вы увидите образец скрипта.

В окне редактора можно написать свой сценарий или методом Copy/Paste (Ctrl-C/Ctrl-V) вставить любой текст скрипта. Образцы скриптов можно найти в разделе Шаблоны сценариев.

2. Для редактирования сценариев в виде блок-схем нажать кнопку настройки сценариев и выбрать «Блок-схема»:

Откроется пустое окно для добавления элементов блок-схемы.

Загрузка/Выгрузка сценария

Вы можете сохранить сценарий на свой компьютер и в дальнейшем загрузить его в новом проекте:

Нажать кнопку вызова Дополнительного меню и выбрать «Выгрузить в ZIP файл».
Выбрав команду «Загрузить из ZIP файла» можно загрузить сценарий из ZIP файла.

Мультисценарий

Мультисценарий — это круто! Делаем один сценарий и применяем его ко всем типовым задачам.

Сценарий можно создать для конкретных устройств проекта:
const lamp = Device(‘LAMP1‘);

А можно задать только классы устройств, тогда это будет мультисценарий:
const lamp = Device(‘ActorD‘);

Конкретные наборы устройств привязываются к сценариям в настройках сценариев в пункте  «Запуск для устройств»

Такой подход имеет много плюсов:

  1. Можно использовать один и тот же сценарий много раз, задавая наборы устройств
  2. Можно загрузить (выгрузить) сценарий и использовать его в других проектах
  3. Можно использовать сценарии, написанные коллегами, не методом «copy-paste, и здесь подставь свой идентификатор xyz56758765-tyutu«
  4. Замена устройства, подключение новых сценариев становится тривиальной задачей — просто добавить — изменить — удалить набор устройств

Еще один существенный плюс — превращение из мультисценария и обратно не требует практически никаких усилий, так как отличие заключается только в параметрах объявления Device.

Например, написан сценарий для конкретных устройств: датчик движения и светильник. Отлажен, прекрасно работает:
const motion = Device(‘MOTION1‘);
const lamp = Device(‘LAMP1‘);

Легким движением руки превращаем его в мультисценарий:
const motion = Device(‘SensorD‘,’Датчик движения’);
const lamp = Device(‘ActorD‘,’Светильник’);

Возможна обратная задача: есть мультисценарий, для 20 случаев подходит, но 21 с фокусами, хочется допилить.
Не проблема — копируем мультисценарий, в декларативной части ставим конкретные устройства и добавляем уникальный код.

Привязка мульти-сценариев к устройствам

В обычных сценариях устройства уже присутствуют в коде скрипта.
В коде мульти-сценария присутствует не конкретное устройство, а класс устройства. Поэтому необходимо выполнить привязку мульти-сценария к устройствам. 

Выбрать пункт «Запуск для устройств» и нажать кнопку плюс:

В всплывающем окне свойств выбрать устройства, которые будут работать по этому сценарию.
Повторив эту операцию несколько раз, можно назначить другие комбинации устройств, которые будут работать по этому сценарию.
Это позволяет вам использовать один скрипт для многих комбинаций устройств, а не писать каждый раз новый сценарий.

Отладка сценария

Для проверки работы скрипта или поиска ошибок можно воспользоваться отладчиком.
Выбрать «Отладчик» и нажать кнопку Запуска:

Все сообщения в отладчике поступают в режиме реального времени.

Обратите внимание: Сценарий продолжает работать независимо от запуска или остановки отладчика.