Why do some devices have a visit entry in the ESPHome builder screen?

I have a few ESP devices and some, the older ones I think, have no visit link. The newer ones all have a visit link that takes me to the devices web portal.
The YAML has no web server config in any of the devices other than the fall back access point entry under the Wifi section, they all have this.

Can anyone explain why this is? I do not actually want a web portal for any of them, I get all the data in HA and that is all I need.

Thank you for any help on this.

Doesn’t sound correct. Web server can’t pop up from thin air.

If you click the visit link for a device without the web portal enabled it will take you to the esphome builder addon web page instead.


As you can see, there is no visit entry for one, but there is for the other in this picture.
And as far as I can tell they have the same components that are not related to their sensors and devices.
I just can not find anything that would make the difference except that the one without was created a good while ago and the one on the right is fairly recent.

Here is an extract of what is in the YAML in each, everything after the Captive portal is a device or display or sensor etc, nothing that would affect a web browser beinmg there or not.

esphome:
  name: esphome-web-91d8b0
  friendly_name: Kitchen Display
  on_boot:
    then:
      - delay: 200s
      - component.update: epaperDisplay

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "redacted"

ota:
  - platform: esphome
    password: "redacted"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Kitchen Display Fallback"
    password: "redacted"

captive_portal:


=========================================

esphome:
  name: esphome-web-c96020
  friendly_name: Lounge Presence
  min_version: 2024.11.0
  name_add_mac_suffix: false

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "redacted"

# Allow Over-The-Air updates
ota:
  - platform: esphome
    password: "redacted"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password


  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Lounge-Presence Fallback Hotspot"
    password: "redacted"

captive_portal:


So post screenshot what opens when you click visit.

Here you go:

That means you have web server component on your Esp.

Maybe you experience failed updates, making you think that the last yaml that doesn’t have web server, really is running on your Esp.
Read carefully your logs when you upload.

I have updated these manually and am watching the process all the way to the logs from the device starting to go past, there are no errors in compilation or upload and the logs show the expected output.
I just can not figure out why there is a web server included.

Can you post your complete yaml.

This one has the visit link, which I do not want or need:

esphome:
  name: esphome-web-c96020
  friendly_name: Lounge Presence
  min_version: 2024.11.0
  name_add_mac_suffix: false

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "redacted"

# Allow Over-The-Air updates
ota:
  - platform: esphome
    password: "redacted"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password


  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Lounge-Presence Fallback Hotspot"
    password: "redacted"

captive_portal:


# Sets up the improv via serial client for Wi-Fi provisioning.
# Handy if your device has a usb port for the user to add credentials when they first get it.
# improv_serial: # Commented until improv works with usb-jtag on idf

uart:
  id: uart_bus
  baud_rate: 115200
  rx_pin: 4
  tx_pin: 5
  parity: NONE
  stop_bits: 1

seeed_mr24hpc1:
  id: my_seeed_mr24hpc1

text_sensor:
  - platform: seeed_mr24hpc1
    heart_beat:
      name: "Heartbeat"
    product_model:
      name: "Product Model"
    product_id:
      name: "Product ID"
    hardware_model:
      name: "Hardware Model"
    hardware_version:
      name: "Hardware Version"
    keep_away:
      name: "Active Reporting Of Proximity"
    motion_status:
      name: "Motion Information"
    custom_mode_end:
      name: "Custom Mode Status"

binary_sensor:
  - platform: seeed_mr24hpc1
    has_target:
      name: "Presence Information"

sensor:
  - platform: seeed_mr24hpc1
    custom_presence_of_detection:
      name: "Static Distance"
    movement_signs:
      name: "Body Movement Parameter"
    custom_motion_distance:
      name: "Motion Distance"
    custom_spatial_static_value:
      name: "Existence Energy"
    custom_spatial_motion_value:
      name: "Motion Energy"
    custom_motion_speed:
      name: "Motion Speed"
    custom_mode_num:
      name: "Current Custom Mode"

switch:
  - platform: seeed_mr24hpc1
    underlying_open_function:
      name: Underlying Open Function Info Output Switch

button:
  - platform: seeed_mr24hpc1
    restart:
      name: "Module Restart"
    custom_set_end:
      name: "End Of Custom Mode Settings"

