IoT Otomasyon

BACnet'ten MQTT'ye: HVAC Kontrolörlerini Python Gateway ile Entegre Etmek

BACnet/IP HVAC kontrolörlerinden analog ve binary nesneleri BAC0 ile okuyun ve bunları ölü bantlar, retained meta veri ve güvenli geri yazma ile temiz JSON olarak bir MQTT broker'a yayınlayın.

BACnet'ten MQTT'ye entegrasyonda BACnet/IP ağındaki bir HVAC kontrolörünün küçük bir gateway üzerinden bulut broker'a bağlanması
Bunu kendi MQTT broker’ınızda çalıştırınFree planla özel, yönetilen bir MQTT broker alın ve hazır kod örnekleriyle başlayın.
Ücretsiz deneyin Kod örnekleri Kart gerekmez

Bir BACnet'ten MQTT'ye gateway, yerel BACnet/IP ağındaki HVAC kontrolörlerinden noktaları okur ve bunları panellerin ve analitik araçlarının her yerden abone olabileceği MQTT mesajları olarak yeniden yayınlar. Bu eğitimde bu gateway'i BAC0 ve paho-mqtt ile Python'da kuracak, herkese açık broker'da test edecek ve ardından TLS üzerinden özel bir ConnectMQ broker'a yönlendireceksiniz.

Gateway Nasıl Çalışır

BACnet/IP cihazları 47808 portunda UDP konuşur ve istemciyle aynı broadcast alanında olmayı bekler. Bu yüzden bir bulut servisini doğrudan BACnet'e bağlamazsınız. Bunun yerine bina ağında küçük bir gateway durur:

  1. BACnet/IP ağına bir istemci cihaz olarak katılır.
  2. Yapılandırılmış bir nesne listesinin (analog girişler, analog değerler, binary girişler, çok durumlu değerler) presentValue değerini okur.
  3. Her değeri giden yönlü bir TLS bağlantısı üzerinden bir MQTT topic'ine JSON olarak yayınlar.
  4. İsteğe bağlı olarak küçük bir komut topic'i kümesine abone olur ve onaylanmış set değerlerini kontrolöre geri yazar.

MQTT bağlantısı yalnızca giden yönlü olduğu için gelen yönde hiçbir güvenlik duvarı portu açmazsınız. Bu, bina otomasyon sistemi MQTT mimarileri hakkındaki genel bakışımızda anlatılan kalıbın aynısıdır.

İhtiyacınız Olanlar

  • BACnet/IP kontrolörleriyle aynı alt ağda bir Linux makine, Raspberry Pi veya Windows PC.
  • Python 3.10 veya daha yenisi.
  • En az bir kontrolörün cihaz örnek numarası (device instance) ve IP adresi, ayrıca istediğiniz nesne tipleri ve örnek numaraları. BMS ön yüzünüz veya bir BACnet keşif aracı bunları gösterir.
  • Bir MQTT broker. İlk test için broker.connectmq.com adresindeki ücretsiz herkese açık MQTT broker'ı kullanın. Gerçek bina verisi için özel bir broker kullanın.

Kütüphaneleri kurun:

python -m venv bacnet-gw
source bacnet-gw/bin/activate
pip install "BAC0>=2025.6" "paho-mqtt>=2.0"

Güncel BAC0 sürümleri bacpypes3 üzerine kuruludur ve asyncio kullanır; aşağıdaki kod da bunu hedefler.

Adım 1: Cihazları ve Noktaları Keşfedin

Bir gateway yazmadan önce makinenin kontrolörleri görebildiğini doğrulayın. BAC0'ın BACnet alt ağındaki arayüze bağlanması gerekir; bu yüzden adresini alt ağ maskesiyle birlikte verin.

import asyncio
import BAC0

async def main():
    async with BAC0.start(ip="192.168.10.50/24") as bacnet:
        devices = await bacnet.who_is()
        for dev in devices:
            print(dev)
        value = await bacnet.read("192.168.10.21 analogInput 1 presentValue")
        print("AI1 =", value)

