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 ourobjectStates
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 }
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.