Мой умный дом уже длительное время работает на промышленном контроллере - стабильно, без сбоев и без привязки к облакам. Но с годами технологии изменились: появились новые беспроводные протоколы, веб-интерфейсы, голосовые ассистенты и гибкие сценарии автоматизации. 

Проблема заключалась не в том, что существующая система устарела, а в том, что она уже не позволяла реализовать многие современные сценарии без серьезной переделки. Передо мной стояла задача не заменить работающую систему, а расширить ее возможности, сохранив все то, что доказало свою надежность. Для этого я создал поверх контроллера современный интеграционный слой. А чтобы полностью отказаться от проприетарного ПО, написал собственный OPC-сервер.

В этой статье не только история модернизации моего умного дома, но и попытка показать, какие архитектурные решения, принятые еще в 2011 году, позволили спустя многие годы добавить новые технологии без полной перестройки системы. Надеюсь, этот опыт окажется полезен тем, кто только начинает строить свой умный дом и хочет, чтобы его можно было развивать, а не переделывать через несколько лет.

Сегодня умный дом чаще всего ассоциируется с относительно простой автоматизацией на уровне сценариев и интеграций между готовыми устройствами. В большинстве случаев такой подход строится вокруг готовых экосистем, облачных сервисов и правил формата «если произошло событие А – выполнить действие Б». Это удобно, быстро и зачастую не требует глубоких технических знаний. Однако в 2011 году, когда я проектировал и строил свой умный дом, ситуация выглядела совершенно иначе.

В то время я занимался промышленной автоматизацией: программировал контроллеры, проектировал АСУ ТП (автоматизированные системы управления технологическим процессом) и запускал производственные объекты. Поэтому и к планированию оснащения собственного дома я подошел как к полноценному объекту автоматизации по всем правилам АСУ ТП.

В основу моего умного дома легли:

  • промышленный контроллер DirectLOGIC DL205;

  • проводные датчики и исполнительные механизмы;

  • OPC-сервер для обмена данными;

  • SCADA-система для визуализации и управления;

  • централизованная логика управления.

С точки зрения современного подхода в ИТ, такую систему скорее можно сравнить не с набором IoT-устройств (Internet of Things, интернет вещей), а с классической трехуровневой архитектурой приложения, где есть уровни данных, логики и представления.

Спустя 15 лет самое интересное заключается в том, что контроллер, шкаф автоматики и проводная инфраструктура продолжают работать практически без изменений. Зато мир вокруг них изменился полностью.

Появились доступные одноплатные компьютеры (Raspberry/Orange), контейнерная виртуализация (Docker), легкие протоколы обмена сообщениями (MQTT и т.п.), беспроводные сети для устройств умного дома (Zigbee/Wi-Fi), веб-интерфейсы управления, мессенджеры и голосовые помощники. Все это позволяет строить более гибкие и расширяемые системы автоматизации, которые уже не привязаны к одному компьютеру или специализированному программному обеспечению.

В какой-то момент передо мной встала та же задача, которую регулярно решают в ИТ: как модернизировать работающее решение, не разрушив того, что уже хорошо функционирует.

Как строился мой умный дом в 2011 году

Большинство современных технологий в 2011 году либо еще не существовало, либо не было доступно массовому пользователю. Поэтому решение было очевидным - применить подходы, которые я хорошо знал по работе.

Значительная часть оборудования покупалась на интернет-аукционах  и была бывшей в употреблении. Для домашнего проекта это был практически единственный способ получить промышленную надежность за разумные деньги.

Основой моей системы стал контроллер DirectLOGIC DL205, максимально доступный на тот момент.

К сожалению, фотографий полностью собранного шкафа автоматики, сделанных в 2011 году, у меня не сохранилось. Тогда умный дом был просто личным проектом, и мысли о том, что спустя 15 лет я буду рассказывать о нем в блоге ЛАНИТ на Хабре, конечно, не возникало.

Поэтому ниже фотография начала сборки шкафа в 2011 году и фотография, сделанная в 2021 году. Поскольку за это время архитектура системы осталась почти без изменений, снимки показывают умный дом в его исходном виде – до начала модернизации.

Начало сборки шкафа автоматики (2011 год) 
Начало сборки шкафа автоматики (2011 год) 
Шкаф автоматики перед модернизацией (2021 год)
Шкаф автоматики перед модернизацией (2021 год)

Архитектуру, какой она была на протяжении десятка лет с 2011 года, я выстроил как типичную для промышленной автоматизации:

  • контроллер;

  • OPC-сервер Kepware;

  • SCADA под Windows;

  • проводные датчики;

  • проводные исполнительные механизмы.

Исходная архитектура умного дома (2011 год)
Исходная архитектура умного дома (2011 год)

На начальном этапе создания умного дома мой бюджет был ограничен, а потому мне удалось реализовать порядка 55 сигналов ввода-вывода. При этом я прекрасно понимал, что этого недостаточно для всех идей, которые хотелось воплотить. Но это был максимум, который я мог себе позволить. Таким образом часть задумок была отложена на будущее, а вся реализация целиком заняла, как оказалось, почти полтора десятилетия.

