Intra Support

Закрытая

Группа технической поддержки

Сообщения

  • RE: Общие вопросы

    @ChirkOFF, извините за задержку с ответом. Проверили, действительно, в системный плагин reportmaker была внесена ошибка. В ближайшей версии (v5.19.9) пофиксим, версия выйдет на следующей неделе.

  • 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

  • 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: Общие вопросы

    @ChirkOFF

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

    @ChirkOFF

    1. Какую задачу вы пытаетесь решить периодическим вызовом скрипта визуализации?
    2. Если вам нужно отобразить страничку другого устройства, то лучше воспользуйтесь виджетом iframe
    3. Встроенные отчеты не предусматривают получение данных из внешних источников. Если вам надо получать данные со сторонних БД и сервисов, то воспользуйтесь отчетами Стимулсофт. Индикатор загрузки добавим в ближайшее время.
    4. Для того, чтобы данные сохранялись после перезагрки, то нужно использовать либо свойства устройства типа Parameter, либо сохраняйте эти параметры в пользовательскую таблицу. Тут все зависит от задачи. Хотите ли вы эти параметры передавать в контроллер или нет.
    5. По графикам предлагаю сразу начать использовать виджет 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>
    

Список участников