Control a steppermotor with Domoticz

This post describe how to control a steppermotor with Domoticz.

Supplies needed:

Control the stepper with domoticz

In the previous post we made it possible to run the steppermotor with a python script. Hower, in the big picture of the swimmingpool automation, i want control the valve for the sunheating. In this case i want to decide how far the valve must be opened. For now i have 4 steps. From 0 steps if the valve is closed to 1000 steps if the valve is complete open.

Step 1: Create a python webserver and motormanagement

I want to have the following endpoints:
GET http://localhost:9090/getWanted -> give me the current wanted step
GET http://localhost:9090/getCurrent -> give me the current step
GET http://localhost:9090/setWanted/<steps> -> set the wanted steps to the given number

From domoticz we can add a dummy switch to control the stepper.
I created 2 pythonscripts, motormanagement.py and stepper-motor.py.

~/motormanagement.py
In this file we manage all the functionality. Here we need to import the GPIO libs. For testing purposes, the motor is not driven bug logs are printed to the screen. The motormanagement is controlled by stepper-motor.py.

~/stepper-motor.py
This is the file with the main thread. We want to have two threads (as deamon). One thread is handling the webserver events and the other thread is controlling the motor. Every second the thread (MyMotor) is watching the current and wanted variables. If they are different, the motor is turned on and the motor spint into the right direction (max 250 steps).

motormanagment.py

import time

class MotorManagement:
    GPIO_DIRECTION = 11
    GPIO_ENABLE = 16
    GPIO_ENGINE = 18

    CURRENT_ENGINE_STATE = None
    CURRENT_DIRECTION_STATE = None

    def __init__(self):
        print('INIT MotorManagement, set pins')
        

    def resetMotor(self):
        pinTriggert = False
        self.turnOnEngine()
        print('RESET MOTOR, TURN LEFT UNTIL PIN TRIGGERT')
        while not pinTriggert:
            self.spinLeft(1)
            pinTriggert = True
        print('MOTOR IS RESET')

        
    def spinLeft(self, steps):
        self.setDirection('L')
        self.spinMotor(steps)
        return True

    def spinRight(self, steps):
        self.setDirection('R')
        self.spinMotor(steps)
        return True

    def spinMotor(self, steps):
        if (self.CURRENT_ENGINE_STATE != True):
            print('CANT SPIN, ENGINE OFF')
            return False

        print('MOTORSPIN: ' + str(steps))
        while (steps > 0):
            steps -= 1
            time.sleep(0.001)

        print('MOTORSPIN DONE')
        return True

    def setDirection(self, direction):
        if (self.CURRENT_DIRECTION_STATE != direction):
            self.CURRENT_DIRECTION_STATE = direction
            print('MOTORMANGEMENT: DIRECTION:' + str(direction))
        
        return True

    def turnOnEngine(self):
        if (self.CURRENT_ENGINE_STATE != True):
            print('MOTORMANGEMENT: TURNON')
            self.CURRENT_ENGINE_STATE = True

        return True

    def turnOffEngine(self):
        if (self.CURRENT_ENGINE_STATE != False):
            print('MOTORMANGEMENT: TURNOFF')
            self.CURRENT_ENGINE_STATE = False

        return True    

stepper-motor.py

# threading.Thread
import threading
import time
import bottle
import sys, os, signal
from motormanagement import MotorManagement

class Settings():
    currentStep = 0
    wantedStep = 0

class ServerT():
    deamon = True

    def __init__(self,settings):
        self.settings = settings

    def setWanted(self, steps):
        self.settings.wantedStep = int(steps)
        return ("OK")

    def setCurrent(self, steps):
        self.settings.currentStep = int(steps)
        return ("OK")

    def getWanted(self):
        return(str(self.settings.wantedStep))

    def getCurrent(self):
        return(str(self.settings.currentStep))


class ServerThread(threading.Thread):
    deamon = True

    def __init__(self, settings):
        self.settings = settings
        super().__init__()

    def kill(self):
        try:
            os.kill(self, signal.SIGKILL)
        except OSError: pass 

    def run(self):
        myapp = ServerT(settings = self.settings)
        bottle.route("/getWanted")(myapp.getWanted)
        bottle.route("/getCurrent")(myapp.getCurrent)
        bottle.route("/setWanted/<steps>")(myapp.setWanted)
        bottle.route("/setCurrent/<steps>")(myapp.setCurrent)
        bottle.run(host='localhost', port=9090)

class MyMotor(threading.Thread):
    deamon = True  
    STEP_SIZE = 250


    def __init__(self, settings):
        super().__init__()
        self.settings = settings
        self.motorManagement = MotorManagement()
        self.motorManagement.resetMotor()
        self.settings.currentStep = 0
        self.kill_received = False

    def run(self):
        while not self.kill_received:
            print('Motor current/wanted:' + str(self.settings.currentStep) + "/" + str(self.settings.wantedStep) )
            if (self.settings.wantedStep != self.settings.currentStep):
                self.motorManagement.turnOnEngine()

                if (self.settings.wantedStep > self.settings.currentStep):
                    stepsToTake = self.settings.wantedStep - self.settings.currentStep
                    if (stepsToTake > self.STEP_SIZE):
                        stepsToTake = self.STEP_SIZE
                    self.motorManagement.spinRight(stepsToTake)
                    self.settings.currentStep += stepsToTake
                elif (self.settings.wantedStep < self.settings.currentStep):
                    stepsToTake = self.settings.currentStep - self.settings.wantedStep
                    if (stepsToTake > self.STEP_SIZE):
                        stepsToTake = self.STEP_SIZE
                    self.motorManagement.spinLeft(stepsToTake)
                    self.settings.currentStep -= stepsToTake

            if (self.settings.currentStep == self.settings.wantedStep):
                self.motorManagement.turnOffEngine()

            time.sleep(1)

        print('Main thread died,  turn engine off')
        self.motorManagement.turnOffEngine()


def has_live_threads(threads):
    return True in [t.isAlive() for t in threads]

if __name__ == '__main__':
    threads = []

    settings = Settings()

    t = ServerThread(settings)
    t.start() 
    threads.append(t)

    t2 = MyMotor(settings)
    t2.start()
    threads.append(t2)

    while has_live_threads(threads):
        try:
            [titem.join(1) for titem in threads
             if titem is not None and titem.isAlive()]
        except KeyboardInterrupt:
            # Ctrl-C handling and send kill to threads
            print ("Sending kill to threads...")
            for t in threads:
                t.kill_received = True
           
            #todo stop bottle webserver.. for now press 2times ctr+c
            break  

    print ("Exited")
    sys.exit()

You can start the stepper-motor with python3.

python ~/stepper-motor.py

Navigate to http://localhost:9090/setWanted/1000 and see what happend 🙂

NOTE: If you get an error like this

Traceback (most recent call last):
File "./stepper-motor.pi", line 4, in
import bottle
ImportError: No module named bottle

You need to install bottle, but to install bottle you need to have pip

sudo apt-get install python3-pip

After that, install bottle

pip3 install bottle

For the latest version of the stepper-motor, see the code on gitlab

https://gitlab.com/ebonenberg/swimmingpool

Step 2: Add virtual controller for the steppermotor

a) Add dummy hardware: Setup -> Hardware -> Type = Dummy
b) Add a dummy Virtual switch by clicking on the button in the list of hardware.

In the popup enter the following parameters:
Name: Steppermotor
Type: Switch
Press OK to add the switch

Step 3: Configure the steppermotor switch

In Domoticz goto Switches en edit the Steppermotor switch.

We give the new switch the following settings:
Switch type: Selector
Selector Levels: 0%, 25%, 50%, 75%, 100%
Selector Actions:
0 = http://localhost:9090/setWanted/0
10 = http://localhost:9090/setWanted/250
20 = http://localhost:9090/setWanted/500
30 = http://localhost:9090/setWanted/750
40 = http://localhost:9090/setWanted/1000

It should look like this.
Ofcourse you can play with the percentages and steps.

Step 4: Monitoring

We also want to now the current step of the motor. For this we created an endpoint http://localhost:9090/getCurrent. We can put the result of that call into a measure utility.

In domoticz, on the hardware page (setup -> hardware), create a virtual sensor of the type Counter. Give it the name: ‘Steppermotor current’.
We now create a dVents script to retrieve the current value from the Python script.
Goto: Setup -> More Options -> Events
Add a new script of the type: ‘dzVents’ with the following script.

return {
	on = {
		timer = {
			'every minute' -- just an example to trigger the request
		},
		httpResponses = {
			'trigger' -- must match with the callback passed to the openURL command
		}
	},
	execute = function(domoticz, item)
		if (item.isTimer) then
			domoticz.openURL({
				url = 'http://localhost:9090/getCurrent',
				method = 'GET',
				callback = 'trigger', -- see httpResponses above.
			})
		end

		if (item.isHTTPResponse) then
			if (item.statusCode == 200) then
			    local someValue = item.data
				domoticz.devices('Steppermotor current').updateCounter(someValue)
			
			else
				domoticz.log('There was a problem handling the request', domoticz.LOG_ERROR)
				domoticz.log(item, domoticz.LOG_ERROR)
			end

		end

	end
}

Here you see a script witch runs every minute, get the currentStep from the stepper motor and put that value into the measuresensor. Make sure you activate the event.

Step 5: Autostart python on boot with screen

You can autostart a script by adding it to the /etc/rc.local file. The disadvantage of this is that it is executes as the root user. Another issue is that the rc.local file needs to be finished, while this python script will run as a deamon. A command to start the stepper-motor server is:

(sleep 10; su pi -c '/home/pi/scripts/stepper-motor.py')&

A more elegant way is with screen. With screen you can dettached and reattach to the screen where the stepper-motor is running in. To install screen type:

sudo apt-get install screen

To start a screen as the pi user, use this command:

su pi -c '/usr/bin/screen -S py -d -m /usr/bin/python3.5 /home/pi/scripts/stepper-motor.py'

Now you can use these commands to attach or detach.

#list all screens
screen -ls

#start a new screen
screen

#dettach the current screen
ctrl + a + d

#reattach to a screen
screen -r

#stop a screen
kill

About the author