Intra Forum
    • Категории
    • Последние
    • Метки
    • Популярные
    • Зарегистрироваться
    • Войти
    • Профиль
    • Подписки 0
    • Подписчики 0
    • Темы 3
    • Сообщения 26
    • Группы 2
    M Не в сети
    1. Главная
    2. maksimvershinin
    3. Сообщения

    Сообщения

    Последние Лучшие сообщения Спорные
    • RE: Выборочная передача данных в OPC сервер

      @tako Если возникнут вопросы по разработке плагина, можете написать на почту maksim.vershinin@ih-systems.com

      написал в IntraOPC
      M
      maksimvershinin
    • RE: Выборочная передача данных в OPC сервер

      @tako На текущий момент в плагине opcua server данный функционал отсутствует. Есть три варианта решения вашей задачи:

      1. Вы можете сами расширить этот функционал, доработав плагин под себя. Все плагины у нас с открытым исходным кодом. API открытые, на сайте документации в разделе Источники данных, Плагины. По сути на уровне получения устройств из системы вам просто нужно будет отфильтровать по тем свойствам, которые вам необходимы и отдать дальше уже только то, что нужно. Правда это уже будет ваш плагин и назвать надо будет его по своему. В конце названия плагинов не должно быть цифр. https://github.com/intrahouseio/ih-v5-opcuaserver-plugin
        Подсказака. Менять нужно в одном месте. Файл /lib/utils.js. Функция getDevicesByOneFilter. Сейчас она возвращает все устройства со всеми свойствами в зависимости от локации или тега. Сюда достаточно добавить условие, чтобы ограничить свойства в устройстве.
      2. Есть еще вариант ограничить доступ. На сервер можно активировать учетную запись пользователя, у которой доступ к переменным будет только для чтения.
      3. Создавать отдельные устройства и через сценарий связать их с вашими.
      написал в IntraOPC
      M
      maksimvershinin
    • RE: Выборочная передача данных в OPC сервер

      Добрый день. А не могли бы вы описать более подробно вашу задачу? Вообще на клиенте можно не подписываться на те свойства, которые вам не нужны и тогда они не будут приходить.

      написал в IntraOPC
      M
      maksimvershinin
    • RE: Пример ГРАФИКА на HTML виджете.

      @grinsva Так же прошу обратить внимание, что данные по точкам, которые необходимо отрисовать на графике, желательно получать через RESTApi, а не через переменные клиента, так как это большой объем данных и от этого может пострадать производительность системы.

      написал в HTML виджеты
      M
      maksimvershinin
    • RE: Пример ГРАФИКА на HTML виджете.

      @grinsva Для того, чтобы ваш HTML виджет мог работать через p2p, то вам необходимо понимать откуда закачивать сторонние библиотеки. Для этого в HTML виджете можете воспользоваться window.location.hostname, который вернет hostname из адресной строки. В случае подключения через p2p вернется p2p.ih-systems.com. В этом случае ссылку на библиотеку делаем из публичных cdn, если запускается локально, без доступа в интернет, то библиотеку берем из "/files/chart.js"

      написал в HTML виджеты
      M
      maksimvershinin
    • RE: Пример ГРАФИКА на HTML виджете.

      @grinsva Контейнер с готовым HTML виджетом доступен во вложении vc291_1.ihpack. Установить можно через импорт.

      написал в HTML виджеты
      M
      maksimvershinin
    • Вертикальный текст

      Пример HTML виджета в шаблоне визуализации для вывода вертикального текста прижатого к низу и автоматическим переносом на следующую строку.@vt099.ihpack
      Снимок экрана 2026-08-31 в 09.50.27.png

      написал в HTML виджеты
      M
      maksimvershinin
    • RE: Привязка локальных переменных для Графиков в HTML

      Да вы все делаете правильно. Нужно еще добавить window.ihapi.addEventListener(uuid, "data", onData);
      чтобы при изменении переменных клиента вызывалась функция onData и данные обновлялись

      написал в HTML виджеты
      M
      maksimvershinin
    • Всплывающая пользовательская клавиатура

      С версии 5.19.24 мы расширили возможности HTML виджета и добавили команды window.ihapi.getTriggerUuid, window.ihapi.getTriggerMeta для получения данных о связях элементов. Это позволило нам перехватывать ввод/вывод например с Input. Во вложении пример клавиатуры которая создана с помощью HTML виджета, которую можно вызываеть по клику мыши на Input.@di0015.ihpack

      Снимок экрана 2026-08-19 в 08.51.07.png

      написал в HTML виджеты
      M
      maksimvershinin
    • RE: Невозможно зайти на панель после смены проекта

      @Sboro Добрый день. Попробуйте из папки /var/lib/intrahouse/папка проекта/private удалить файл users.db и перезагрузить сервис. Это сбросит пароль по умолчанию. И у вас очень старая версия системы. Рекомендую обновить до последней.

      написал в IntraHouse
      M
      maksimvershinin
    • Всплывающая подсказка Tooltip. Шаблон визуализации.

      Многие из вас просили в системе реализовать всплывающие подсказки на элементах. Мы сделали лучше, теперь вы можете реализовать такие подсказки с помощью шаблона визуализации c HTML виджетом внутри, при этом расширив функционал как вам будет нужно. Пример шаблона визуализации во вложении@vt032.ihpack . Достаточно импортировать его в проект, вставьте шаблон tooltip в нужные шаблоны, в родительском шаблоне для шаблона tooltip выполните привязку и можно пользоваться. Напомню с версии 5.19.22 есть возможность использовать шаблон в шаблоне.

      написал в HTML виджеты
      M
      maksimvershinin
    • RE: Корявое изображение в Графиках CHART Multiline GL при зумировании по оси Х

      @grinsva Если у вас график заполняется через обработчик, то проблему с которой вы столкнулись я уже описал. Если ставить NULL, то вы просто прерываете сам график, но он все равно будет двойной.

      написал в IntraSCADA
      M
      maksimvershinin
    • RE: Ошибка при работе базы данных SQLite

      @yurijak77777 Скорее всего проблемы с SD картой. Попробуйте переустановить на новую sd карточку. Не забудьте выгрузить проект перед переустановкой.

      написал в IntraHouse
      M
      maksimvershinin
    • RE: как настроить плагин мах.

      Добрый день. Такой сценарий должен быть запущен через механизм срабатывания тревог (выбор сценария настраивается в тревоге). Тогда в aleObj будет свойство txt. При ручном запуске сценария в aleObj будет undefined. Для тестирования отправки в мессенджеры может просто статичный текст отправить.

      написал в IntraSCADA
      M
      maksimvershinin
    • RE: Ошибка при работе базы данных SQLite

      @yurijak77777 Добрый день. У вас на какое железо развернута система? Возможно проблемы с EMMC либо с SD картой

      написал в IntraHouse
      M
      maksimvershinin
    • RE: Корявое изображение в Графиках CHART Multiline GL при зумировании по оси Х

      @grinsva Добрый день.

      1. По первому графику у вас начало графика соединяется с концом, так как вы используете в обработчике графика start, end из переменных клиента, а не из фильтра, который присылает виджет Graphgl. Так как graphgl при включении перемещении по X запрашивает сразу два диапазона: текущий и предыдущий такой же, чтобы можно было переместить график назад, у вас приходит два запроса и уходит два одинаковых ответа, поэтому и соединяется начало и конец графика. Для решения проблемы просто в обработчике используйте start, end из фильтра
        Снимок экрана 2026-05-25 в 09.18.50.png
      2. По поводу отображения шкалы Х, нужно удалить текущий график, сохранить и поставить такой же, чтобы уже установилась последняя версия графика.
      написал в IntraSCADA
      M
      maksimvershinin
    • RE: Редактируемый из интерфейса пользователя график

      Добрый день. Есть пример такого графика. Он реализован на графике квадратичной функции, где с помощью мышки можно менять три точки, по сути три коэффициента a,b,c функции y = ax² − bx − c. Для удобства сделано три режима: День, Ночь, Эконом. Для каждого можно настроить свой график. Вот пример HTML виджета.

      <style type="text/css">
          #mode-select {
              margin-bottom: 10px;
              font-size: 14px;
              padding: 5px;
          }
          #canvas {
              border: 1px solid #000;
              cursor: grab;
              display: block;
              margin: 0 auto;
          }
          #canvas:active {
              cursor: grabbing;
          }
          #formula {
              margin-top: 15px;
              font-size: 16px;
              font-weight: bold;
              text-align: center;
              padding: 5px;
          }
          #info {
              margin-top: 10px;
              color: #666;
              font-size: 12px;
              text-align: center;
              padding: 0 10px;
          }
          #tooltip {
              position: absolute;
              background: rgba(0, 0, 0, 0.8);
              color: white;
              padding: 5px 10px;
              border-radius: 4px;
              font-size: 12px;
              pointer-events: none;
              display: none;
              z-index: 10;
          }
      </style>
      
      <script type="text/javascript">
          // Диагностика UUID (временно, для отладки — удалите после)
          console.log('Available UUID sources:', {
              template: '${uuid}',
              window_uuid: window.uuid,
              ihapi_uuid: window.ihapi?.uuid,
              global_uuid: typeof uuid !== 'undefined' ? uuid : 'undefined'
          });
      
          // Используем глобальный uuid, если доступен
          const currentUuid = uuid || window.uuid || window.ihapi?.uuid || '';
      
          let canvas, ctx, formulaDiv, tooltip, modeSelect;
          let points = [
              {x: -20, y: 70},
              {x: 0, y: 55},
              {x: 10, y: 40}
          ];
          let selectedPoint = null;
          let isDragging = false;
          let currentMode = 'day'; // Текущий режим по умолчанию
          let modeCoeffs = { // Коэффициенты для каждого режима
              day: {a: 0, b: 0, c: 0},
              night: {a: 0, b: 0, c: 0},
              eco: {a: 0, b: 0, c: 0}
          };
          let currentCoeffs = {a: 0, b: 0, c: 0}; // Текущие для рисования
      
          // Параметры устройства и свойств
          const deviceId = 'd0228';
          const modes = ['day', 'night', 'eco'];
          const fullProps = {
              day: `${deviceId}_propABC_day`,
              night: `${deviceId}_propABC_night`,
              eco: `${deviceId}_propABC_eco`
          };
      
          // Параметры графика (фиксированные, как в начале)
          const margin = 50;
          const xMin = -30, xMax = 10;
          const yMin = 45, yMax = 75;
      
          // Функции преобразования координат (фиксированные)
          function toCanvasX(x) {
              return margin + (x - xMin) / (xMax - xMin) * (canvas.width - 2 * margin);
          }
          function toCanvasY(y) {
              return (canvas.height - margin) - (y - yMin) / (yMax - yMin) * (canvas.height - 2 * margin);
          }
          function fromCanvasX(canvasX) {
              return xMin + (canvasX - margin) / (canvas.width - 2 * margin) * (xMax - xMin);
          }
          function fromCanvasY(canvasY) {
              return yMin + ((canvas.height - margin) - canvasY) / (canvas.height - 2 * margin) * (yMax - yMin);
          }
      
          // Вычисление коэффициентов квадратичной кривой
          function computeQuadraticCoeffs() {
              if (points.length !== 3) return {a: 0, b: 0, c: 0};
              let [p1, p2, p3] = points;
              let x1 = p1.x, y1 = p1.y;
              let x2 = p2.x, y2 = p2.y;
              let x3 = p3.x, y3 = p3.y;
              let d12 = x2 - x1;
              let d13 = x3 - x1;
              let dd12 = x2 * x2 - x1 * x1;
              let dd13 = x3 * x3 - x1 * x1;
              let dy12 = y2 - y1;
              let dy13 = y3 - y1;
              let det = dd12 * d13 - dd13 * d12;
              if (Math.abs(det) < 1e-10) {
                  let n = 3;
                  let sumX = x1 + x2 + x3;
                  let sumY = y1 + y2 + y3;
                  let sumXY = x1*y1 + x2*y2 + x3*y3;
                  let sumX2 = x1*x1 + x2*x2 + x3*x3;
                  let slope = (3 * sumXY - sumX * sumY) / (3 * sumX2 - sumX * sumX);
                  let intercept = (sumY - slope * sumX) / 3;
                  return {a: 0, b: slope, c: intercept};
              }
              let a = (dy12 * d13 - dy13 * d12) / det;
              let b = (dd12 * dy13 - dd13 * dy12) / det;
              let c = y1 - a * x1 * x1 - b * x1;
              return {a, b, c};
          }
      
          // Вычисление y по x и коэффициентам
          function computeY(x, coeffs) {
              return coeffs.a * x * x + coeffs.b * x + coeffs.c;
          }
      
          // Обновление коэффициентов в устройстве (JSON в свойство для режима)
          function updateCoeffsInDevice() {
              const coeffs = computeQuadraticCoeffs();
              currentCoeffs = coeffs; // Сохраняем текущие
              const jsonStr = JSON.stringify(coeffs);
              if (window.ihapi && window.ihapi.deviceCommand) {
                  window.ihapi.deviceCommand(deviceId, `propABC_${currentMode}`, jsonStr);
                  console.log(`Sent JSON for ${currentMode}:`, jsonStr); // Для отладки
              }
          }
      
          // Обновление точек по коэффициентам (из подписки)
          function updatePointsFromCoeffs(a, b, c) {
              const coeffs = {a, b, c};
              points.forEach(p => {
                  p.y = Math.max(yMin, Math.min(yMax, computeY(p.x, coeffs)));
              });
          }
      
          // Смена режима
          function switchMode(newMode) {
              currentMode = newMode;
              currentCoeffs = {...modeCoeffs[newMode]}; // Копируем
              updatePointsFromCoeffs(currentCoeffs.a, currentCoeffs.b, currentCoeffs.c);
              draw();
              modeSelect.value = newMode; // Синхронизация селекта
          }
      
          // Рисование осей (фиксированные подписи, без наложения)
          function drawAxes() {
              ctx.clearRect(0, 0, canvas.width, canvas.height);
              ctx.strokeStyle = '#000';
              ctx.lineWidth = 1;
              // X-ось
              ctx.beginPath();
              ctx.moveTo(margin, canvas.height - margin);
              ctx.lineTo(canvas.width - margin, canvas.height - margin);
              ctx.stroke();
              // Y-ось
              ctx.beginPath();
              ctx.moveTo(margin, margin);
              ctx.lineTo(margin, canvas.height - margin);
              ctx.stroke();
              // Подписи (подвинуты ниже/выше для избежания наложения)
              ctx.fillText('Уличная T (°C)', canvas.width / 2, canvas.height - 10); // X-label ниже
              ctx.save();
              ctx.translate(10, canvas.height / 2);
              ctx.rotate(-Math.PI / 2);
              ctx.fillText('Теплоноситель T (°C)', 0, 0); // Y-label слева
              ctx.restore();
              // Метки X (с отступом)
              for (let i = 0; i <= 5; i++) {
                  let x = xMin + i * (xMax - xMin) / 5;
                  let cx = toCanvasX(x);
                  ctx.fillText(x.toFixed(0), cx - 10, canvas.height - margin + 20); // Отступ от оси
                  ctx.beginPath();
                  ctx.moveTo(cx, canvas.height - margin - 5);
                  ctx.lineTo(cx, canvas.height - margin + 5);
                  ctx.stroke();
              }
              // Метки Y (с отступом)
              for (let i = 0; i <= 5; i++) {
                  let y = yMin + i * (yMax - yMin) / 5;
                  let cy = toCanvasY(y);
                  ctx.fillText(y.toFixed(0), 5, cy + 5); // Отступ слева
                  ctx.beginPath();
                  ctx.moveTo(margin - 5, cy);
                  ctx.lineTo(margin + 5, cy);
                  ctx.stroke();
              }
          }
      
          // Рисование кривой и точек
          function drawCurveAndPoints() {
              const coeffs = computeQuadraticCoeffs();
              // Кривая
              ctx.strokeStyle = '#ff0000';
              ctx.lineWidth = 2;
              ctx.beginPath();
              let step = (xMax - xMin) / 100;
              for (let i = 0; i <= 100; i++) {
                  let x = xMin + i * step;
                  let y = Math.max(yMin, Math.min(yMax, computeY(x, coeffs)));
                  let cx = toCanvasX(x);
                  let cy = toCanvasY(y);
                  if (i === 0) ctx.moveTo(cx, cy); else ctx.lineTo(cx, cy);
              }
              ctx.stroke();
              // Формула
              formulaDiv.textContent = `y = ${coeffs.a.toFixed(3)}x² + ${coeffs.b.toFixed(3)}x + ${coeffs.c.toFixed(3)} (${currentMode})`;
              // Точки
              ctx.fillStyle = '#0000ff';
              points.forEach((p, idx) => {
                  let cx = toCanvasX(p.x);
                  let cy = toCanvasY(p.y);
                  ctx.beginPath();
                  ctx.arc(cx, cy, 5, 0, 2 * Math.PI);
                  ctx.fill();
                  ctx.fillStyle = '#000';
                  ctx.fillText(idx + 1, cx + 8, cy - 8);
                  ctx.fillStyle = '#0000ff';
              });
          }
      
          // Рисование перекрестия
          function drawCrosshair(mouseX, mouseY) {
              if (mouseX < margin || mouseX > canvas.width - margin || mouseY < margin || mouseY > canvas.height - margin) return;
              ctx.strokeStyle = '#888';
              ctx.lineWidth = 1;
              ctx.setLineDash([5, 5]);
              // Вертикальная
              ctx.beginPath();
              ctx.moveTo(mouseX, margin);
              ctx.lineTo(mouseX, canvas.height - margin);
              ctx.stroke();
              // Горизонтальная
              ctx.beginPath();
              ctx.moveTo(margin, mouseY);
              ctx.lineTo(canvas.width - margin, mouseY);
              ctx.stroke();
              ctx.setLineDash([]);
          }
      
          // Общая отрисовка
          function draw(mouseX = null, mouseY = null) {
              drawAxes();
              drawCurveAndPoints();
              if (mouseX !== null && mouseY !== null) {
                  drawCrosshair(mouseX, mouseY);
              }
          }
      
          // Обработчики мыши
          function initMouseHandlers() {
              canvas.addEventListener('mousemove', (e) => {
                  const rect = canvas.getBoundingClientRect();
                  const mx = e.clientX - rect.left;
                  const my = e.clientY - rect.top;
      
                  if (isDragging && selectedPoint !== null) {
                      const clampedX = Math.max(margin, Math.min(canvas.width - margin, mx));
                      const clampedY = Math.max(margin, Math.min(canvas.height - margin, my));
                      points[selectedPoint].x = fromCanvasX(clampedX);
                      points[selectedPoint].y = fromCanvasY(clampedY);
                      draw(); // Без crosshair во время dragging
                  } else {
                      const xReal = fromCanvasX(mx);
                      if (mx >= margin && mx <= canvas.width - margin && my >= margin && my <= canvas.height - margin) {
                          const yReal = Math.max(yMin, Math.min(yMax, computeY(xReal, currentCoeffs)));
                          tooltip.style.display = 'block';
                          tooltip.innerHTML = `Уличная: ${xReal.toFixed(1)}°C<br>Котел: ${yReal.toFixed(1)}°C`;
                          tooltip.style.left = `${e.pageX + 10}px`;
                          tooltip.style.top = `${e.pageY - 10}px`;
                          draw(mx, my);
                      } else {
                          tooltip.style.display = 'none';
                          draw();
                      }
                  }
              });
      
              canvas.addEventListener('mousedown', (e) => {
                  const rect = canvas.getBoundingClientRect();
                  const mx = e.clientX - rect.left;
                  const my = e.clientY - rect.top;
                  selectedPoint = null;
                  points.forEach((p, idx) => {
                      const cx = toCanvasX(p.x);
                      const cy = toCanvasY(p.y);
                      if (Math.hypot(mx - cx, my - cy) < 10) {
                          selectedPoint = idx;
                          isDragging = true;
                          tooltip.style.display = 'none';
                      }
                  });
              });
      
              canvas.addEventListener('mouseup', () => {
                  if (isDragging && selectedPoint !== null) {
                      isDragging = false;
                      selectedPoint = null;
                      updateCoeffsInDevice();
                      draw(); // Финальное обновление без crosshair
                  }
              });
      
              canvas.addEventListener('mouseleave', () => {
                  tooltip.style.display = 'none';
                  draw(); // Очистить crosshair
              });
          }
      
          // Update от подписки на коэффициенты (парсим JSON)
          function updateData(data) {
              let updated = false;
              modes.forEach(mode => {
                  const fullProp = fullProps[mode];
                  if (data[fullProp]) {
                      try {
                          const parsed = JSON.parse(data[fullProp]);
                          modeCoeffs[mode] = {a: parsed.a || 0, b: parsed.b || 0, c: parsed.c || 0};
                          if (mode === currentMode) {
                              currentCoeffs = {...modeCoeffs[mode]};
                              updatePointsFromCoeffs(currentCoeffs.a, currentCoeffs.b, currentCoeffs.c);
                              updated = true;
                          }
                      } catch (e) {
                          console.warn(`Invalid JSON for ${mode}:`, data[fullProp]);
                      }
                  }
              });
              if (updated) {
                  draw();
              }
          }
      
          // Init
          function init() {
              canvas = document.getElementById('canvas');
              if (!canvas) {
                  console.error('Canvas element not found');
                  return;
              }
              ctx = canvas.getContext('2d');
              formulaDiv = document.getElementById('formula');
              tooltip = document.getElementById('tooltip');
              modeSelect = document.getElementById('mode-select');
              if (!formulaDiv) {
                  console.error('Formula div not found');
                  return;
              }
      
              // Настройка селекта режима
              modeSelect.addEventListener('change', (e) => {
                  switchMode(e.target.value);
              });
      
              // Безопасная настройка подписки и слушателей
              if (window.ihapi && currentUuid) {
                  const localDestroy = function() {
                      if (window.ihapi && window.ihapi.deviceUnsub) {
                          window.ihapi.deviceUnsub(currentUuid, modes.map(m => fullProps[m]));
                      }
                  };
                  window.ihapi.addEventListener(currentUuid, 'destroy', localDestroy);
                  window.ihapi.addEventListener(currentUuid, 'data', updateData);
                  window.ihapi.deviceSub(currentUuid, modes.map(m => fullProps[m]));
                  console.log('ihapi setup successful with UUID:', currentUuid);
              } else {
                  console.warn('ihapi setup skipped (UUID or ihapi not available)');
              }
      
              initMouseHandlers();
              draw(); // Начальная отрисовка (обновится при 'data')
          }
      
          // Запуск init после загрузки DOM
          if (document.readyState === 'loading') {
              document.addEventListener('DOMContentLoaded', init);
          } else {
              init();
          }
      </script>
      
      <select id="mode-select">
          <option value="day">День</option>
          <option value="night">Ночь</option>
          <option value="eco">Эконом</option>
      </select>
      
      <canvas id="canvas" width="600" height="400"></canvas>
      <div id="formula">Формула: y = 0x² + 0x + 0</div>
      <div id="info">Перетаскивайте точки для настройки кривой. Выберите режим. Наведите мышь для перекрестия и тултипа с температурами. Изменения сохраняются в propABC_[режим].</div>
      <div id="tooltip"></div>
      

      Так же необходимо создать новый тип устройства, который будет состоять из трех свойств типа String propABC_day, propABC_night, propABC_eco. Создать экземпляр этого устройства в проекте и в HTML виджете указать id устройства в строке 72, переменная deviceId.
      Далее необходимо создать сценарий который будет запускаться например раз в час и пересчитывать температуру теплоносителя в зависимости от температуры на улице, режима работы и коэффициентов кривых. Вот пример сценария:

      /**
       * @desc
       * @version 5
       */
      const mode = Device("DN001");
      const curves = Device("DN002");
      const outdoor = Device("AI_003");
      const kotel_temp = Device("DT_003");
      
      startOnChange([mode.state, curves])
      
      const script = {
        check() {
          return mode.state !== 3;
        },
        start() {
      
            let curvesArr = [];
            curvesArr.push(JSON.parse(curves.propABC_day));
            curvesArr.push(JSON.parse(curves.propABC_night));
            curvesArr.push(JSON.parse(curves.propABC_eco));
            const result = curvesArr[mode.state].a * outdoor.value * outdoor.value + curvesArr[mode.state].b * outdoor.value + curvesArr[mode.state].c
            const clipped = Math.max(45, Math.min(75, result));
            this.log(Math.round(clipped))
            kotel_temp.setValue("setpoint", Math.round(clipped));
      
        }
        
      };
      

      Снимок экрана 2026-05-04 в 09.00.13.png

      написал в HTML виджеты
      M
      maksimvershinin
    • RE: Ошибка при перезагрузке intraHouse

      @kanyck Добрый день. Мы не тестировали работу системы на devuan. Система должна запуститься и быть доступной на порту 8088. Ошибка в логах означает что не получилось запустить p2p плагин, но это не должно повлиять на работу системы в целом. Рекомендуем использовать те ОС, которые указаны в документации.

      написал в IntraHouse
      M
      maksimvershinin