Integrating Daikin using Modbus and Airzone

There are many ways to add a Daikin HVAC into HA but there are also even more varieties of Daikin/Airzone out there that may not work with integrations or prefabricated products out there. My unit happened to not work in any of the pre made integrations from HACS or HA proper so I went down the road of connecting via modbus/MQTT/RPI to my unit.
If you are looking to integrate your HVAC try the integrations already available.
DKN Cloud for HASS

Daikin AC

Go DAIKIN

Also check out these prefabricated products:
Faikin
Faikout

Additionally, I think a simple RS485 to Wi-Fi gateway device would work with the built-in HA Modbus integration with minimal effort. These can run about $60 and make this project a lot easier.
I had some spare parts on hand and old RPI (overkill) that needed a home somewhere.

My Goal was to create a simple way to integrate my Daikin HVAC into Home Assistant without cloud dependencies.

My Equipment:

  • Airzone AZWSDAI0630 (required as it is the Modbus hub but any Airzone controller should work not this specific model number)
  • Indoor ducted air handler FDMQ24WVJU9 (Airzone is mounted on this device)

Parts list:

  • RPi zero 2 (any RPi with Wi-Fi or w/adapter will work) (An ESP32 device would work as well but your code would need to be updated. Use AI to code quickly!)
  • USB to RS485 module ($16 on * amazon). Buy one with FTDI based or SP485EEN chip for best experience. Also note do not try to use uart(GPIO pins) to rs485 as they DO NOT WORK in my experience, for this project.
  • micro usb to usb A (female) wire (connect rs485 module to rpi zero 2.) ***You may not need this based on your RPi of choice but most likely will need a short USB extension cord
  • power brick and usb cable
  • Wires for connecting RS485 module to the Airwave device. Twisted pairs is preferred such as ethernet cable. I used doorbell wire laying around and twisted it with a drill.
  • One 4-pin 3.81 pitch phoenix screw terminal block connector. (on * amazon) (only needed if your HVAC techs didn’t leave the terminal block in the Airzone)
  • SD card that fits your RPi (8gb or larger should be sufficient)

The build:

Pretty simple but connect the RS485 module to an available USB on your RPI (ensure you use the data OTG usb socket rather than the PWR socket if using the RPi Zero) Connect the RS485 device to the Airzone device using two wires of your choosing. The USB device clearly indicates A+ and B- Terminals while the Airzone device provides less than desired indicators on the physical terminals. I believe all Airzone modules use the same terminals but double check your airzone modbus documentation to be sure. Otherwise connect to the terminals as shown in the photo below. The RS485 A+ terminal to the A/BMS+ (blue) terminal and RS485 Terminal B- to the B/BMS- (green) terminal. If they are reversed there will be no issues or damage done it just won’t communicate. **If you don’t get communication on the first try swap the wires.

When you are done testing make sure to print or buy an enclosure for your RPi.

Programming RPI:

Start by using Rpi Imager to flash the appropriate version of RPi OS Lite on your device. (64 or 32 bit is fine) Enable SSH, set Wi-Fi SSID and password, and all the other initial setup info then start the program writing to your SD card. When RPi Imager is done put that SD right into your RPi and fire it up. Now head back to your PC and SSH into the RPI using Putty or your preferred program.

Update your RPi software:
sudo apt update
sudo apt upgrade -y

Install, create and activate python environment:
sudo apt install python3-venv -y
python3 -m venv ~/airzone-env
source ~/airzone-env/bin/activate

Install MQTT:
pip install pyserial paho-mqtt

ID your rs485 adapter:
ls /dev/serial/by-id/
expected out put (similiar): usb-FTDI_FT232R_USB_UART_BG01PRL4-if00-port0
Copy down full path for use later example:
/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_BG01PRL4-if00-port0

Create python MQTT Modbus script:
sudo nano airzone_bridge.py
paste the snippet below into your new file: update password and IP first!

Remember the password as you will use it when creating a user in MQTT in HA later.
Update the IP address to your HA instance or wherever your mosquito broker is.

#!/home/pi/airzone-env/bin/python

