Build

Building a tvOS Smart Home Controller

Michael Carroll on Jul 13, 2016
Building a tvOS Smart Home Controller

As expected, the popularity of IoT has been growing. Being able to connect to all of your physical objects and collect data and interact with them can significantly change how we live. It allows for an easier and more efficient life. There are many different setups available in creating an IoT project, in this project we will be using tvOS, Raspberry Pi and PubNub.

This blog is part of a two part series, the software side of this project will be covered here. In Part 1 Building a Smart Home: The Hardware we covered the hardware side. We'll discuss here how to build a tvOS smart home controller application using Swift.

Combining Apple TV, Raspberry Pi and PubNub

Apple has created and released tvOS for developers, which allows for revolutionary Apple TV applications. I thought the TV could prove to be a great controller for IoT devices, allowing the user to control everything from their couch or bed brings about a ton of different and great use cases. The Raspberry Pi has been used for many different IoT projects to actually perform the commands on the object. Putting both of these together with PubNub's real-time data stream network, which sends small chunks of data in 1/10 of a second or less, is a great combo.

Getting Started with the PubNub Swift SDK

We'll start off by including the SDK into our project. The easiest way to do this is with CocoaPods, which is a dependency manager for Swift projects. If you've never used CocoaPods before, check out how to install and use CocoaPods!

After you have installed CocoaPods, go into your project's root directory and type:

$ (your favorite editor) Podfile

Paste the following code into your Podfile:

source 'https://github.com/CocoaPods/Specs'

use_frameworks!

platform :ios, '9.0'

target 'YourProjectName' do

pod 'PubNub'

end

Finish it off by typing:

$ pod install

Now open the file YourProjectName.xcworkspace

App Setup in Account

You will need a PubNub account in order to get your own unique keys and to turn on some PubNub features we will need for this project. If you don't already have an account, go ahead and sign up for a free account to get your own publish and subscribe keys. Head over to the PubNub Developer Dashboard and create a new app, enable the Presence feature.

Connect to PubNub from the AppDelegate

In order to use the PubNub features, we must create a PubNub instance property.

Open your AppDelegate.swift file and add this before the class declaration:

import PubNub
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, PNObjectEventListener {
    var window: UIWindow?
    var objectStates: [String : String] = [:]
    lazy var client: PubNub = {
        let config = PNConfiguration(publishKey: "pub-c-63c972fb-df4e-47f7-82da-e659e28f7cb7", subscribeKey: "sub-c-28786a2e-31a3-11e6-be83-0619f8945a4f")
        let pub = PubNub.clientWithConfiguration(config)
        return pub
    }()

Receive IoT Object States

When our main view controller loads, we want to add it as a listener for messages by using addListener(self). The Pi uses Presence, to be notified if a user joins the same channel that it is on. When our tvOS application joins the same channel, the Pi will send out the initial state of every object it's connected to. In this case, we have a small led light, a temperature sensor detecting the temperature of water in a kettle and a led screen simulating a thermostat.

The light will send out a message saying whether its off or on, the temperature sensor will send out the current temperature its detecting and the thermostat will also send out a temperature.

We will receive this message using the func client(client: PubNub, didReceiveMessage message: PNMessageResult) function. Once we parse this message we will store it an array so we can display this information on the user interface.

func client(client: PubNub, didReceiveMessage message: PNMessageResult) {
       if let receivedobjectStates = message.data.message as? [String : String] {
           //Receive initial object states when first subscribing to the channel
           if !objectStatesSet {
               objectStates = receivedobjectStates
               activityIndicator.stopAnimating()
               collectionView.reloadData()
               objectStatesSet = true
               //Receive temperature sensor updates
           } else if let kettleTemp = receivedobjectStates["temp"] {
               objectStates["temp"] = kettleTemp
               collectionView.reloadData()
           }
       }
   }

The else if statement takes in the temperature sensor data that is received, as that message will be sent out whenever the temperature changes.

Collection View Cell Inheritance

We will set up a base class for our custom collection view cells. This base class will represent an IoT object that our other custom cell classes will inherit from. The reasoning for this is that each cell has a different user interface and functionality.

import UIKit
import PubNub
//Super class that all object cells inherit 
class IoTObjectCell: UICollectionViewCell, PNObjectEventListener {
    
    var progressView: UIProgressView!
    var objectImage: UIImageView!
    var temperature: UILabel!
    var objectTitle: UILabel!
    var offButton: UIButton!
    var onButton: UIButton!
    var progressBarTimer: NSTimer!
    var activityIndicator: UIActivityIndicatorView = UIActivityIndicatorView(frame: CGRectMake(250, 250, 50, 50)) as UIActivityIndicatorView
    let appDelegate: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
    //Set cell view pertaining to which object it is
    func configureCell(object: String, state: String) {}
    
    func toggleOffButton(sender: AnyObject) {}
    
    func toggleOnButton(sender: AnyObject) {}
    
    //Dialogue showing error
    func showAlert(error: String) {
        let alertController = UIAlertController(title: "Error", message: error, preferredStyle: .Alert)
        let OKAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
        alertController.addAction(OKAction)
        self.window?.rootViewController?.presentViewController(alertController, animated: true, completion:nil)
    }
    
    //Update progress bar for change in temperature sensor
    func updateProgressBar(progress: Float) {
        self.progressView.progress = progress
        if(self.progressView.progress == 1.0) {
            self.progressView.removeFromSuperview()
        }
    }

The class for the led light has buttons which send a message over the PubNub network using the publish() function to the Pi to tell it to turn the light on or off.

//Turn light off
    override func toggleOffButton(sender: AnyObject) {
        appDelegate.client.publish(["light" : "off"], toChannel: "Smart_Home", withCompletion: { (status) in
            self.showActivityIndicator()
            if status.error {
                self.showAlert(status.errorData.description)
            } else {
                dispatch_async(dispatch_get_main_queue(), {
                    self.activityIndicator.stopAnimating()
                    self.objectImage.image = UIImage(named: "light_off")
                })
            }
        })
    }
    
    //Turn light on
    override func toggleOnButton(sender: AnyObject) {
        appDelegate.client.publish(["light" : "on"], toChannel: "Smart_Home", withCompletion: { (status) in
            self.showActivityIndicator()
            if status.error {
                self.showAlert(status.errorData.description)
            } else {
                dispatch_async(dispatch_get_main_queue(), {
                    self.activityIndicator.stopAnimating()
                    self.objectImage.image = UIImage(named: "light_on")
                })
            }
        })
    }

In the cell for displaying the sensor temperature data, we have a progress bar which shows how far your water is from being boiled. This is updated every time we receive a temperature message from the Pi.

override func configureCell(object: String, state: String) {
        temperature.text = NSString(format:"\(state)%@", "\u{00B0}C") as String
        objectTitle.text = "Tea Kettle"
        if let kettleTemp = Float(state) {
            progressView.progress = kettleTemp/100
            if(progressView.progress == 1.0) {
                temperature.text = "Water is boiled!"
            }
        }
    }


The cell displaying the thermostat temperature has two buttons allowing the user to turn the temperature of the house up or down. Just like the led light, we will send a message over the PubNub network using the publish() function to the Pi to tell the led screen what temperature to display.

//Turn thermostat temperature display up by one
    override func toggleOffButton(sender: AnyObject) {
        if let strippedTemp = self.temperature.text?.stringByReplacingOccurrencesOfString("\u{00B0}F", withString: "") {
            appDelegate.client.publish(["thermostat" : String(Int(strippedTemp)! - 1)], toChannel: "Smart_Home", withCompletion: { (status) in
                self.showActivityIndicator()
                if status.error {
                    self.showAlert(status.errorData.information)
                } else {
                    dispatch_async(dispatch_get_main_queue(), {
                        self.activityIndicator.stopAnimating()
                        self.temperature.text = NSString(format:"\(String(Int(strippedTemp)! - 1))%@", "\u{00B0}F") as String
                    })
                }
            })
        }
    }
    
    //Turn thermostat temperature display up by one
    override func toggleOnButton(sender: AnyObject) {
        if let strippedTemp = self.temperature.text?.stringByReplacingOccurrencesOfString("\u{00B0}F", withString: "") {
            appDelegate.client.publish(["thermostat" : String(Int(strippedTemp)! + 1)], toChannel: "Smart_Home", withCompletion: { (status) in
                self.showActivityIndicator()
                if status.error {
                    self.showAlert(status.errorData.information)
                } else {
                    dispatch_async(dispatch_get_main_queue(), {
                        self.activityIndicator.stopAnimating()
                        self.temperature.text = NSString(format:"\(String(Int(strippedTemp)! + 1))%@", "\u{00B0}F") as String
                    })
                }
            })
        }
    }

Displaying the Custom Collection View Cells

Now that we have our custom collection view cell classes set up, we can display the data that we receive from the Pi.

override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.registerClass(LightBulbCell.self, forCellWithReuseIdentifier: "lightCell")
        collectionView.registerClass(KettleCell.self, forCellWithReuseIdentifier: "kettleCell")
        collectionView.registerClass(ThermostatCell.self, forCellWithReuseIdentifier: "thermostatCell")
        collectionView.registerClass(IoTObjectCell.self, forCellWithReuseIdentifier: "objectCell")
        appDelegate.client.addListener(self)
        appDelegate.client.subscribeToChannels(["Smart_Home"], withPresence: true)
        showActivityIndicator()
    }
//Create and set cells for UI
    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        var cellIdentifier: String? = nil
        let object = Array(objectStates.keys)[indexPath.row]
        let state = Array(objectStates.values)[indexPath.row]
        
        switch object {
            case "light":
                cellIdentifier = "lightCell"
            case "temp":
                cellIdentifier = "kettleCell"
            case "thermostat":
                cellIdentifier = "thermostatCell"
            default:
                cellIdentifier = "objectCell"
        }
        
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier!, forIndexPath: indexPath) as! IoTObjectCell
        
        cell.configureCell(object, state: state)
        
        return cell
    }

Let's take this last part step by step:

  • We register each of our object cell classes in the viewDidLoad function
  • We grab the object and state using the indexPath.row from our objectStates dictionary
  • A switch statement is used to switch over the object and set the correct cell identifier
  • The cell is created with the cell identifier
  • The object and state is passed into the configureCell() function which displays the correct UI elements and data for that cell

Another thing to note when programming for tvOS, is that you have to be aware of the new Focus Engine. On the Apple TV, a user controls everything with a remote. Every time a user navigates to a different item on the screen, that item becomes focused. This calls for a few different functions you may need to implement to have your tvOS application working the way you want it to. For the collection view, I had to return false for the
collectionView(_:canFocusItemAt:) function. This made it so only my UIButton's can be focused because they are selectable and so that the user couldn't focus on any other UI elements in the cell.

func collectionView(collectionView: UICollectionView, canFocusItemAtIndexPath indexPath: NSIndexPath) -> Bool {
        return false
    }
The finished Smart Home app interface

Taking it further

If you have a TV in your bedroom, you can now turn on the lights, change the thermostat temperature and know when your water is boiled, all from your bed in the morning! Of course, you can always take this much further by being able to roll up your shades, turn on your coffee pot or turn on and warm up your car in the morning all from your Apple TV. I would love to see what you come up with, let me know on twitter! The future of IoT is very bright and now is the perfect time to get involved.

Resources