Global Moderators

Закрытая

Forum wide moderators

Сообщения

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

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

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

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

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

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

  • RE: Ошибка при работе базы данных SQLite

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

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

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

  • RE: Ошибка при работе базы данных SQLite

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

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

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

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

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