import serial
import time
import struct
import json
import paho.mqtt.client as mqtt

# =========================================================
# CONFIG
# =========================================================

MQTT_BROKER = "192.168.1.31"
MQTT_PORT = 1883
MQTT_USER = "airzone"
MQTT_PASS = "create_your_password"

PORT = "/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_BG01PRL4-if00-port0"

SLAVE_ID = 1

DEVICE_NAME = "Daikin Mini Split"
DEVICE_ID = "daikin_mini_split"

# =========================================================
# SERIAL
# =========================================================

ser = serial.Serial(
    port=PORT,
    baudrate=19200,
    bytesize=8,
    parity=serial.PARITY_EVEN,
    stopbits=1,
    timeout=2
)

# =========================================================
# MQTT
# =========================================================

client = mqtt.Client(
    callback_api_version=mqtt.CallbackAPIVersion.VERSION2
)

client.username_pw_set(MQTT_USER, MQTT_PASS)

# =========================================================
# MODBUS CRC
# =========================================================

def modbus_crc(data):

    crc = 0xFFFF

    for pos in data:

        crc ^= pos

        for _ in range(8):

            if crc & 1:
                crc >>= 1
                crc ^= 0xA001
            else:
                crc >>= 1

    return struct.pack('<H', crc)

# =========================================================
# READ REGISTER
# =========================================================

def read_register(register):

    request = bytes([
        SLAVE_ID,
        0x03,
        (register >> 8) & 0xFF,
        register & 0xFF,
        0x00,
        0x01
    ])

    request += modbus_crc(request)

    ser.write(request)

    time.sleep(0.2)

    response = ser.read(100)

    if len(response) >= 5:

        value = response[3] << 8 | response[4]

        return value

    return None

# =========================================================
# WRITE REGISTER
# =========================================================

def write_register(register, value):

    request = bytes([
        SLAVE_ID,
        0x06,
        (register >> 8) & 0xFF,
        register & 0xFF,
        (value >> 8) & 0xFF,
        value & 0xFF
    ])

    request += modbus_crc(request)

    print(f"Writing register {register} value {value}")

    ser.write(request)

    time.sleep(0.2)

    response = ser.read(100)

    if response:

        print(f"Write response: {response.hex()}")

        return True

    print("No write response")

    return False

# =========================================================
# MQTT DISCOVERY
# =========================================================

def publish_discovery():

    discovery_topic = (
        f"homeassistant/climate/{DEVICE_ID}/config"
    )

    payload = {

        "name": DEVICE_NAME,
        "unique_id": DEVICE_ID,

        # -------------------------------------------------
        # HVAC MODES
        # -------------------------------------------------

        "mode_state_topic":
            "airzone/mode/state",

        "mode_command_topic":
            "airzone/mode/set",

        "modes": [
            "off",
            "auto",
            "cool",
            "heat",
            "fan_only",
            "dry"
        ],

        # -------------------------------------------------
        # TEMPERATURES
        # -------------------------------------------------

        "temperature_state_topic":
            "airzone/setpoint/state",

        "temperature_command_topic":
            "airzone/setpoint/set",

        "current_temperature_topic":
            "airzone/roomtemp/state",

        "temp_step": 1,
        "min_temp": 60,
        "max_temp": 86,

        "precision": 1.0,

        # -------------------------------------------------
        # FAN MODES
        # -------------------------------------------------

        "fan_mode_state_topic":
            "airzone/fan/state",

        "fan_mode_command_topic":
            "airzone/fan/set",

        "fan_modes": [
            "auto",
            "1",
            "2",
            "3"
        ],

        # -------------------------------------------------
        # DEVICE INFO
        # -------------------------------------------------

        "device": {
            "identifiers": [DEVICE_ID],
            "name": DEVICE_NAME,
            "manufacturer": "Daikin/Airzone",
            "model": "Airzone Aidoo",
            "sw_version": "1.0"
        }
    }

    client.publish(
        discovery_topic,
        json.dumps(payload),
        retain=True
    )

    print("Published MQTT discovery")

