Fall back to relay if not connected to HA

I have a Shelly 1L connected with a toggle switch. The output is connected to a smart light bulb and the relay is in detached mode (always on). Under normal circumstances (if the Shelly is connected to the network and can access HA) it should call a HA service to turn the smart light bulb. However, if something is wrong, I want to use the relay to toggle the light. Below you can find my config, which is basically an adapted version of a Shelly 1 config in the documentation.

The way I test this setup is that I turn on the smart light and then disconnect HA from the network by pulling the ethernet plug. This should make the if-statement evaluate to False and the relay should kick in when I toggle the switch. However, it doesnā€™t.

Iā€™m wondering if anyone here can help me with this problem. Perhaps Iā€™m doing something wrong in the config or the way I test this isnā€™t a supported use-case?

Shelly config
substitutions:
  device_name: kitchen

# Basic Config
esphome:
  name: ${device_name}
  platform: ESP8266
  board: esp01_1m

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: ${device_name}
    password: !secret wifi_password

logger:
api:
ota:

web_server:
  port: 80

#==============================================================
switch:
  - platform: gpio
    pin: GPIO5
    id: shelly_1l_relay

binary_sensor:
  - platform: gpio
    name: ${device_name} Input
    pin:
      number: GPIO4
    # small delay to prevent debouncing
    filters:
      - delayed_on_off: 50ms
    # config for state change of input button
    on_state:
        then:
          - if:
              condition:
                and:
                  - wifi.connected:
                  - api.connected:
                  - switch.is_on: shelly_1l_relay
              # toggle smart light if wifi and api are connected and relay is on
              then:
                # - switch.toggle: shelly_1l_relay
                - homeassistant.service:
                    service: light.toggle
                    data:
                      entity_id: light.kitchen_light
              # else, toggle relay
              else:
                - switch.toggle: shelly_1l_relay
    id: button
3 Likes

Why not use on_click: or on_press:?

I would have thought that by using on_state: you will trigger this twice, once on press and once on release, thus switching the relay on then off again?

Thanks for your response! Iā€™m using a toggle/rocker switch, not a push/momentary switch. If I flip the switch it stays on either ā€œonā€ or ā€œoffā€ until I switch it back, so thereā€™s only one state change per toggle.

As an aside, you can do clicks with toggle switches, but itā€™s a kind of special control. It works by flipping the switch from ON to OFF and then ON again for a single click. Iā€™m actually using it to dim the smart bulb: 100% brightness with a long click, 50% with a short click, and 10% with a double click.

Iā€™ve checked the source code for the ESPHome API server and it relies on a list of connected clients to decide the value for api.connected. If the list of clients is empty, itā€™s False and True otherwise (I see this is also in the documentation here). I assume each client is added to the list after connecting, but theyā€™re only removed if theyā€™re explicitly disconnecting. This would mean that when I pull the ethernet cable from HA, the ESPHome device is left with a stale connection, e.g., it thinks thereā€™s still a client connected, even though it canā€™t be reached. This makes sense since the alternative is to ping the client to check if itā€™s still alive, which is a huge pain.

If this is the case, Iā€™d need a way to periodically clean the list of clients (e.g. by pinging them) or find an alternative way to ensure the HA service call will be picked up. Alternatively, I could handle this by handling a timeout on the service call: attempt to toggle the smart bulb, and if it times out, assume HA is offline and fall back to the relay. Does anyone have experience with any of these approaches?

Ah - a toggle switchā€¦ Noisy - no wonder you need a debounce.

Iā€™m assuming your HA instance doesnā€™t disconnect and reconnect all day, and this is just to reliably toggle the relay when disconnected? If so then some overhead in determining if you are still connected is probably acceptable, but will will require more complex code than the simple api.connected test you are doing.

Or maybe more time? Unplug then wait a minute or so.

Indeed, itā€™s only to reliably toggle the relay when disconnected. Iā€™m fine with more complex code than just api.connected, but I donā€™t know where to start. Do you happen to have any pointers for me?

Ok - I have read a few things on Reddit and finally found the one I remembered.

Seems api.connected is only useful on startup and you use Status Binary Sensor to detect disconnects:

You can then supposedly:

  1. Detect and display connected state in HA (not useful for you)
  2. On ESPHome trigger an automation on_release:
  3. Use the state as a check in other automations (what you want)

Let us know if it works.

1 Like

Also - I noticed you donā€™t have reboot_timeout: configured in your api: section.

If you donā€™t disable this by setting it to 0s, the device will reboot after 15 minutes disconnected from HA.

Need to buy some Shellies. This has the perfect WAF potential.