За это время изменился не дом, а я, ведь в процессе моя профессиональная деятельность сместилась в сторону DevOps. Вместо промышленной автоматизации в моей ежедневной работе появились Linux, Docker, управление инфраструктурой, системы мониторинга и разработки. Постепенно этот опыт и навыки начали находить применение и в собственном проекте умного дома. Стало очевидно, что многие новые идеи для него можно претворить в жизнь, используя знания и современные инструменты из быстро развивающегося мира ИТ.

Что перестало устраивать

Сам контроллер продолжал отлично выполнять свою работу. Шкаф автоматики функционировал без сбоев. Проводная инфраструктура также не вызывала нареканий. Проблема была в другом – хотелось добавить новые функции, которые невозможно было предусмотреть в 2011 году:

  • дополнительные датчики;

  • интеграцию с мессенджерами;

  • веб-интерфейс;

  • доступ со смартфона;

  • голосовое управление;

  • новые сценарии автоматизации.

При этом полностью переделывать существующую систему я не планировал. Наоборот, было желание сохранить надежное промышленное ядро и расширить его возможности современными технологиями, построить новый интеграционный слой. 

В процессе модернизации я определил для себя три основные задачи.

1. Перейти от классической SCADA с привязкой к конкретному компьютеру на современный веб-интерфейс управления. Хотелось получить перспективу для управления домом с любого устройства через браузер, а также использовать более гибкую платформу для дальнейшего совершенствования логики и интеграций.

2. Развить возможности умного дома новыми технологиями. Проводная инфраструктура, заложенная на этапе строительства, была надежной, но ее расширение спустя годы стало сложнее. Нужно было обеспечить добавление новых датчиков и устройств без изменения существующей проводки.

3. Перестать зависеть от закрытого программного обеспечения и перейти к кросс-платформенному решению. Существующая связка с OPC-сервером под Windows хорошо выполняла свою задачу, но ограничивала дальнейшее развитие системы. Хотелось получить возможность работать в современной Linux-среде и иметь полную свободу в управлении обменом данными с контроллером. Для решения этих задач я выбрал Raspberry Pi.

Raspberry Pi как интеграционная платформа 

Raspberry Pi обладала достаточной производительностью, имела необходимый набор интерфейсов, работала под управлением Linux и, что самое важное, позволяла постепенно расширять возможности умного дома, разворачивая новые сервисы без каких-либо изменений в существующей системе. 

При этом эта платформа не заменяла промышленный контроллер. Она стала дополнительным уровнем системы, отвечающим за интеграцию с современными технологиями и выполнение задач, которые в 2011 году было невозможно реализовывать средствами PLC (Programmable Logic Controller, программируемый логический контроллер). 

Шкаф автоматики после модернизации. В систему добавлена Raspberry Pi, ставшая интеграционной платформой для современных сервисов 
Шкаф автоматики после модернизации. В систему добавлена Raspberry Pi, ставшая интеграционной платформой для современных сервисов 

На Raspberry Pi были развернуты ioBroker, MQTT-брокер, Zigbee2MQTT, собственный OPC-сервер и другие сервисы, которые значительно расширили возможности умного дома. Не затрагивая существующую архитектуру они позволили реализовать функции, не предусмотренные на первоначальном этапе создания проекта.

Модернизированная архитектура
Модернизированная архитектура

Задача №1. От SCADA к веб-интерфейсу

Первоначально управление умным домом осуществлялось через классическую SCADA-систему. На тот момент этот подход был вполне естественным. Используя встроенные средства SCADA, я самостоятельно разработал мнемосхемы и экраны, которые обеспечивали визуализацию состояния датчиков и исполнительных механизмов, позволяли контролировать оборудование, а также отображали состояние всех подключенных сигналов. 

Интерфейс управления умным домом в классической SCADA-системе (2011 год)
Интерфейс управления умным домом в классической SCADA-системе (2011 год)

Современные технологии предлагают новую модель работы с системой. Поэтому, как я уже отмечал, вместо привязки к конкретному компьютеру с установленным SCADA-клиентом хотелось получить доступ к умному дому с любого устройства через обычный веб-браузер. Также нужна была возможность легко добавлять новые функции и интеграции. В качестве новой платформы управления я выбрал ioBroker.

Новый веб-интерфейс умного дома, разработанный в ioBroker VIS-2.0 
Новый веб-интерфейс умного дома, разработанный в ioBroker VIS-2.0 

Одним из ключевых преимуществ ioBroker для моего проекта стала именно веб-архитектура. Как и требовалось, пользовательский интерфейс больше не зависел от конкретного рабочего места: управлять системой через браузер можно было с компьютера, планшета или смартфона.

Для разработки интерфейса был выбран адаптер VIS-2.0. Это инструмент визуализации ioBroker, который позволяет создавать собственные веб-панели управления. В отличие от классической SCADA-визуализации, где структура экранов обычно определяется архитектурой конкретного проекта, VIS-2.0 дает гибкую возможность выстраивания интерфейса под любые задачи. 

Применяя готовые виджеты и дополнительные расширения, можно формировать различные элементы управления: панели состояния, кнопки, графики, индикаторы, отображение показаний датчиков и другие компоненты. Это позволило сделать пользовательскую среду не только функциональной, но и адаптировать ее под удобную эксплуатацию в повседневной жизни. 

Кроме этого, ioBroker предоставляет подходящую основу для дальнейшего развития системы: возможность добавлять новые адаптеры, создавать собственные сценарии автоматизации и интегрировать дополнительные сервисы.

Реализовать такой подход мне помог опыт работы в DevOps. Он позволил применить полученные знания на практике: от развертывания ioBroker на Linux до настройки сервисов и создания собственного интерфейса управления в VIS-2.0. При его проектировании также пригодились навыки JavaScript и веб-разработки: я мог создавать более сложные сценарии и адаптировать визуализацию под конкретные задачи умного дома. 

В итоге первая цель обновления была решена: умный дом получил современный веб-интерфейс вместо классического SCADA-клиента. При этом существующая архитектура, как и хотелось, не была нарушена: промышленный контроллер DL205 продолжил выполнять свою основную функцию, а ioBroker стал новым уровнем взаимодействия с системой. 

Задача №2. Расширение возможностей: как модернизировать дом спустя 15 лет 

Следующей задачей стало расширение возможностей умного дома, которые появились с развитием технологий.

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

Главная сложность заключалась в том, что система умного дома уже была реализована и встроена в готовый жилой объект. На этапе приобретения новостройки, когда квартира еще была без чистовой отделки, я мог заранее заложить необходимую проводную инфраструктуру, проложить кабельные линии и подключить основные датчики и исполнительные механизмы. Спустя годы делать это было бы сложнее, так как пришлось бы нарушать отделку, проводить ремонтные работы или использовать открытую прокладку проводов.

Именно здесь проявилась ценность выбранного подхода, который заключался в том, чтобы не заменять существующую систему, а дополнять ее новыми возможностями.

Появление Zigbee-устройств позволило добавлять новые датчики и исполнительное оборудование без переформатирования существующей проводной инфраструктуры. Благодаря связке Zigbee2MQTT и шлюза Sonoff Zigbee все это органично встроилось в общую архитектуру умного дома.

Но расширение произошло не только на уровне дооснащения. ioBroker предоставил возможность наращивать функции через адаптеры и собственные сценарии автоматизации. Появились JavaScript-сценарии, Node-RED, интеграции с внешними сервисами, уведомления в мессенджеры, управление мультимедийными системами через MPD и другие возможности, которых не было в первоначальной архитектуре.

Установленные адаптеры ioBroker
Установленные адаптеры ioBroker
Дополнительные сценарии автоматизации в ioBroker, реализованные на JavaScript 
Дополнительные сценарии автоматизации в ioBroker, реализованные на JavaScript 
Пользовательские сценарии автоматизации в ioBroker (Node-RED)
Пользовательские сценарии автоматизации в ioBroker (Node-RED)

В итоге спустя почти 15 лет умный дом начал развиваться дальше без переделки существующего промышленного ядра.

Контроллер DL205 продолжил выполнять свою основную задачу – стабильное управление проводными датчиками и исполнительными механизмами, а новый уровень на базе Raspberry Pi и ioBroker позволил добавлять новые технологии, устройства и сценарии, превратив первоначальную систему автоматизации в открытую платформу, способную меняться вместе с появлением перспективных возможностей.

Кстати, такой подход хорошо перекликается с архитектурой, которая давно используется в ИT. В современных приложениях редко все функции реализуются в одном компоненте и есть отдельные уровни: backend, frontend, сервисы интеграции и внешние интерфейсы. Каждый слой выполняет свою задачу и может развиваться независимо. Аналогичный принцип появился и в моем умном доме: промышленный контроллер остался надежным backend для управления оборудованием, а новый слой на базе Raspberry Pi и ioBroker стал платформой для расширения пользовательских функций и интеграций.

Задача №3. Последний шаг к открытой архитектуре: свой OPC-сервер вместо Kepware 

После появления ioBroker, MQTT, Docker и Zigbee в системе оставался один компонент, который ограничивал дальнейшее развитие архитектуры.

Это был OPC-сервер Kepware. Сам по себе он отлично выполнял свою функцию: обеспечивал обмен данными между промышленным контроллером DirectLOGIC 205 и системой управления. Однако он оставался единственным элементом, который привязывал всю инфраструктуру к конкретной ОС и коммерческому программному обеспечению.

Хотелось получить полностью открытую архитектуру, где все основные компоненты можно было бы запускать в Linux-окружении и управлять ими самостоятельно. Поэтому я решил разработать собственный OPC-сервер.