select:
  - platform: seeed_mr24hpc1
    scene_mode:
      name: "Scene"
    unman_time:
      name: "Time For Entering No Person State (Standard Function)"
    existence_boundary:
      name: "Existence Boundary"
    motion_boundary:
      name: "Motion Boundary"

number:
  - platform: seeed_mr24hpc1
    sensitivity:
      name: "Sensitivity"
    custom_mode:
      name: "Custom Mode"
    existence_threshold:
      name: "Existence Energy Threshold"
    motion_threshold:
      name: "Motion Energy Threshold"
    motion_trigger:
      name: "Motion Trigger Time"
    motion_to_rest:
      name: "Motion To Rest Time"
    custom_unman_time:
      name: "Time For Entering No Person State (Underlying Open Function)"
    

And this one does not:

esphome:
  name: esphome-web-91d8b0
  friendly_name: Kitchen Display
  on_boot:
    then:
      - delay: 200s
      - component.update: epaperDisplay

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "redacted"

ota:
  - platform: esphome
    password: "redacted"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Kitchen Display Fallback"
    password: "redacted"

captive_portal:

time:
  - platform: homeassistant
    id: homeassistant_time

text_sensor:
  - platform: homeassistant
    entity_id: sensor.weather
    id: weather_3hr
    internal: true
  - platform: homeassistant
    id: weather_icon
    entity_id: sensor.weather
    attribute: icon
    internal: true
  - platform: homeassistant
    entity_id: input_text.motd
    id: MOTD
    internal: true

sensor:
  - platform: homeassistant
    id: external_temperature_3h
    entity_id: sensor.temperature
  - platform: homeassistant
    id: UVIndex_3h
    entity_id: sensor.uv_index
  - platform: homeassistant
    id: internal_temperature
    entity_id: sensor.hall_sensor_temperature
  #- platform: homeassistant
  #  id: solar_excess
  #  entity_id: sensor.smart_meter_electricity_power
  #  filters:
  #    - lambda: !lambda |-
  #        if (x < 0) { return 0.0-x; } else { return 0; } ;
  - platform: homeassistant
    id: solar_excess
    entity_id: sensor.smart_meter_electricity_power
  - platform: homeassistant
    id: battery_soc
    entity_id: sensor.esphome_web_ccba78_bat_soc
  - platform: wifi_signal
    id: wifisignal
    update_interval: 60s

graph:
  # Show bare-minimum auto-ranged graph
  - id: single_power_graph
    sensor: solar_excess
    duration: 1h
    width: 250
    height: 101
