Blinds (Shades) Custom Cover

Hello,

I’ll be grateful for assistance please.

I’ve created a custom cover for my blinds (shades) so that the slider is consistent with % closed (open is 0.0 0% and closed is 1.0 100%) and not % open as the cover component is currently (open is 1.0 100% and closed is 0.0 0%)

The custom cover works correctly when the stepper motor is anywhere in the 10% to 90% range (ie not at the endpoints) clicking the up arrow button decreases the % closed and clicking the down arrow button, % closed increases and the slider and ESPHome log follow correctly. However when the blinds are at the end points (0% and 100%) the incorrect indicator is shown (blinds open and closed icon) and the incorrect arrow button is enabled. At 0% the up arrow button is enabled and the stepper motor doesn’t move as it’s already at 0% and similarly at 100%.

How do I reverse the up/down buttons in the custom cover card as well as the open/closed indicator icon?

Here’s my test .yaml

substitutions:
  devicename: roller_blind
  upper_devicename: Roller Blind
  mystepper: my_stepper # Name of the stepper motor (leave as is)
  speed: 5000 steps/s # Set the speed of the motor
  acc: inf #acceleration steps/s^2
  dec: 2000 steps/s^2 #decceleration steps/s^2
  dir: GPIO18 # Direction Pin
  step: GPIO4 # Step Pin
  sleep: GPIO23 # Sleep Pin
  switch_pin: GPIO25 # Switch Pin
  ssid: *********
  password: *********
  ap_ssid: Blind Single Fallback Hotspot
  ap_password: KuLimqA7M2OQ

esphome:
  name: blind_single
  platform: ESP32
  board: esp32dev
  
wifi:
  ssid: ${ssid}
  password: ${password}

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: ${ap_ssid}
    password: ${ap_password}

web_server:
  port: 80
  
stepper:
  - platform: a4988
    id: $mystepper
    step_pin: $step
    dir_pin: $dir
    sleep_pin: 
     number: $sleep
     inverted: True
    max_speed: ${speed}
    acceleration: ${acc}
    deceleration: ${dec}

globals:
  - id: ${mystepper}_global 
    type: int
    restore_value: no
    initial_value: '0'

  - id: closed_position # Variable for storing end position
    type: int
    restore_value: no
    initial_value: '10000'
    
  - id: open_position # Variable for storing open position
    type: int
    restore_value: no
    initial_value: '0'  

sensor:
  - platform: template
    name: $upper_devicename Blind Position
    id: blind_position
    lambda: !lambda 'return (float(id($mystepper).current_position) /(id(closed_position) - (id(open_position))));'
    internal: true
    update_interval: 100ms
    filters:
      - or:
        - throttle: 5s
        - delta: 5.0
    on_value:
      then:
        - cover.template.publish:
            id: blind
            state: !lambda 'return x;'  
            
cover:
  - platform: template
    device_class: shade
    name: $upper_devicename
    id: blind
    has_position: true
    open_action:
      - stepper.set_target:
          id: $mystepper
          target: !lambda 'return id(open_position);'

    close_action:
      - stepper.set_target:
          id: $mystepper
          target: !lambda 'return id(closed_position);'
    
    stop_action:
      - stepper.set_target:
          id: $mystepper
          target: !lambda 'return id($mystepper).current_position;'
    
    position_action:
      - stepper.set_target:
          id: $mystepper
          target: !lambda 'return (1.0f-pos) * id(closed_position);'

captive_portal:

# Enable logging
logger:
 
# Enable Home Assistant API
api:
  
ota:

and custom cover, the 3 lines which I have changed are annotated with // original:…

#include "cover.h"
#include "esphome/core/log.h"

namespace esphome {
namespace cover {

static const char *TAG = "cover";

const float COVER_OPEN = 0.0f;  // original: 1.0f
const float COVER_CLOSED = 1.0f; // original: 0.0f

const char *cover_command_to_str(float pos) {
  if (pos == COVER_OPEN) {
    return "OPEN";
  } else if (pos == COVER_CLOSED) {
    return "CLOSED";
  } else {
    return "UNKNOWN";
  }
}
const char *cover_operation_to_str(CoverOperation op) {
  switch (op) {
    case COVER_OPERATION_IDLE:
      return "IDLE";
    case COVER_OPERATION_OPENING:
      return "OPENING";
    case COVER_OPERATION_CLOSING:
      return "CLOSING";
    default:
      return "UNKNOWN";
  }
}

Cover::Cover(const std::string &name) : Nameable(name), position{COVER_OPEN} {}

uint32_t Cover::hash_base() { return 1727367479UL; }

CoverCall::CoverCall(Cover *parent) : parent_(parent) {}
CoverCall &CoverCall::set_command(const char *command) {
  if (strcasecmp(command, "OPEN") == 0) {
    this->set_command_open();
  } else if (strcasecmp(command, "CLOSE") == 0) {
    this->set_command_close();
  } else if (strcasecmp(command, "STOP") == 0) {
    this->set_command_stop();
  } else {
    ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command);
  }
  return *this;
}
CoverCall &CoverCall::set_command_open() {
  this->position_ = COVER_OPEN;
  return *this;
}
CoverCall &CoverCall::set_command_close() {
  this->position_ = COVER_CLOSED;
  return *this;
}
CoverCall &CoverCall::set_command_stop() {
  this->stop_ = true;
  return *this;
}
CoverCall &CoverCall::set_position(float position) {
  this->position_ = 1.0f - position; //original: this->position_ = position;
  return *this;
}
CoverCall &CoverCall::set_tilt(float tilt) {
  this->tilt_ = tilt;
  return *this;
}
void CoverCall::perform() {
  ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
  auto traits = this->parent_->get_traits();
  this->validate_();
  if (this->stop_) {
    ESP_LOGD(TAG, "  Command: STOP");
  }
  if (this->position_.has_value()) {
    if (traits.get_supports_position()) {
      ESP_LOGD(TAG, "  Position: %.0f%%", *this->position_ * 100.0f);
    } else {
      ESP_LOGD(TAG, "  Command: %s", cover_command_to_str(*this->position_));
    }
  }
  if (this->tilt_.has_value()) {
    ESP_LOGD(TAG, "  Tilt: %.0f%%", *this->tilt_ * 100.0f);
  }
  this->parent_->control(*this);
}
const optional<float> &CoverCall::get_position() const { return this->position_; }
const optional<float> &CoverCall::get_tilt() const { return this->tilt_; }
void CoverCall::validate_() {
  auto traits = this->parent_->get_traits();
  if (this->position_.has_value()) {
      auto pos = *this->position_;
    if (!traits.get_supports_position() && pos != COVER_OPEN && pos != COVER_CLOSED) {
      ESP_LOGW(TAG, "'%s' - This cover device does not support setting position!", this->parent_->get_name().c_str());
      this->position_.reset();
    } else if (pos < 0.0f || pos > 1.0f) {
      ESP_LOGW(TAG, "'%s' - Position %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), pos);
      this->position_ = clamp(pos, 0.0f, 1.0f);
    }
  }
  if (this->tilt_.has_value()) {
    auto tilt = *this->tilt_;
    if (!traits.get_supports_tilt()) {
      ESP_LOGW(TAG, "'%s' - This cover device does not support tilt!", this->parent_->get_name().c_str());
      this->tilt_.reset();
    } else if (tilt < 0.0f || tilt > 1.0f) {
      ESP_LOGW(TAG, "'%s' - Tilt %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), tilt);
      this->tilt_ = clamp(tilt, 0.0f, 1.0f);
    }
  }
  if (this->stop_) {
    if (this->position_.has_value()) {
      ESP_LOGW(TAG, "Cannot set position when stopping a cover!");
      this->position_.reset();
    }
    if (this->tilt_.has_value()) {
      ESP_LOGW(TAG, "Cannot set tilt when stopping a cover!");
      this->tilt_.reset();
    }
  }
}
CoverCall &CoverCall::set_stop(bool stop) {
  this->stop_ = stop;
  return *this;
}
bool CoverCall::get_stop() const { return this->stop_; }
void Cover::set_device_class(const std::string &device_class) { this->device_class_override_ = device_class; }
CoverCall Cover::make_call() { return {this}; }
void Cover::open() {
  auto call = this->make_call();
  call.set_command_open();
  call.perform();
}
void Cover::close() {
  auto call = this->make_call();
  call.set_command_close();
  call.perform();
}
void Cover::stop() {
  auto call = this->make_call();
  call.set_command_stop();
  call.perform();
}
void Cover::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); }
void Cover::publish_state(bool save) {
  this->position = clamp(this->position, 0.0f, 1.0f);
  this->tilt = clamp(this->tilt, 0.0f, 1.0f);

  ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str());
  auto traits = this->get_traits();
  if (traits.get_supports_position()) {
    ESP_LOGD(TAG, "  Position: %.0f%%", this->position * 100.0f);
  } else {
    if (this->position == COVER_OPEN) {
      ESP_LOGD(TAG, "  State: OPEN");
    } else if (this->position == COVER_CLOSED) {
      ESP_LOGD(TAG, "  State: CLOSED");
    } else {
      ESP_LOGD(TAG, "  State: UNKNOWN");
    }
  }
  if (traits.get_supports_tilt()) {
    ESP_LOGD(TAG, "  Tilt: %.0f%%", this->tilt * 100.0f);
  }
  ESP_LOGD(TAG, "  Current Operation: %s", cover_operation_to_str(this->current_operation));

  this->state_callback_.call();

  if (save) {
    CoverRestoreState restore{};
    memset(&restore, 0, sizeof(restore));
    restore.position = this->position;
    if (traits.get_supports_tilt()) {
      restore.tilt = this->tilt;
    }
    this->rtc_.save(&restore);
  }
}
optional<CoverRestoreState> Cover::restore_state_() {
  this->rtc_ = global_preferences.make_preference<CoverRestoreState>(this->get_object_id_hash());
  CoverRestoreState recovered{};
  if (!this->rtc_.load(&recovered))
    return {};
  return recovered;
}
Cover::Cover() : Cover("") {}
std::string Cover::get_device_class() {
  if (this->device_class_override_.has_value())
    return *this->device_class_override_;
  return this->device_class();
}
bool Cover::is_fully_open() const { return this->position == COVER_OPEN; }
bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; }
std::string Cover::device_class() { return ""; }

CoverCall CoverRestoreState::to_call(Cover *cover) {
  auto call = cover->make_call();
  auto traits = cover->get_traits();
  call.set_position(this->position);
  if (traits.get_supports_tilt())
    call.set_tilt(this->tilt);
  return call;
}
void CoverRestoreState::apply(Cover *cover) {
  cover->position = this->position;
  cover->tilt = this->tilt;
  cover->publish_state();
}

}  // namespace cover
}  // namespace esphome

I’ll be grateful for any help or suggestions please.

Thanks in advance

Hello,

I’ve spend several day trying to solve this and I apologise if I’m missing something obvious, I still new to Home Assistant and may be using incorrect terms.

The default cover component is

Open -> Closed
100% -> 0%

and what I like to achieve is

Open -> Closed
0%   -> 100%

Any help or suggestions will be very much appreciated please.

Hi there, it seems this is a feature that cannot be attained with the current design of the Cover Template. I currently have a working template that inverts the open/close state of the Cover, but it doesn’t dynamically get the position state from the Blinds hardware, since using the position_template attribute from the Cover Template in unison with the value_template attribute will overwrite the state behavior of the value_template and instead base it on position again making 0=closed and 100=open. But if you manually set a position, my template will store it and go to that position. In case you’re interested I may have posted it in my topic listed below.

I’ve also made a feature request for this behavior, please upvote it here so the devs might make the requested changes to the Cover Template: