Plugin API

Plugin API

Введение

Plugin API позволяет разработчикам создавать собственные плагины (драйверы) для интеграции новых устройств с системой BARY. Плагины работают как отдельные процессы и взаимодействуют с основным приложением через IPC (Inter-Process Communication).

Возможности:

  • Поддержка любых IoT устройств и протоколов
  • Изоляция драйверов в отдельных процессах
  • Автоматическая установка зависимостей
  • Логирование и мониторинг
  • Публикация событий в реальном времени

Установка и структура проекта

Базовая структура плагина

my-plugin/
├── package.json          # Зависимости и метаданные
├── tsconfig.json         # Конфигурация TypeScript
├── webpack.config.js     # Сборка плагина
├── plugin.json           # Метаданные плагина для BARY
├── core/                 # Базовые классы (наследуются)
│   ├── base-module.ts
│   └── base-driver-module.ts
├── src/
│   └── my-plugin.ts      # Основной файл плагина
└── dist/                 # Скомпилированные файлы

package.json

{
  "name": "bary-plugin-mydevice",
  "version": "1.0.0",
  "main": "src/my-plugin.ts",
  "dependencies": {
    "async-mutex": "^0.5.0",
    "moment": "^2.27.0",
    "node-ipc": "^9.1.1",
    "winston": "^3.17.0"
  },
  "devDependencies": {
    "@types/node": "^14.14.34",
    "ts-loader": "^8.0.14",
    "typescript": "^4.1.3",
    "webpack": "^4.46.0"
  }
}

plugin.json

{
  "module": "my-plugin",
  "name": "My Device Plugin",
  "version": "1.0.0",
  "description": "Поддержка устройств MyDevice",
  "dependencies": {
    "custom-library": "^2.0.0"
  }
}

Базовые классы

baseModule

Базовый класс для всех плагинов, обеспечивает IPC коммуникацию с BARY.

Основные свойства:

  • config — конфигурация приложения
  • ipc — IPC клиент для связи с BARY
  • events — массив зарегистрированных событий

Основные методы:

// Отправка запроса с ожиданием ответа
request(eventName: string, params: object): Promise<any>

// Отправка запроса без ожидания ответа
requestEx(eventName: string, params: object): void

// Логирование
log(message: any): void
error(message: any): void

// Динамическая загрузка npm модулей
require(ident: string, require: boolean): Promise<any>

baseDriverModule

Расширяет baseModule дополнительными методами для работы с драйверами устройств.

Дополнительные свойства:

  • device_id — ID устройства
  • params — параметры устройства
  • config — конфигурация устройства
  • logger — Winston logger для записи логов

Жизненный цикл:

// 1. Инициализация плагина
initDeviceEx(resolve, reject): void

// 2. Подключение к устройству
connectEx(resolve, reject): void

// 3. Обработка команд
commandEx(command, value, params, options, resolve, reject, status): void

// 4. Получение списка дочерних устройств
getSubDevicesEx(resolve, reject, zones): void

Создание плагина

Минимальный пример

import {baseDriverModule} from '../core/base-driver-module';
import {EventTypes} from '../enums/EventTypes';

class MyPlugin extends baseDriverModule {

  /**
   * Инициализация плагина
   */
  initDeviceEx(resolve, reject) {
    super.initDeviceEx(() => {
      this.app.log('Плагин инициализирован');
      resolve({});
    }, reject);
  }

  /**
   * Подключение к устройству
   */
  connectEx(resolve, reject) {
    this.app.log('Подключение к устройству...');
    // Здесь код подключения к устройству
    resolve({});
  }

  /**
   * Обработка команд
   */
  commandEx(command, value, params, options, resolve, reject, status) {
    this.app.log('Команда:', command, 'Значение:', value);
    
    switch(command) {
      case 'on':
        // Включить устройство
        resolve({state: 'on'});
        break;
      case 'off':
        // Выключить устройство
        resolve({state: 'off'});
        break;
      case 'status':
        // Получить статус
        resolve({connected: true, state: 'on'});
        break;
      default:
        reject({message: 'Неизвестная команда'});
    }
  }
}

// Создание экземпляра плагина
const app = new MyPlugin();
app.logging = true;

API методы

Работа с устройствами

Создание дочернего устройства

checkSubDevice(
  model: string,      // Модель устройства (например, 'zigbee2mqtt.light')
  key: string,        // Уникальный ключ
  name: string,       // Название устройства
  params: object,     // Параметры устройства
  zone_id: number     // ID зоны
): Promise<any>

Параметры устройства:

{
  icon: 'light',           // Иконка устройства
  identifier: 'unique_id', // Уникальный идентификатор
  capabilities: [          // Возможности устройства
    {
      index: 0,
      property: 'state',
      ident: 'state',
      display_name: 'Состояние',
      access: 'rw',        // r=read, w=write
      homekit: true,
      yandex: true,
      options: {
        value_on: true,
        value_off: false
      }
    }
  ],
  parent_identifier: 123   // ID родительского устройства
}

Доступные иконки:

  • Освещение: light, chandelier, rgb_lamp, rgb_strip, table_lamp, spotlight, sconce
  • Безопасность: camera, security, intercom, doorlock, door_sensor, motion, leak, gas_leak, smoke
  • Климат: heater, convector, humidifier, ac_unit, fan, temp_sensor, humidity_sensor
  • Мультимедиа: tv, gamepad, music_note
  • Бытовая техника: kettle, fridge, washing_machine, microwave, vacuum_cleaner
  • Другое: socket, gates, valve, hub

Получение списка устройств

getDevices(params?: object): Promise<Device[]>

Пример:

const devices = await this.getDevices({currentStatus: true});
devices.forEach(device => {
  this.app.log('Устройство:', device.name, 'Статус:', device.currentStatus);
});

Отправка команды другому устройству

deviceCommand(
  ident: string,    // Идентификатор устройства
  command: string,  // Команда
  data: object,     // Данные
  value: any        // Значение
): Promise<any>

Пример:

await this.deviceCommand('light_bedroom', 'on', {}, true);

Публикация событий

Публикация статуса устройства

publishStatus(eventType: EventTypes, status: object): void

Пример:

// Отправка данных с датчика
this.publishStatus(EventTypes.UpdateTemperature, {
  temperature_living_room: 22.5,
  humidity_living_room: 45,
  connected: true
});

Оптимизация публикации:

Метод publishStatus автоматически:

  • Публикует только измененные данные
  • Отправляет числовые данные при изменении >10% или раз в 15 секунд
  • Всегда отправляет события датчиков движения, открытия и т.д.

Прямая публикация событий

publish(eventType: EventTypes, ...params: any[]): void

Пример:

this.publish(EventTypes.ChangedMotion, {
  parent_identifier: this.device_id,
  motion_sensor_1: true
});

Уведомления

Отправка уведомления пользователю

sendNotify(message: string, options?: object): void

Пример:

this.sendNotify('Устройство подключено успешно!');

Push-уведомление

sendPushNotification(
  message: string,
  email: string,
  title: string
): void

Пример:

this.sendPushNotification(
  'Обнаружено движение в гостиной',
  'user@example.com',
  'Датчик движения'
);

Работа с данными

Получение конфигурации

getConfig(): Promise<any>

Работа с базой данных

// Получение записей
getTable(table: string, options: object): Promise<any[]>

// Создание записи
createTable(table: string, options: object): Promise<any>

// Обновление записей
updateTable(table: string, options: object, where: object): Promise<any>

Пример:

// Получение всех устройств типа 'light'
const lights = await this.getTable('devices', {
  where: {type: 'light'}
});

// Создание записи
await this.createTable('custom_data', {
  device_id: 123,
  key: 'last_seen',
  value: new Date().toISOString()
});

Облачные запросы

cloudRequest(params: object): Promise<any>

Типы событий (EventTypes)

Устройства

  • DeviceCreate — создание устройства
  • DeviceUpdate — обновление устройства
  • DeviceDelete — удаление устройства
  • DeviceConnect — подключение устройства
  • DeviceDiscover — обнаружение нового устройства

Обновления данных

  • UpdateTemperature — обновление температуры
  • UpdatePower — обновление мощности
  • UpdateAlert — предупреждение

События датчиков

  • ChangedMotion — изменение датчика движения
  • ChangedMagnet — изменение геркона (открытие/закрытие)
  • ChangedPower — изменение состояния питания
  • ChangedAction — пользовательское действие
  • ChangedLeakChange — обнаружение протечки

Приложение

  • ApplicationReady — приложение готово
  • ApplicationDeviceCommand — команда устройству
  • NewEvent — новое событие

База данных

  • DatabaseReady — БД готова
  • DatabaseQuery — запрос к БД
  • DatabaseUpdate — обновление БД

Примеры плагинов

Пример 1: Плагин для HTTP устройств

import {baseDriverModule} from '../core/base-driver-module';
import {EventTypes} from '../enums/EventTypes';
import axios from 'axios';

class HttpDevicePlugin extends baseDriverModule {
  
  private baseUrl: string;

  initDeviceEx(resolve, reject) {
    super.initDeviceEx(() => {
      // Получаем URL из параметров
      this.baseUrl = this.params.url || 'http://192.168.1.100';
      this.app.log('HTTP устройство:', this.baseUrl);
      resolve({});
    }, reject);
  }

  connectEx(resolve, reject) {
    // Проверяем доступность устройства
    axios.get(`${this.baseUrl}/status`)
      .then(response => {
        this.app.log('Устройство онлайн');
        this.startPolling();
        resolve({});
      })
      .catch(error => {
        reject({message: 'Устройство недоступно'});
      });
  }

  startPolling() {
    setInterval(() => {
      axios.get(`${this.baseUrl}/sensors`)
        .then(response => {
          this.publishStatus(EventTypes.UpdateTemperature, {
            temperature_sensor: response.data.temperature,
            humidity_sensor: response.data.humidity,
            connected: true
          });
        })
        .catch(error => {
          this.app.error('Ошибка получения данных:', error.message);
        });
    }, 10000); // Каждые 10 секунд
  }

  commandEx(command, value, params, options, resolve, reject, status) {
    switch(command) {
      case 'on':
        axios.post(`${this.baseUrl}/control`, {power: 'on'})
          .then(() => resolve({state: 'on'}))
          .catch(error => reject({message: error.message}));
        break;
      
      case 'off':
        axios.post(`${this.baseUrl}/control`, {power: 'off'})
          .then(() => resolve({state: 'off'}))
          .catch(error => reject({message: error.message}));
        break;
      
      case 'status':
        axios.get(`${this.baseUrl}/status`)
          .then(response => resolve(response.data))
          .catch(error => reject({message: error.message}));
        break;
      
      default:
        reject({message: 'Неизвестная команда'});
    }
  }
}

const app = new HttpDevicePlugin();
app.logging = true;

Пример 2: Плагин-шлюз с дочерними устройствами

import {baseDriverModule} from '../core/base-driver-module';
import {EventTypes} from '../enums/EventTypes';

class GatewayPlugin extends baseDriverModule {

  private devices: Map<string, any> = new Map();

  initDeviceEx(resolve, reject) {
    super.initDeviceEx(() => {
      this.app.log('Шлюз инициализирован');
      resolve({});
    }, reject);
  }

  connectEx(resolve, reject) {
    // Сканирование устройств
    this.scanDevices().then(() => {
      resolve({});
    });
  }

  async scanDevices() {
    // Обнаружение устройств (например, через Zigbee, Z-Wave и т.д.)
    const foundDevices = [
      {id: '0x123', type: 'light', name: 'Свет в гостиной'},
      {id: '0x456', type: 'sensor', name: 'Датчик температуры'}
    ];

    for (const device of foundDevices) {
      await this.registerDevice(device);
    }
  }

  async registerDevice(device: any) {
    let model, icon, capabilities;

    if (device.type === 'light') {
      model = 'gateway.light';
      icon = 'light';
      capabilities = [
        {
          property: 'state',
          ident: 'state',
          display_name: 'Включено',
          access: 'rw',
          homekit: true,
          options: {value_on: true, value_off: false}
        },
        {
          property: 'brightness',
          ident: 'brightness',
          display_name: 'Яркость',
          access: 'rw',
          homekit: true,
          options: {minValue: 0, maxValue: 100, stepValue: 1}
        }
      ];
    } else if (device.type === 'sensor') {
      model = 'gateway.temperature';
      icon = 'temp_sensor';
      capabilities = [
        {
          property: 'temperature',
          ident: 'temperature',
          display_name: 'Температура',
          access: 'r',
          scale: '°C'
        }
      ];
    }

    const result = await this.checkSubDevice(
      model,
      device.id,
      device.name,
      {icon, identifier: device.id, capabilities},
      null
    );

    this.devices.set(device.id, device);
    this.app.log('Зарегистрировано устройство:', device.name);
  }