# =========================================================
# MQTT CALLBACK
# =========================================================

def on_message(client, userdata, msg):

    topic = msg.topic
    payload = msg.payload.decode()

    print(f"MQTT command received:")
    print(f"Topic: {topic}")
    print(f"Payload: {payload}")

    try:

        # -------------------------------------------------
        # SETPOINT
        # -------------------------------------------------

        if topic == "airzone/setpoint/set":

            temp = int(float(payload) * 10)

            success = write_register(1, temp)

            print(f"Setpoint write success: {success}")

        # -------------------------------------------------
        # HVAC MODES
        # -------------------------------------------------

        elif topic == "airzone/mode/set":

            # OFF
            if payload == "off":

                success = write_register(0, 0)

                print(f"Power OFF success: {success}")

            else:

                # Ensure power ON
                write_register(0, 1)

                modes = {
                    "auto": 1,
                    "cool": 2,
                    "heat": 3,
                    "fan_only": 4,
                    "dry": 5
                }

                if payload in modes:

                    success = write_register(
                        3,
                        modes[payload]
                    )

                    print(f"Mode write success: {success}")

        # -------------------------------------------------
        # FAN SPEED
        # -------------------------------------------------

        elif topic == "airzone/fan/set":

            fan_modes = {
                "auto": 0,
                "1": 1,
                "2": 2,
                "3": 3
            }

            if payload in fan_modes:

                success = write_register(
                    54,
                    fan_modes[payload]
                )

                print(f"Fan write success: {success}")

    except Exception as e:

        print(f"MQTT write error: {e}")

# =========================================================
# MQTT CONNECT
# =========================================================

print("Connecting to MQTT broker...")

client.on_message = on_message

client.connect(
    MQTT_BROKER,
    MQTT_PORT,
    60
)

print("Connected to MQTT broker")

client.subscribe("airzone/setpoint/set")
client.subscribe("airzone/mode/set")
client.subscribe("airzone/fan/set")

print("MQTT subscriptions active")

client.loop_start()

publish_discovery()

# =========================================================
# MAIN LOOP
# =========================================================

while True:

    try:

        power = read_register(0)
        setpoint = read_register(1)
        room_temp = read_register(2)
        mode = read_register(3)
        fan = read_register(54)

        # -------------------------------------------------
        # SETPOINT
        # -------------------------------------------------

        if setpoint is not None:

            client.publish(
                "airzone/setpoint/state",
                setpoint / 10,
                retain=True
            )

        # -------------------------------------------------
        # ROOM TEMP
        # -------------------------------------------------

        if room_temp is not None:

            client.publish(
                "airzone/roomtemp/state",
                room_temp / 10,
                retain=True
            )

        # -------------------------------------------------
        # POWER
        # -------------------------------------------------

        if power is not None:

            client.publish(
                "airzone/power/state",
                power,
                retain=True
            )

        # -------------------------------------------------
        # HVAC MODE
        # -------------------------------------------------

        if power == 0:

            client.publish(
                "airzone/mode/state",
                "off",
                retain=True
            )

        elif mode is not None:

            mode_map = {
                1: "auto",
                2: "cool",
                3: "heat",
                4: "fan_only",
                5: "dry"
            }

            client.publish(
                "airzone/mode/state",
                mode_map.get(mode, "unknown"),
                retain=True
            )

        # -------------------------------------------------
        # FAN MODE
        # -------------------------------------------------

        if fan is not None:

            fan_map = {
                0: "auto",
                1: "1",
                2: "2",
                3: "3"
            }

            client.publish(
                "airzone/fan/state",
                fan_map.get(fan, "auto"),
                retain=True
            )

        # -------------------------------------------------
        # STATUS OUTPUT
        # -------------------------------------------------

        room_display = "?"
        setpoint_display = "?"
        mode_display = "?"
        fan_display = "?"

        if room_temp is not None:
            room_display = room_temp / 10

        if setpoint is not None:
            setpoint_display = setpoint / 10

        if mode is not None:
            mode_display = mode

        if fan is not None:
            fan_display = fan

        print(
            f"Room: {room_display}F | "
            f"Setpoint: {setpoint_display}F | "
            f"Mode: {mode_display} | "
            f"Fan: {fan_display}"
        )

        time.sleep(5)

    except Exception as e:

        print(f"Main loop error: {e}")

        time.sleep(5)

