Creating a Foscam Controller

Overview

A controller handles the communication between your device and InControl. A device could be anything ranging from a piece of hardware such as a z-wave light switch to another program running on your computer, such as Windows Media Player.

This tutorial will step you through the creation of a controller that will communicate with and control a Foscam IP Camera on your same network.

This requires InControl version 2.200 or higher. Download it here: http://www.moonlitzwave.com/InControl_Setup_2.200.exe

The source code for this controller can be cloned from Github.

Setup your Visual Studio Project

I'm using Visual Studio 2012, but version 2010 should work too if that's all you have access to. Go ahead and start Visual Studio and create a new Class Library project and name itFoscamController. Be sure to target the .NET Framework 4.

Code tutorial 1.png

After you have your project created, you'll need to add a reference to the InControl plugin framework. You can find this in the folder where you installed InControl -- this is normally C:\Program Files (X86)\MLS\InControl HA. Find the file named MLS.HA.DeviceController.Common.dll and add it as a reference to your project by right clicking the "References" folder and selecting "Add Reference." You'll need to browse to your InControl install and find the DLL file mentioned earlier. Your finished reference should look like this:

Code tutorial 2.png

Create FoscamController class and add basic implementation

Now add a new class named FoscamController.cs to your project. Make your class inherits from HaController and implements the IHaController interface. Be sure to add a using directive pointing to MLS.HA.DeviceController.Common.HaControllerInterface.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using MLS.HA.DeviceController.Common.HaControllerInterface;
 
    namespace FoscamController {
        public class FoscamController : HaController, IHaController {
        }
    }

Let's get the basic interface implemented by right clicking on IHAController and choosing Implement Interface. VS should put a bunch of property and method stubs into your code file that looks similar to this:

        //...

        public string controllerErrorMessage {
            get {
                throw new NotImplementedException();
            }
            set {
                throw new NotImplementedException();
            }
        }

        public string controllerName {
            get {
                throw new NotImplementedException();
            }
            set {
                throw new NotImplementedException();
            }
        }

        public void executeSpecialCommand(object providerDeviceId, MLS.HA.DeviceController.Common.SpecialCommand command, object value) {
            throw new NotImplementedException();
        }

        //...

Setting up the ControllerName

Next, let's program the name of our controller. I created a static method in mine which returns the name since I'll be using it from the GUI as well. I then implement the controllerName property to return the value from this static method:

        public static string getControllerName() {
            return "HaFoscamController";
        }

        public string controllerName {
            get {
                return FoscamController.getControllerName();
            }
            set {
                // Does nothing
            }
        }

Device Tracking

It's important to understand how device tracking works. Your controller should never track a device unless it gets notified from InControl that a device should be tracked. If your controller knows about a new device, it should first call base.raiseDiscoveredDevice, thus notifying InControl about the new device. Once InControl knows about a device, it will call the trackDevice method in your controller to notify you to begin tracking the device.


Ideally, you would have a mechanism built into your controller for auto-discovery of new devices that would call the base.raiseDiscoveredDevice method; however, Foscam cameras are not auto-discoverable so we'll provide a user interface later on to allow the user to provide the camera's IP address. For the sake of testing, we'll be hard-coding a device and calling thebase.raiseDiscoveredDevice to notify InControl about it. Just be sure to remove this code once you deploy your controller.


Add a list of type HaDevice as a property to your class. This will be used to keep track of devices that we are notified about from InControl:

    List<HaDevice> localDevices { get; set; }


Now create a constructor for your class. In it we'll setup some initial values as well as add our test code for notifying InControl of our test camera device.

        public FoscamController() {
            writeLog("Foscam: starting FoscamController");

            localDevices = new List<HaDevice>();
            
            // To help with testing, add a hard-coded device. Be sure to remove this when you deploy your actual plugin.
            Thread t = new Thread(() => {
                Thread.Sleep(3000);
                
                // Replace the IP address, user & password matching an IP camera on your network
                base.raiseDiscoveredDevice(DeviceProviderTypes.PluginDevice, controllerName, DeviceType.IpCamera, FoscamController.getDeviceName("10.4.3.51:8050""user""password")"IpCamera");
            });
            t.IsBackground = true;
            t.Start();

            writeLog("Foscam: Startup complete");
        }

        public static string getDeviceName(string ipAddress, string username, string password) {
            return string.Format("http://{0}|{1}|{2}", ipAddress, username, password);
        }


Let's go ahead and implement the trackDevice method now. InControl will call this method and provide some basic information about the device, such as the device Id, the deviceName and the uniqueName. The deviceName and uniqueName are what we provided when we called base.raiseDiscoveredDevice.

In this method, we setup our own HaDevice -- in this case a CameraDevice -- and assign some starting values for it such as the ip address, username, password, etc. We then add this device to our local tracking list and then report the new HaDevice back to InControl.

         public HaDevice trackDevice(HaDeviceDto dbDevice) {

            // CameraDevice inherits from HaDevice, so we'll use it to track the camera.
            // Get some basic info from the dbDevice, such as the device id (assigned by InControl), the uniqueName and the deviceName.
            var haDev = new CameraDevice() {
                deviceId = dbDevice.deviceId,
                providerDeviceId = dbDevice.uniqueName,
                deviceName = dbDevice.deviceName                
            };

            try {
                // Get the ip, username and password from the devicename
                var split = dbDevice.uniqueName.Split('|');
                haDev.ip = split[0];
                haDev.userName = split[1];
                haDev.password = split[2];
            } catch { }

            // Generate the URL for where the live stream is accessible from
            haDev.liveStreamUrl = string.Format("{0}{1}", haDev.ip, replaceStringValues(LIVESTREAM_URL, haDev));

            // Track our device locally so that we can use it inside the controller
            localDevices.Add(haDev);

            // InControl needs to know what HaDevice came out of this
            return haDev;
        }

 

Polling for a Snapshot

InControl can display a snapshot from the camera. In order to get this snapshot, we are going to setup a polling mechanism to request it from the device every 30 seconds.

Add a local variable for a polling thread as well as the following to your constructor:

    Thread tPolldevices;
    
    public FoscamController() {

        // You'll have more code in your constructor that you created earlier... it's trimmed out for readability  

        tPolldevices = new Thread(() => { pollDevices(); });
        tPolldevices.IsBackground = true;
        tPolldevices.Start();

        // ...
    }

Here is the code you'll use for the polling. It basically loops over your devices and requests a new image if it hasn't been polled in the last 30 seconds. Note that this code is incomplete, please view the complete listing for the other methods that you'll need.

        /// <summary>
        /// Loops through all tracked devices and gets an image from it.
        /// </summary>
        private void pollDevices() {
            while (isRunning) {
                try {
                    foreach (var d in localDevices) {
                        if (d.pollDevice) {

                            if (new TimeSpan(DateTime.Now.Ticks - d.lastPollTime.Ticks).TotalSeconds < d.pollDeviceSeconds) {
                                continue;
                            }

                            d.lastPollTime = DateTime.Now;

                            // Get a snapshot from the camera
                            Thread t = new Thread(() => { getSnapshot(as CameraDevice); });
                            t.IsBackground = true;
                            t.Start();
                        }
                    }
                } catch {
                }
            }
        }

 

Get device methods

On occasion, InControl will call the getHaDevice methods. We need to implement those as well. They are simply going to take an id and find a matching device in our localDevice list and return it.

        /// <summary>
        /// Gets a device by the 
        /// </summary>
        /// <param name="providerId"></param>
        /// <returns></returns>
        public HaDevice getHaDevice(object providerId) {
            foreach (var d in localDevices) {
                if (d.providerDeviceId.ToString() == providerId.ToString()) {
                    return d;
                }
            }

            return null;
        }

        /// <summary>
        /// Gets a device by the deviceId.
        /// </summary>
        /// <param name="deviceId"></param>
        /// <returns></returns>
        public HaDevice getHaDevice(Guid deviceId) {
            foreach (var d in localDevices) {
                if (d.deviceId == deviceId) {
                    return d;
                }
            }

            return null;
        }

Other Implementation Methods

There are a number of methods that our FoscamController won't use. If one desired, they could implement this methods to provide special functionality for the camera.

public void finishedTracking() {}

finishedTracking is called by InControl when no more devices are found in the database.

public void setLevel(object providerDeviceId, int newLevel)

setLevel is called by InControl when the user adjust the slider/level of a device either by interacting with it in the PC GUI, the phone app or by setting it in a scene or a script. The level will be a value between 0 and 99.

public void setPower(object providerDeviceId, bool powered)

setPower is called by InControl when the user powers on or off the device.

Complete Code Listing for FoscamController.cs

Here is a complete code listing for the FoscamController.cs. You can also download the complete project and all source (https://github.com/rscott78/InControl-Foscam-Controller).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MLS.HA.DeviceController;
using MLS.HA.DeviceController.Common.Device;
using MLS.HA.DeviceController.Common.HaControllerInterface;
using MLS.HA.DeviceController.Common;
using System.Threading;
using System.Net;
using System.IO;
using System.Reflection;
using System.Drawing;
using System.Drawing.Imaging;

namespace FoscamController {
    public class FoscamController : HaController, IHaController {

        public static string getControllerName() {
            return "HaFoscamController";
        }

        public string controllerName {
            get {
                return FoscamController.getControllerName();
            }
            set {
                // Does nothing
            }
        }


        const string SNAPSHOT_URL = "/snapshot.cgi?user={username}&pwd={password}";
        const string LIVESTREAM_URL = "/videostream.cgi?user={username}&pwd={password}&resolution=32";

        Thread tPolldevices;
        bool isRunning = true;
        Dictionary<stringbool> currentSnapshots;
        object dictionaryLockObject = new object();

        public FoscamController() {
            writeLog("Foscam: starting FoscamController");

            localDevices = new List<HaDevice>();
            currentSnapshots = new Dictionary<stringbool>();

            tPolldevices = new Thread(() => { pollDevices(); });
            tPolldevices.IsBackground = true;
            tPolldevices.Start();

            // TEST - add a local device
            //Thread t = new Thread(() => {
            //    Thread.Sleep(3000);
            //    base.raiseDiscoveredDevice(DeviceProviderTypes.PluginDevice, controllerName, DeviceType.IpCamera, FoscamController.getDeviceName("http://10.4.3.51:8050|admin|mwisvs2m"), "IpCamera");
            //});
            //t.IsBackground = true;
            //t.Start();

            writeLog("Foscam: Startup complete");
        }

        #region Device polling
        /// <summary>
        /// Loops through all tracked devices and gets an image from it.
        /// </summary>
        private void pollDevices() {
            while (isRunning) {
                try {
                    foreach (var d in localDevices) {
                        if (d.pollDevice) {

                            if (new TimeSpan(DateTime.Now.Ticks - d.lastPollTime.Ticks).TotalSeconds < d.pollDeviceSeconds) {
                                continue;
                            }

                            d.lastPollTime = DateTime.Now;

                            // Get a snapshot from the camera
                            Thread t = new Thread(() => { getSnapshot(as CameraDevice); });
                            t.IsBackground = true;
                            t.Start();
                        }
                    }
                } catch {
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="device"></param>
        /// <returns></returns>
        private bool isCurrentlySnapshotting(CameraDevice device) {
            bool isSnapping = false;
            lock (dictionaryLockObject) {
                if (currentSnapshots.ContainsKey(device.name)) {
                    isSnapping = currentSnapshots[device.name];
                } else {
                    // create the key
                    currentSnapshots.Add(device.namefalse);
                }
            }

            return isSnapping;
        }

        /// <summary>
        /// 
        /// </summary>
        private void getSnapshot(CameraDevice device) {
            if (device != null && !isCurrentlySnapshotting(device)) {

                lock (dictionaryLockObject) {
                    currentSnapshots[device.name] = true;
                }

                var url = string.Format("{0}{1}", device.ip, replaceStringValues(SNAPSHOT_URL, device));
                //var tempFile = Path.Combine(Path.GetDirectoryName( Assembly.GetExecutingAssembly().Location), "");

                try {
                    var tempFile = Path.GetTempFileName();

                    //var request = (HttpWebRequest)WebRequest.Create(url);
                    //var response = (HttpWebResponse)request.GetResponse();

                    using (var c = new WebClient()) {
                        c.DownloadFile(url, tempFile);
                    }

                    // Write the timestamp on the image
                    var message = DateTime.Now.ToString();
                    //var fillColor = Color.FromArgb(127, 255, 255, 255);
                    //var fillBrush = new SolidBrush(fillColor);

                    //var textFont = new Font("Comic Sans MS", 9);
                    //var textBrush = new SolidBrush(Color.White);
                    //var textFormat = new StringFormat();

                    using (var image = (Bitmap)Image.FromFile(tempFile)) {
                        using (var graphics = Graphics.FromImage(image)) {
                            using (var arialFont = new Font("Arial"12)) {                                
                                PointF firstLocation = new PointF(10f, 10f);
                                graphics.DrawString(DateTime.Now.ToString(), arialFont, Brushes.White, firstLocation);
                            }

                            graphics.Flush();
                        }
                        
                        using (MemoryStream ms = new MemoryStream()) {
                            image.Save(ms, ImageFormat.Jpeg);
                            device.b64Image = Convert.ToBase64String(ms.ToArray());
                        }
                    }

                    File.Delete(tempFile);

                    // Check that the remote file was found. The ContentType
                    // check is performed since a request for a non-existent
                    // image file might be redirected to a 404-page, which would
                    // yield the StatusCode "OK", even though the image was not
                    // found.
                    //if ((response.StatusCode == HttpStatusCode.OK ||
                    //    response.StatusCode == HttpStatusCode.Moved ||
                    //    response.StatusCode == HttpStatusCode.Redirect) &&
                    //    response.ContentType.StartsWith("image", StringComparison.OrdinalIgnoreCase)) {

                    //    // If the remote file was found, download it
                    //    using (Stream inputStream = response.GetResponseStream()) {
                    //        var buffer = new byte[response.ContentLength];
                    //        inputStream.Read(buffer, 0, buffer.Length);

                    //        var b64String = Convert.ToBase64String(buffer);
                    //        device.b64Image = b64String;

                    //        inputStream.Close();

                    //        //using (Stream outputStream = File.OpenWrite(fileName)) {
                    //        //    byte[] buffer = new byte[4096];
                    //        //    int bytesRead;
                    //        //    do {
                    //        //        bytesRead = inputStream.Read(buffer, 0, buffer.Length);
                    //        //        outputStream.Write(buffer, 0, bytesRead);
                    //        //    } while (bytesRead != 0);
                    //        //}
                    //    }
                    //}

                    //device.lastPollTime = DateTime.Now;
                    //response.Close();

                } catch (Exception ex) {
                    writeLog("Foscam: Error getting snapshot", ex);

                    try {
                        //device.b64Image = ex.Message;
                    } catch { }
                } finally {
                    // Tell it there's no longer a snapshot happening
                    lock (dictionaryLockObject) {
                        currentSnapshots[device.name] = false;
                    }
                }
            } else {
                writeLog("Foscam: Existing snapshot detected. Skipping snapshot for device " + device.name);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="stringToReplace"></param>
        /// <param name="device"></param>
        /// <returns></returns>
        private string replaceStringValues(string stringToReplace, CameraDevice device) {
            return stringToReplace.Replace("{password}", device.password).Replace("{username}", device.userName);
        }

        #endregion

        List<HaDevice> localDevices { get; set; }
        public string controllerErrorMessage { get; set; }
        public bool hasControllerError { get; set; }

        public List<HaDevice> getHaDevices() {
            return localDevices;
        }

        /// <summary>
        /// Gets a device by the 
        /// </summary>
        /// <param name="providerId"></param>
        /// <returns></returns>
        public HaDevice getHaDevice(object providerId) {
            foreach (var d in localDevices) {
                if (d.providerDeviceId.ToString() == providerId.ToString()) {
                    return d;
                }
            }

            return null;
        }

        /// <summary>
        /// Gets a device by the deviceId.
        /// </summary>
        /// <param name="deviceId"></param>
        /// <returns></returns>
        public HaDevice getHaDevice(Guid deviceId) {
            foreach (var d in localDevices) {
                if (d.deviceId == deviceId) {
                    return d;
                }
            }

            return null;
        }

        /// <summary>
        /// Called when the server pulls a saved device from the db.
        /// </summary>
        /// <param name="dbDevice"></param>
        /// <returns></returns>
        public HaDevice trackDevice(HaDeviceDto dbDevice) {

            writeLog("FosCam: Tracking new device " + dbDevice.uniqueName);

            var haDev = new CameraDevice() {
                deviceId = dbDevice.deviceId,
                providerDeviceId = dbDevice.uniqueName,
                deviceName = dbDevice.deviceName                
            };

            try {
                // Get the ip, username and password from the devicename
                var split = dbDevice.uniqueName.Split('|');
                haDev.ip = split[0];
                haDev.userName = split[1];
                haDev.password = split[2];
            } catch { }

            haDev.liveStreamUrl = string.Format("{0}{1}", haDev.ip, replaceStringValues(LIVESTREAM_URL, haDev));

            localDevices.Add(haDev);
            writeLog("FosCam: device added to local list " + dbDevice.uniqueName);

            return haDev;
        }

        public void finishedTracking() {
            // na
        }

        public void setLevel(object providerDeviceId, int newLevel) {
            // Does nothing for a camera
        }

        public void setPower(object providerDeviceId, bool powered) {
            // Does nothing for a campera
        }

        public void executeSpecialCommand(object providerDeviceId, SpecialCommand command, object value) {
            // na
        }

        public HaDeviceDetails getDeviceDetails(object providerDeviceId) {
            // na
            return new HaDeviceDetails() { };
        }

        public ControllerTestResult testController() {
            // na
            return new ControllerTestResult() {
                result = false,
                message = "Not supported"
            };
        }

        public void stop() {
            isRunning = false;
        }

        public static string getDeviceName(string ipAddress, string username, string password) {
            return string.Format("http://{0}|{1}|{2}", ipAddress, username, password);
        }

    }
}

Deploying your Plugin

Copy your plugin to the plugins folder of your InControl install. Restart and test!