Как я данные со станков снимал: девять машин, три способа и Odoo

Задача звучала просто: руководство хотело знать, сколько станки реально работают. Не по словам мастеров и не по журналу, который заполняют в конце смены, а по факту. Счётчик выпущенных деталей вторым приоритетом. Обязательное условие: полностью автоматически, без участия человека.
Станков девять, производители разные, возраст тоже. Я был уверен, что главная сложность будет в Odoo. Оказалось наоборот: Odoo здесь самая спокойная часть, а вся работа ушла на то, чтобы вытащить данные из самих станков.
Почему марка станка не главное
Первое, что приходится принять: станки делятся не по бренду, а по тому, как они отдают данные о себе. В нашем парке получилось три семейства.
Первое. Станки Biesse: Rover A, Rover B1, Rover B2, Skipper, Clever. ЧПУ пишет суточную статистику в файл на локальном диске. Файл дозаписывается в течение дня, формат табличный, с разделителем табуляцией, несмотря на расширение xls.
DATA ORDER OPERATOR SERIAL NUMBER WORKLIST EVENT-PROG START END TIME QUANTITY
2026-03-11 1047 ivanov 888888 WL_221 LAVORO 06:04:12 14:31:50 30458 412
2026-03-11 1047 ivanov 888888 WL_221 PRG_1180.cix 07:12:03 07:19:44 461 18
Второе. Кромкооблицовочные станки IMA: Novimat, второй Novimat, Laser. Здесь никаких файлов статистики. Стоит софт ICOS, под ним MS SQL Server, у каждого станка своя локальная база. Интервалов работы станок не отдаёт вообще, зато отдаёт счётчики: метраж наклеенной кромки и количество деталей.
Третье. HOMAG. Своя система учёта производства со своей базой и собственным справочником состояний. Интервалы есть, счётчик деталей есть, но всё в своих терминах.
Ну и что делать?: считать надо не количество станков, а количество разных способов отдачи данных. Три одинаковых станка это одна интеграция и три установки. Два станка разных вендоров в одной линии это две интеграции. Смета, посчитанная по головам, промахнётся в разы.
Разведка перед установкой
Соблазн велик: написать агента по документации и поехать ставить. Так делать нельзя, потому что про цех по документации вы не знаете почти ничего.
Я начинал каждый тип с чекера. Это маленький скрипт, который ничего не меняет, а только отвечает на вопросы: какая здесь операционная система, какие пути существуют, что лежит в файлах, какой интерпретатор доступен, какие библиотеки есть, видно ли сервер, проходит ли авторизация.
Это сэкономило больше времени, чем любой другой приём в проекте. Три примера того, что выяснилось только так.
Я исходил из того, что в цеху везде Windows XP. На деле парк оказался смешанным: Skipper под Windows 10 LTSC, Rover B1 и B2 под Windows Embedded 7, и только Rover A действительно под Windows XP.
Python на станках нет, и ставить его туда никто не разрешит. Значит агент пишется на том, что уже есть в системе.
Сервер Odoo опубликован наружу по HTTPS на нестандартном порту. С современных станков это работает. С Rover A под XP не работает никак: система не умеет договориться по нужной версии TLS. Для него пришлось согласовать отдельный путь, обычный HTTP на внутренний адрес в локальной сети, где трафик не выходит за периметр цеха.
И ещё одна деталь, которую без разведки не найти: на сервере работает скрипт автоматической блокировки по подозрительной активности, и он уже успел однажды заблокировать один из станков. Агент, который стучится каждые двадцать секунд, выглядит для такого скрипта ровно как то, что он ловит. Договариваться об исключении надо до установки, а не после первого ночного молчания.
Чей это станок
В файлах статистики Biesse есть поле серийного номера. Казалось бы, вот и идентификатор.
На практике там стоят заводские заглушки: 888888, 1234, строка из иксов. Поле заполняется при пусконаладке, и заполняют его далеко не всегда.
Рабочим идентификатором оказалось имя компьютера ЧПУ. Оно уникально, задано при установке, и его никто не меняет, потому что в заводской сети оно используется. Последние цифры имени и есть реальный номер станка. Агент берёт идентификатор оттуда, а не из содержимого файла.
Почему сумма врёт?
Самая дорогая ошибка в этом проекте чуть не случилась в арифметике.
Строки статистики Biesse не являются непересекающимися отрезками. Они вложены друг в друга: общий интервал работы содержит внутри себя интервалы конкретных программ и операций. Если просто просуммировать длительности, получается красивое число, которое завышает наработку примерно в полтора раза.
Заметить это на глаз невозможно, потому что результат выглядит правдоподобно. Нашлось только при сверке с ручным журналом за один день.
Решение: не суммировать, а схлопывать. Интервалы объединяются в непересекающиеся отрезки, и только потом считается длительность.
def merge(intervals):
"""intervals: список кортежей (start, end). Возвращает непересекающиеся отрезки."""
if not intervals:
return []
ordered = sorted(intervals, key=lambda iv: iv[0])
merged = [list(ordered[0])]
for start, end in ordered[1:]:
if start <= merged[-1][1]: # пересекается или касается
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return [tuple(iv) for iv in merged]
def busy_seconds(intervals):
return sum((end - start).total_seconds() for start, end in merge(intervals))
Для HOMAG, кстати, пересечений нет вообще, я проверял отдельно. Это не то, что можно предположить, это то, что надо проверить на данных каждого вендора.
Сорок одно состояние и справочник из базы
У HOMAG в системе учёта сорок одно состояние станка. Прописывать их соответствие в коде означает гарантированно сломаться на сорок втором.
Выяснилось, что рядом лежит колонка, которая сводит все состояния в шесть групп. То есть вендор уже сделал классификацию за нас. Группы ложатся на модель Odoo почти без правок: основное использование это работа, вспомогательное и обслуживание это наладка, простой это пауза, неисправность это авария, вне эксплуатации это выключенный станок.
Агент читает справочник прямо из базы станка при каждом запуске:
SELECT s.StateId,
s.StateName,
g.GroupKeyName -- шесть групп, их сделал вендор
FROM MachineState s
JOIN MachineStateGroup g ON g.GroupId = s.GroupId
Появится новое состояние, агент подхватит его сам и положит в ту же группу, куда его положил вендор.
Две тысячи аварий, из которых аварий нет
Журнал аварий казался самой простой частью. Берём сообщения уровня FAULT и отправляем в Odoo.
За неделю таких сообщений оказалось 2152. Из них 1545 это одно и то же: подсказка оператору, что подъёмный стол не в верхнем положении. Ещё 307 это просьба перевернуть деталь. Это не поломки, это нормальная работа оператора, которую система помечает высоким уровнем.
Фильтр по уровню не помогает совсем: более высоких уровней в логе не встречается вообще, ни одного за неделю.
Рабочее решение оказалось в другом измерении. В журнал аварий попадают только те сообщения, которые совпали по времени с состоянием неисправности, то есть когда станок действительно стоял. Само время простоя по аварии всё равно берётся из состояний, а не из сообщений. Режим переключается в конфигурации на все сообщения или на ни одного, но по умолчанию работает именно совпадение.
Если бы я этого не сделал, начальник цеха открыл бы журнал, увидел две тысячи аварий за неделю и закрыл его навсегда.
Как это устроено со стороны Odoo
Модель получилась из трёх сущностей.
События с интервалами: начало, конец, состояние, станок. Из них считаются время под питанием, время в цикле, простой и загрузка.
Показания счётчиков: нарастающие значения, из разности которых получается выработка за период. Для станков, которые интервалов не отдают, это единственный источник.
Суточные итоги: агрегат за сутки по станку, который пересчитывается и служит основой отчётов.
Приём данных сделан только на вставку, с уникальным ключом по идентификатору события:
_sql_constraints = [
('event_uniq',
'unique(machine_id, external_event_id)',
'Событие с таким идентификатором уже загружено'),
]
Агент может отправить одно и то же дважды, дубликат не появится. Это важнее чем кажется: связь в цеху рвётся, агент перезапускается, и единственный способ не считать одно и то же два раза это ключ на стороне приёмника, а не аккуратность на стороне отправителя.
Курсор чтения агент хранит у себя одним числом, а не смещением в файле: число надёжнее переживает перезапуск и дозапись.
Такт опроса двадцать секунд. Молчание дольше получаса трактуется как выключенный станок, а не как обрыв связи. Это важная мелочь: если этого не сделать, ночная остановка выглядит как авария мониторинга, и дежурный получает письмо в три часа ночи.
Где я ошибся
Полезная часть любой статьи. Здесь три ошибки, и все три одного рода: неправильная абстракция.
Флаг счётчикового станка. Я определил его как станок присылает показания счётчиков. HOMAG присылает счётчик деталей, и флаг записал сверлильный станок в кромочники. В карточке пропала загрузка, пропали кнопки событий и аварий, зато появились смены и метраж. Правильное определение оказалось другим: счётчики есть, а интервалов нет вообще. Разница в одном слове, а поведение системы меняется полностью.
Мнимые смены. Смены собирались из приращений счётчика по близости во времени. У кромочного станка счётчик сбрасывают в конце смены, там это осмысленно. У сверлильного он тикает на каждую деталь, и получилась одна фиктивная смена, а со временем набежали бы десятки. Теперь смены собираются только у станков без интервалов.
Строка, которая показывает ноль. На карточку станка выводился метраж, прибитый намертво, без проверки значения. Сверлильный станок получил строку метраж ноль целых ноль. Ноль там, где величины не существует, это не пустое поле, это враньё в интерфейсе. Поправил по значению, а не по типу станка: показывается то, что реально пришло.
И отдельно, бытовое. В день установки агента на одном из станков суточный итог показал 7156 часов под питанием за одни сутки. В файле наработки рядом оказались строки из разных эпох, и разность дала семь тысяч часов. Такие вещи всплывают именно в первый день и именно на том экране, который первым откроет руководство.
Проект закончился не тогда, когда пошли данные
Когда всё заработало, я считал, что сделал работу. Потом сел и написал шесть страниц про то, куда смотреть.
Три вопроса и три места: что со станком прямо сейчас, сколько он отработал за сутки, сколько сделал кромочник за смену. Отдельным разделом то, на что смотреть с осторожностью: почему у кромочных станков пустая загрузка, а не нулевая, почему у сверлильного одна смена вместо двух и это норма, почему в день установки агента сутки неполные, что означает серая плитка без данных, и почему часовой пояс в профиле пользователя должен совпадать с цеховым, иначе даты уедут.
Этот документ занял примерно столько же времени, сколько последний агент. Без него дашборд умеет отвечать на вопросы, но спросить его может только автор.
Что я бы посоветовал, если вам предстоит похожее
Считайте не станки, а способы отдачи данных.
Начинайте с чекера, который ничего не меняет. Час на разведку экономит день на переделку.
Идентичность устройства берите из того, что завязано на его работу, а не из поля, которое заполняют руками.
Проверяйте, пересекаются ли интервалы, прежде чем их складывать. На каждом вендоре отдельно.
Если у вендора есть собственный справочник состояний, читайте его из базы, а не переносите в код.
Не стройте журнал аварий на уровне важности сообщения, пока не посмотрели, что в нём лежит на самом деле.
Делайте приём данных идемпотентным. Связь в цеху рвётся всегда.
И закладывайте время на инструкцию. Система, которую нельзя прочитать без автора, работает ровно до того дня, когда автор занят.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.