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

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

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

Что представляет собой сценарий под капотом

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

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

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

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

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

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

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

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

Подробнее по созданию сценариев можно прочитать в разделе Работа со сценариями

Мультисценарий - это круто!

Сценарий можно создать для конкретных устройств проекта:
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 с фокусами, хочется допилить.
Не проблема — копируем мультисценарий, в декларативной части ставим конкретные устройства и добавляем уникальный код.

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

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

  • 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()

Примеры

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

1. 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,
то сообщение уйдет еще по двум каналам. Если же нет — в журнале будут сообщения об ошибке, но ничего фатального не произойдет.

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

2. 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 сек лампа выключится и сценарий завершится (зеленая галочка должна пропасть).
Почему? Потому что делать сценарию больше нечего — активных таймеров не осталось, и сценарий завершился

3. 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().

4. 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-и и другие асинхронные возможности.
Организацию асинхронной работы берет на себя движок сценариев.

Закрыть меню