Automation to set fan speed using momentary button cycle

I have a dumb-fan which I turned into a smart-fan using a Shelly UNI. The fan has a push-button to set the speed by cycling through modes, and I measure that speed using an ADC sensor.

0v = off
6.4v = low speed
8.6v = medium speed
11.2v = high speed

Each push of the button moves from the current speed to the next.

Ultimately I’d like to be able to choose a speed for the fan, either with a lovelace UI or by asking Alexa for a particular speed. I can think of quite a few ways to automate this with long conditional chains. If command = turn fan off, [and] current speed = low, [then] press “speed” button [3x] … etc

but I’m wondering if anyone has a recommendation for the most elegant / simple way to do this?

You are probably going to need three things:

  1. A template sensor to convert the voltages to 0%, 33%, 66% and 100%.

  2. A script that takes a percentage (the 4 above) and converts that to button presses to send to the fan.

  3. Use the two above entities in this:

There’s no way you can add two more buttons is there?

One button for each speed setting would considerably simplify things.

1 Like

I can’t! Unfortunately - the fan is very small and doesn’t have room for extra mods. But I’ll definitely try your suggestions :slight_smile: thank you!

@tom_l I got this fully working based around your suggestions (thank you!) - but it still feels a little clunky. I wonder if you can see any obvious room for improvement.

In particular

  • Are there any parts of this you think I could combine or minimise?
  • Is there any easy way to make any parts of this re-usable?
    I have 6 fans, and I’d love to not duplicate the entire thing for each one.

Appreciate any ideas you might have!

Here’s the fan template:

fan:
  - platform: template
    fans:
      starboard_saloon_fan:
        friendly_name: "Starboard Saloon Fan"
        value_template: "{{ states('binary_sensor.stb_saloon_fan_state') }}"
        percentage_template: >
          {% if states('sensor.starboard_saloon_fan_adc') | int > 10 %} 100
          {% elif states('sensor.starboard_saloon_fan_adc') | int > 7 %} 66
          {% elif states('sensor.starboard_saloon_fan_adc') | int > 5 %} 33
          {% else %} 0
          {% endif %}
        turn_on:
          service: script.stb_saloon_fan_2
        turn_off:
          service: script.stb_saloon_fan_0
        set_percentage:
          service: script.stb_saloon_fan_set_speed
          data:
            percentage: "{{ percentage }}"
        speed_count: 3

Then I ended up with individual scripts to set each speed. To figure out the correct sequence of button presses, the script has to check the current speed of the fan, then initiate the correct number of presses to get to the required speed.

Example of script.stb_saloon_fan_2 below. TLDR: If on speed 1, press once. If on speed 0, press twice, etc. So this script will always get the fan to speed 2 (66%), no matter what the current speed is.

alias: Starboard Saloon Fan - 2
sequence:
  - choose:
      - conditions:
          - type: is_voltage
            condition: device
            device_id: b81b55fc579b049aec65e85022070aac
            entity_id: sensor.starboard_saloon_fan_adc
            domain: sensor
            above: 5
            below: 7
        sequence:
          - type: turn_on
            device_id: b81b55fc579b049aec65e85022070aac
            entity_id: switch.starboard_saloon_fan_speed
            domain: switch
      - conditions:
          - type: is_voltage
            condition: device
            device_id: b81b55fc579b049aec65e85022070aac
            entity_id: sensor.starboard_saloon_fan_adc
            domain: sensor
            below: 5
        sequence:
          - type: turn_on
            device_id: b81b55fc579b049aec65e85022070aac
            entity_id: switch.starboard_saloon_fan_speed
            domain: switch
          - delay:
              hours: 0
              minutes: 0
              seconds: 0
              milliseconds: 250
          - type: turn_on
            device_id: b81b55fc579b049aec65e85022070aac
            entity_id: switch.starboard_saloon_fan_speed
            domain: switch
      - conditions:
          - type: is_voltage
            condition: device
            device_id: b81b55fc579b049aec65e85022070aac
            entity_id: sensor.starboard_saloon_fan_adc
            domain: sensor
            above: 10
        sequence:
          - type: turn_on
            device_id: b81b55fc579b049aec65e85022070aac
            entity_id: switch.starboard_saloon_fan_speed
            domain: switch
          - delay:
              hours: 0
              minutes: 0
              seconds: 0
              milliseconds: 250
          - type: turn_on
            device_id: b81b55fc579b049aec65e85022070aac
            entity_id: switch.starboard_saloon_fan_speed
            domain: switch
          - delay:
              hours: 0
              minutes: 0
              seconds: 0
              milliseconds: 250
          - type: turn_on
            device_id: b81b55fc579b049aec65e85022070aac
            entity_id: switch.starboard_saloon_fan_speed
            domain: switch
    default: []
mode: single
icon: mdi:fan-speed-2

Finally, I have a “set fan speed” script which calls the other individual scripts based on the input from the UI:

alias: Starboard Saloon Fan - Set Speed
sequence:
  - choose:
      - conditions:
          - condition: template
            value_template: '{{ percentage == 0 }}'
        sequence:
          - service: script.stb_saloon_fan_0
      - conditions:
          - condition: template
            value_template: '{{ percentage == 33 }}'
        sequence:
          - service: script.stb_saloon_fan_1
      - conditions:
          - condition: template
            value_template: '{{ percentage == 66 }}'
        sequence:
          - service: script.stb_saloon_fan_2
      - conditions:
          - condition: template
            value_template: '{{ percentage == 100 }}'
        sequence:
          - service: script.stb_saloon_fan_3
    default: []
mode: single
icon: mdi:fan-auto

Drop the device actions and use service calls instead, e.g. this:

          - type: turn_on
            device_id: b81b55fc579b049aec65e85022070aac
            entity_id: switch.starboard_saloon_fan_speed
            domain: switch

Becomes:

          - service: switch.turn_on
            target:
              entity_id: switch.starboard_saloon_fan_speed

This way you can re-use the script by passing it the entity id:

          - service: switch.turn_on
            target:
              entity_id: '{{ entity }}'

So now you call the script with the percentage and entity:

        set_percentage:
          service: script.fan_set_speed
          data:
            percentage: "{{ percentage }}"
            entity: "{{ switch.starboard_saloon_fan_speed }}"

It is always better to use service calls rather than device actions. If you ever have to replace a device you have to replace the device id everywhere it is used. If you only use service calls you can set the new entity id to match the old one in one place (configuration / entities).

1 Like

Some ideas to simplify the code:

  1. create a function to calculate the required number of button pushes n

  2. create a button push loop in your code that is run n times

There are four speed modes, so lets call them 0, 1, 2 and 3 (off is 0).
Now lets call the current speed cs and the required speed rs, and lets call the required number of button pushes n.
Now we can do this:

n = rs – cs

So if cs is 1 and rs is 3 then n = rs – cs = 3 – 1 = 2
But what if rs is smaller than cs, like rs = 0 and cs = 2?
n = rs – cs = 0 – 2 = -2
In those cases we can do this:

if n < 0 n += 4

So in the example above: n = rs – cs = 0 – 2 = -2 (negative) + 4 = 2

So the complete function is:

fSetspeed(cs, rs)
n = rs – cs
if n < 0 n += 4
return n

Of course this still has to be transferred into working HA code, but I hope the method is clear.

For the push-button loop you can find some examples here: Repeat a Group of Actions

@tom_l Perfect! I was hoping to learn exactly this - thank you


@thusassistint I came up with roughly the same theoretical expression …
function t(c, d) { return (d + 4 - c) % 4 }

but “has to be transferred into working HA code” is the part that still eludes me :sweat_smile: if you know how to do it, I’d love to learn

Alright I got this dramatically simpler, and now device-agnostic so can be re-used across fans. Was also able to simplify some logic by calling the “turn off” script briefly before setting speed. Basically acts as a common function to return fan state to a known baseline, and then number of button presses needed for desired speed is automatically known. No extra calculations needed.

Here are the final files:
Note: The delay lines are to account for a separate automation that makes the fan switches into momentary buttons. Anytime the switch.turn_on is called, the automation will automatically turn it off again. The delay in the scripts gives a little padding time for that to happen to avoid race condition bugs.

configuration.yaml

define fan(s) and which scripts to use to control them

fan:
  - platform: template
    fans:
      starboard_saloon_fan:
        friendly_name: "Starboard Saloon Fan"
        value_template: "{{ states('binary_sensor.stb_saloon_fan_state') }}"
        percentage_template: >
          {% if states('sensor.starboard_saloon_fan_adc') | int > 10 %} 100
          {% elif states('sensor.starboard_saloon_fan_adc') | int > 7 %} 66
          {% elif states('sensor.starboard_saloon_fan_adc') | int > 5 %} 33
          {% else %} 0
          {% endif %}
        turn_on:
          service: script.fan_on
          data:
            sensor: "sensor.starboard_saloon_fan_adc"
            button: "switch.starboard_saloon_fan_speed"
        turn_off:
          service: script.fan_off
          data:
            sensor: "sensor.starboard_saloon_fan_adc"
            button: "switch.starboard_saloon_fan_speed"
        set_percentage:
          service: script.fan_speed
          data:
            percentage: "{{ percentage }}"
            sensor: "sensor.starboard_saloon_fan_adc"
            button: "switch.starboard_saloon_fan_speed"
        speed_count: 3

script.fan_off:

Read current ADC value to figure out how many button presses to get it back to “off”

alias: Fan off
sequence:
  - choose:
      - conditions:
          - condition: template
            value_template: '{{ states(sensor) | float > 10 }}'
        sequence:
          - service: switch.turn_on
            target:
              entity_id: '{{ button }}'
      - conditions:
          - condition: template
            value_template: '{{ states(sensor) | float > 7 and states(sensor) | float < 10 }}'
        sequence:
          - service: switch.turn_on
            target:
              entity_id: '{{ button }}'
          - delay: '00:00:00.1'
          - service: switch.turn_on
            target:
              entity_id: '{{ button }}'
      - conditions:
          - condition: template
            value_template: '{{ states(sensor) | float > 5 and states(sensor) | float < 7 }}'
        sequence:
          - service: switch.turn_on
            target:
              entity_id: '{{ button }}'
          - delay: '00:00:00.1'
          - service: switch.turn_on
            target:
              entity_id: '{{ button }}'
          - delay: '00:00:00.1'
          - service: switch.turn_on
            target:
              entity_id: '{{ button }}'
    default: []
  - delay: '00:00:00.1'
mode: queued
icon: mdi:fan-off
max: 10

script.fan_speed

To set speed, first call previous script to turn fan off. Then press button the correct number of times to set the speed requested by the UI.

alias: Fan speed
sequence:
  - service: script.fan_off
    data:
      sensor: '{{ sensor }}'
      button: '{{ button }}'
  - choose:
      - conditions:
          - condition: template
            value_template: '{{ percentage == 33 }}'
        sequence:
          - service: switch.turn_on
            target:
              entity_id: '{{ button }}'
      - conditions:
          - condition: template
            value_template: '{{ percentage == 66 }}'
        sequence:
          - service: switch.turn_on
            target:
              entity_id: '{{ button }}'
          - delay: '00:00:00.1'
          - service: switch.turn_on
            target:
              entity_id: '{{ button }}'
      - conditions:
          - condition: template
            value_template: '{{ percentage == 100 }}'
        sequence:
          - service: switch.turn_on
            target:
              entity_id: '{{ button }}'
          - delay: '00:00:00.1'
          - service: switch.turn_on
            target:
              entity_id: '{{ button }}'
          - delay: '00:00:00.1'
          - service: switch.turn_on
            target:
              entity_id: '{{ button }}'
    default: []
  - delay: '00:00:00.5'
mode: queued
icon: mdi:fan-auto
max: 10

script.fan_on

Last, the utility script for default “fan on” behaviour - which just sets the fan to the “medium” speed.

alias: Fan on
sequence:
  - service: switch.turn_on
    target:
      entity_id: '{{ button }}'
  - delay: '00:00:00.1'
  - service: switch.turn_on
    target:
      entity_id: '{{ button }}'
  - delay: '00:00:00.2'
mode: queued
icon: mdi:fan
max: 10

Thanks for the help everyone! I’ve done a full write-up of the project here, in case anyone else is interested in making Shelly UNI work with pushbutton fans: