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

    maksimvershinin

    @maksimvershinin

    Intra Support
    3
    Репутация
    3
    Просмотры профиля
    8
    Сообщения
    0
    Подписчики
    0
    Подписки
    Регистрация
    Последнее посещение

    maksimvershinin Отписаться Подписаться
    Intra Support Global Moderator

    Недавние сообщения 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
    • RE: Общие вопросы

      @ChirkOFF

      1. Данные по http нужно получать не через визуализацию, а через http плагин либо сценарий и данные нужно разместить в устройстве, чтобы на визуализации отработала подписка на изменение этих данных.
      2. Если сервер не поддерживает работу через iframe, единственный способ остается открыть его в новой вкладке браузера с помощью html виджета
      3. Шапку можно спрятать просто прикрыв любой панелью либо прямоугольником, а вместо кнопки ОК можно воспользовать Командой элемента submit для отчета. При вызове данной команды например по кнопке отчет перезапросится. По поводу индикаторы мы подумаем как это сделать в ближайшее время.
      4. Если вам нужно только статусы визуализировать то рекомендую хранить эти состояния лучше в устройстве, так как на них можно будет подписаться с визуализации и любое изменение будет приходить автоматически, а с пользовательскими таблицами придет организовать периолический вызов скрипта визуализации либо по команде вызывать.
      написал в IntraSCADA
      M
      maksimvershinin
    • RE: Общие вопросы

      @ChirkOFF

      1. Какую задачу вы пытаетесь решить периодическим вызовом скрипта визуализации?
      2. Если вам нужно отобразить страничку другого устройства, то лучше воспользуйтесь виджетом iframe
      3. Встроенные отчеты не предусматривают получение данных из внешних источников. Если вам надо получать данные со сторонних БД и сервисов, то воспользуйтесь отчетами Стимулсофт. Индикатор загрузки добавим в ближайшее время.
      4. Для того, чтобы данные сохранялись после перезагрки, то нужно использовать либо свойства устройства типа Parameter, либо сохраняйте эти параметры в пользовательскую таблицу. Тут все зависит от задачи. Хотите ли вы эти параметры передавать в контроллер или нет.
      5. По графикам предлагаю сразу начать использовать виджет MultichartGL
      написал в IntraSCADA
      M
      maksimvershinin
    • RE: Изменение атрибутов сохранения в БД для устройства из скрипта/сценария

      @grinsva Не совсем понятен вопрос. Вы можете динамически из обработчика включать и отключать запись в БД. Чем больше у вас настроено записей в БД, тем больше будет нагрузка на базу данных.

      написал в IntraSCADA
      M
      maksimvershinin
    • RE: Появиться ли возможность обновить виджет REPORT из скрипта визуализации?

      Добрый день. В новой версии системы 5.19.4 добавили в HTML виджет возможность вызывать Команду Элемента window.ihapi.elementCommand() . Для решения вашей задачи, можно добавить HTML виджет на экран или контейнер, спрятать его на задний фон, подписаться в нем на изменение переменной клиента, которая привязана к дереву и вызывать Команду Элемента Submit для Отчета. Пример Виджета:

      <style type="text/css">
      
      </style>
      
      <script type="text/javascript">
      
          function destroy() {
            window.ihapi.deviceUnsub(uuid, ['local003_var846'])
          }
      
          function update(data) {
            console.log(data)
            window.ihapi.elementCommand('report_1', 'submit') 
          }
      
          function init() {
            window.ihapi.addEventListener(uuid, 'destroy', destroy)
            window.ihapi.addEventListener(uuid, 'data', update)
      
            window.ihapi.deviceSub(uuid, ['local003_var846'])
          }
        
          init()
      </script>
      
      <div id="${uuid}">Hello World!</div>
      
      написал в IntraSCADA
      M
      maksimvershinin
    • RE: Изменение атрибутов сохранения в БД для устройства из скрипта/сценария

      @grinsva said:

      скрипта/сценария

      Данный функционал в разработке. Если вам нужно срочно изменить данные параметры, то вы это можете сделать вручную из системы или написать скрипт, который изменит файл devicedb.db в папке jbase проекта, но этот способ может привести к тому, что проект может не запуститься, если вы допустите ошибки в синтаксисе

      написал в IntraSCADA
      M
      maksimvershinin
    • RE: JS Плагин: onSub для devices - обработчик не вызывается

      @tallerhorizon Чтобы подписаться на изменения любых свойств устройств в системе, вам нужно, чтобы в расширении было выбрано location со значением ВСЕ, пример реализован в opcuaserver плагине, в раширении добавьте location и значение Все

      написал в IntraSCADA
      M
      maksimvershinin