Sliding window opener

I am trying to keep my garage as cool as possible in summer. I have an automated extraction fan. It helps, but not as much as I would like it to. The next step is to open a window while the fan is running. It is a sliding window, by the way.

So, I made a prototype window opener. Here’s what I wanted:

  • Easy to install, easy to remove
  • No drilling holes (new windows!)
  • Reasonably low force (none of these finger-cutting, arm-breaking, leg-bruising 6kN actuators!)
  • 100mm/4" travel - that’s how far I can open my window while keeping the anti burglary lock engaged

Here’s what I ended up with:

Basically, a small linear actuator, attached to window glass with suction cups. Logic is provided by ESP32 running EPSHome, and muscle by a L298N motor driver module.

Shopping list:
ESP32 ($10.99): https://www.amazon.com/dp/B0718T232Z
Motor drivers ($8.69 for two): https://www.amazon.com/dp/B01M29YK5U
Actuator, ($33.05): https://www.ebay.com/itm/233704416227
Suction cups ($18.89 for four) : https://www.amazon.com/dp/B08N13T8X2

Also needed are jumper wires and 12V power supply - I had these. Oh, and a small piece of silicone tubing to keep the actuator centered in the scution cups.

Yeah, the electronics I ended up with are quite similar to what @DrZzs and @Mahko_Mahko used. Great minds, etc., etc.

The actuator has some sort of overload protection built in. This makes open loop control possible - I just drive it for a predetermined period of time and then stop. If it hits the limit earlier (as it usually does), it will just turn off by itself.

The next step will be to design a small motor controller board with a socket for the ESP32. What I have now looks too much like a science fair project for my liking.

If anyone wants to play with a similar project, I’ll be happy to share my code and wiring diagram.

6 Likes

Love it!

For version two of my Cranky I went with the mini L298N and a wemos d1 mini to help keep overall size down…

Are the suction caps holding on ok?

To let more warm air in?

Too bulky in my opinion.

Yeah but it is a garage. I agree I wouldn’t have it in a living area.

Well I would, my wife wouldn’t.

The suction caps seem to be doing their job. As a matter of fact, they are pretty difficult to detach, even with the latches off.
Of course, I have only installed them two days ago. Who knows what happens after several months in sunlight.

1 Like

To let cooler air in. The whole thing - fans and window - is controlled by HA based on the temperature difference between my garage and outdoors. In the morning, when it gets warm, the window will close and fans will turn off.

In a living area I would have a very different set of criteria. Yes, aesthetics would be pretty high on the list. But I would also want to be able to open the full width of the window, and some means of opening the window - quickly! - manually would be needed. Emergency egress and all that…
Honestly, I am not sure how I would approach this.

I’ve been working on a whole bunch of rules for whether the windows should be open or not. They are “kind of working”…

Internal/external absolute thresholds and differences in temp/humidity, forecasted high for the day, whether external temp is rising quickly, a rain sensor, a dev pollen sensor, I’ve got a CO2 sensor on the way. I tinkered with “feels like” temps too.

By default I want the windows open and I get an announcement when my rules close them and why.

Bit out of control but it’s getting there.

OK, you got me interested. Why do you care about rate of change and forecast?

Yeah still figuring that out myself:)

In my mind it’s a “conservation mode” that banks thermal mass for use later…

If it’s cold(ish) inside, but going to be hot, I’d prob want to keep windows closed to “bank” that cooling mass to offset later heat, rather than open the place and warm it now, and for it to heat up even more later.

Similarly if outside heat is really starting to ramp up, I’d prob keep windows closed even if it’s cold inside - be “outside of range now” to help passively control temperature “later”.

Seems like reasonable logic to me but happy to have it explained otherwise:).

OK, I think I get it. It is basically an optimization exercise with several contradictory goals. Something like “I want the windows to be open as much as possible, but I do not want the home to get warmer than XYZ.” A weather forecast can help with decision making here.

1 Like

Yep. And rules change depending on whether I’m home or not.

If I’m away I’d still like to aerate the place and don’t really care so much about temp. But I do still care about making the place damp if it’s too humid outside etc . And prob don’t want pollen coming in.

How is your suction cup setup holding up?

Thinking of doing something similar

Funny you should ask. At this very moment my 3D printer is working on a replacement for the suction cup. It will be something very similar in shape, but instead of suction, it will be attached to glass with hot glue.

The suction cup I have works fine - for a couple of weeks. Then it falls off. Then I reattach it and get another couple of weeks, and so on. I guess I lost patience and decided to try an alternative.

How is hot glue working out so far?

So far so good, but it has not even been two weeks since I installed it.

Could you please post your yaml code? I would like to replicate some of your setup for opening the outdoor aviary for my birds with a linear actuator and a wemos d1 mini esp32

opener003.yaml

esphome:
  name: opener003
  friendly_name: opener003
  libraries:
    - SPI  
  includes:
  - WindowOpener.h

esp32:
  board: esp32dev


switch:

  - platform: custom
    lambda: |-
      auto my_window_opener = new MyWindowOpener();
      App.register_component(my_window_opener);
      return {my_window_opener};
  
    switches:
      name: "WindowSwitch_003"
      id: window_switch_003

# Enable logging
logger:

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

ota:
  password: "**********************"

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


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

captive_portal:
    

WindowOpener.h (The current version uses a different motor driver IC than what I described in the original post, but you should get the general idea)

#include "esphome.h"
#include "SPI.h"

using namespace esphome;


#define DIR 17
#define PWM 16
#define DIS 4
#define nFAULT 0
#define nSLEEP 21



#define SPICLK 1000000


#define WRITE 0x40
#define READ 0x00
#define DEVICE_ID 0

#define MOTOR_CURRENT 34



#define REG_DEVICE_ID 0x00
#define REG_FAULT_SUMMARY 0x01
#define REG_STATUS1 0x02
#define REG_STATUS2 0x03
#define REG_COMMAND 0x08
#define REG_SPI_IN 0x09
#define REG_CONFIG1 0x0A
#define REG_CONFIG2 0x0B
#define REG_CONFIG3 0x0C
#define REG_CONFIG4 0x0D


#define CMD_CLR_FLT 0x80


#define BUTTON_OPEN 2
#define BUTTON_CLOSE 27



// Custom binary output, for exposing binary states
class MyWindowOpener:public Component, public Switch {
 public:

void writeRegister( uint8_t addr, uint8_t value )
{
  uint16_t valueToWrite = (addr & 0x3f );
  valueToWrite <<= 8;
  valueToWrite |= value;

  digitalWrite(SS, LOW); // Set Slave Select pin LOW to start SPI communication
  delay(1);
  SPI.transfer16(valueToWrite); // Send and receive 16-bit word
  delay(1);
  digitalWrite(SS, HIGH); // Set Slave Select pin HIGH to end SPI communication
}

uint16_t readRegister( uint8_t addr )
{
  uint16_t returnValue = 0;
  uint16_t valueToWrite = (addr & 0x3f ) || READ;
  valueToWrite <<= 8;
  digitalWrite(SS, LOW); // Set Slave Select pin LOW to start SPI communication
  delay(1);
  returnValue = SPI.transfer16(valueToWrite); // Send and receive 16-bit word
  delay(1);
  digitalWrite(SS, HIGH); // Set Slave Select pin HIGH to end SPI communication

  return returnValue;
}





  void setup() override {
const int pwmFrequency = 10000; // PWM frequency in Hz
const int pwmResolution = 8; // PWM resolution in bits (1 to 16 bits)

pinMode(2, OUTPUT);
pinMode(DIR, OUTPUT);
pinMode(nSLEEP, OUTPUT);
pinMode(DIS, OUTPUT);
pinMode(nFAULT, INPUT);
pinMode(MOTOR_CURRENT, ANALOG);

pinMode(BUTTON_CLOSE, INPUT);
pinMode(BUTTON_OPEN, INPUT);



ledcSetup(0, pwmFrequency, pwmResolution);
ledcAttachPin(PWM, 0);


digitalWrite( 2, 1 );
digitalWrite( DIR, 1 );
ledcWrite(0, 255);
digitalWrite( nSLEEP, 0 );
digitalWrite( DIS, 0 );


  pinMode(SCK, OUTPUT);
  pinMode(MISO, INPUT);
  pinMode(MOSI, OUTPUT);
  pinMode(SS, OUTPUT);
  SPI.begin(SCK, MISO, MOSI, SS);
  SPI.setBitOrder(MSBFIRST); // Set bit order: Most Significant Bit first
  SPI.setDataMode(SPI_MODE1); // Set data mode: SPI_MODE0, SPI_MODE1, SPI_MODE2, or SPI_MODE3
  SPI.setClockDivider(SPI_CLOCK_DIV128); // Set clock speed: SPI_CLOCK_DIV2 to SPI_CLOCK_DIV128
  SPI.setFrequency(100000);


delay(100);

int fault = digitalRead( nFAULT );
digitalWrite( nSLEEP, 1 );
while( fault == 1 )
{
  fault = digitalRead( nFAULT );
}

 writeRegister(REG_COMMAND, CMD_CLR_FLT );




    myStateKnown = false;
    stopClosingAt = millis();
    stopOpeningAt = millis();
    firstLoop = 1;

  }

  void write_state(bool state) override 
  {
    
    digitalWrite( 2, state );

   // if( !myStateKnown || ( myState != state ) )
    {
      if( state )
      {
        openDelay = initialDelay;
        closeDelay = 0;        
      }
      else
      {
        openDelay = 0;
        closeDelay = initialDelay * 1.5;        
      }

      ESP_LOGD( "DEBUG", "close: %d", closeDelay );  
      ESP_LOGD( "DEBUG", "open: %d", openDelay );  


      myState = state;
      myStateKnown = true;
    }

      // Acknowledge new state by publishing it
    publish_state(state);
  }

typedef enum
{
  OPEN, CLOSE, OFF
} BridgeState;

void setHBridge( BridgeState state )
{
  if( state == OPEN )
  {
    digitalWrite( DIR, 0 );
    ledcWrite(0, 255 ) ;
  }
  else if( state == CLOSE )
  {
    digitalWrite( DIR, 1 );
    ledcWrite(0, 255 ) ;
    //openDelay--;
  }
  else
  {
    digitalWrite( DIR, 0 );
    ledcWrite(0, 0 ) ;
  }

}



void loop() override 
  {
    // This will be called by App.loop()
    //ESP_LOGD( "DEBUG", "first loop: %d", firstLoop );
    unsigned long timeElapsed = 0;

    if( firstLoop )
    {
      //firstLoop = 0;
      firstLoop--;
      lastTime = millis();
    }
    else
    {
      publish_state(state);
//      ESP_LOGD( "DEBUG", "publish %d", state);
      timeElapsed = millis() - lastTime;
      lastTime = millis();

      if( digitalRead( BUTTON_CLOSE ) == 0 )
      {
        write_state( false );
      }

      if( digitalRead( BUTTON_OPEN ) == 0 )
      {
        write_state( true );
      }


      if( closeDelay > 0 )
      {
        //digitalWrite( 2, (millis()/250) % 2 );

        setHBridge( CLOSE );
        closeDelay -= timeElapsed;
  //      ESP_LOGD( "DEBUG", "close: %d", closeDelay );  
  //      ESP_LOGD( "DEBUG", "open: %d", openDelay );  

      }
      else if( openDelay > 0 )
      {
        setHBridge( OPEN );
        openDelay -= timeElapsed;
  //      ESP_LOGD( "DEBUG", "close: %d", closeDelay );  
  //      ESP_LOGD( "DEBUG", "open: %d", openDelay );  
      }
      else
      {
        setHBridge( OFF );
      }
    }
  }

protected:
  static const int initialDelay = 18 * 1000;

  int openDelay;
  int closeDelay;

  unsigned long stopClosingAt;
  unsigned long stopOpeningAt;

  unsigned long lastTime;


  int firstLoop;

  int reasonableActuatorTime;

  bool myState;
  bool myStateKnown;

  const int LEFT = 1;
  const int RIGHT = 3;


};