Sonoff Dual Window Cover with set position

Hello,

I worked on the Sonoff Dual that I installed the firmware Sonoff-Tasmota. I’ve integrated it with Home-Assistant using MQTT commands.
You can set a Window Cover position from the Window Set Position which is converted from 0-100 to 0-16. I’ve set the timer 2 seconds more (18 sec) in order to go fully down or up.
The Window Position is the current position of the Window Cover that is changed with the MQTT Commands (ON/OFF). If all automations are working correctly, you don’t have to change this bar.

This is the result:
image

To make it work I had to count the time it takes my window cover to go up/down. For me, it was 18 seconds, so this is the configuration on Home Assistant:

Basic Config


configuration.yaml

timer:
  window_1_up:
    duration: '00:00:18'
  window_1_down:
    duration: '00:00:18'

input_number:
  window_1_position:
    name: "Window Position"
    min: 0
    max: 100
  window_1_set_position:
    name: "Window Set Position"
    min: 0
    max: 100


cover:
  - platform: template
    covers:
      window_1_cover:
        friendly_name: "Window 1 Cover"
        position_template: "{{ (states.input_number.window_1_position.state | int) }}"
        open_cover:
          - service: mqtt.publish
            data:
              topic: 'cmnd/window_1/power2'
              payload: 'OFF'
          - service: mqtt.publish
            data:
              topic: 'cmnd/window_1/power1'
              payload: 'ON'
        close_cover:
          - service: mqtt.publish
            data:
              topic: 'cmnd/window_1/power1'
              payload: 'OFF'
          - service: mqtt.publish
            data:
              topic: 'cmnd/window_1/power2'
              payload: 'ON'
        stop_cover:
          - service: mqtt.publish
            data:
              topic: 'cmnd/window_1/power1'
              payload: 'OFF'
          - service: mqtt.publish
            data:
              topic: 'cmnd/window_1/power2'
              payload: 'OFF' 

automations.yaml

- id: '301'
  alias: window_1 - MQTT Button1 OFF (up)
  trigger:
  - payload: 'OFF'
    platform: mqtt
    topic: stat/window_1/POWER1
  action:
  - service: timer.cancel
    entity_id: timer.window_1_up
  - service: input_number.set_value
    data_template:
      entity_id: input_number.window_1_set_position
      value: >-
          {{ states.input_number.window_1_position.state }}
- id: '302'
  alias: window_1 - MQTT Button2 OFF (down)
  trigger:
  - payload: 'OFF'
    platform: mqtt
    topic: stat/window_1/POWER2
  action:
  - service: timer.cancel
    entity_id: timer.window_1_down
  - service: input_number.set_value
    data_template:
      entity_id: input_number.window_1_set_position
      value: >-
          {{ states.input_number.window_1_position.state }}
- id: '303'
  alias: window_1 - Timer stopped
  trigger:
  - platform: event
    event_type: timer.finished
    event_data:
      entity_id: timer.window_1_up
  - platform: event
    event_type: timer.finished
    event_data:
      entity_id: timer.window_1_down
  action:
  - service: cover.stop_cover
    data:
      entity_id: cover.window_1_cover
- id: '311'
  alias: window_1 - MQTT Button1 ON (up)
  trigger:
  - payload: 'ON'
    platform: mqtt
    topic: stat/window_1/POWER1
  condition:
    condition: template
    value_template: '{{ as_timestamp(now()) - as_timestamp(states.input_number.window_1_set_position.last_updated) > 3 }}'
  action:
  - service: timer.start
    data:
      duration: 00:00:16
    entity_id: timer.window_1_up
- id: '312'
  alias: window_1 - MQTT Button2 ON (down)
  trigger:
  - payload: 'ON'
    platform: mqtt
    topic: stat/window_1/POWER2
  condition:
    condition: template
    value_template: '{{ as_timestamp(now()) - as_timestamp(states.input_number.window_1_set_position.last_updated) > 3 }}'
  action:
  - service: timer.start
    data:
      duration: 00:00:16
    entity_id: timer.window_1_down
- id: '411'
  alias: window_1 - Position UP
  trigger:
    platform: time_pattern
    seconds: '/1'
  condition:
    condition: state
    entity_id: timer.window_1_up
    state: active
  action:
  - service: input_number.set_value
    data_template:
      entity_id: input_number.window_1_position
      value: >-
          {% set step=100/16 %}
          {% if (states.input_number.window_1_position.state | float + step) < 100 %}
            {{ states.input_number.window_1_position.state | float + step | int }}
          {% else %}
            100
          {% endif %}
- id: '412'
  alias: window_1 - Position Down
  trigger:
    platform: time_pattern
    seconds: '/1'
  condition:
    condition: state
    entity_id: timer.window_1_down
    state: active
  action:
  - service: input_number.set_value
    data_template:
      entity_id: input_number.window_1_position
      value: >-
          {% set step=100/16 %}
          {% if (states.input_number.window_1_position.state | float - step) > 0 %}
            {{ states.input_number.window_1_position.state | float - step | int}}
          {% else %}
            0
          {% endif %}
- id: '511'
  alias: window_1 - Set Position Down
  trigger:
    platform: state
    entity_id: input_number.window_1_set_position
  condition:
    condition: template
    value_template: >-
        {{ (states.input_number.window_1_position.state | int) >
           (states.input_number.window_1_set_position.state | int) }}
  action:
  - service: timer.start
    data_template:
      entity_id: timer.window_1_down
      duration: >-
          {{ '00:00:%02d' | format(((
              (states.input_number.window_1_position.state | int) - 
              (states.input_number.window_1_set_position.state | int)
             ) * 16 /100) | int | abs )
          }}
  - service: cover.close_cover
    data:
      entity_id: cover.window_1_cover
- id: '512'
  alias: Window_1 - Set Position UP
  trigger:
    platform: state
    entity_id: input_number.window_1_set_position
  condition:
    condition: template
    value_template: >-
        {{ (states.input_number.window_1_position.state | int) <
           (states.input_number.window_1_set_position.state | int) }}
  action:
  - service: timer.start
    data_template:
      entity_id: timer.window_1_up
      duration: >-
          {{ '00:00:%02d' | format(((
              (states.input_number.window_1_position.state | int) - 
              (states.input_number.window_1_set_position.state | int)
             ) * 16 /100) | int | abs )
          }}
  - service: cover.open_cover
    data:
      entity_id: cover.window_1_cover

UI Config


lovlace.yaml

entities:
  - entity: cover.window_cover
  - entity: input_number.window_set_position
    name: Set Position
  - entity: input_number.window_position
    name: Current Position
show_header_toggle: false
title: Window Cover
type: entities

groups.yaml

window_cover:
  name: Window Cover
  entities:
  - cover.window_cover
  - input_number.window_set_position
  - input_number.window_position
  - timer.window_up
  - timer.window_down

API Call


Set position:

http://[home-assistant-url]:8123/api/services/input_number/set_value?api_password=password
{ "entity_id": "input_number.window_set_position", "value": 50 }

IFTTT


Home Asssistant Automations:

- id: ifttt_window_set
  alias: IFTTT Window Set
  trigger:
    platform: event
    event_type: ifttt_webhook_received
    event_data:
      action: window_set
  action:
    service_template: '{{ trigger.event.data.service }}'
    data_template:
      entity_id: '{{ trigger.event.data.entity_id }}'

- id: ifttt_window_position
  alias: IFTTT Window Position
  trigger:
    platform: event
    event_type: ifttt_webhook_received
    event_data:
      action: window_position
  action:
    service_template: '{{ trigger.event.data.service }}'
    data_template:
      entity_id: '{{ trigger.event.data.entity_id }}'
      value: '{{ trigger.event.data.value }}'

IFTT WebHook POST Reqest for close:

{"action": "window_set", "service": "cover.close_cover", "entity_id": "cover.window_cover" }

IFTT WebHook POST Reqest for set position:

{"action": "window_position", "service": "input_number.set_value", "entity_id": "input_number.window_set_position", "value": "{{NumberField}}"}

Alternative option

If you are using ESPHome, then this can be used instead, which automatically integrates with home-assistant.

esphome:
  name: myroom_window
  platform: ESP8266
  board: esp01_1m

wifi:
  ssid: 'myssid'
  password: 'mypass'

api:

logger:

ota:

binary_sensor:
- platform: gpio
  pin:
    number: GPIO09
    mode: INPUT_PULLUP
    inverted: True
  id: ph_button3
  on_press:
  - cover.open: my_cover
  on_release:
  - cover.stop: my_cover
- platform: gpio
  pin: 
    number: GPIO00
    mode: INPUT_PULLUP
    inverted: True
  id: ph_button4
  on_press:
  - cover.close: my_cover
  on_release:
  - cover.stop: my_cover

switch:
- platform: gpio
  pin: GPIO12
  interlock: &interlock [open_cover, close_cover]
  id: open_cover
- platform: gpio
  pin: GPIO5
  interlock: *interlock
  id: close_cover

cover:
- platform: time_based
  name: "myroom_window"
  id: my_cover
  open_action:
    - switch.turn_on: open_cover
  open_duration: 18s
  close_action:
    - switch.turn_on: close_cover
  close_duration: 18s
  stop_action:
    - switch.turn_off: open_cover
    - switch.turn_off: close_cover

FAQ


  • Use record on configuration.conf to save the state of the cover and sliders after a restart.
  • On Sonoff Tashoma Firmware there is the SetOption14 to interlock the 2 relays, though I haven’t tested it. It could help have fewer automations.
  • Physical switch connection diagram: here
  • Physical switch Sonoff configuration (GPIO0 - 09 switch 1, GPIO9 - 10 switch 2)
  • Allow Cover arrows to be always enabled: On configuration.yaml on cover section set “optimistic: true
  • Save the automation on a separate file. Add this line on configuration.yaml: automation old: !include_dir_merge_list automation/
  • Easily add another window sensor by changing all the window_1 to something else.
  • Split configuration: Instead of writing the whole configuration on configuration.yaml, you could create a package.yaml file by following the official doc about packages here.

I hope it helps!

26 Likes

Hi

I have been trying to achieve this for some time now but gave up as it was getting complicated. You have succeeded. Well done!

Copied and pasted your code into my config, adjusted the mqtt topic and time to 16 sec and voilá. It works. Also the Up/Down Arrows now work in a sensible manner. ie if you stop it mid-way, both arrows are selectable.

Is there a reason why you use POWER2 OFF and POWER1 ON for open_cover and POWER1 OFF and POWER2 ON for close_cover?

If I may make a suggestion: You can use SetOption14 1 to interlock the 2 relays. This make the Open/Close definition a little shorter as you only need to switch on the appropriate relay. The other one is automatically switched off.

The only problem I have is that I have 11 such shutters. That’s a lot of code!

In any case, terrific job.

2 Likes

thank you too for sharing, I was trying to do the same using calculations but your implementation is much simpler and makes sense. Can you help me (and others watching this topic) how can I retain the state of the sliders during reboots? defaults to 5 every time after reboot.

thanks a lot for sharing.

What do you use to open and close the blinds other than the sonoff dual?

1 Like

I personally use sonoff T1 Eu 2ch, before that, I was using sonoff dual and external manual switches in parallel, but now only the T1 is sufficient for both actions.

@bkbilly Would you mind sharing the blind motors you are using please?

1 Like

I’m quite curious too which motor was used, I believe you could use a servo motor but it seems fairly bulky to attach to the blinds.

This is awesome. Tried to get it working myself but failed… now I copied your code and set up my window covers and my garage door. Thanks for sharing!

I added the 5 on startup in order to be able to use the up-down arrows even if it is on the wrong states. In order to maintain the state you will have to use the retain flag on the mqtt messages which is something that I do not like.

I don¨t know which motor is used. The one that installed it just added a switch with two buttons and I just connected the two wires to the Sonoff, the other to the power and the other to the ground.

Thank you for this suggestion, I didn¨t know about this option.
It looks very promising.
If I have time I will try it myself.

Interesting setup. I’ll give it a try soon.
Just a quick question: When you operate the cover with the up /down / stop buttons, does the position update?

Yes, the position changes with with physical buttons and the home assistant buttons.

1 Like

Brilliant implementation.
I am modifying the automations and the cover to work with my 433MHz RF covers, cause since I moved HA to hassio I haven’t been able to make work the old scripts.

The only drawback that I’ve found is that if you open/close several times, a small error appears (the sum of accumulated errors)

Again, thanks and good job!

Hello,
First I want to congratulate all of you guys for all awesome projects.
I’m been trying to use your code on my windows but I can’t use it since it gives me a error in the cover part.

I use the packages to organize myself better.
The code is in the following link https://hastebin.com/mekunojure.pl

When I try to check the configuration I got errors on cover part. Also the last phrase “API” gives me errors.
It’s possible for some of you guys to check it?

Just a final contribution to your brilliant solution: I got some errors when the input numbers reach the maximum or minimum values:

Invalid value: 19.0 (range 0.0 - 18.0)
20:13 components/input_number.py (WARNING)

And the same in the lower side.

Which can be solved in your automations ‘411’ and ‘412’ with this template:
‘411’…

      value: >-
        {% if (states.input_number.window_position.state | int) < 18 %}
          {{ (states.input_number.window_position.state | int) + 1 }}
        {% else %}
          18
        {% endif %}

‘412’…

  value: >-
    {% if (states.input_number.window_position.state | int) > 1 %}
      {{ (states.input_number.window_position.state | int) - 1 }}
    {% else %}
      0
    {% endif %}

Frederico, if your covers opening-closing time is 22 seconds, you should replace all the 18’s with 22’s in all the code.

Regarding the errors, it should be better to paste the errors you get.

As I understand, the “API” is only necessary if you plan to change the cover position remotely (or at least from another device in your network)

If by remotely you mean hadashboard and the HA app YES I will need remote access to it and I don’t now how to use that API phrase.
All the the coding is ok now (the problem was just the formatting of the code… So problem solved in that part however now I have a different problem that I will try to explain:
The first time I run ha after the code has been installed I have to click on set position slider for the arrows and stop to appear.

Secondly when I operate them via the arrows the slider does not move like if I press down the slider does not move at all. When using the slider the timer starts working as it should but only when triggered by slider
Lastly every time I restart the HA and we do that’s a lot when coding I have to set again the position, is there a way to set the usually remember last state on sonoff and on ha?
Thanks for the help

Ps. Im testing via GUI without the sonoff plugged in so could it need the Mqtt feedback from the dual?

1 Like

Or you could:

action:

  • service: input_number.increment
    entity_id: input_number.window_position

in automation 411

and

action:

  • service: input_number.decrement
    entity_id: input_number.window_position

in automation 412

I’ve implemented your idea like so:


The automation files have been split into individual files in a directory otherwise it would be too long. I’ve trimmed down some code here and there and hid the timers as these do not really have to be visible.

I’ve enclosed a screen grab of my setup.

It would be nice if the 2 sliders of each cover were combined to reduce clutter. Have you tried this?

In any case great work and thank you for sharing.

1 Like