esphome / feature-requests

ESPHome Feature Request Tracker
https://esphome.io/
411 stars 26 forks source link

Template Fan Support #60

Open michaelwoods opened 5 years ago

michaelwoods commented 5 years ago

I'm researching whether this library can fulfill my use case.

I have a Vornado fan that I am going to solder some GPIO pins to the buttons on the PCB. There's a power button and 4 speeds (will ignore one speed for Speed Fan support).

As this is not directly a float compatible setup, it doesn't seem currently possible to map ranges of a Float Output to multiple Binary Outputs. Is this true? If so, I'd consider extending the Float Output to accept a dictionary of outputs with ranges.

Example:

output:
  - platform: map
    id: fan_map_output_id
    power_supply: power_button_id
    speeds:
      low:
        id: low_button_id
        range: [0.0, 0.33]
      medium:
        id: med_button_id
        range: [0.33, 0.66]
      high:
        id: high_button_id
        range: [0.66, 1]

Alternatively, I suppose the Speed Fan component can be modified to associate outputs with speeds.

brandond commented 5 years ago

Right now you could probably do this as a custom output that turned the various binary outputs on and off as necessary.

I'm not sure if @OttoWinter has plans to implement template binary output or template float output, but it seems like those are a logical next step given the other cool things he's allowed us to do with templates + lambdas. A template float output would let you do what you want without being specifically tied to the speed fan use case.

OttoWinter commented 5 years ago

Yeah I guess template outputs can also arrive at some time. For now, I would suggest using the custom output (usage is quite simple and pretty much exactly the same as it would be in a template output, just with more boilerplate code)

Or... Use a Home Assistant template fan. Probably a bit easier.

cculbreath commented 5 years ago

I'm looking into implementing Sonoff's iFan02, which also requires a mapping from analog speeds to discreet relay outputs. This should be straightforward to do with a custom float output. What I'm not sure about is how to integrate both off/on messages and speed commands. The HASS fan UI card has an on/off switch that toggles the fan's motion. Ideally the fan's speed should be retained independently of powering the fan on and off. In the ESPHome documentation, it's not clear to me if there's functionality that would allow both power state and speed to be stored/referenced locally and through the HASS API. I'm contemplating writing some "off->speed 0" logic using a HA template fan, but local logic and storage would be preferable. Any advice @OttoWinter ?

OttoWinter commented 5 years ago

What I'm not sure about is how to integrate both off/on messages and speed commands.

That is handled by ESPHome, custom float outputs only have to map values 0.0 (off), 0.33 (low), 0.66 (medium), 1.0 (high) to the relays.

All of the things you mentioned are handled by ESPHome, you only need to implement the custom float output.

Apogee19832312 commented 4 years ago

I'm trying to create this and having a lot of difficulty, is someone able to post a full example including the fan component and not just the output?

How the ids tie to the relays would be great also!

Apogee19832312 commented 4 years ago

Maybe if I explain what I want to achieve I might get better responses...

I plan to use 3 relays to control the speeds of a ceiling fan, I have the wiring part sorted.

I want to use esphome and create rules so the fan can still be used if the wifi goes down.

I plan to have essentially 2 buttons, one to increase the speed (but goes from high back to off), and the other button decreases the speed (goes from off to high).

I would like to use a pwm led to represent the speeds (0% off, 33% low, 66% med, 100% high).

I would love if it could be displayed like a fan component in home assistant! But if it needs to be a slider or a drop down menu I can do that.

My thoughts was to use lamda code to modify a global variable to count from 0-3 (and loops around) and this relates to the speeds. The counting would be obviously controlled by the two buttons. 0 would turn the led and all the relays off, 1 would set the led to 33% and turn relay1 on and the other relays off, 2 would set the led to 66% and turn relay2 on and the other relays off, and so on for 3.

I'm not sure if the speed select in home assistant would directly modify the global variable or if they can tie together somehow.

I know what I need to achieve but just no idea how to write it, any help would be greatly appreciated!

glmnet commented 4 years ago

check this cookbook https://deploy-preview-218--esphome.netlify.com/cookbook/ifan02.html

Apogee19832312 commented 4 years ago

thanks, I think I can work with that. I managed to get it to work without the fan component (using an input select and node-red to "link" everything).

api:

ota:

globals:
 - id: var_speed
   type: int
   restore_value: no
   initial_value: '0'

switch:
  - platform: gpio
    pin: D4
    id: relay1
    inverted: yes
    interlock: &interlock_group [relay1, relay2, relay3]

  - platform: gpio
    pin: D3
    id: relay2
    inverted: yes   
    interlock: *interlock_group

  - platform: gpio
    pin: D2
    id: relay3
    inverted: yes    
    interlock: *interlock_group

  - platform: gpio
    pin: D1
    id: relay4
    name: "Fan Light"
    inverted: yes

  - platform: template
    name: "Fan Speed Off"
    lambda: |-
      if (id(var_speed) == 0) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - switch.turn_off: relay1
      - switch.turn_off: relay2
      - switch.turn_off: relay3
      - globals.set:
          id: var_speed
          value: '0'    

  - platform: template
    name: "Fan Speed 1"
    id: fan_speed_1
    lambda: |-
      if (id(var_speed) == 1) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - switch.turn_on: relay1
      - globals.set:
          id: var_speed
          value: '1'      
    turn_off_action:
      - switch.turn_on: relay1

  - platform: template
    name: "Fan Speed 2"
    id: fan_speed_2
    lambda: |-
      if (id(var_speed) == 2) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - switch.turn_on: relay2
      - globals.set:
          id: var_speed
          value: '2'
    turn_off_action:
      - switch.turn_on: relay2

  - platform: template
    name: "Fan Speed 3"
    id: fan_speed_3
    lambda: |-
      if (id(var_speed) == 3) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - switch.turn_on: relay3
      - globals.set:
          id: var_speed
          value: '3'      
    turn_off_action:
      - switch.turn_on: relay3      

binary_sensor:
  - platform: gpio
    pin: D8
    name: "Up"
    id: up
    on_press:
      then:
        - lambda: |-
            if (id(var_speed) < 3) {
              id(var_speed) +=1;
            } else {
              id(var_speed) = 0;
            }
            ESP_LOGD("main", "Global value is: %d", id(var_speed));
        - if:
            condition:
              lambda: 'return id(var_speed) == 0;'
            then:
              - switch.turn_off: relay1
              - switch.turn_off: relay2
              - switch.turn_off: relay3
        - if:
            condition:
              lambda: 'return id(var_speed) == 1;'
            then:
              - switch.turn_on: relay1
            else:
              - switch.turn_off: relay1
        - if:
            condition:
              lambda: 'return id(var_speed) == 2;'
            then:
              - switch.turn_on: relay2
            else:
              - switch.turn_off: relay2   
        - if:
            condition:
              lambda: 'return id(var_speed) == 3;'
            then:
              - switch.turn_on: relay3
            else:
              - switch.turn_off: relay3                 

  - platform: gpio
    pin: D7
    name: "Down"    
    id: down
    on_press:
      then:
        - lambda: |-
            if (id(var_speed) > 0) {
              id(var_speed) -=1;
            } else {
              id(var_speed) = 3;
            }
            ESP_LOGD("main", "Global value is: %d", id(var_speed));
        - if:
            condition:
              lambda: 'return id(var_speed) == 0;'
            then:
              - switch.turn_off: relay1
              - switch.turn_off: relay2
              - switch.turn_off: relay3
        - if:
            condition:
              lambda: 'return id(var_speed) == 1;'
            then:
              - switch.turn_on: relay1
            else:
              - switch.turn_off: relay1
        - if:
            condition:
              lambda: 'return id(var_speed) == 2;'
            then:
              - switch.turn_on: relay2
            else:
              - switch.turn_off: relay2   
        - if:
            condition:
              lambda: 'return id(var_speed) == 3;'
            then:
              - switch.turn_on: relay3
            else:
              - switch.turn_off: relay3               
Apogee19832312 commented 4 years ago

check this cookbook https://deploy-preview-218--esphome.netlify.com/cookbook/ifan02.html

I made a mixture of the two and got it to work perfectly, thank you very much.

Apogee19832312 commented 4 years ago

So with the following code I managed to get all of what I was hoping to achieve to work perfectly. I did use the link above and modified the ifan02.h file which I'll share in my next post to include updating the light that is on my two-way switch.

I also used node-red to update from the template switches that are hidden in hassio to the fan speed settings also in hassio. Also in node-red when the fan in hassio is turned off it updates the "off" template switch in hassio so the global variable in esphome is updated correctly.

The combinations of relays on/off by default are a different combination, I will update the ons and offs according to my particular fans.

I hope this helps anyone else trying to achieve something similar.

esphome:
  name: fan_test2
  platform: ESP8266
  board: d1_mini
  includes:
    - ifan02.h
  on_boot:
    priority: 225
    # turn off the light as early as possible
    then:
      - switch.turn_off: mancave_light    

wifi:
  ssid: "*********"
  password: "**********"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Mancave Fan Fallback Hotspot"
    password: "***********"

captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

globals:
 - id: var_speed
   type: int
   restore_value: no
   initial_value: '0'

binary_sensor:
##---right rotation on the two-way switch---------
  - platform: gpio
    pin: D8
    id: up
    on_press:
      then:
        - lambda: |-
            if (id(var_speed) < 3) {
              id(var_speed) +=1;
            } else {
              id(var_speed) = 0;
            }
            ESP_LOGD("main", "Global value is: %d", id(var_speed));
        - if:
            condition:
              lambda: 'return id(var_speed) == 0;'
            then:
              - switch.turn_off: relay1
              - switch.turn_off: relay2
              - switch.turn_off: relay3
        - if:
            condition:
              lambda: 'return id(var_speed) == 1;'
            then:
              - switch.turn_on: relay1
            else:
              - switch.turn_off: relay1
        - if:
            condition:
              lambda: 'return id(var_speed) == 2;'
            then:
              - switch.turn_on: relay2
            else:
              - switch.turn_off: relay2   
        - if:
            condition:
              lambda: 'return id(var_speed) == 3;'
            then:
              - switch.turn_on: relay3
            else:
              - switch.turn_off: relay3                 

##---left rotation on the two-way switch---------                 
  - platform: gpio
    pin: D7
    id: down
    on_press:
      then:
        - lambda: |-
            if (id(var_speed) > 0) {
              id(var_speed) -=1;
            } else {
              id(var_speed) = 3;
            }
            ESP_LOGD("main", "Global value is: %d", id(var_speed));
        - if:
            condition:
              lambda: 'return id(var_speed) == 0;'
            then:
              - switch.turn_off: relay1
              - switch.turn_off: relay2
              - switch.turn_off: relay3
        - if:
            condition:
              lambda: 'return id(var_speed) == 1;'
            then:
              - switch.turn_on: relay1
            else:
              - switch.turn_off: relay1
        - if:
            condition:
              lambda: 'return id(var_speed) == 2;'
            then:
              - switch.turn_on: relay2
            else:
              - switch.turn_off: relay2   
        - if:
            condition:
              lambda: 'return id(var_speed) == 3;'
            then:
              - switch.turn_on: relay3
            else:
              - switch.turn_off: relay3     
##----sensors for updating the global varible------
##----these are tied to the relays-----------------
  - platform: gpio
    pin: 
      number: D4
      inverted: True
    id: fan_relay1
    on_press:
      then:  
        - globals.set:
            id: var_speed
            value: '1'    
  - platform: gpio
    pin: 
      number: D3
      inverted: True
    id: fan_relay2
    on_press:
      then:  
        - globals.set:
            id: var_speed
            value: '2'    
  - platform: gpio
    pin: 
      number: D2
      inverted: True
    id: fan_relay3
    on_press:
      then:  
        - globals.set:
            id: var_speed
            value: '3'                

output:
  - platform: custom
    type: float
    outputs:
      id: fanoutput
    lambda: |-
      auto mancave_fan = new ManCaveFanOutput();
      App.register_component(mancave_fan);
      return {mancave_fan};

##-----ring light on two-way switch------    
  - platform: esp8266_pwm
    id: switch_light
    pin: D5 

switch:
##-----template switches----------
  - platform: template
    name: "Fan Speed Off"
    lambda: |-
      if (id(var_speed) == 0) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - switch.turn_off: relay1
      - switch.turn_off: relay2
      - switch.turn_off: relay3
      - output.set_level:
          id: switch_light
          level: 0%
      - globals.set:
          id: var_speed
          value: '0'    

  - platform: template
    name: "Fan Speed 1"
    id: fan_speed_1
    lambda: |-
      if (id(var_speed) == 1) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - switch.turn_on: relay1
      - output.set_level:
          id: switch_light
          level: 10%      
      - globals.set:
          id: var_speed
          value: '1'      
    turn_off_action:
      - switch.turn_on: relay1

  - platform: template
    name: "Fan Speed 2"
    id: fan_speed_2
    lambda: |-
      if (id(var_speed) == 2) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - switch.turn_on: relay2
      - output.set_level:
          id: switch_light
          level: 50%      
      - globals.set:
          id: var_speed
          value: '2'
    turn_off_action:
      - switch.turn_on: relay2

  - platform: template
    name: "Fan Speed 3"
    id: fan_speed_3
    lambda: |-
      if (id(var_speed) == 3) {
        return true;
      } else {
        return false;
      }
    turn_on_action:
      - switch.turn_on: relay3
      - output.set_level:
          id: switch_light
          level: 100%      
      - globals.set:
          id: var_speed
          value: '3'      
    turn_off_action:
      - switch.turn_on: relay3    
##-----Relays-----------------
  - platform: gpio
    pin: D4
    id: relay1
    inverted: yes

  - platform: gpio
    pin: D3
    id: relay2
    inverted: yes

  - platform: gpio
    pin: D2
    id: relay3
    inverted: yes

  - platform: gpio
    pin: D1
    id: mancave_light
    name: "Mancave light"
    inverted: yes    

fan:
  - platform: speed
    output: fanoutput
    id: mancave_fan
    name: "Mancave Fan"
Apogee19832312 commented 4 years ago

and this is the ifan02.h file...

#include "esphome.h"
using namespace esphome;

class ManCaveFanOutput : public Component, public FloatOutput {
  public:
    void write_state(float state) override {
      if (state < 0.3) {
        // OFF
        digitalWrite(2, HIGH); //D4 on D1 Mini
        digitalWrite(0, HIGH); //D3 on D1 Mini
        digitalWrite(4, HIGH); //D2 on D1 Mini
        analogWrite(14, 0);    //D5 on D1 Mini (led status light) 0%
      } else if (state < 0.6) {
        // low speed
        digitalWrite(2, LOW); 
        digitalWrite(0, HIGH);
        digitalWrite(4, HIGH);
    analogWrite(14, 25); //25 is 10% brightness
      } else if (state < 0.9) {
        // medium speed
        digitalWrite(2, HIGH);
        digitalWrite(0, LOW);
        digitalWrite(4, HIGH);
    analogWrite(14, 127); // 127 is 50% brightness
      } else {
        // high speed
        digitalWrite(2, HIGH);
        digitalWrite(0, HIGH);
        digitalWrite(4, LOW);
    analogWrite(14, 255); //255 is 100% brightness
      }
    }
};
brandond commented 4 years ago

FYI - this is mostly doable with a Custom Output. The current issue is that there's no way to feed back the fan state if there's additional input to the current system. For example, I'm wrapping a Tuya dimmer as a FloatOutput so that I can use it to control a fan. However, if I turn the fan off using the actual switch on the dimmer, there's no way to tell the fan that the output value has changed, since the output is write-only.

Apogee19832312 commented 4 years ago

FYI - this is mostly doable with a Custom Output. The current issue is that there's no way to feed back the fan state if there's additional input to the current system. For example, I'm wrapping a Tuya dimmer as a FloatOutput so that I can use it to control a fan. However, if I turn the fan off using the actual switch on the dimmer, there's no way to tell the fan that the output value has changed, since the output is write-only.

This is not true, I was able to get it to work. I have written a new version that seems to work a lot better

esphome:
  name: newer_fan_test
  platform: ESP8266
  board: d1_mini
  includes:
    - ifan02.h  

wifi:
  ssid: "****"
  password: "****"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Newer Fan Test Fallback Hotspot"
    password: "****"

captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

binary_sensor:

  - platform: gpio
    id: motion
    name: "Mancave motion"
    pin: D1
    device_class: motion
    on_press:
      then:
        if:
          condition:
            - switch.is_off: mancave_light
          then:
            - light.turn_on:
                id: button_led
                brightness: 100%
            - delay: 10s
            - light.turn_off: button_led 

  - platform: template
    id: led_low
    lambda: |-
      if (id(test_fan).speed == 0){
        if(id(test_fan).state){
          return true;
        } else {
          return false;
        }
      } else {
        return false;
      }
    on_press:
      then:
        - light.turn_on:
            id: switch_led
            brightness: 30%
        - switch.turn_on:
            id: dim_led

  - platform: template
    id: led_mid
    lambda: |-
      if (id(test_fan).speed == 1){
        if(id(test_fan).state){
          return true;
        } else {
          return false;
        }
      } else {
        return false;
      }
    on_press:
      then:
        - light.turn_on:
            id: switch_led
            brightness: 60%
        - switch.turn_on:
            id: dim_led  

  - platform: template
    id: led_high
    lambda: |-
      if (id(test_fan).speed == 2){
        if(id(test_fan).state){
          return true;
        } else {
          return false;
        }
      } else {
        return false;
      }
    on_press:
      then:
        - light.turn_on:
            id: switch_led
            brightness: 100%
        - switch.turn_on:
            id: dim_led       

  - platform: template
    id: led_off
    lambda: |-
      if (id(test_fan).state){
        return false;
      } else {
        return true;
      }
    on_press:
      then:
        - light.turn_off:
            id: switch_led
        - switch.turn_off:
            id: dim_led
##---light switch button--------------------------
  - platform: gpio
    pin: D6
    id: light_button
    on_release:
      then:
        - switch.toggle: mancave_light
        - if:
            condition:
              - switch.is_on: mancave_light
            then:
              - light.turn_off: button_led

##---right rotation on the two-way switch---------
  - platform: gpio
    pin: D8
    id: up
    on_press:
      then:
        - lambda: |-
            if (id(test_fan).state) {
              if (id(test_fan).speed == 0) {
                id(fan_med).turn_on();
                ESP_LOGD("main", "Fan set to Medium");
              } else {
                if (id(test_fan).speed == 1){
                  id(fan_high).turn_on();
                  ESP_LOGD("main", "Fan set to High");
                } else {
                  id(fan_off).turn_on();
                  ESP_LOGD("main", "Fan set to Off");
                }
              }
            } else {
              id(fan_low).turn_on();
              ESP_LOGD("main", "Fan set to Low");
            }

  - platform: gpio
    pin: D7
    id: down  
    on_press:
      then:
        - lambda: |-
            if (id(test_fan).state) {
              if (id(test_fan).speed == 0) {
                id(fan_off).turn_on();
                ESP_LOGD("main", "Fan set to Off");
              } else {
                if (id(test_fan).speed == 1){
                  id(fan_low).turn_on();
                  ESP_LOGD("main", "Fan set to Low");
                } else {
                  id(fan_med).turn_on();
                  ESP_LOGD("main", "Fan set to Medium");
                }
              }
            } else {
              id(fan_high).turn_on();
              ESP_LOGD("main", "Fan set to High");
            }

fan:
  - platform: speed
    output: fanoutput
    id: test_fan
    name: "Test Fan"

output:
  - platform: custom
    type: float
    outputs:
      id: fanoutput
    lambda: |-
      auto test_fan = new FanOutput();
      App.register_component(test_fan);
      return {test_fan};
##-----ring light on two-way switch------    
  - platform: esp8266_pwm
    id: switch_light
    pin: D0

##-----ring light for light button-------
  - platform: esp8266_pwm
    id: button_light
    pin: D5      

switch:      
  - platform: gpio
    pin: D4
    id: relay1
    inverted: yes

  - platform: gpio
    pin: D3
    id: relay2
    inverted: yes

  - platform: gpio
    pin: D2
    id: relay3
    inverted: yes

  - platform: gpio
    pin: GPIO1
    id: mancave_light
    name: "Mancave light"
    inverted: yes
    restore_mode: "RESTORE_DEFAULT_OFF"

  - platform: template
    id: fan_off
    lambda: |-
      if (id(test_fan).state){
        return false;
      } else {
        return true;
      }
    turn_on_action:
      - fan.turn_off:
          id: test_fan

  - platform: template
    id: fan_low
    turn_on_action:
      - fan.turn_on:
          id: test_fan
          speed: LOW

  - platform: template
    id: fan_med
    turn_on_action:
      - fan.turn_on:
          id: test_fan
          speed: MEDIUM

  - platform: template
    id: fan_high
    turn_on_action:
      - fan.turn_on:
          id: test_fan
          speed: HIGH 

  - platform: template
    id: dim_led
    turn_on_action:
       - delay: 5s
       - if:
           condition:
             - switch.is_on: fan_off
           then:
             - light.turn_off:
                 id: switch_led
           else:
             - light.turn_on:
                 id: switch_led
                 brightness: 10%

light:
  - platform: monochromatic
    name: "Mancave fan switch led"
    id: switch_led
    output: switch_light    
    default_transition_length: 0.5s

  - platform: monochromatic
    name: "Mancave light switch led"
    id: button_led
    output: button_light    
    default_transition_length: 0.5s  
Apogee19832312 commented 4 years ago

image what it is connected to.

brandond commented 4 years ago

There's no way to get it to work without some additional code, that is. I ended up with a much shorter version of what you have, using an interval lambda that syncs the light value back to the fan speed.

Apogee19832312 commented 4 years ago

Oh, yeah at the moment you require the extra code. Mine would be a lot smaller if I didn't use the led ring as an indicator as what speed you are on.

One day there will be a solution without a custom component and life will be easy.

boelle commented 3 years ago

@michaelwoods did you ever figure something out?

@Apogee19832312 do you have a guide posted somewhere? i'm having the same problem: i need to convert a 3 speed fan (pull string) that also have a reverse switch. The light does not matter as i used an ikea bulp so that i can control allready

@OttoWinter are there something in the works so we dont have to add custom code etc?

snicker commented 3 years ago

would love to see a template fan integration as well. Just used a 4ch Sonoff hooked up to a floor fan to complete something similar to the example with a custom output but it was a little hairy.

nagyrobi commented 2 years ago

The problem with the custom output workaround is that there's no Lambda available to determine the current state of the fan if changed from elsewhere.

Say look at the modbus integration: speed of the fan can be controlled by a register value, which can be set with an output. But how do we read the register value first, when the node boots up? The fan is already running at speed 2...

bernikr commented 2 years ago

I just wanted to show my config because I have a similar fan to @Apogee19832312 where there is 3 different wires for 3 different speeds. But thanks to the Template Output component I managed to do this config without a custom component:

output:
  - platform: gpio
    id: fan_1
    pin: GPIO12
  - platform: gpio
    id: fan_2
    pin: GPIO5
  - platform: gpio
    id: fan_3
    pin: GPIO4

  - platform: template
    id: fan_template_output
    type: float
    write_action:
      - then:
        - output.turn_off: fan_1
        - output.turn_off: fan_2
        - output.turn_off: fan_3
        - lambda: |-
            if (state > 0.99) {
              id(fan_3).turn_on();
            } else if (state > 0.66) {
              id(fan_2).turn_on();
            } else if (state > 0) {
              id(fan_1).turn_on();
            }

fan:
  - platform: speed
    output: fan_template_output
    name: "Extraction Fan"
    speed_count: 3