jonathonlui / esphome-ikea-uppatvind

Modify Ikea UPPÅTVIND into "smart" purifier with ESPHome
60 stars 3 forks source link

Making the uppavind show as a fan #6

Open Mikey887 opened 1 year ago

Mikey887 commented 1 year ago

Hi, I have been trying to build on this code to make the purifier show as a fan type entity, I have got it so if you change with the manual button on the device or via the button exposed by the code in this repo it updates a fan entity correctly but getting the feedback from when you alter the fan in the home assistant ui is getting a bit tricky , I think I also may to implement the pwm fix in one of the other issues which may be tripping me up too.

Mikey887 commented 1 year ago
fan:
  - platform: speed
    output: my_output
    name: "Office Air Purifier"
    id: my_fan
    speed_count: 3
    on_speed_set:
      then:
        - if:
            condition:
              lambda: 'return ((id(my_fan).speed == 1) && (id(led1).state == 0));'
            then:
              - logger.log: "mismatch 1 0"
              - output.turn_on: output1
              - logger.log: "ammended"
        - if:
            condition:
              lambda: 'return ((id(my_fan).speed == 1) && (id(led1).state == 66));'
            then:
              - logger.log: "mismatch 1 66"
              - output.turn_on: output1
              - delay: 0.1s
              - output.turn_on: output1
              - delay: 0.1s
              - output.turn_on: output1
              - delay: 0.1s
              - logger.log: "ammended"
        - if:
            condition:
              lambda: 'return ((id(my_fan).speed == 1) && (id(led1).state == 100));'
            then:
              - logger.log: "mismatch 1 100"
              - output.turn_on: output1
              - delay: 0.1s
              - output.turn_on: output1
              - delay: 0.1s
              - logger.log: "ammended"
        - if:
            condition:
              lambda: 'return ((id(my_fan).speed == 2) && (id(led1).state == 33));'
            then:
              - logger.log: "mismatch 2 33"
              - output.turn_on: output1
              - logger.log: "ammended"
        - if:
            condition:
              lambda: 'return ((id(my_fan).speed == 2) && (id(led1).state == 100));'
            then:
              - logger.log: "mismatch 2 100"
              - output.turn_on: output1
              - delay: 0.1s
              - output.turn_on: output1
              - delay: 0.1s
              - output.turn_on: output1
              - delay: 0.1s
              - logger.log: "ammended"
        - if:
            condition:
              lambda: 'return ((id(my_fan).speed == 2) && (id(led1).state == 0));'
            then:
              - logger.log: "mismatch 2 0"
              - output.turn_on: output1
              - delay: 0.1s
              - output.turn_on: output1
              - delay: 0.1s
              - logger.log: "ammended"
        - if:
            condition:
              lambda: 'return ((id(my_fan).speed == 3) && (id(led1).state == 66));'
            then:
              - logger.log: "mismatch 3 66"
              - output.turn_on: output1
              - logger.log: "ammended"
        - if:
            condition:
              lambda: 'return ((id(my_fan).speed == 3) && (id(led1).state == 0));'
            then:
              - logger.log: "mismatch 3 0"
              - output.turn_on: output1
              - delay: 0.1s
              - output.turn_on: output1
              - delay: 0.1s
              - output.turn_on: output1
              - delay: 0.1s
              - logger.log: "ammended"
        - if:
            condition:
              lambda: 'return ((id(my_fan).speed == 3) && (id(led1).state == 33));'
            then:
              - logger.log: "mismatch 3 33"
              - output.turn_on: output1
              - delay: 0.1s
              - output.turn_on: output1
              - delay: 0.1s
              - logger.log: "ammended"
sensor:
  - platform: pulse_width
    pin: GPIO18
    id: led1
    name: UPPÅVIND Fan Speed LED
    update_interval: 0.1s
    accuracy_decimals: 0
    unit_of_measurement: ""
    icon: mdi:led-outline
    filters:
      - multiply: 1000000
      - lambda: !lambda |-
          switch (int(x)) {
            case 20:
              return 33;
            case 100:
              return 66;
            case 320:
              return 100;
            default:
              return 0;
            }
      - max:
          window_size: 3
          send_every: 3
          send_first_at: 1
      - or:
          - delta: 0.5
          - throttle: 60s
    on_value_range:
      - below: 1
        then:
          - fan.turn_off:
              id: my_fan
      - above: 5
        below: 40
        then:
          - fan.turn_on:
              id: my_fan
              speed: 1
      - above: 40
        below: 70
        then:
          - fan.turn_on:
              id: my_fan
              speed: 2
      - above: 70
        then:
          - fan.turn_on:
              id: my_fan
              speed: 3
output:
  - platform: gpio
    pin:
      number: GPIO5
      inverted: true  
      # "Open drain" used so the Wemos does't pull the pin 
      # up or down when it's not being pressed.
      mode: OUTPUT_OPEN_DRAIN
    id: output1
  - platform: template
    id: my_output
    type: float
    write_action:
      - if:
          condition:
            lambda: return (state = 99999);
          then:
            - output.turn_off: output1
button:
  - platform: output
    name: "UPPÅTVIND Button"
    output: output1
    duration: 100ms
    id: button_press
antonio1475 commented 1 year ago

This doesn't really work for me. I don't know if it's because it's unfinished due to what you mentioned in the OP: It mostly works to show the status (off/low/med/high) in the Fan UI based on the led readings after pressing the button via UI/finger, but selecting a speed does nothing or does crazy things (random speeds, turns off, etc.).

I guess it goes into a loop?

antonio1475 commented 1 year ago

OK it seems I made progress, although I am ashamed of how many hours this took me...

I prioritised the change from the fan UI to then press the button to get there (press, wait 2 seconds for LED status, see if needs to press again to get to target), and ultimately sync the statuses after 10s.

If the button is pressed manually, it takes those 10 seconds to update the fan status. Otherwise, changing the fan status/speed in the UI will cause a loop.

This is of course a workaround and there's probably a right way to do this, but my ESPHome knowledge is mostly zero so this was a lot of trial and error and google...

Problem: the led doesn't report 0 many times when off, so sometimes it takes a few loops to get to 0 :cry:

removed. see next comments
antonio1475 commented 1 year ago

It seems I got it! @Mikey887 🥳 Here's my proposal based on yours (I don't understand certain things like the my_output part you added but it seems necessary).

Seems to work great. It took some thinking to make the bi-directional sync work...

It relies on the PWM monitoring explained in the fan speed detection thread, but it should work with the LED fan speed (adjust the sensor section), provided that it reports the speed correctly...

I mostly moved the actions and sync to scripts to allow for mode: restart which is crucial to give it time to stabilize, then sync, and avoid a loop.

image

Here's my full code (including other improvements like Power Off button, Restart button):

# *** THIS FIRST SECTION IS INCLUDED PURELY FOR REFERENCE. USE YOURS! ***
esphome:
  name: purificador-habitacion
  friendly_name: Purificador Habitación

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

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

  ap:
    ssid: "Purificador Habitacion"
    password: "antonio1098"

#captive_portal:

# web_server:
#   local: true

# esp32_ble_tracker:
#   scan_parameters:
#     active: false

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

ota:
  password: "xxx"

# *** HERE THE ACTUAL CODE STARTS ***

output:
  - platform: gpio
    pin:
      number: GPIO18
      inverted: true  
      mode: OUTPUT_OPEN_DRAIN # "Open drain" used so the Wemos does't pull the pin up or down when it's not being pressed.
    id: output1
  - platform: template
    id: my_output
    type: float
    write_action:
      - if:
          condition:
            lambda: return (state = 99999);
          then:
            - output.turn_off: output1

button:
  - platform: output
    name: "Increase speed"
    output: output1
    duration: 100ms
    id: button_press
    icon: mdi:fan-chevron-up
  - platform: output
    name: "Power Off"
    output: output1
    duration: 2s
    id: power_off
    icon: mdi:fan-off
  - platform: restart
    name: "Purifier Board Restart"
    id: reboot
    icon: mdi:restart

sensor:
  - platform: pulse_counter
    pin: 21
    name: "Fan Speed PWM"
    update_interval: 0.5s
    accuracy_decimals: 0
    unit_of_measurement: ''
    icon: mdi:speedometer
    id: pwm
    filters:
      - lambda: !lambda |- 
          int poff = 0;
          int y = int(x);
          if (y < 100) {
            return 0;
          } else if (y <7700) {
            return 1;
          } else if (y <12100) {
            return 2;
          } else if (y < 17000) {
            return 3;
          } else {
            return 0;
          }
      - median:
          window_size: 3
          send_every: 3
          send_first_at: 1
      - delta: 0.5
    on_value:
      script.execute: sync_fan  # see script below

fan:
  - platform: speed
    name: "Air Purifier"
    icon: mdi:air-purifier
    id: my_fan
    speed_count: 3
    output: my_output
    on_turn_off:
      if:
        condition:
          fan.is_off: my_fan
        then:
          button.press: power_off
    on_speed_set:
      if:
        condition:
          fan.is_on: my_fan
        then:
          script.execute: set_speed  # see script below

script:
  - id: set_speed
    mode: restart # handle the accidental presses or quick changes of mind
    then:
      - if:
          condition:
            lambda: return (id(my_fan).speed > id(pwm).state);
          then:
            - repeat:
                count: !lambda 'return (id(my_fan).speed - id(pwm).state);'
                then:
                  - button.press: button_press
                  - delay: 1s
      - if:
          condition:
            lambda: return (id(my_fan).speed < id(pwm).state);
          then:
            - button.press: power_off
            - delay: 2.2s  # allow the 2s of the power_off button to finish
            - repeat:
                count: !lambda 'return (id(my_fan).speed - id(pwm).state);'
                then:
                  - button.press: button_press
                  - delay: 1s
  - id: sync_fan  # sync the real-life status of the purifier back to the ESP/HA fan. This both confirms that all changes were executed (provides the real status) and also syncs the button presses done by your finger in HA or real life.
    mode: restart     # see comment below as to why restart
    then:
      - delay: 5s # allow the PWM speed detection to stabilize and only act after that. This way it skips the transition speeds.
      - if:
          condition:
            lambda: 'return id(pwm).state == 0;'
          then:
            - fan.turn_off:
                id: my_fan
          else:
            - fan.turn_on:
                id: my_fan
                speed: !lambda return id(pwm).state;

@jonathonlui can you review the code and give your opinion? I did it all via googling and trial&error, so I might have done things improperly or inneficiently, especially the filters and the scripts. I also don't know how to create PRs so feel absolutely free to take these ideas and add them to your project if you wish! I'd be honored 😄

antonio1475 commented 1 year ago

Re-uploaded the code in previous comment (removed the throttle which caused issues) and changed the script section, to make it more stable and not cycle through high speeds if eg. it's in speed 2 and want 1. In this scenario, turn off, then press as button X times, without needing to wait and check the current real speed, which wasn't robust. Requires power_off button (which is there)

This should now be more robust, and also snappier. Note that I've been very conservartive with all the update_intervals, delays, etc, as I believe that for this device precision and stability of the changes is better than speed. They could be adjusted.

nicopasla commented 1 year ago

Last yaml work perfectly for me, you can also add the Change filter LED and do a sensor for it

hex commented 10 months ago

@antonio1475 how did you retrieve the PWM? What TP did you use on the UPPÅTVIND board?

antonio1475 commented 10 months ago

@antonio1475 how did you retrieve the PWM? What TP did you use on the UPPÅTVIND board?

Hi. It's all explained between my posts here and in the other issue (first link in my long comment above). As I mention there, it's not a TP but the 4th pin in the connector (2nd from the top) that is unused in the original procedure, which allows you to insert a cable in that pin, and take it to a pin in the ESP board. jonathonlui guessed that it was PWM sensor and he was right.

hex commented 10 months ago

@antonio1475 how did you retrieve the PWM? What TP did you use on the UPPÅTVIND board?

Hi. It's all explained between my posts here and in the other issue (first link in my long comment above). As I mention there, it's not a TP but the 4th pin in the connector (2nd from the top) that is unused in the original procedure, which allows you to insert a cable in that pin, and take it to a pin in the ESP board. jonathonlui guessed that it was PWM sensor and he was right.

I managed to use TP9 to get the PWM, I didn't quite get how to use the 4th pin when the connector is already occupied.

FullSizeRender

antonio1475 commented 10 months ago

I managed to use TP9 to get the PWM, I didn't quite get how to use the 4th pin when the connector is already occupied.

Cool, thanks for sharing @hex! More consistent with the rest of the setup.

Regarding using the connection pin, since the Dupont cables are very thin (not sure if you're using those or thicker cables), I just peeled one side and stuck it around the metal connector (which I think I pulled out from the white plastic to do this). Sorry that I don't have a picture, but the end result was something like this (note: not sure if I used the right connector in this painting!) image

I did this because I didn't have a soldering iron at the time that I did this second part regarding PWM, plus I didn't know about TP9.

galant-ho commented 2 months ago

Late gamer here! Managed to soldered the wires to the pins on my ESP8266MOD (di_mini). To get the PWM to the board, which pin on my ESP8266 should that be soldered on? Or it has to be a ESP32 to connect with PWM?

EnricoBettella commented 2 months ago

Late gamer here! Managed to soldered the wires to the pins on my ESP8266MOD (di_mini). To get the PWM to the board, which pin on my ESP8266 should that be soldered on? Or it has to be a ESP32 to connect with PWM?

Late gamer here too: I've just managed to get my wemos D1 mini (esp8266) working by connecting the PWM signal coming from the connector to the D7 pin. Seems there's no need for ESP32 for the pulse_counter component.