Skip to content

Instantly share code, notes, and snippets.

@lbschenkel
Last active January 6, 2025 14:34
Show Gist options
  • Save lbschenkel/514c7a6c343e93498a2cf436e75f3c6c to your computer and use it in GitHub Desktop.
Save lbschenkel/514c7a6c343e93498a2cf436e75f3c6c to your computer and use it in GitHub Desktop.
ESPhome example for smart light switches controlling smart lights via HASS that degrade to dumb mode automatically

Posting by request from https://www.reddit.com/r/homeassistant/comments/1hufw9c/i_got_my_shellys_to_keep_working_in_case_of_a/

My ESPHome file is higly modularized with a lot of other functionality that is not relevant right now, so here's an extract of the relevant bits. It's a bit more verbose than usual because it's the output of ESPHome validation process, which dumps the processed YAML after "expanding" it.

Context:

  • light switch is a Shelly, but not really relevant — any ESP relay with a button would work
  • connected to the relay are smart lights talking Zigbee
  • when switch is "smart", relay remains on all the time and switch talks to HASS that turns on/off the light accordingly
  • when switch is "dumb", it takes control by turning the relay on/off instead
    • this requires that the light is configured to turn on when power is cut and back on, i.e., acts as a dumb light
  • I have installed springs behind every light switch, so they act as momentary switches instead of flipping
    • one click toggles the light on/off
    • multiple clicks or holding the button does other stuff (omitted here for brevity)

How it works:

  • there is a switch component that controls if the relay is in dumb mode or not
    • when disconnected from Home Assistant, dumb mode is assumed automatically irrespective of the state of this switch
  • Shelly has a multi-click handler attached to the button that calls the relevant scripts (for brevity I included only button_click_single)
  • when not in dumb mode:
    • send event to home assistant indicating which light to turn on
    • wait to see if light changes state
    • if light has not changed state, assume there's something wrong in HASS side (misconfiguration, Zigbee is not working, etc.):
      • when turning on the light, toggle the relay off and back on to force it to turn on (actually it is done twice in quick succession because this makes Philips Hue lights revert to maximum brightness)
      • when turning off the light, just turn off the relay
      • enter dumb mode automatically
  • when in dumb mode, just toggle the relay
  • Home Assistant notices any Shelly changing to unavailable or dumb mode and will:
    • notify me so I know something's off
    • turn off the dumb mode to make the light switch smart again, if it concludes that it was due to a temporary disconnection

Other implementation notes:

  • the functions in C act as a glorified "macro", so it's shorter to refer to them than repeating the same logic everywhere
  • state of the light in ESPhome side is text and not boolean so it can differentiate a light being off and being unavailable
    • if the light itself is offline for some reason (let's say Zigbee network down), it will make Shelly enter dumb mode

The result of this is that a single press of the light switch will always work, and will always succeed in toggling the light even if:

  • the Wi-Fi is down
  • Home Assistant is down
  • Zigbee network is down
  • everything is up but a misconfiguration is preventing the click on the switch from "reaching" the right light
  • the smart light has been substituted with a dumb light
bool is_light_on() {
bool relay_on = id(relay).state;
if(id(light_dumb).state) {
return relay_on;
} else {
return relay_on
&& (global_api_server->is_connected()
&& id(light_entity_hass).state == "on");
}
}
bool is_light_off() {
bool relay_off = !id(relay).state;
if(id(light_dumb).state) {
return relay_off;
} else {
return relay_off
|| (
global_api_server->is_connected()
&& id(light_entity_hass).state == "off"
);
}
}
bool should_turn_light_on() {
if(is_light_on()) {
return false;
} else if(is_light_off()) {
return true;
} else {
// State is intederminate, turn light off in that case
// since it's marginaly better than a light being stuck
// in the on state.
return false;
}
}
esphome:
includes:
- functions.h
binary_sensor:
- platform: template
id: light_state
name: light state
lambda: !lambda |-
return is_light_on();
icon: mdi:lightbulb-outline
entity_category: diagnostic
disabled_by_default: true
switch:
- platform: template
id: light_dumb
name: light dumb mode
icon: mdi:head-remove
entity_category: config
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
disabled_by_default: false
assumed_state: false
text_sensor:
- platform: homeassistant
id: light_entity_hass
entity_id: light.ceiling
disabled_by_default: false
internal: true
name: light_entity_hass
script:
- id: button_click_single
then:
- if:
condition:
lambda: !lambda |-
return should_turn_light_on();
then:
- logger.log:
format: light was off, attempting to turn on
- script.execute:
id: light_on
- script.wait:
id: light_on
else:
- logger.log:
format: light was on, attempting to turn off
- script.execute:
id: light_off
- script.wait:
id: light_off
- id: light_on
then:
- script.execute:
id: relay_on_and_wait_light
- script.wait:
id: relay_on_and_wait_light
- if:
condition:
and:
- switch.is_off:
id: light_dumb
- api.connected: {}
then:
- logger.log:
format: publishing event to HASS
- homeassistant.event:
event: esphome.light_switch
data:
action: toggle_on
light: light.ceiling
- script.execute:
id: wait_light_on
- script.wait:
id: wait_light_on
- if:
condition:
lambda: !lambda |-
return is_light_on();
else:
- logger.log:
format: light did not turn on, cyling relay and enabling dumb mode
- switch.turn_off:
id: relay
- delay: 500ms
- switch.turn_on:
id: relay
- delay: 500ms
- switch.turn_off:
id: relay
- delay: 500ms
- script.execute:
id: relay_on_and_wait_light
- script.wait:
id: relay_on_and_wait_light
- switch.turn_on:
id: light_dumb
else:
- logger.log:
format: HASS offline, cannot request to turn light on
- id: light_off
then:
- if:
condition:
and:
- switch.is_off:
id: relay
then:
- logger.log:
format: relay is off, nothing to do
- script.stop:
id: light_off
- if:
condition:
and:
- switch.is_off:
id: light_dumb
- api.connected: {}
then:
- logger.log:
format: publishing event to HASS
- homeassistant.event:
event: esphome.light_switch
data:
action: toggle_off
light: light.ceiling
- script.execute:
id: wait_light_off
- script.wait:
id: wait_light_off
- if:
condition:
lambda: !lambda |-
return is_light_off();
then:
- script.stop:
id: light_off
else:
- logger.log:
format: light not turning off, will turn off relay and enable dumb mode
else:
- logger.log:
format: HASS offline, will turn off relay instead
- logger.log:
format: turning off relay
- switch.turn_off:
id: relay
- switch.turn_on:
id: light_dumb
- id: relay_on_and_wait_light
then:
- if:
condition:
switch.is_on:
id: relay
then:
- script.stop:
id: relay_on_and_wait_light
- logger.log:
format: relay is off, turning on relay
- switch.turn_on:
id: relay
- wait_until:
condition:
switch.is_on:
id: relay
timeout: 1s
- logger.log:
format: relay turned on
- script.execute:
id: wait_light_on
- script.wait:
id: wait_light_on
- id: wait_light_on
then:
- logger.log:
format: waiting for light to report that is on
- wait_until:
timeout: 5s
condition:
lambda: !lambda |-
return is_light_on();
- if:
condition:
switch.is_off:
id: relay
then:
- logger.log:
format: light off, relay is off
- script.stop:
id: wait_light_on
- if:
condition:
api.connected: {}
else:
- logger.log:
format: light state unknown, disconnected from HASS
- script.stop:
id: wait_light_on
- if:
condition:
lambda: !lambda |-
return is_light_on();
then:
- logger.log:
format: light reported on
else:
- logger.log:
format: gave up waiting for light to report on
- id: wait_light_off
then:
- logger.log:
format: waiting for light to report that is off
- wait_until:
timeout: 5s
condition:
lambda: !lambda |-
return is_light_off();
- if:
condition:
switch.is_off:
id: relay
then:
- logger.log:
format: light off, relay is off
- script.stop:
id: wait_light_on
- if:
condition:
api.connected: {}
else:
- logger.log:
format: not connected to HASS, light state unknown
- script.stop:
id: wait_light_on
- if:
condition:
lambda: !lambda |-
return is_light_off();
then:
- logger.log:
format: light reported that is off
else:
- logger.log:
format: gave up waiting for light to report off
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment