@ChirkOFF, извините за задержку с ответом. Проверили, действительно, в системный плагин reportmaker была внесена ошибка. В ближайшей версии (v5.19.9) пофиксим, версия выйдет на следующей неделе.
Global Moderators
Forum wide moderators
Сообщения
-
RE: Общие вопросы
-
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)); } };
-
RE: Изменение атрибутов сохранения в БД для устройства из скрипта/сценария
@grinsva, добрый день!
Вышла версия 5.19.6, в нее добавили метод устройства device.updateDbrule для изменения правил сохранения свойств. Описание и примеры в документации -
RE: Как переносить элементы между проектами?
Для выгрузки большинства сущностей (шаблона визуализации, диалога, контейнера, типа устройства, изображений,... но не экрана!) есть операции Экспорт/Импорт, они описаны в документации.
Вот, например, Экспорт/Импорт диалога
Каждая сущность выгружается отдельно, ей присваивается новый ID типа user@dixxxx.
Если при попытке экспорта выдается сообщение о невозможности экспорта импортированного компонента, просто скопируйте экспортированный элемент, ему будет присвоен новый стандартный id внутри проекта и проблемы не будет.Второй вариант - можно создать пакет для выгрузки в разделе Ресурсы - Пакеты для выгрузки.
Загружается пакет аналогично, кнопкой Импорт в верхнем правом меню. Здесь уже будут выгружены/загружены все выбранные элементы во взаимосвязи, но это более сложное действие, которое может перезаписать сушествующие элементы, перед выполнением импорта желательно сделать копию текущего проектаСамый же простой вариант - скопировать/вставить демо проект целиком в дереве Проекты и в копии убрать все лишнее.
Из практики, при создании первого проекта большинство идет по этому пути
-
RE: Ошибка при перезагрузке intraHouse
@kanyck, добрый вечер.
Так понимаю, система в devuan у вас установилась. При штатной установке система сразу стартует как служба, но у вас этого не произошло, так как в devuan нет system-d ( менеджер служб, используемый в большинстве современных дистрибутивов Linux).
Итак, вы запустили систему не как службу, а как приложение в консоли.
Система запустилась, админка у вас доступна, вы нажимаете кнопку "Перезагрузить" в админке. При этом система останавливается (а не вылетает) - это штатное поведение intraHouse, перезапущена она должна быть менеджером служб. Сообщение об ошибке плагина - это не причина, а следствие остановки системы, плагин p2p просто не успел завершить работу.
Вы можете настроить менеджер служб, доступный в вашей ОС или по старинке создать демон для запуска службы при ее остановке.
Есть более простой способ - установить intraHouse на любой Linux, на котором система протестирована: Debian, Ubuntu, RedHat, CentOS, Alt Linux, ...
И кстати ключ лицензионный для intraHouse не нужен -
RE: Ошибка при перезагрузке intraHouse
@kanyck Добрый день. Мы не тестировали работу системы на devuan. Система должна запуститься и быть доступной на порту 8088. Ошибка в логах означает что не получилось запустить p2p плагин, но это не должно повлиять на работу системы в целом. Рекомендуем использовать те ОС, которые указаны в документации.
-
RE: Общие вопросы
- Данные по http нужно получать не через визуализацию, а через http плагин либо сценарий и данные нужно разместить в устройстве, чтобы на визуализации отработала подписка на изменение этих данных.
- Если сервер не поддерживает работу через iframe, единственный способ остается открыть его в новой вкладке браузера с помощью html виджета
- Шапку можно спрятать просто прикрыв любой панелью либо прямоугольником, а вместо кнопки ОК можно воспользовать Командой элемента submit для отчета. При вызове данной команды например по кнопке отчет перезапросится. По поводу индикаторы мы подумаем как это сделать в ближайшее время.
- Если вам нужно только статусы визуализировать то рекомендую хранить эти состояния лучше в устройстве, так как на них можно будет подписаться с визуализации и любое изменение будет приходить автоматически, а с пользовательскими таблицами придет организовать периолический вызов скрипта визуализации либо по команде вызывать.
-
RE: Общие вопросы
- Какую задачу вы пытаетесь решить периодическим вызовом скрипта визуализации?
- Если вам нужно отобразить страничку другого устройства, то лучше воспользуйтесь виджетом iframe
- Встроенные отчеты не предусматривают получение данных из внешних источников. Если вам надо получать данные со сторонних БД и сервисов, то воспользуйтесь отчетами Стимулсофт. Индикатор загрузки добавим в ближайшее время.
- Для того, чтобы данные сохранялись после перезагрки, то нужно использовать либо свойства устройства типа Parameter, либо сохраняйте эти параметры в пользовательскую таблицу. Тут все зависит от задачи. Хотите ли вы эти параметры передавать в контроллер или нет.
- По графикам предлагаю сразу начать использовать виджет MultichartGL
-
RE: Изменение атрибутов сохранения в БД для устройства из скрипта/сценария
@grinsva Не совсем понятен вопрос. Вы можете динамически из обработчика включать и отключать запись в БД. Чем больше у вас настроено записей в БД, тем больше будет нагрузка на базу данных.
-
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>