Make script executable:

chmod +x ~/airzone_bridge.py

Create systemd service:

sudo nano airzone-bridge.service

Then paste the below into the new file

[Unit]
Description=Airzone MQTT Modbus Bridge
After=network.target

[Service]
ExecStart=/home/pi/airzone-env/bin/python /home/pi/airzone_bridge.py
WorkingDirectory=/home/pi
StandardOutput=inherit
StandardError=inherit
Restart=always
User=pi

[Install]
WantedBy=multi-user.target

Enable autostart:

sudo systemctl daemon-reload
sudo systemctl enable airzone-bridge
sudo systemctl start airzone-bridge

Log out of your RPi, pat your self on the back and open Home Assistant.

Home assistant Config:

Install MQTT broker, in the apps section of HA, if you don’t already have it. Link here. Then go into the configuration and create a new username and password. This is from your python script above. Username: airzone password: create_your_own. Once complete make sure you save out of the dialog box AND save out of the config section!

Install the MQTT integration if you don’t already have it. Open the integration and it should auto discover your new Daikin mini split system. May have to reload or restart HA to get the auto discovery to see the data.

Create a cool lovelace card:

Install Mushroom card from HACS click here

Then open your dashboard and create new card using the manual card option (select manual when adding new card) paste the yaml below into your new manual card;

type: vertical-stack
cards:
  - type: custom:mushroom-title-card
    title: Daikin Mini Split
    subtitle: Airzone Modbus HVAC
  - type: thermostat
    entity: climate.daikin_mini_split
    features:
      - type: climate-hvac-modes
        hvac_modes:
          - "off"
          - auto
          - cool
          - heat
          - fan_only
          - dry
  - type: horizontal-stack
    cards:
      - type: button
        entity: climate.daikin_mini_split
        name: "OFF"
        tap_action:
          action: call-service
          service: climate.set_hvac_mode
          target:
            entity_id: climate.daikin_mini_split
          data:
            hvac_mode: "off"
      - type: button
        entity: climate.daikin_mini_split
        name: AUTO
        tap_action:
          action: call-service
          service: climate.set_hvac_mode
          target:
            entity_id: climate.daikin_mini_split
          data:
            hvac_mode: auto
      - type: button
        entity: climate.daikin_mini_split
        name: COOL
        tap_action:
          action: call-service
          service: climate.set_hvac_mode
          target:
            entity_id: climate.daikin_mini_split
          data:
            hvac_mode: cool
      - type: button
        entity: climate.daikin_mini_split
        name: HEAT
        tap_action:
          action: call-service
          service: climate.set_hvac_mode
          target:
            entity_id: climate.daikin_mini_split
          data:
            hvac_mode: heat
  - type: horizontal-stack
    cards:
      - type: button
        name: FAN
        tap_action:
          action: call-service
          service: climate.set_hvac_mode
          target:
            entity_id: climate.daikin_mini_split
          data:
            hvac_mode: fan_only
      - type: button
        name: DRY
        tap_action:
          action: call-service
          service: climate.set_hvac_mode
          target:
            entity_id: climate.daikin_mini_split
          data:
            hvac_mode: dry
  - type: entities
    title: Fan Speed
    entities:
      - entity: climate.daikin_mini_split
        name: Fan Mode
  - type: custom:mini-climate
    entity: climate.daikin_mini_split
    name: Daikin HVAC
    icon: mdi:air-conditioner

That should just about do it for this guide. Let me know if i skipped on any details you find useful.

1 Like

So the Raspberry Pi is basically a RS485/ModBus to MQTT converter?
If I use a ESP32 RS485 to MQTT device instead to get the data into MQTT, will all your other associated code and dashboard panels still work?

Correct the RPi is just the rs485 to MQTT converter. The rest should work as long as the names are retained.