Готового решения для Linux найти не удалось. Кроме того, отсутствовала документация по используемому сетевому протоколу обмена данными с контроллером.

Поэтому пришлось использовать подход, который хорошо знаком разработчикам при анализе неизвестных систем: сначала важно понять принципы существующего обмена данными.

Связь между контроллером DirectLOGIC 205 и OPC-сервером Kepware осуществляется по сети Ethernet с использованием UDP-протокола через порт 28784.

Первым этапом стал разбор уже работающего обмена между Kepware и контроллером с помощью Wireshark.

Анализ сетевого обмена между Kepware и контроллером DirectLOGIC DL205 в Wireshark 
Анализ сетевого обмена между Kepware и контроллером DirectLOGIC DL205 в Wireshark 

В процессе изучения сетевого обмена стало понятно, что OPC-сервер циклически выполняет определенный набор повторяющихся запросов к контроллеру.

Было выявлено девять основных типов запросов, которые использовались для работы с различными областями памяти DirectLOGIC 205. Каждый тип запроса соответствовал своему диапазону данных контроллера:

Тип запроса

Область данных

A

X

B

Y

C

C

D

T

E

CT

F

SP

G

V

H

C расширенная

I

V расширенная

Такой анализ позволил понять не только структуру сетевого обмена, но и логику работы OPC-сервера Kepware: какие области памяти контроллера он опрашивает, каким образом формируются запросы и как полученные данные преобразуются в значения тегов.

После определения типов запросов и структуры пакетов стало возможным реализовать собственный клиент обмена с контроллером на Python.

koyo_opc.py
import asyncio
import csv
import logging
import socket
import struct
import time
from asyncua import ua, Server

# --- КОНФИГУРАЦИЯ ---
PLC_IP = "АДРЕС_КОНТРОЛЛЕРА"
CSV_PATH = "tags.csv" 

logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logging.getLogger("asyncua").setLevel(logging.WARNING)
logger = logging.getLogger("KoyoGateway")

def crc16_ccitt(data):
    crc = 0x0000
    for byte in data:
        crc ^= (byte << 8)
        for _ in range(8):
            if crc & 0x8000: crc = (crc << 1) ^ 0x1021
            else: crc <<= 1
        crc &= 0xFFFF
    return crc

class KoyoPLC:
    def __init__(self, ip):
        self.ip = ip
        self.port = 28784
        self.sock = None
        self.lock = asyncio.Lock()
        self._seq = 0xC2
        self.is_online = False
        self.loop = asyncio.get_event_loop()
        self._reconnect()

    def _reconnect(self):
        if self.sock: self.sock.close()
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.sock.setblocking(False)
        logger.info(f"Сетевой сокет инициализирован для {self.ip}")

    async def _safe_recv(self, expected_seq, timeout=0.8):
        start = time.time()
        while time.time() - start < timeout:
            try:
                data, _ = await asyncio.wait_for(self.loop.sock_recvfrom(self.sock, 2048), timeout=timeout)
                if len(data) > 3 and data[3] == expected_seq: return data
            except: await asyncio.sleep(0.01)
        return None

    async def _exchange(self, setup, seq, trigger=None):
        async with self.lock:
            try:
                await self.loop.sock_sendto(self.sock, setup, (self.ip, self.port))
                if not await self._safe_recv(seq): return None
                if trigger:
                    await asyncio.sleep(0.01)
                    await self.loop.sock_sendto(self.sock, trigger, (self.ip, self.port))
                    resp = await self._safe_recv(seq)
                    if resp and resp[9] == 0x22:
                        self.is_online = True
                        return resp[13:]
                self.is_online = True
                return True
            except:
                self.is_online = False
                return None

    def map_address(self, addr_str):
        addr_str = addr_str.replace('"', '').strip()
        if addr_str.startswith('V'):
            parts = addr_str[1:].split('.')
            return int(parts[0], 8), (int(parts[1]) if len(parts) > 1 else None)
        prefixes = {'X': (40400, 1), 'Y': (40500, 1), 'C': (40600, 1), 'T': (41100, 1), 'SP': (41200, 2), 'S': (41000, 1)}
        for pref, (base, plen) in prefixes.items():
            if addr_str.startswith(pref):
                try:
                    idx = int(addr_str[plen:], 8)
                    return int(str(base), 8) + (idx // 16), (idx % 16)
                except: continue
        return None, None

# --- ОБРАБОТЧИК ЗАПИСИ ---
class PLCWriteHandler:
    def __init__(self, manager):
        self.manager = manager

    async def datachange_notification(self, node, val, data):
        node_id = str(node)
        if node_id not in self.manager.node_to_meta: return
        m = self.manager.node_to_meta[node_id]

        if m['last_val'] == val: return
        if m['v_dec'] not in self.manager.word_cache: return

        logger.info(f"ЗАПИСЬ В PLC: {m['name']} -> {val} (Тип: {m['type']})")
        self.manager.polling_suspended = True

        try:
            if m['ua_type'] == ua.VariantType.Boolean:
                curr_w = self.manager.word_cache[m['v_dec']]
                state = bool(val)
                new_w = (curr_w | (1 << m['bit'])) if state else (curr_w & ~(1 << m['bit']))
                if await self.manager.write_word_to_plc(m['v_dec'], new_w):
                    self.manager.word_cache[m['v_dec']] = new_w
            elif m['ua_type'] == ua.VariantType.Float:
                l, h = struct.unpack('<HH', struct.pack('<f', float(val)))
                await self.manager.write_word_to_plc(m['v_dec'], l)
                await self.manager.write_word_to_plc(m['v_dec']+1, h)
                self.manager.word_cache[m['v_dec']] = l
                self.manager.word_cache[m['v_dec']+1] = h
            elif 'BCD' in m['type']:
                bcd_val = int(str(int(val)), 16)
                if await self.manager.write_word_to_plc(m['v_dec'], bcd_val):
                    self.manager.word_cache[m['v_dec']] = bcd_val
            else:
                if await self.manager.write_word_to_plc(m['v_dec'], int(val)):
                    self.manager.word_cache[m['v_dec']] = int(val)

            m['last_val'] = val
            await asyncio.sleep(0.1)
        finally:
            self.manager.polling_suspended = False

# --- МЕНЕДЖЕР СЕРВЕРА ---
class OpcServerManager:
    def __init__(self, ip_plc, csv_path):
        self.plc = KoyoPLC(ip_plc)
        self.csv_path = csv_path
        self.server = Server()
        self.node_to_meta = {}
        self.word_cache = {}
        self.polling_suspended = False
        self.idx = 0

    async def write_word_to_plc(self, v_dec, value):
        addr_val = v_dec + 1
        v_l, v_h = (int(value) & 0xFF), (int(value) >> 8) & 0xFF
        payload = bytearray([0x19, 0x00, 0x01, 0x20, 0x02, addr_val & 0xFF, (addr_val >> 8) & 0xFF, 0x31, v_l, v_h])
        seq = (self.plc._seq + 1) & 0xFF
        self.plc._seq = seq
        packet = bytearray([0x48, 0x41, 0x50, seq, seq ^ 0x10, crc16_ccitt(payload) & 0xFF, (crc16_ccitt(payload) >> 8) & 0xFF, 0x0A, 0x00]) + payload
        return await self.plc._exchange(packet, seq)

    async def sync_plc(self):
        needed = sorted(list(set(w for m in self.node_to_meta.values() for w in m['needed_words'])))
        if not needed: return
        idx = 0
        while idx < len(needed):
            curr_start = needed[idx]
            j = idx
            while j + 1 < len(needed) and (needed[j+1] - curr_start < 30): j += 1
            b_len = needed[j] - curr_start + 1
            payload = bytearray([0x19, 0x00, 0x01, 0x1e, b_len*2, (curr_start+1)&0xFF, ((curr_start+1)>>8)&0xFF, 0x31])
            setup = bytearray([0x48, 0x41, 0x50, 0x48, 0x58, crc16_ccitt(payload)&0xFF, (crc16_ccitt(payload)>>8)&0xFF, 0x08, 0x00]) + payload
            trigger = bytearray.fromhex("48415048582e990600201900000000")
            raw = await self.plc._exchange(setup, 0x48, trigger)
            if raw:
                for k in range(len(raw)//2): self.word_cache[curr_start + k] = raw[k*2] | (raw[k*2+1] << 8)
            idx = j + 1

    async def init(self):
        await self.server.init()
        self.server.set_endpoint("opc.tcp://0.0.0.0:4840/freeopcua/server/")
        self.idx = await self.server.register_namespace("http://koyo.gateway")

        # 1. Загрузка конфигурации
        logger.info("Загрузка тегов из CSV...")
        raw_tags = []
        with open(self.csv_path, 'r') as f:
            reader = csv.reader(f)
            for row in reader:
                if not row or row[0].startswith('Tag'): continue
                v_dec, bit = self.plc.map_address(row[1])
                if v_dec is not None:
                    raw_tags.append({'name': row[0], 'addr': row[1], 'type': row[2], 'v_dec': v_dec, 'bit': bit})
                    # Временные метаданные для первичной синхронизации
                    dummy_id = f"tmp_{row[0]}"
                    self.node_to_meta[dummy_id] = {'v_dec': v_dec, 'needed_words': [v_dec, v_dec+1] if 'Float' in row[2] or 'LBCD' in row[2] else [v_dec]}

        # 2. Синхронизация с PLC
        await self.sync_plc()
        self.node_to_meta.clear()

        # 3. Создание дерева OPC UA
        for t in raw_tags:
            parent = self.server.nodes.objects
            parts = t['name'].split('.')
            for part in parts[:-1]: parent = await self.get_or_create_folder(parent, part)

            # Определение типов на основе t['type']
            current_type = t['type']
            is_bool = (t['bit'] is not None) or ('Boolean' in current_type)
            is_float = 'Float' in current_type
            is_lbcd = 'LBCD' in current_type
            is_bcd = 'BCD' in current_type and not is_lbcd

            w1 = self.word_cache.get(t['v_dec'], 0)
            if is_bool:
                ua_type = ua.VariantType.Boolean
                val = bool((w1 >> t['bit']) & 1) if t['bit'] is not None else bool(w1 & 1)
            elif is_float:
                ua_type = ua.VariantType.Float
                w2 = self.word_cache.get(t['v_dec'] + 1, 0)
                val = struct.unpack('<f', struct.pack('<HH', w1, w2))[0]
            elif is_lbcd:
                ua_type = ua.VariantType.Int32
                w2 = self.word_cache.get(t['v_dec'] + 1, 0)
                val = int(f"{w2:04x}{w1:04x}")
            elif is_bcd:
                ua_type = ua.VariantType.Int32
                val = int(f"{w1:04x}")
            else:
                ua_type = ua.VariantType.Int32
                val = w1 if w1 <= 32767 else w1 - 65536

            tag_node = await parent.add_variable(self.idx, parts[-1], val, varianttype=ua_type)
            await tag_node.set_writable()

            self.node_to_meta[str(tag_node)] = {
                'node': tag_node, 'v_dec': t['v_dec'], 'bit': t['bit'], 'type': current_type,
                'ua_type': ua_type, 'name': t['name'], 'last_val': val,
                'needed_words': [t['v_dec'], t['v_dec']+1] if is_float or is_lbcd else [t['v_dec']]
            }

        # Включение обработчика записи
        handler = PLCWriteHandler(self)
        sub = await self.server.create_subscription(100, handler)
        await sub.subscribe_data_change([m['node'] for m in self.node_to_meta.values()])
        logger.info(f"Сервер готов. Загружено {len(self.node_to_meta)} тегов.")

    async def get_or_create_folder(self, parent, name):
        for child in await parent.get_children():
            if (await child.read_browse_name()).Name == name: return child
        return await parent.add_folder(self.idx, name)

    async def poll_loop(self):
        while True:
            if not self.polling_suspended:
                await self.sync_plc()
                for m in self.node_to_meta.values():
                    try:
                        w1 = self.word_cache.get(m['v_dec'])
                        if w1 is None: continue

                        if m['ua_type'] == ua.VariantType.Boolean:
                            val = bool((w1 >> m['bit']) & 1) if m['bit'] is not None else bool(w1 & 1)
                        elif m['ua_type'] == ua.VariantType.Float:
                            w2 = self.word_cache.get(m['v_dec'] + 1, 0)
                            val = struct.unpack('<f', struct.pack('<HH', w1, w2))[0]
                        elif 'LBCD' in m['type']:
                            w2 = self.word_cache.get(m['v_dec'] + 1, 0)
                            val = int(f"{w2:04x}{w1:04x}")
                        elif 'BCD' in m['type']:
                            val = int(f"{w1:04x}")
                        else:
                            val = w1 if w1 <= 32767 else w1 - 65536

                        m['last_val'] = val
                        await m['node'].write_value(ua.DataValue(ua.Variant(val, m['ua_type'])))
                    except: continue
            await asyncio.sleep(0.5)

async def main():
    manager = OpcServerManager(PLC_IP, CSV_PATH)
    await manager.init()
    async with manager.server:
        await manager.poll_loop()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass

Этот скрипт выполняет необходимые для моего проекта операции: устанавливает обмен с контроллером, отправляет запросы, получает ответы, выполняет чтение значений и запись данных в нужные области памяти DirectLOGIC 205. Чтобы он мог работать постоянно в составе новой архитектуры умного дома, Python-скрипт был оформлен как systemd-служба Linux.

koyo-opc.service
[Unit]
Description=OPC Server Koyo
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=pi
Group=pi

WorkingDirectory=/opt/koyo_opc

ExecStart=/opt/koyo_opc/venv/bin/python /opt/koyo_opc/koyo_opc.py

Restart=always
RestartSec=10

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

В результате система окончательно перестала зависеть от Windows и коммерческого OPC-сервера. Контроллер DL205 продолжил работать как надежное промышленное ядро, а программная часть умного дома стала полностью открытой и кросс-платформенной. 

Освобождая место для новых идей

После того, как все три задачи были решены, неожиданно проявился еще один приятный эффект – шкаф автоматики начал постепенно избавляться от оборудования, которое когда-то было необходимо, но постепенно устарело.

Одним из таких устройств стал FM-тюнер с усилителем, который многие годы обеспечивал музыкальное сопровождение, например, в ванной комнате. Для своего времени это было вполне рабочее решение, однако оно занимало место в шкафу и ограничивалось только эфирными радиостанциями.

Ему на смену пришла плата Suptronics X400, установленная на Raspberry Pi. В качестве программного обеспечения был выбран Mopidy, работающий в Docker-контейнере.

В результате вместо отдельного аппаратного устройства появился программный сервис. Теперь стали доступны интернет-радио, локальная музыкальная коллекция и возможность управлять воспроизведением через веб-интерфейс ioBroker.

Другим примером стал источник бесперебойного питания (UPS). Раньше для его подключения к системе использовался отдельный преобразователь RS-232 ↔ Ethernet. После модернизации он оказался больше не нужен: взаимодействие с бесперебойником было реализовано программно через SNMP (Simple Network Management Protocol, простой протокол сетевого управления) с использованием соответствующего адаптера ioBroker. 

В итоге новая архитектура сделала возможным подобный апгрейд. Теперь там, где раньше потребовалось бы устанавливать отдельное устройство, во многих случаях достаточно развернуть программный сервис или использовать готовый адаптер. Это позволило постепенно уменьшать количество оборудования в шкафу автоматики, сохраняя его функциональность и упрощая дальнейшее развитие системы. 

Что получилось в итоге

На первый взгляд может показаться, что за 15 лет умный дом был практически построен заново. Но на самом деле произошло обратное – все, что было хорошо спроектировано в 2011 году, осталось работать.

В результате модернизации удалось:

  • сохранить промышленный контроллер DirectLOGIC DL205 как основное ядро системы;

  • сохранить всю проводную инфраструктуру и существующую логику управления;

  • перейти от классической SCADA к современному веб-интерфейсу;

  • интегрировать Zigbee-устройства без прокладки новых кабелей;

  • добавить MQTT, Node-RED, JavaScript-сценарии и другие современные инструменты автоматизации;

  • реализовать уведомления в мессенджеры и новые пользовательские сценарии;

  • расширить систему новыми датчиками и исполнительными устройствами;

  • отказаться от зависимости от Windows и коммерческого программного обеспечения;

  • заменить OPC-сервер Kepware собственным кросс-платформенным решением на Python;

  • создать архитектуру, которую можно развивать дальше без переделки существующей системы.

Считаю самым интересным, что за все время модернизации мне не пришлось менять промышленный контроллер или переписывать его программу. Все изменения происходили вокруг него, постепенно расширяя возможности системы и сохраняя ее надежность.

Оглядываясь назад, я понимаю, что главным результатом этой модернизации стали не Zigbee, не веб-интерфейс и даже не собственный OPC-сервер. Самым ценным оказалось то, что умный дом, построенный давно, получил возможность эволюционировать. При этом надеюсь, что через следующие 15 лет мне не придется начинать все с нуля. Но технологии покажут. 

Вместо послесловия

В рамках одной статьи невозможно подробно рассказать обо всех этапах создания и модернизации умного дома. За кадром остались схемы подключения, программирование контроллера, отдельные инженерные решения и многие другие технические детали. 

Предвосхищая один из самых популярных вопросов: «А были ли проблемы за годы эксплуатации?», поделюсь небольшими наблюдениями.

За все время в проводной части умного дома серьезных бед практически не возникло. Большинство отказов были связаны не с контроллером, а с периферийным оборудованием:

  • примерно через пять лет вышел из строя блок питания 24 В и был заменен;

  • еще через несколько лет аналогичная история произошла с блоком питания 12 В;

  • спустя примерно 10 лет отказал один дискретный выход модуля ввода-вывода, но к счастью, свободные каналы были предусмотрены заранее, поэтому нагрузка была просто перенесена на другой выход;

  • несколько лет назад начал некорректно работать аналоговый модуль – его также пришлось заменить.

Как я уже не раз отмечал, сам промышленный контроллер все это время продолжает работать без каких-либо сложностей. Не потребовалось ни переписывать программу, ни менять архитектуру системы.

Что касается Raspberry Pi и ioBroker, то срок их эксплуатации пока значительно меньше – около двух-трех лет. За это время единственной неприятностью оказалась нестабильная работа встроенного Wi-Fi модуля. Проблема была решена довольно просто: Raspberry Pi была подключена к сети одновременно по Ethernet и Wi-Fi с объединением интерфейсов в мост, после чего связь стала значительно стабильнее.

Поэтому, если сегодня меня спросят, стал бы я снова строить проводной умный дом на базе промышленного контроллера, мой ответ будет однозначным – да. За 15 лет именно аппаратная часть доказала свою надежность, а современные информационные технологии позволили вдохнуть в нее новую жизнь, не меняя фундамент, который был заложен еще в 2011 году.

Комментарии (3)


  1. VsBirdEye
    21.07.2026 07:57

    Несколько мыслей «на подумать», исходя из собственного опыта запиливания аналогичной системы.

    Про интерфейс. Интерфейсы SCADA-систем предназначены для оператора, которому нужно следить за технологическим процессом «сверху» и реагировать на отклонения — там логично наличие общей мнемосхемы с массой деталей. Но и там детали должны «говорить» визуально: всё по плану — или что-то требует внимания.

    Домашняя автоматизация нацелена на другое:

    а) типовые действия (включить/выключить свет и технику вручную или по сценарию, глянуть температуру);

    б) уведомления и реакция на них — датчик протечки, звонок домофона и т.п.

    Общую план-схему, где есть «всё», редко использует кто-либо кроме создателя системы. Более того, зрелая домашняя автоматика вообще уезжает в плоскость, где панель почти не нужна: свет работает по расписанию или датчикам движения, домофон открывается из чата или видеозвонка на телефоне, показания счётчиков скрипт сам «сдаёт» на сайт. Панель остаётся инструментом отладки и «пульта на крайний случай».

    Замечу, что вы фактически прошли этот путь сами — ушли от SCADA-клиента к вебу, добавили уведомления в мессенджеры и сценарии. Но интерфейс в VIS-2.0, судя по скриншоту, воспроизводит ту же SCADA-парадигму «общей мнемосхемы», только в браузере. Подозреваю, что через пару лет эксплуатации им будете пользоваться в основном вы, и в основном для диагностики. Впрочем, тут промышленный и домашний подходы в итоге сходятся в одной точке: «управление по отклонениям» — система молчит, пока всё хорошо, и активно сигналит, когда что-то не так. К этому же идёт и high-performance HMI в промышленности.

    Про оборудование. Общий дрейф идёт в сторону отказа от проприетарщины и закрытых платформ, в которых были зашиты и логика, и интерфейс связи с исполнительными устройствами, — в сторону открытых протоколов и деления системы на взаимозаменяемые элементы. Под каждую шину с устройствами нижнего уровня ставится «по вкусу» шлюз, у которого с другой стороны открытый и задокументированный интерфейс (MQTT и т.п.), а логика взаимодействия переезжает на сторону открытых программных решений.

    Ваша история это, кстати, отлично иллюстрирует с обеих сторон. С одной — вам пришлось реверсить недокументированный протокол DL205 через Wireshark, чтобы избавиться от Kepware: закрытый протокол чуть не стал якорем всей системы, и повезло, что он оказался достаточно простым для разбора. С другой — Zigbee2MQTT и ioBroker встали в архитектуру без единой переделки именно потому, что стыкуются через открытые интерфейсы.

    Единственное, где я бы с вами поспорил, — центральная логика в PLC. Понимаю аргумент надёжности (15 лет аптайма — веский довод), но цена этого — логика заперта в закрытой платформе с вымирающим инструментарием. Если через 15 лет контроллер всё-таки умрёт, программу придётся не мигрировать, а писать заново — уже на чём-то другом.


    1. Vutshi Автор
      21.07.2026 07:57

      Спасибо за такой развернутый комментарий.

      Про интерфейс. Для себя я не отказываюсь от общей схемы полностью - просто ее назначение изменилось. Если раньше это был основной способ управления домом, то сейчас это скорее возможность быстро оценить «общую температуру по больнице»: посмотреть, как работает автоматизация в целом, понять, что можно улучшить, найти неисправность или проверить новую функцию. Кроме того, общий экран служит своеобразным чек-листом перед выходом из дома: обратить внимание на важные события, убедиться, что все системы работают штатно. Разумеется, есть и специализированные экраны со статистикой, управлением и настройкой сценариев. Но в целом тенденция действительно такая, как вы описали: все больше взаимодействия происходит без открытия панели, а через автоматические сценарии, уведомления, в перспективе, скорее всего, все будет управляться через голосового помощника.

      Про оборудование. Если бы вся бизнес-логика, интеграции и пользовательские сценарии по-прежнему жили в PLC, то это действительно был бы тупиковый путь. Как раз целью модернизации было максимально разгрузить контроллер. Сегодня PLC занимается тем, что у него получается лучше всего - детерминированным управлением дискретными входами и выходами. Все, что связано с интеграциями, интерфейсами, уведомлениями, Zigbee, сценариями высокого уровня и внешними сервисами, уже живет вне него. Понимаю, что однажды PLC все-таки придется заменить. Но благодаря нынешней архитектуре переносить придется только слой низкоуровневой логики ввода-вывода, а не всю экосистему дома. Именно ради этого и затевалась модернизация, чтобы оборудование перестало быть якорем для дальнейшего развития системы.


  1. NutsUnderline
    21.07.2026 07:57

    занятно, как раз в комментариях к статьям по автоматизации регулярно появляются те кому правильно все автоматизировать на языках iec (без всяких там линуксов, ага, из зачем вообще этот MQTT), изредка - которые не хотят беспроводного, на проводах надежнее. Тут им очень наглядный ответ - все равно придем к малинке на синей изоленте (кстати есть корпуса din для малинок).

    кстати современные scada вполне умеют веб-интерфейс, это и можно и совеременно, и графический движок не надо выдумывать