I have a similar situation: my house had manually operated roller shutters, where you had to keep the button pressed to open or close them.
Importantly when the end stop is reached, the motor is not powered in the moving direction anymore (probably some integrated reed contact or something). If this is the case for you, my solution will work well:
Just make a normal arduino relay (1 for every direction) and calibrate them with a timer. Then just make sure that you add some time for the opening direction (a few seconds), it will make sure that for every cycle any drift is compensated. This even worked for me when I had to reset the system with the rollers at a certain percentage => just move up and down a few times and it's ok again.
In my system I have implemented the manual switches (which used to switch the 220V by connecting them with the 5V node source and switching dedicated input pins on the arduino. I have two nodes, one with 3 roller shutters and one with 1 shutter and 1 awning.
(I think on the 3 up/down variant, there were not enough input pins for manual input, so I had all bound to one up-down switch).
Here's my code (it has become somewhat messy, and if I remember correctly I changed from 100% open to 100% closed mode when changing from home assistant to openhab2):
/**
The MySensors Arduino library handles the wireless radio link and protocol
between your home built sensors/actuators and HA controller of choice.
The sensors forms a self healing radio network with optional repeaters. Each
repeater and gateway builds a routing tables in EEPROM which keeps track of the
network topology allowing messages to be routed to nodes.
Created by Henrik Ekblad <henrik.ekblad@mysensors.org>
Copyright (C) 2013-2015 Sensnology AB
Full contributor list: https://github.com/mysensors/Arduino/graphs/contributors
Documentation: http://www.mysensors.org
Support Forum: http://forum.mysensors.org
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
version 2 as published by the Free Software Foundation.
*******************************
REVISION HISTORY
Version 0.4 - October 13, 2017 - Powerpenguin
Added dedicated inputs for all manual switches
(This is possible for 2 up/down actuators: D2-D5 relay, D9-D13 nRF24, A0-A3=D14-D17: switches)
Version 0.3 - October 9, 2017 - PowerPenguin
DESCRIPTION
Based on Dimmable LED Light using PWM from Henrik Ekblad
Simple control of window shades which are already motorised and don't provide
any more feedback, i.e. we want to send a target percentage from the controller
to the shade which then moves as tgt_percentage * time_to_fully_close
In the first implementation we assume that the starting state is fully open, so
some "drift" is to be expected? Or "overshoot" opening by a margin to make sure
100% corresponds to fully open? (yes: home assistant takes % open, not % closed)
The sketch supports multiple blinds, i.e. multiple relays connected to the arduino.
There are two relays per blind, for up/down respectively
Also, additional pins are read for a push-button, to manually give the up or down signal
(will start/stop moving one or covers simultaneously, based on btcidmap[])
To avoid the relays coils from being powered in normal operation, and most relays are active low,
high/low has been reversed (so high unless activated)
(better buy active high relays!)
The code has become somewhat messy, better to use interrupt on manual switch? Event based?
*/
// Enable debug prints to serial monitor
#define MY_DEBUG
// Enable and select radio type attached
#define MY_RADIO_NRF24
//#define MY_RADIO_RFM69
#include <MySensors.h>
#define SN "WindowShades"
#define SV "0.4"
#define PIN_BASE 2 // first of the Arduino pins attached to relay
#define LOOP_DELAY 950 // loop delay in ms
#define LOOP_DELAY_SMALL 30 // delay between transmissions for different sensors
#define PIN_BASEA 14 // base of the analog pins to be used as digital input
#define NCOV 2 // number of covers, each have an up and down relay; maximum is half the number of available digital i/o pins
#define NCSW 2 // number of pairs (up/down) of manual override switches
enum coverstates_enum {UP, DOWN, STOPPED};
static bool btmanup_pressed[NCSW];
static bool btmandn_pressed[NCSW];
bool initialValueSent[NCOV];
static int8_t currentLevel[NCOV];
static int8_t targetLevel[NCOV];
static int8_t time_open[NCOV];
static int8_t time_close[NCOV];
static unsigned long tstart[NCOV];
static int8_t lstart[NCOV];
// the manual button vs. CID mapping has to be specified manually, since sometimes we want one manual button to control more channels
static int8_t btcidmap[NCOV] = {0, 1};
static coverstates_enum state[NCOV]; // number of values must be NCOV
MyMessage msgPct[NCOV];
void setup() {
// initialise all child actuators and related vars
for (int8_t cid = 0; cid < NCOV; cid++) {
msgPct[cid] = MyMessage(cid, V_PERCENTAGE);
pinMode(PIN_BASE + 2 * cid, OUTPUT);
pinMode(PIN_BASE + 2 * cid + 1, OUTPUT);
state[cid] = STOPPED;
initialValueSent[cid] = false;
currentLevel[cid] = 100;
targetLevel[cid] = 100;
time_open[cid] = 29; // seconds to open, if not the same for all covers specify independently
time_close[cid] = 27; // seconds to close
}
// specify times independently if not the same for all covers below
time_close[0] = 40;
time_open[0] = time_close[0] + 3; // overshoot to make sure we open completely, motor stops anyway
time_close[1] = 26;
time_open[1] = time_close[1] + 3; // overshoot to make sure we open completely, motor stops anyway
// time_close[2] = 22;
// time_open[2] = time_close[2] + 3; // overshoot to make sure we open completely, motor stops anyway
for (int8_t i = 0; i < NCSW; i++) {
pinMode(PIN_BASEA + 2 * i, OUTPUT);
pinMode(PIN_BASE + 2 * i + 1, OUTPUT);
btmanup_pressed[i] = false;
btmandn_pressed[i] = false;
}
}
void presentation() {
// Register the Cover with the controller
sendSketchInfo(SN, SV);
for (int cid = 0; cid < NCOV; cid++) {
present(cid, S_COVER);
}
}
void loop() {
int8_t cid;
int8_t delta;
for (cid = 0; cid < NCOV; cid++) {
if (!initialValueSent[cid]) {
Serial.println("Sending initial value");
send(msgPct[cid].set(currentLevel[cid]));
Serial.println("Requesting initial value from controller");
request(cid, V_PERCENTAGE);
wait(LOOP_DELAY); // extra delay to give more time for initial handshake
} else {
// toggle moving if manual button pressed (since we arrive heare about once a second,
// we don't need debounce, but we need to press the button up to 1 s to have a reaction
btmanup_pressed[btcidmap[cid]] = (bool)digitalRead(PIN_BASEA + 2 * btcidmap[cid]);
btmandn_pressed[btcidmap[cid]] = (bool)digitalRead(PIN_BASEA + 2 * btcidmap[cid] + 1);
// if both pressed, reset (no reaction)
if (btmanup_pressed[btcidmap[cid]] && btmandn_pressed[btcidmap[cid]]) btmanup_pressed[btcidmap[cid]] = btmandn_pressed[btcidmap[cid]] = false;
if (btmanup_pressed[btcidmap[cid]]) {
btmanup_pressed[btcidmap[cid]] = false;
if (state[cid] == STOPPED) {
targetLevel[cid] = 100;
} else { // if already moving, then stop by setting target to current
currentLevel[cid];
}
}
if (btmandn_pressed[btcidmap[cid]]) {
btmandn_pressed[btcidmap[cid]] = false;
if (state[cid] == STOPPED) { // if (reference) not moving, then make go up
targetLevel[cid] = 0;
} else { // if already moving, then stop by setting target to current
currentLevel[cid];
}
}
if (currentLevel[cid] > targetLevel[cid]) { // move down (close) => means decreasing pct!
if (state[cid] != DOWN) { // we just started going down
state[cid] = DOWN;
tstart[cid] = millis();
lstart[cid] = currentLevel[cid];
// mstotgt[cid] = (currentLevel[cid] - targetLevel[cid]) * 10 * time_close[cid]; // remaining time to target
}
digitalWrite(PIN_BASE + 2 * cid, 0); // down pin
digitalWrite(PIN_BASE + 2 * cid + 1, 1); // up pin
// level change over elpased time, don't exceed targetLevel (will lead to control settle behaviour)
currentLevel[cid] = max(lstart[cid] - (millis() - tstart[cid]) / (10 * time_close[cid]), targetLevel[cid]);
Serial.print("Sending percentage feedback...");
Serial.println(currentLevel[cid]);
send(msgPct[cid].set(currentLevel[cid]));
} else if (currentLevel[cid] < targetLevel[cid]) { // move up (open)
if (state[cid] != UP) { // we just started going up
state[cid] = UP;
tstart[cid] = millis();
lstart[cid] = currentLevel[cid];
// mstotgt[cid] = (currentLevel[cid] - targetLevel[cid]) * 10 * time_close[cid]; // remaining time to target
}
digitalWrite(PIN_BASE + 2 * cid, 1); // down pin
digitalWrite(PIN_BASE + 2 * cid + 1, 0); // up pin
// level change over elpased time, don't exceed targetLevel (will lead to control settle behaviour)
currentLevel[cid] = min(lstart[cid] + (millis() - tstart[cid]) / (10 * time_open[cid]), targetLevel[cid]);
Serial.print("Sending percentage feedback...");
Serial.println(currentLevel[cid]);
send(msgPct[cid].set(currentLevel[cid]));
} else if (currentLevel[cid] == targetLevel[cid]) { // we always stop when the level is reached
// we don't change the state here, because we want to send a final level value to the controller first
digitalWrite(PIN_BASE + 2 * cid, 1); // down pin
digitalWrite(PIN_BASE + 2 * cid + 1, 1); // up pin
if (state[cid] != STOPPED) send(msgPct[cid].set(currentLevel[cid]));
state[cid] = STOPPED;
}
// Serial.println("Sending current level to controller");
// Serial.println(currentLevel[cid]);
// Serial.println("targetLevel = ");
// Serial.println(targetLevel[cid]);
// if (state[cid] != STOPPED) send(msgPct[cid].set(currentLevel[cid])); // send current estimated percentage back to gateway, until stopped state reached
// if (currentLevel[cid] != targetLevel[cid]) state[cid] = STOPPED;
}
wait(LOOP_DELAY_SMALL);
}
wait(LOOP_DELAY);
}
void receive(const MyMessage &message) {
// the message sets the target value for each sensor.
// as long as the target is not reached, the approriate relay pin is kept high
// and the controller is informed every LOOP_DELAY ms about the progress
// Serial.println("Message received from gateway (cid):");
// Serial.println(message.sensor);
if (message.isAck()) {
Serial.println("This is an ack from gateway");
}
switch (message.type) {
case V_PERCENTAGE:
if (!initialValueSent[message.sensor]) {
Serial.println("Receiving initial value from controller");
initialValueSent[message.sensor] = true;
}
targetLevel[message.sensor] = atoi(message.data);
targetLevel[message.sensor] = targetLevel[message.sensor] > 100 ? 100 : targetLevel[message.sensor];
targetLevel[message.sensor] = targetLevel[message.sensor] < 0 ? 0 : targetLevel[message.sensor];
Serial.println("Received V_PERCENTAGE from controller");
break;
case V_UP:
targetLevel[message.sensor] = 100;
Serial.println("Received V_UP from controller");
break;
case V_DOWN:
targetLevel[message.sensor] = 0;
Serial.println("Received V_DOWN from controller");
break;
case V_STOP:
Serial.println("Received V_STOP from controller");
targetLevel[message.sensor] = currentLevel[message.sensor];
break;
}
}
Good luck!