font:
  - file: "fonts/DejaVuSans.ttf"
    id: djsans10
    size: 10
  - file: "fonts/DejaVuSans.ttf"
    id: djsans30
    size: 30
  - file: "fonts/DejaVuSans.ttf"
    id: djsans40
    size: 40
  - file: "fonts/DejaVuSans.ttf"
    id: djsans60
    size: 60
  - file: "fonts/MaterialDesignIconsDesktop.ttf"
    id: mdi100
    size: 100
    glyphs: [
      "\U000F05D6", # alert-circle-outline - exeptionnal
      "\U000F002A", # alert-outline
      "\U000F058E", # water-percent
      "\U000F0D43", # air-filter
      "\U000F0F2F", # weather-cloudy-alert
      "\U000F0590", # weather-cloudy
      "\U000F0594", # weather-night - clear-night
      "\U000F0F31", # weather-night-partly-cloudy
      "\U000F0591", # weather-fog
      "\U000F0592", # weather-hail
      "\U000F0593", # weather-lightning
      "\U000F067E", # weather-lightning-rainy
      "\U000F0595", # weather-partly-cloudy
      "\U000F0F32", # weather-partly-lightning
      "\U000F0F33", # weather-partly-rainy
      "\U000F0F34", # weather-partly-snowy
      "\U000F0F35", # weather-partly-snowy-rainy
      "\U000F0596", # weather-pouring
      "\U000F0597", # weather-rainy
      "\U000F0598", # weather-snowy
      "\U000F0F36", # weather-snowy-heavy
      "\U000F067F", # weather-snowy-rainy
      "\U000F0599", # weather-sunny
      "\U000F059D", # weather-windy
      "\U000F059E", # weather-windy-variant
      "\U000F050F"  # thermometer
      ]
  - file: "fonts/MaterialDesignIconsDesktop.ttf"
    id: mdi60
    size: 60
    glyphs: [
      "\U000F05D6", # alert-circle-outline - exeptionnal
      "\U000F002A", # alert-outline
      "\U000F058E", # water-percent
      "\U000F050F", # thermometer
      "\U000F0079", # mdi:battery #full
      "\U000F007A", # mdi:battery-10
      "\U000F007B", # mdi:battery-20
      "\U000F007C", # mdi:battery-30
      "\U000F007D", # mdi:battery-40
      "\U000F007E", # mdi:battery-50
      "\U000F007F", # mdi:battery-60
      "\U000F0080", # mdi:battery-70
      "\U000F0081", # mdi:battery-80
      "\U000F0082", # mdi:battery-90
      "\U000F0599", # weather-sunny
      "\U000F1902",  # mdi:home-battery-outline
      "\U000F1A74", # mdi:solar-power-variant-outline
      "\U000F17FF"  # mdi:sun-wireless-outline
      ]
  - file: "fonts/MaterialDesignIconsDesktop.ttf"
    id: mdi30
    size: 30
    glyphs: [
      "\U000F05D6", # alert-circle-outline - exeptionnal
      "\U000F002A", # alert-outline
      "\U000F058E", # water-percent
      "\U000F050F", # thermometer
      "\U000F0079", # mdi:battery #full
      "\U000F007A", # mdi:battery-10
      "\U000F007B", # mdi:battery-20
      "\U000F007C", # mdi:battery-30
      "\U000F007D", # mdi:battery-40
      "\U000F007E", # mdi:battery-50
      "\U000F007F", # mdi:battery-60
      "\U000F0080", # mdi:battery-70
      "\U000F0081", # mdi:battery-80
      "\U000F0082", # mdi:battery-90
      "\U000F0599", # weather-sunny
      "\U000F1902",  # mdi:home-battery-outline
      "\U000F1A74", # mdi:solar-power-variant-outline
      "\U000F17FF"  # mdi:sun-wireless-outline
      ]
  - file: "fonts/MaterialDesignIconsDesktop.ttf"
    id: mdi14
    size: 14
    glyphs: [
      "\U000F092B", # wifi-strength-alert-outline
      "\U000F092F", # wifi-strength-outline not in below range
      "\U000F091F", # wifi-strength-1 >= -100
      "\U000F0922", # wifi-strength-2 >= -75
      "\U000F0925",  # wifi-strength-3 >= -60
      "\U000F0928"  # wifi-strength-4 >= -50
      ]
      
spi:
  clk_pin: 13
  mosi_pin: 14