@zoogara Thanks a lot for taking the time to investigate this! Your suggestion made a lot of sense to me and I immediately tried it. Iā€™m sorry to say it doesnā€™t work :disappointed: Iā€™ve looked into the source code for the status component and it seems to use exactly the same logic as api.connected, so the result is the same. The state of the status component stays ā€œonā€ even though HA is disconnected from the network. It doesnā€™t matter how long I wait; it stays this way.

The ESPHome devices and HA donā€™t seem to maintain an open connection (something like a WebSocket) which would be implemented by the two ends pinging each other to keep the connection alive. This means that one end can go offline without the other end knowing about it.

I found out you can send HTTP requests from ESPHome and set a timeout for them. I could call the HA light.toggle service this way. However, there doesnā€™t seem to be a way to bind a callback that executes when the request times out.

So it seems Iā€™m still out of luck. I really hope someone can figure this one out, as I think itā€™s very useful! Today I cut off the power when I was fiddling with the wall switches. When I turned it back on it took almost 10 minutes for my router to fully startup. In the meantime, the only light that worked was the one with the Shelly thatā€™s configured with the fallback mode. It worked because HA never connected to it, so the number of clients it counted was zero and api.connected was False, which enabled the relay. Worked like a charm! This was an uncommon situation though and it wouldnā€™t work during a more frequent HA reboot, unfortunately.

Based on the function description, maybe itā€™s worth logging a ticket on the ESPhome GitHub.

Iā€™ve managed to implement the fallback using the amazing ping sensor, which is an external component by trombik. I use it to regularly ping Home Assistant from the ESP. The sensor exposes two values: latency and packet loss. When flipping the wall switch, I simply check if the packet loss is 100%. If it is, something must be amiss and it falls back to the relay instead.

Itā€™s not a pretty solution and there are some downsides, but it works well enough for me. I can tweak the number of pings and the interval. More pings take more time but make false positives less likely. If I set the interval too short, Iā€™m pinging a lot, which can become a problem if I have many devices doing this, though I donā€™t expect to run into this.

Shelly 1L config with fallback mode
substitutions:
  device_name: kitchen

# Basic Config
esphome:
  name: ${device_name}
  platform: ESP8266
  board: esp01_1m
  libraries:
    - ESP8266WiFi
    - https://github.com/akaJes/AsyncPing#95ac7e4

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: ${device_name}
    password: !secret wifi_password

logger:
api:
ota:

web_server:
  port: 80
  
external_components:
  - source: github://trombik/esphome-component-ping
    components: [ ping ]

#==============================================================
switch:
  - platform: gpio
    pin: GPIO5
    id: shelly_1l_relay
    restore_mode: ALWAYS_ON

binary_sensor:
  - platform: gpio
    name: ${device_name} Input
    pin:
      number: GPIO4
    # small delay to prevent debouncing
    filters:
      - delayed_on_off: 50ms
    on_state:
        then:
          - if:
              condition:
                and:
                  - wifi.connected:
                  - api.connected:
                  - switch.is_on: shelly_1l_relay
                  - lambda: return id(packet_loss).state < 100;
              # toggle smart light if wifi and api are connected and relay is on
              then:
                # - switch.toggle: shelly_1l_relay
                - logger.log:
                    format: "Packet loss '%.1f'"
                    args: [ 'id(packet_loss).state' ]
                - homeassistant.service:
                    service: light.toggle
                    data:
                      entity_id: light.kitchen_lights
              # else, toggle relay
              else:
                - switch.toggle: shelly_1l_relay
    id: button

sensor:
  - platform: ping

    # IP address of Home Assistant
    ip_address: 192.168.178.22

    # number of packets to send
    num_attempts: 5

    # the timeout. however, this is not what you usually expect from `ping`
    # implementation: the timeout is also the interval to send packets. if you
    # set this value to 10 sec, and the network is fine (no packet loss), then
    # the component sends a packet at 10 sec interval, and the total time to
    # finish would be 10 sec * num_attempts = 10 * 17 = 170 sec.
    timeout: 1sec

    loss:
      # the name to be shown.
      id: packet_loss
      name: Packet loss

    latency:
      # the name to be shown.
      id: latency
      name: Latency
      # this should be 3 as the value is float, unit is sec, and the raw
      # values are in ms.
      accuracy_decimals: 3

    # the interval for checking the sensors. defaults to 60s.
    update_interval: 10s

status_led:
  pin: GPIO0

Note that ping depends on two additional libraries, which are included in the first block.

To make the fallback mode complete, Iā€™ve configured my Zigbee smart bulbs to turn on by default after loss of power. This ensures that if a bulb is ā€˜soft offā€™ (powered, but shut off by HA) and a problem occurs, I can take the power off using the wall switch (utilizing the Shelly in fallback mode) and then on again, which will turn the light on (since thatā€™s the default).

Iā€™m pretty confident this will enable me to use my lights as long as I have power :grin: Let me know if this was useful to any of you!

4 Likes