  commandEx(command, value, params, options, resolve, reject, status) {
    const deviceId = options.ident;
    
    // Отправка команды конкретному дочернему устройству
    if (this.devices.has(deviceId)) {
      // Здесь отправка команды через протокол шлюза
      this.app.log('Команда для устройства', deviceId, ':', command, value);
      resolve({success: true});
    } else {
      reject({message: 'Устройство не найдено'});
    }
  }

  getSubDevicesEx(resolve, reject, zones) {
    // Возврат списка всех дочерних устройств
    const devices = Array.from(this.devices.values()).map(device => ({
      class_name: 'GatewayDevice',
      identifier: device.id,
      name: device.name,
      params: {
        icon: device.type === 'light' ? 'light' : 'temp_sensor',
        identifier: device.id
      }
    }));

    resolve(devices);
  }
}

const app = new GatewayPlugin();
app.logging = true;

Отладка плагинов

Локальный запуск

Для ручного тестирования плагина:

const app = new MyPlugin();
app.logging = true;

// Инициализация с тестовыми параметрами
app.initDevice({
  params: {
    url: 'http://192.168.1.100',
    port: 80
  }
}).then(() => {
  // Подключение
  app.connect({id: 1}).then(() => {
    // Тестовая команда
    app.command({
      command: 'status',
      id: 1
    }).then(result => {
      console.log('Результат:', result);
    });
  });
});

Логирование

// Разные уровни логирования
this.app.log('Информация');         // INFO
this.app.error('Ошибка');           // ERROR
this.app.debug('Отладка');          // DEBUG
this.app.info('Информация');        // INFO

Логи сохраняются в /var/log/bary/plugin-name.log с ротацией:

  • Максимальный размер файла: 10 МБ
  • Количество файлов: 5

Параметры запуска

# Запуск с логированием
node dist/my-plugin.js 123 logging=true

# Удаленное подключение к BARY
node dist/my-plugin.js 123 host=192.168.1.100 port=8000

Лучшие практики

1. Обработка ошибок

commandEx(command, value, params, options, resolve, reject, status) {
  try {
    // Ваш код
    resolve(result);
  } catch (error) {
    this.app.error('Ошибка выполнения команды:', error.message);
    reject({message: error.message, ignore: false});
  }
}

2. Переподключение при ошибках

connectEx(resolve, reject) {
  const tryConnect = (attempts = 0) => {
    this.connectToDevice()
      .then(() => resolve({}))
      .catch(error => {
        if (attempts < 3) {
          this.app.log(`Попытка переподключения ${attempts + 1}/3`);
          setTimeout(() => tryConnect(attempts + 1), 5000);
        } else {
          reject({message: 'Не удалось подключиться'});
        }
      });
  };
  
  tryConnect();
}

3. Оптимизация публикации данных

// Используйте publishStatus для автоматической оптимизации
this.publishStatus(EventTypes.UpdateTemperature, {
  temperature: 22.5,  // Отправится только при изменении >10%
  humidity: 45        // или раз в 15 секунд
});

// Для важных событий используйте publish напрямую
this.publish(EventTypes.ChangedMotion, {
  motion: true  // Отправится сразу
});

4. Управление зависимостями

В plugin.json указывайте только специфичные для вашего плагина зависимости:

{
  "dependencies": {
    "my-device-sdk": "^2.0.0"
  }
}

BARY автоматически установит их при первом запуске плагина.


Публикация плагина

  1. Соберите плагин:
    npm run build
    
  2. Создайте архив:
    tar -czf my-plugin.tar.gz dist/ package.json plugin.json
    
  3. Установка пользователем:
    • Загрузите архив в BARY через веб-интерфейс
    • BARY автоматически установит зависимости и запустит плагин

Поддержка

Если у вас возникли вопросы по разработке плагинов: