I've used python scripts to run both the r.los and r.viewshed grass commands that Peter mentioned.
The advantage with r.los is that it works straight out-of-the-box with grass6, but is slightly slower to run over large raster DEMs. r.viewshed is a bit of a pain to get running, but is a vast speed improvement on very large rasters. See if the times for your DEM are acceptable using r.los, then give r.viewshed a go if it's taking too long. Here's a quick python example using grass64 r.viewshed, but you'll need to get the python grass bindings working first (see http://grass.osgeo.org/wiki/GRASS_and_Python).
import grass.script as grass
with a better description of the parameters available at http://grass.osgeo.org/gdp/html_grass70/r.viewshed.html