Govee Direct IP Control : LAN-only integration, works across VLANs

Claude and I built a custom integration for controlling Govee lights via direct unicast UDP — no cloud, no multicast, no external dependencies.

Why another Govee integration?

Existing options either rely on the cloud API (rate-limited to 10K requests/day) or use multicast discovery (breaks across VLANs). This integration sends commands directly to each device's IP address, so it works with standard routed networks.

Features:

  • On/off, brightness, RGB color, color temperature (2000K–9000K)
  • 238 built-in scene effects for H706A and H6076
  • Fully async (asyncio.DatagramProtocol) — no threads, no blocking
  • Works across VLANs/subnets with standard IP routing
  • Configure button to edit device IP and name
  • Zero external dependencies

Supported devices: Any Govee light with LAN Control support (H6076, H706A, H610A, H619A, H6072, and more)

Repo: Brian Miller / Govee LAN Direct · GitLab

Feedback and contributions welcome!

Hi Brian,

Thanks for creating this integration, I am very interested in trying it out. When I try to paste the Gitlab PhospherURL into HAcs as a custom repository, nothing happens. It seems like it will only accept Github URLs.

This is the URL I am pasting:

I would prefer to have HACS look at the repository for updates, rather than manual copy into HA instance, then manually overwrite with updates.

Any thoughts on fixing the custom repository issue?

Totally agree with @labmaster-kc. Would love to have it as a HACS repo.

Also would be nice if the integration could pick up the scenes automatically.

Tried adding a second light. Didnt work. Claude proposed a fix to the config_flow.py. Scan_sync section.

def _scan_sync(target_ip: str | None = None) -> list[dict]:
    msg = json.dumps(
        {"msg": {"cmd": "scan", "data": {"account_topic": "reserve"}}}
    ).encode()

    devices = []
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    if hasattr(socket, "SO_REUSEPORT"):
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)

    try:
        sock.bind(("0.0.0.0", GOVEE_SCAN_RESP_PORT))
    except OSError:
        sock.bind(("0.0.0.0", 0))

    try:
        mreq = struct.pack(
            "4sl",
            socket.inet_aton(GOVEE_MULTICAST_ADDR),
            socket.INADDR_ANY,
        )
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
    except OSError:
        pass

    sock.settimeout(1.0)
    seen = set()

    for _ in range(3):
        if target_ip:
            sock.sendto(msg, (target_ip, GOVEE_SCAN_PORT))
        else:
            sock.sendto(msg, (GOVEE_MULTICAST_ADDR, GOVEE_SCAN_PORT))
        try:
            while True:
                data, addr = sock.recvfrom(4096)
                resp = json.loads(data.decode())
                device_data = resp.get("msg", {}).get("data", {})
                device_id = device_data.get("device")
                if device_id and device_id not in seen:
                    seen.add(device_id)
                    devices.append({
                        CONF_DEVICE_IP: device_data.get("ip", addr[0]),
                        CONF_DEVICE_ID: device_id,
                        CONF_DEVICE_MODEL: device_data.get("sku", "unknown"),
                        CONF_DEVICE_NAME: f"Govee {device_data.get('sku', '')} {device_id[-5:]}",
                    })
        except socket.timeout:
            if devices:
                break
            continue

    sock.close()
    return devices

Working like a charm now. Also made some progress in fetching the scenes…