ESPHome + Hubitat

I'd put native ESPHome support near the top of a wish list. @support_team

1 Like

its going to be really tricky for them - since ESPHome is now almost a competitor the foundation is closely tied to HA Foundation. Also I looked at the encrpytion support and it will be pretty much impossible to do on older Hubitat hw. Think HADB is the way to go

1 Like

Wonder why I didn't see it - will look again thanks.

here is the updated driver @bill.d - I added your espHomeConnectRequest changes to it as well also fixed a continous disconnect that you see in ESPHome device longs @wyn.carlton took changes from @nate fork as well as @nclark fork -- Please test if you can and let me know if you find bugs, I will raise a PR for @jonathanb

ESPHome-API-Library.groovy

change log


/**
 * ============================================================================
 *  CHANGE LOG (relative to Bradshaw v1.2 baseline)
 * ============================================================================
 *
 *  v1.3  2026-02-28  (contributors: enishoca, nclark, heythisisnate, community fixes)
 *
 *  FIX-1  openSocket()
 *         Clear espReceiveBuffer BEFORE connect to prevent stale bytes from a
 *         prior session corrupting the first frame after reconnect.
 *
 *  FIX-2  socketStatus()
 *         Ignore errno=11 (EAGAIN / EWOULDBLOCK).  ESP8266 non-blocking TCP
 *         stack emits this transiently; treating it as a hard error caused
 *         unnecessary disconnects on that platform.
 *
 *  FIX-3  espHomeSwitchCommand()
 *         key cast to Long (was Integer).  ESPHome entity keys are uint32 on
 *         the wire; values > 2147483647 overflowed to negative with Integer,
 *         sending the wrong key and silently failing the command.
 *
 *  FIX-4  espHomeButtonCommand()
 *         Same Long key fix as FIX-3.
 *
 *  FIX-5  All other command functions (cover, fan, light, lock, media, number,
 *         select, siren, executeService)
 *         Promoted key to Long for consistency.  No existing device is broken
 *         by this; it only fixes devices whose key happens to be > 2^31-1.
 *
 *  FIX-6  espHomeConnectRequest() / Handshake stall + missing ping scheduler
 *         Two related bugs fixed in the same method:
 *
 *         (a) HANDSHAKE STALL: ESPHome API >= 1.12 (2024.12+) does NOT send a
 *             ConnectResponse (type 4) on success, even with a password set.
 *             Waiting for it caused the connection to stall indefinitely.
 *             Fixed: branch now checks negotiated API version from HelloResponse
 *             (state.apiVersionMajor/Minor).  API <= 1.11 waits for
 *             ConnectResponse as before.  API >= 1.12 advances immediately.
 *
 *         (b) MISSING PING SCHEDULER: The new API >= 1.12 fast path was not
 *             calling espHomeSchedulePing() before runInMillis('espHomeDeviceInfoRequest').
 *             Without it, healthCheck() was never registered by runIn(), so no
 *             keepalive pings were ever sent.  The Hubitat raw socket platform
 *             silently dropped the idle TCP connection every ~120 seconds.
 *             Symptom: ESPHome device logs showed repeated
 *             "Reading failed CONNECTION_CLOSED errno=11" every ~2 minutes.
 *             Fixed: espHomeSchedulePing() added to the API >= 1.12 path.
 *
 *  FIX-7  espHomeBluetoothLeResponse()
 *         Null-safe UUID in manufacturer-data block.  Devices that advertise
 *         a manufacturer-data record with an empty UUID field threw a
 *         NullPointerException when .toLowerCase() was called on null.
 *
 *  FIX-8  hexDecode()
 *         payload.length instead of payload.size().  On a Java primitive
 *         byte[] array, .size() is not defined and silently returns 0 in
 *         some Groovy/JVM configurations, causing buffer.write() to copy
 *         zero bytes and drop every incoming packet.
 *
 *  FIX-9  stashBuffer()
 *         Re-write the 0x00 frame-delimiter byte before stashing the partial
 *         payload.  Without it, when parse() resumes on the next TCP chunk it
 *         reads the first varint byte as the delimiter, miscounts the length,
 *         and corrupts every reassembled multi-chunk message.
 *
 *  FIX-10 espHomeDeviceInfoResponse()
 *         Removed duplicate updateDataValue 'Board Model' call (wrote the
 *         same key twice in one device.with{} block).
 *
 *  FIX-11 handleNoiseProtocolDetected()  (new private helper)
 *         The prior 0x01 Noise-byte handler in parse() only logged a warning
 *         and returned.  Problems:
 *           (a) The socket stayed open, so the hub hammered the device every
 *               PING_INTERVAL_SECONDS with more failed frames.
 *           (b) The warning was easy to miss; no fix instructions were given.
 *           (c) reconnectDelay was never raised, causing rapid reconnect loops.
 *         Fixed:
 *           • parse() now calls handleNoiseProtocolDetected() on 0x01.
 *           • That method logs an ERROR with a one-line fix instruction
 *             (remove api_encryption: from YAML + re-flash).
 *           • Calls closeSocket() then raises reconnectDelay to
 *             MAX_RECONNECT_SECONDS before scheduling the next connect.
 *           • Sets state.noiseDetected = true so drivers/UIs can query it.
 *         Backward compatible: drivers on plaintext devices are untouched.
 *         Drivers on encrypted devices now fail fast with a clear fix path
 *         instead of looping silently.
 *
 *  FIX-12 PING_INTERVAL_SECONDS reduced from 120 to 60
 *         Hubitat's raw socket platform silently closes TCP connections after
 *         ~120 seconds of inactivity.  With the prior value of 120s the
 *         healthCheck/ping was scheduled to fire at 120-179s — almost always
 *         AFTER the platform had already killed the socket.  Reducing to 60s
 *         ensures a keepalive ping is sent well within Hubitat's idle window
 *         and also satisfies ESPHome's own 60s server-side API timeout.
 *         Symptom: ESPHome device logs showed "Reading failed CONNECTION_CLOSED"
 *         every ~120 seconds followed by a double reconnect sequence.
 *
 *  ADD-1  espHomeListEntitiesClimateResponse()   [msg 46]
 *         Full climate entity decoder.  Fields 5-25 per current api.proto
 *         including humidity support, temperature step, entityCategory, icon.
 *         NOTE: supportedPresets reads tag 16 (nclarck had tag 15, which
 *         collides with supportedCustomFanModes — that bug is fixed here).
 *
 *  ADD-2  espHomeClimateState()                  [msg 47]
 *         Climate state decoder with field numbers verified against live
 *         ESPHome device captures and current api.proto.  nclarck's version
 *         had all fields after target_temperature offset by -3 due to not
 *         accounting for the removal of legacy dual-setpoint fields.
 *         Correct mapping:
 *           1=key  2=mode  4=target_temperature  5=current_temperature
 *           6=fan_mode  7=swing_mode  8=action  9=custom_fan_mode
 *           10=preset  11=custom_preset  12=current_humidity  13=target_humidity
 *
 *  ADD-3  espHomeClimateCommand()                [msg 48]
 *         Climate command encoder.  key uses Long (FIX-3/4 consistency).
 *         Uses explicit containsKey() guards so only fields the caller
 *         actually sets are included in the wire message — avoids sending
 *         null/zero values that would reset modes the caller did not intend
 *         to change.
 *
 *  ADD-4  parseMessage() routing
 *         Added case MSG_LIST_CLIMATE_RESPONSE (46) and
 *         MSG_CLIMATE_STATE_RESPONSE (47) to the switch dispatcher.
 *
 *  ADD-5  CLIMATE_FAN_QUIET = 9
 *         Missing enum constant present in ESPHome firmware but absent from
 *         all known Hubitat forks of this library.
 *
 *  ADD-6  toClimateSupportedModes()
 *         Static helper to convert a ClimateMode int to a human-readable
 *         string.  Useful for drivers that log or display supported modes.
 *
 *  ADD-7  MSG_AUTHENTICATION_REQUEST / MSG_AUTHENTICATION_RESPONSE
 *         ESPHome 2025.10 renamed ConnectRequest→AuthenticationRequest in the
 *         proto source.  Wire message type numbers (3 and 4) are UNCHANGED so
 *         this is a naming-only update.  Backward-compatible aliases
 *         MSG_CONNECT_REQUEST and MSG_CONNECT_RESPONSE are retained so all
 *         existing drivers continue to compile without modification.
 *
 *  ADD-8  parseEntity() — deviceId field (ESPHome 2025.7+ sub-device support)
 *         parseEntity() now returns a fifth key:
 *           deviceId: String   — empty string ('') on pre-2025.7 devices
 *                                  (field absent → getStringTag default)
 *         Wire proto field number is defined by ENTITY_DEVICE_ID_PROTO_FIELD.
 *         See that constant's block comment for verification instructions.
 *         Backward compatible: all existing drivers that do not reference
 *         deviceId continue to compile and run without modification.
 *         Drivers that want sub-device routing check:
 *           if (entity.deviceId) {  route to child   }
 *
 *  ADD-9  ENTITY_DEVICE_ID_PROTO_FIELD constant
 *         Centralises the proto field number for device_id so a one-line
 *         change updates all entity decoders simultaneously.
 *         *** MUST BE VERIFIED against api.proto before sub-device routing
 *         is activated in a driver — see the constant's block comment. ***
 *
 * ============================================================================
 *  KNOWN LIMITATIONS (remaining)
 * ============================================================================
 *
 *  - Noise protocol encryption (ESPHome api_encryption:) now fails fast with
 *    a clear fix instruction and throttled reconnects (FIX-11 above).  Full
 *    Noise handshake / decryption is still not implemented; remove
 *    api_encryption: from the ESPHome YAML to use this driver.
 *
 *  - Sub-device routing (ESPHome 2025.7+) is now DECODED (ADD-8) — the
 *    deviceId field is present in every entity map.  However, acting on it
 *    (creating child devices, routing state to them) is a driver-level
 *    responsibility outside this library.  The library change is zero-risk
 *    for single-device YAML configs where deviceId is always ''.
 *
 *  - ENTITY_DEVICE_ID_PROTO_FIELD field-number verification (ADD-9):
 *    The ESPHome api.proto adds device_id to each ListEntitiesXxxResponse at
 *    a field number that must not conflict with existing entity-specific
 *    fields.  The constant is set to the value inferred from api.proto
 *    inspection; confirm by searching "device_id" in the ESPHome or
 *    aioesphomeapi api.proto before enabling child-device routing in a driver.
 *    On pre-2025.7 devices the field is simply absent → deviceId = ''.
 *
 *  - ESPHome 2025.10 HomeassistantServiceResponse was renamed
 *    HomeassistantActionRequest at the proto source level.  Wire message
 *    type number (35) is unchanged; no functional impact.
 */