Is_connected is only true when a client is connected

I have an ESP32 (esp32-c3-devkitm-1 with esp-idf framework) connected to an OLED display. The device is not meant to be used with Home Assistant.

I was trying to achieve displaying different information (and icon) when

  • WiFi did not find the configured SSID and is basically not working
  • WiFI is in AP mode (SSID timed out or no configuration)
  • WiFI is properly connected to the configured SSID

I’m struggling to sense the latest, when the WiFi is simply connected to the SSID. I’m expecting that id(wifi_id).state or wifi::global_wifi_component->is_connected() would tell whether the WiFi is connected to the network, but even when it is properly connected, those return false.

Only if a client is connected to the ESP32 server (API for example, using esphome run or esphome logs) would those sensors/lambda return true. As soon as the TCP connection is lost, the WiFi is reported as disconnected.

How can I really sense the connection at the WiFi level (PHY/STA)? It seems that connected_ is aggregating several other state members, but those are all protected and cannot be checked separately.

Thanks

Lke this.
id(wifi_id).is_connected()

Firstly, ESP32-C3 is supported by ESPHome (though that could be a fairly recent addition).

Secondly, ESPHome has had some issues with Wi-Fi - but in the last month ESPHome Wi-Fi received updates, so I suggest checking that you are using the latest version.

As for the rest, I’m not sure what you are meaning. Posting your full yaml code (formatted for the forum) and log including the particular error would make it much easier for people to suggest the right answer.

FWIW, I had issues with my own wi-fi connections a while back, and added a fair bit of debugging code … which I have included below. Note that a lot happens before wi-fi connects; to debug this you will need to connect the ESP by serial/USB to a serial terminal emulator (eg minicom) running on a PC while the ESP boots up.

Note that my main device yaml includes a substitution for deviceIP so all ESPHome devices have static IPs to speed up joining the WLAN. Also, Ponder is the name of my desktop PC.

###########################################################
#
# Start with the Wi-fi connection
#
#   Sept 2025 Added more sensors, esp for debugging which WAP
#             a device has connected to.  prefix all with wifi-
#
#  As at Sept 2025 ESPHome still DOES NOT SUPPORT WI-FI ROAMING,
#    and will sometimes connect to a WAP with lower signal strength.
#
#  When an ESPHome device boots up it scans for the allowable 
#    network (SSID) with highest signal strength. Having made 
#    the connection it does not check for a stronger signal, 
#    even though a stronger signal may later become available. 
#  Even worse, if the scan finds multiple WAPs on the same WLAN
#    it connects to the WAS (BSSID) with lowest channel number 
#    - not the one with strongest signal strength !  
#
###########################################################

wifi:
  ssid:     !secret wifi_ssid
  password: !secret wifi_password
  manual_ip:
    static_ip: 192.168.1.${deviceIP}
    gateway: 192.168.1.1
    subnet: 255.255.255.0
  fast_connect: True
  output_power: 10.5      # 8.5-20.5 reduce output power MAY improve Wi-fi in study
  min_auth_mode: WPA2

  ##### add some debugging when compiled on Ponder - will only be seen if USB debugging
  on_connect:
    then:
      lambda: |-
        ESP_LOGI("TEST", "#####     >>>>>>>>>>> WIFI CONNECT");
  on_disconnect:
    then:
      lambda: |-
        ESP_LOGI("TEST", "#####     >>>>>>>>>>> WIFI DISCONNECT");

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "$devicename Fallback"
    password: !secret wifi_ap_password

ota:
  platform: esphome
  password: !secret esphome_ota_password

captive_portal:

# this displays the device's status at http:IP_Address
web_server:
  port: 80

# Enable Home Assistant API
api:
  encryption:
    key: !secret esphome_api_encryption
  ##### add some debugging when compiled on Ponder
  on_client_connected:
    - logger.log:
        format: "#####     >>>>>>>>>>  API Client '%s' connected with IP %s"
        args: ["client_info.c_str()", "client_address.c_str()"]
  on_client_disconnected:
    - logger.log: "#####     >>>>>>>>>> API client disconnected!"

###########################################################
#
#   Add some common sensors - wi-fi signal strength, 
#       uptime, ESPHome version & compile date/time
#
###########################################################

sensor:
  - platform: wifi_signal # Reports the WiFi signal strength/RSSI in dB
    name: WiFi Signal dB
    id: wifi_signal_db
    update_interval: $update_interval_network
    entity_category: "diagnostic"

  - platform: template
    id: wifi_channel
    name: Wifi channel
    entity_category: "diagnostic"

  # human readable uptime sensor output to the text sensor 
  - platform: uptime
    id: uptime_sensor
    name: Uptime
    type: seconds
    update_interval: $update_interval_network


text_sensor:
  - platform: version
    name: ESPHome Version
    hide_timestamp: False

  - platform: template
    id: wifi_node
    name: WLAN node
    update_interval: $update_interval_network
    entity_category: "diagnostic"

  - platform: wifi_info
    ip_address:
      name: Wifi IP Address
      update_interval: 60s  # We use static IPs everywhere, but using 'never' results in the sensor having no value
    mac_address:
      name: Wifi Mac Address
    ssid:
      name: Wifi Connected SSID
    scan_results:
      name: Wifi Latest Scan Results
      update_interval: $update_interval_network
    dns_address:
      name: Wifi DNS Address
    bssid:
      id: esp_connected_bssid
      name: Wifi Connected BSSID
      on_value:
        then:
          # lookup friendly names indicating which WAP we connected to
          - lambda: |-
              if ( x == "A0:36:BC:0E:29:38") {
                id(wifi_node).publish_state("LivingRoom");
                id(wifi_channel).publish_state(8);
              } else if ( x == "30:5A:3A:C5:B4:20") {
                id(wifi_node).publish_state("Laundry");
                id(wifi_channel).publish_state(3);
              } else if ( x == "5C:E9:31:1C:C1:69") {
                id(wifi_node).publish_state("Bedroom");
                id(wifi_channel).publish_state(11);
              } else if ( x == "64:66:B3:ED:08:C4") {
                id(wifi_node).publish_state("Study");
                id(wifi_channel).publish_state(13);
              } else {
                id(wifi_node).publish_state("Unknown");
                id(wifi_channel).publish_state(-99);
              }


# get the current time from the Home Assistant server
time:
  - platform: homeassistant
    id: homeassistant_time
    update_interval: "12h"         # update from Home Assistant every 12 hours

binary_sensor:
  - platform: status
    name: Wifi "Status"

Thanks, it seems I got really confused. As I had to test many different things, I was using a binary_sensor with template and a lambda containing exactly that.
Then I was doing id(binsens_wifi_connected).state thinking that it was equivalent, but actually it isn’t.

I also tested this hack:

          // Detect WiFi connection by checking if we have an IP address assigned
          auto ips = wifi::global_wifi_component->get_ip_addresses();
          bool has_ip = false;
          for (const auto& ip : ips) {
            if (ip.is_set()) {
              has_ip = true;
              break;
            }
          }

which worked but is not very idiomatic.
I’ll stick with your solution.