display:
  - platform: waveshare_epaper
    id: epaperDisplay
    cs_pin: 15
    dc_pin: 27
    busy_pin: 25
    #  number: 25
    #  inverted: true
    reset_pin: 26
    reset_duration: 2ms
    update_interval: 600s #update every 10 minutes
    rotation: 0
    model: 7.50in-bV3
    lambda: |-
      // screen section coordinates
      int width = it.get_width();
      int height = it.get_height();
      int centre = width /2;
      int tTop = 10;
      int tBottom = tTop + (height - 10) / 2;
      //int mTop = tBottom + 1;
      //int mBottom = mTop + (height - 10) / 2;
      int BTop = tBottom + 1;
      int BBottom = BTop + (height - 10) / 2;

      // Header 0 to 9
      std::string iconstr("\U000F092B"); //it is off to start so set that and change if we have anything better
      if(id(wifisignal).has_state()) {
        if (id(wifisignal).state >= -50) {
            //Excellent
            iconstr = "\U000F0928";
        } else if (id(wifisignal).state  >= -60) {
            //Good
            iconstr = "\U000F0925";
        } else if (id(wifisignal).state  >= -75) {
            //Fair
            iconstr = "\U000F0922";
        } else if (id(wifisignal).state  >= -100) {
            //Weak
            iconstr = "\U000F091F";
        } else {
            //Unlikely working signal
            iconstr = "\U000F092B";
        }
      }
      it.printf(width - 1, 0, id(mdi14), TextAlign::TOP_RIGHT, iconstr.c_str());

      

      // centre divider
      it.line(centre , tTop + (tBottom - tTop)/2, centre, BTop + (BBottom - BTop)/2 );

      //Date Time
      it.strftime((width - centre) /2, tTop, id(djsans60), TextAlign::TOP_CENTER, "%A", id(homeassistant_time).now());
      it.strftime((width - centre) /2, tTop + 70, id(djsans40), TextAlign::TOP_CENTER, "%d %B %Y", id(homeassistant_time).now());
      // removing the time feature as screen does not do quick partial updates
      // it.strftime(width /2, 120, id(djsans40), TextAlign::TOP_CENTER, "%H:%M", id(homeassistant_time).now());


      // Top Right 
      
      //it.line(centre + centre/2 , tTop, centre + centre/2, tTop + 100);
      std::string thermometer("\U000F050F");
      it.printf(centre + centre/2, tTop + 4, id(mdi60), TextAlign::TOP_CENTER, thermometer.c_str());

      //temp in
      it.printf(centre + centre/4, tTop + 2, id(djsans30), TextAlign::TOP_CENTER, "Inside");
      it.printf(centre + centre/4, tTop + 42, id(djsans40), TextAlign::TOP_CENTER, "%.1f°c", id(internal_temperature).state);


      //temp out
      it.printf(width - centre/4, tTop + 2, id(djsans30), TextAlign::TOP_CENTER, "Outside");
      it.printf(width - centre/4, tTop + 44, id(djsans40), TextAlign::TOP_CENTER, "%.0f°c", id(external_temperature_3h).state);
      std::string sunWifi("\U000F17FF");
      it.printf(centre + centre/2, tTop + (tBottom - tTop)/2, id(mdi30), TextAlign::TOP_RIGHT, sunWifi.c_str());
      it.printf(centre + centre/2, tTop + (tBottom - tTop)/2, id(djsans30), TextAlign::TOP_LEFT, "%.0f", id(UVIndex_3h).state);
      if (id(weather_3hr).has_state()) {
        std::map<std::string, std::string> weather_state { 
            { "sunny", "\U000F0599" },                 // mdi:weather-sunny
            { "clear-night", "\U000F0594" },           // mdi:weather-night
            { "cloudy", "\U000F0590" },                // mdi:weather-cloudy
            { "rainy", "\U000F0597" },                 // mdi:weather-rainy
            { "pouring", "\U000F0596" },               // mdi:weather-pouring
            { "sleet", "\U000F067F" },                 // mdi:weather-snowy-rainy
            { "lightning-rainy","\U000F067E"},         // mdi:weather-lightning-rainy
            { "lightning","\U000F0593"},               // mdi:weather-lightning
            { "snowy", "\U000F0598" },                  // mdi:weather-snowy-heavy
            { "wind", "\U000F059E" },                  // mdi:weather-windy-variant
            { "fog", "\U000F0591" },                   // mdi:weather-fog
            { "partlycloudy", "\U000F0595" },          // mdi:weather-partly-cloudy
            { "partly-cloudy-night", "\U000F0F31" }    // mdi:weather-night-partly-cloudy
        };
        std::map<std::string, std::string> weather_state_mdi {
            { "mdi:weather-cloudy-alert", "\U000F0F2F" }, //# weather-cloudy-alert
            { "mdi:weather-cloudy", "\U000F0590" }, //# weather-cloudy
            { "mdi:weather-clear-night", "\U000F0594" }, //# weather-night
            { "mdi:weather-night-partly-cloudy", "\U000F0F31" }, //# weather-night-partly-cloudy
            { "mdi:weather-fog", "\U000F0591" }, //# weather-fog
            { "mdi:weather-hail", "\U000F0592" }, //# weather-hail
            { "mdi:weather-lightning", "\U000F0593" }, //# weather-lightning
            { "mdi:weather-lightning-rainy", "\U000F067E" }, //# weather-lightning-rainy
            { "mdi:weather-partly-cloudy", "\U000F0595" }, //# weather-partly-cloudy
            { "mdi:weather-partly-lightning", "\U000F0F32" }, //# weather-partly-lightning
            { "mdi:weather-partly-rainy", "\U000F0F33" }, //# weather-partly-rainy
            { "mdi:weather-partly-snowy", "\U000F0F34" }, //# weather-partly-snowy
            { "mdi:weather-partly-snowy-rainy", "\U000F0F35" }, //# weather-partly-snowy-rainy
            { "mdi:weather-pouring", "\U000F0596" }, //# weather-pouring
            { "mdi:weather-rainy", "\U000F0597" }, //# weather-rainy
            { "mdi:weather-snowy", "\U000F0598" }, //# weather-snowy
            { "mdi:weather-snowy-heavy", "\U000F0F36" }, //# weather-snowy-heavy
            { "mdi:weather-snowy-rainy", "\U000F067F" }, //# weather-snowy-rainy
            { "mdi:weather-sunny", "\U000F0599" }, //# weather-sunny
            { "mdi:weather-windy", "\U000F059D" }, //# weather-windy
            { "mdi:weather-windy-variant", "\U000F059E" } //# weather-windy-variant
        };
        //if(weather_state.count(id(weather_3hr).state.c_str()) > 0 ) {
        //  it.printf(centre + centre/2, tTop + (tBottom - tTop)/2 + 50, id(mdi100), TextAlign::TOP_CENTER, weather_state[id(weather_3hr).state.c_str()].c_str());
        //}
        if(weather_state_mdi.count(id(weather_icon).state.c_str()) > 0 ) {
          it.printf(centre + centre/2, tTop + (tBottom - tTop)/2 + 50, id(mdi100), TextAlign::TOP_CENTER, weather_state_mdi[id(weather_icon).state.c_str()].c_str());
        }
        else {
          it.printf(centre + centre/2, tTop + (tBottom - tTop)/2 + 50, id(djsans30), TextAlign::TOP_CENTER, id(weather_3hr).state.c_str());
          it.printf(centre + centre/2, tTop + (tBottom - tTop)/2 + 100, id(djsans30), TextAlign::TOP_CENTER, id(weather_icon).state.c_str());
        }
      }

      // Bottom Right 
      std::string houseBat("\U000F1902");
      it.printf(centre + centre/4, BTop + 50, id(mdi30), TextAlign::TOP_CENTER, houseBat.c_str());
      std::string sunsol("\U000F1A74");
      it.printf(centre + centre/2, BTop + 50, id(mdi30), TextAlign::TOP_CENTER, sunsol.c_str());

      it.printf(centre + centre/4, BTop + 80, id(djsans30), TextAlign::TOP_CENTER, "%2.0f%%", (id(battery_soc).state -20)*100/80 );
      it.printf(centre + centre/2, BTop + 80, id(djsans30), TextAlign::TOP_CENTER, "%.1fkw", - id(solar_excess).state );


      // BOTTOM 
      it.printf(width/2, BBottom - (BBottom - BTop)/4, id(djsans30), TextAlign::TOP_CENTER, id(MOTD).state.c_str());

      // Footer  last 10
      

    

I wonder if it is just your web browser cache?

Have you tried clearing it or using a private tab?

But the device actually has a web page with a log section that shows activity as the device does stuff. I would not expect it to have a working web page with data available to see if I have not configured one in the YAML.

Neither would I.
If you remove seeed_mr24hpc1 stuff from your yaml (everything down from uart) what’s the output?

Yeah ok. That’s unlikely to be it then.

You’re 100% sure you don’t have a duplicate device on your network?

No duplicates I can find, and only the devices I have added in the last 3/4 weeks have the web interface.
I have made a change to older devices and uploaded them and they have not aquired a view link or a web interface.
But the ones I added new recently, 2 seeed presence sensors and a Lilligo camera module with display all have a view link and a web interface available. I just can not figure out what is up.

This is the result of compiling with everything from and including UART removed, still get the visit link and a web page:

I have nothing to suggest.
But to test further you could remove ap and captive portal as well.

The one with webserver is compiled with esp-idf framework and the one without with arduino framework, maybe that’s is a clue

1 Like