asyncio.run(main())

Keşif hiçbir şey döndürmezse, gateway ile kontrolörlerin aynı alt ağda olduğunu, UDP 47808'in bir host güvenlik duvarı tarafından engellenmediğini ve aynı makinedeki başka bir BACnet uygulamasının portu zaten sahiplenmediğini kontrol edin. Kontrolör başka bir alt ağda bir BACnet router'ın arkasındaysa bir BBMD'ye veya foreign device kaydına ihtiyacınız olabilir; BMS'in bakımını yapan kişiye danışın.

Adım 2: İstediğiniz Noktaları Tanımlayın

Nokta listesini koda gömmek yerine bir yapılandırma dosyasında tutun. Her giriş bir BACnet nesnesini bir MQTT topic son ekine, bir birime ve bir ölü banda eşler.

{
  "site": "ist01",
  "points": [
    {"addr": "192.168.10.21", "obj": "analogInput 1", "topic": "ahu-1/supply-air-temp", "unit": "degC", "deadband": 0.2},
    {"addr": "192.168.10.21", "obj": "analogInput 2", "topic": "ahu-1/return-air-temp", "unit": "degC", "deadband": 0.2},
    {"addr": "192.168.10.21", "obj": "analogInput 5", "topic": "ahu-1/filter-dp", "unit": "Pa", "deadband": 5},
    {"addr": "192.168.10.21", "obj": "binaryInput 1", "topic": "ahu-1/fan-status", "unit": "", "deadband": 0},
    {"addr": "192.168.10.22", "obj": "analogValue 3", "topic": "vav-301/zone-setpoint", "unit": "degC", "deadband": 0.1, "writable": true, "min": 18, "max": 26}
  ]
}

Bunu points.json olarak kaydedin.

Adım 3: BACnet'ten MQTT'ye Gateway Betiği

Betik her noktayı sorgular, yalnızca değer ölü bandından fazla değiştiğinde (veya kalp atışı olarak beş dakikada bir) yayın yapar ve writable olarak işaretlenmiş noktalar için yazma komutlarını işler.

import asyncio
import json
import time
import uuid
from datetime import datetime, timezone

import BAC0
import paho.mqtt.client as mqtt

BROKER = "broker.connectmq.com"
PORT = 1883                      # özel broker için TLS ile 8883 kullanın
USERNAME = None                  # özel broker için "YOUR_USERNAME"
PASSWORD = None                  # "YOUR_PASSWORD"
CLIENT_ID = f"bacnet-gw-{uuid.uuid4().hex[:8]}"   # özel broker için "YOUR_CLIENT_ID"
PREFIX = "connectmq/demo/bacnet-7k2q"             # herkese açık broker'da benzersiz önek
POLL_SECONDS = 15
HEARTBEAT_SECONDS = 300

with open("points.json", encoding="utf-8") as f:
    CONFIG = json.load(f)

POINTS = {p["topic"]: p for p in CONFIG["points"]}
last_sent = {}              # topic -> (değer, zaman damgası)
write_queue: asyncio.Queue = asyncio.Queue()
loop: asyncio.AbstractEventLoop | None = None


def now_iso():
    return datetime.now(timezone.utc).isoformat(timespec="seconds")


def on_connect(client, userdata, flags, reason_code, properties):
    print("MQTT connected:", reason_code)
    client.subscribe(f"{PREFIX}/{CONFIG['site']}/cmd/#", qos=1)
    client.publish(f"{PREFIX}/{CONFIG['site']}/gateway/status", "online", qos=1, retain=True)


def on_message(client, userdata, msg):
    suffix = msg.topic.split("/cmd/", 1)[1]
    point = POINTS.get(suffix)
    if not point or not point.get("writable"):
        print("Rejected write to", suffix)
        return
    try:
        value = float(json.loads(msg.payload)["value"])
    except (ValueError, KeyError, TypeError):
        print("Bad command payload:", msg.payload)
        return
    if not point["min"] <= value <= point["max"]:
        print("Out of range:", value)
        return
    # paho callback'leri kendi thread'inde çalıştırır: işi asyncio döngüsüne güvenle devret
    loop.call_soon_threadsafe(write_queue.put_nowait, (point, value))


def make_client():
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
    if USERNAME:
        client.username_pw_set(USERNAME, PASSWORD)
        client.tls_set()
    client.will_set(f"{PREFIX}/{CONFIG['site']}/gateway/status", "offline", qos=1, retain=True)
    client.on_connect = on_connect
    client.on_message = on_message
    client.reconnect_delay_set(min_delay=1, max_delay=60)
    client.connect(BROKER, PORT, keepalive=60)
    client.loop_start()
    return client


def should_publish(topic, value, deadband):
    prev = last_sent.get(topic)
    if prev is None:
        return True
    old_value, ts = prev
    if time.time() - ts > HEARTBEAT_SECONDS:
        return True
    if isinstance(value, (int, float)) and isinstance(old_value, (int, float)):
        return abs(value - old_value) >= deadband
    return value != old_value


async def poll(bacnet, client):
    for topic, p in POINTS.items():
        ref = f"{p['addr']} {p['obj']} presentValue"
        try:
            value = await bacnet.read(ref)
            quality = "good"
        except Exception as exc:          # zaman aşımları, bilinmeyen nesne, çevrimdışı cihaz
            print("Read failed", ref, exc)
            value, quality = None, "bad"
        if isinstance(value, float):
            value = round(value, 2)
        if quality == "bad" or should_publish(topic, value, p["deadband"]):
            payload = {"ts": now_iso(), "value": value, "unit": p["unit"],
                       "quality": quality, "source": f"{p['addr']}/{p['obj']}"}
            client.publish(f"{PREFIX}/{CONFIG['site']}/{topic}", json.dumps(payload), qos=0)
            if quality == "good":
                last_sent[topic] = (value, time.time())


async def handle_writes(bacnet, client):
    while True:
        point, value = await write_queue.get()
        ref = f"{point['addr']} {point['obj']} presentValue {value} - 8"   # öncelik 8
        try:
            await bacnet.write(ref)
            print("Wrote", ref)
            client.publish(f"{PREFIX}/{CONFIG['site']}/{point['topic']}/ack",
                           json.dumps({"ts": now_iso(), "value": value, "result": "ok"}), qos=1)
        except Exception as exc:
            client.publish(f"{PREFIX}/{CONFIG['site']}/{point['topic']}/ack",
                           json.dumps({"ts": now_iso(), "value": value, "result": str(exc)}), qos=1)


async def main():
    global loop
    loop = asyncio.get_running_loop()
    client = make_client()
    async with BAC0.start(ip="192.168.10.50/24") as bacnet:
        asyncio.create_task(handle_writes(bacnet, client))
        while True:
            await poll(bacnet, client)
            await asyncio.sleep(POLL_SECONDS)

asyncio.run(main())

python gateway.py ile çalıştırın. Her nokta connectmq/demo/bacnet-7k2q/ist01/... altında görünür.

BACnet Nesnelerini MQTT Topic'lerine Eşlemek

BACnet nesneleri tip ve örnek numarasıyla adlandırır, örneğin 1201 numaralı cihazdaki analogInput 3. Bu tanımlayıcılar topic'lere göz atan bir tesis yöneticisi için hiçbir anlam ifade etmez ve bir kontrolör değiştirildiğinde ya da yeniden programlandığında değişir. Bu yüzden nokta listesi her nesneyi ahu-1/supply-air-temp gibi açıklayıcı bir topic'e eşlerken orijinal referans payload içinde source olarak taşınır.

Birkaç kural, bina büyüdükçe eşlemenin bakımını kolay tutar:

  • Kontrolörleri değil, ekipmanı adlandırın. Bir kontrolör iki klima santraline hizmet edebilir, bir klima santrali de iki kontrolöre yayılabilir. Topic'ler ekipmanı izlemelidir.
  • Tek bir birim sistemi kullanın. Bir sahada farklı tedarikçiler karışıksa Fahrenheit veya inç su sütununu gateway'de dönüştürün ve birimi her payload'a ekleyin.
  • Çok durumlu değerleri çevirin. Bir multiStateValue tamsayı döndürür; nesnenin durum metni listesini kullanarak sayının yanında durum metnini de (örneğin occupied, unoccupied, standby) yayınlayın.
  • Binary nesneleri boolean olarak ele alın. BACnet active veya inactive döndürür; paneller ve uyarı kuralları kolayca karşılaştırabilsin diye true veya false yayınlayın.
  • Listeyi sürüm kontrolünde tutun. Bir mekanik yüklenici nesneleri yeniden numaralandırdığında, points.json dosyasındaki gözden geçirilmiş bir değişikliği denetlemek elle düzenlenmiş bir betikten çok daha kolaydır.

Adım 4: Veriyi Doğrulayın

Herhangi bir makineden abone olun:

mosquitto_sub -h broker.connectmq.com -p 1883 -i viewer-7k2q -t "connectmq/demo/bacnet-7k2q/#" -v

Ardından yazılabilir VAV noktasına bir test set değeri değişikliği gönderin:

mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-7k2q -q 1 \
  -t "connectmq/demo/bacnet-7k2q/ist01/cmd/vav-301/zone-setpoint" -m '{"value": 22.5}'

Gateway değeri öncelik 8'de yazar ve /ack topic'inde bir onay yayınlar. Herkese açık broker'daki her şeyin herkes tarafından okunabildiğini unutmayın; bunu yalnızca bir test kontrolörü veya bir simülatörle yapın. Herkese açık bir broker'daki bir komut topic'i asla gerçek bir binaya ulaşmamalıdır.

Adım 5: TLS Üzerinden Özel Bir Broker'a Geçin

Üretim için ConnectMQ portalında bir broker oluşturun ve oluşturulan kullanıcı adını, parolayı ve client ID'yi kopyalayın. Dört sabiti değiştirin:

BROKER = "broker.connectmq.com"   # veya portalda broker'ınız için gösterilen host
PORT = 8883
USERNAME = "YOUR_USERNAME"
PASSWORD = "YOUR_PASSWORD"
CLIENT_ID = "YOUR_CLIENT_ID"

client.tls_set() sistem CA deposunu kullanır; bu depo broker'ın sunduğu Let's Encrypt sertifikasına zaten güvenir. Demo öneki yerine acme/ist01/... gibi bir topic kökü kullanın. Barındırılan bir MQTT broker bu gateway'i basit tutar: sizin tarafınızda yenilenecek sertifika ve yama yapılacak broker yoktur.

Sorun Giderme Kontrol Listesi

Belirti Olası neden Çözüm
Keşif hiçbir cihaz bulamıyor Yanlış arayüz veya alt ağ maskesi Gateway IP'sini doğru /24 (veya kendi maskenizle) BAC0.start fonksiyonuna verin
Address already in use Başka bir BACnet aracı 47808'e bağlı Diğer aracı durdurun veya gateway'i başka bir host'ta çalıştırın
Tek bir cihazda okumalar zaman aşımına uğruyor Cihaz bir BACnet router'ın arkasında Sahanın BBMD'si ile foreign device kaydını yapılandırın
Değerler MQTT'de hiç değişmiyor Ölü bant çok büyük Ölü bandı düşürün veya kalp atışı aralığını kontrol edin
Yazmalar kabul ediliyor ama değer geri dönüyor Noktayı daha yüksek bir öncelik (zaman programı, operatör) sahipleniyor BMS entegratörüyle bir öncelik üzerinde anlaşın; işiniz bitince bırakın
Özel broker ile MQTT yeniden bağlanma döngüsü Client ID kimlik bilgileriyle eşleşmiyor Kullanıcı adıyla birlikte verilen client ID'nin aynısını kullanın

Üretim İpuçları

  • Destekleniyorsa COV'u tercih edin. BACnet change-of-value abonelikleri eski kontrolörlerdeki sorgulama yükünü azaltır. Basitlik için sorgulamayla başlayın, ardından yoğun noktaları COV'a taşıyın.
  • Çok agresif sorgulamayın. Router'ların arkasındaki eski MS/TP kontrolörleri yavaş yanıt verir; HVAC için 15 ila 60 saniye genellikle fazlasıyla yeterlidir.
  • Meta veriyi retained olarak yayınlayın. Birimleri, nesne adlarını ve min/max değerlerini içeren retained bir mesaj, panellerin ilk saniyeden itibaren doğru görüntülenmesine yardımcı olur.
  • Gateway'in kendisini izleyin. Süreç ölürse last-will mesajı gateway/status değerini offline yapar; önemli telemetri üzerindeki bir ConnectMQ eşik uyarı kuralı da aralık dışına çıkan sensörleri işaretleyebilir.

Bu noktalar üzerine kurulu uygulamalı bir kullanım senaryosu için IoT sensörleriyle HVAC izleme yazısına bakın. Aynı bina aydınlatma için KNX de kullanıyorsa KNX'ten MQTT'ye köprü aynı yapıyı izler. Daha fazla otomasyon projesi IoT otomasyon rehberimizde toplanmıştır; resmi BAC0 belgeleri ise bac0.readthedocs.io adresindedir.

BACnet Verinizi ConnectMQ'ya Taşıyın

app.connectmq.com adresinden kaydolun ve kredi kartı gerekmeden ücretsiz bir Serverless broker oluşturun. Gateway'i ona yönlendirin, canlı topic gezginini açın; her HVAC noktasının gerçek zamanlı olarak geldiğini göreceksiniz. ConnectMQ TLS'i, kimlik doğrulamayı ve çalışma süresini üstlenir; böylece ekibiniz binaya odaklanabilir.

Sık Sorulan Sorular

Bir bulut servisi doğrudan BACnet konuşabilir mi?

Güvenilir biçimde hayır. BACnet/IP yerel bir ağdaki UDP broadcast'lerine dayanır; bu yüzden sahada BACnet noktalarını MQTT'ye dönüştüren ve broker'a giden yönde bağlanan bir gateway'e ihtiyacınız vardır.

Önce hangi BACnet nesnelerini yayınlamalıyım?

Besleme ve dönüş havası sıcaklıkları, filtre fark basıncı, fan durumu ve ana bölge sıcaklıklarıyla başlayın. Bunlar en az noktayla en fazla teşhis değerini verir.

Set değerlerini MQTT üzerinden yazmak güvenli mi?

Gateway yalnızca belirli noktalara izin veriyor, aralıkları doğruluyor, üzerinde anlaşılmış bir öncelikte yazıyor ve özel bir broker'a kimlik doğrulamalı bir TLS bağlantısı kullanıyorsa güvenli olabilir.

Hangi sorgulama aralığını kullanmalıyım?

HVAC için ölü bantla birlikte 15 ila 60 saniye tipiktir. Daha hızlı sorgulama nadiren içgörü katar ve eski kontrolörleri aşırı yükleyebilir.

İlgili yazılar

Zigbee2MQTT Kurulum Rehberi: Koordinatörden Panoya

Zigbee2MQTT'yi Docker ile kurun, koordinatörü ve MQTT bağlantısını yapılandırın, cihazları eşleştirip yeniden adlandırın, onları MQTT üzerinden kontrol edin ve durumlarını bir panoda görüntüleyin; önce ücretsiz herkese açık bir broker'da test edin.

7 dk okuma

Tüm yazılar →

Özel MQTT broker’ınız birkaç tık uzağınızda

Hesabınızı oluşturun, broker kimlik bilgilerinizi alın ve ilk cihazınızı bağlayın — kurulacak, yamalanacak ya da ölçeklendirilecek sunucu yok.