With the Internet of Things (IoT) space surging and new products being added daily, it seemed fitting to explore how to use Apple’s version of home automation. HomeKit can be thought of as many things: a label on products to assure consumers of compatibility, a set of software frameworks to create HomeKit apps, and even a hardware set of specifications if you’re developing hardware under the MFi program. For this section, we’ll start by using the HomeKit framework to create a simple, on-off toggle of an AC outlet to control a disco ball (Figure 16-1). Why a disco ball? Well, why not? Seriously, though, you can use any AC-powered device; a desk or table lamp, for example, works just as well.
Figure 16-1. To make things more fun, we’ll control a disco ball in order to get our home automation party started
Problem
We want to get started with home automation using Apple’s HomeKit iOS features , but current tutorials are far more complicated than we’d like to tackle as our first attempt. Apple offers a HomeKitCatalog sample app that contains pretty much everything you’d need to know to work with HomeKit. However, we just want to control a switch and, for now, nothing else.
Solution
We’ll build the simplest app imaginable in order to control our disco ball. Our app’s UI contains one control: a button that turns the power on or off from the HomeKit accessory device . For our accessory device, I chose the iHome Control SmartPlug , which is, as you can see in Figure 16-2, certified to work with Apple HomeKit.
Figure 16-2. To manage power to our disco ball, we’re using the iHome Control SmartPlug Certified HomeKit accessory
Let’s Work Through the Project
As you may recall from the earliest chapters of this book, my goal was not so much to teach you programming as to help you overcome the typical issues that arise while creating and building iOS apps. In the last project, our coin-flipping app, we addressed making sure that the icons for both the iPhone and Watch devices were correct. Normally, we might want to do that in this project or in any other project. Or, we might save that kind of issue until the very end of the development cycle. It really depends on your style and your organization’s guidance. It certainly seems like something superfluous to be concerned about before the code is written, but if we’re planning on demoing this to a client, we want our first impressions to be at the top of the game. Delivering or even just showing something that looks incomplete at first glance can give the wrong impression. While we may have 50 percent or more of the logic and functionality ready to show, a crappy looking icon or screen can set the wrong mood at the start.
Having said all that, for the sake of brevity, since we addressed icon issues in the last project, we’re going to skip them in this chapter and try to address other common issues certain that may arise in your career.
Create the Project
Create a new project using the single view template and call it whatever you wish. I’ve chosen to call mine DiscoBall, as that is the target device that we’ll control. Initialize the project without using core data or setting any test targets in order to keep it simple for now, as shown in Figure 16-3. I also chose to build it for the iPhone, but that’s up to you. When finishing up project creation, do not choose to create a Git repository (Figure 16-4).
Figure 16-3. Create our disco ball project without core data or testing capability
Figure 16-4. Don’t use source control for this project. We will address that in a different chapter
Note
While using core data would not make any sense for this small app, setting up testing as well as creating a Git repository would, in most cases, be the way to do things. I’ve left them off intentionally to show how they would be handled separately in other chapters.
Once the project has been created and you have saved its folder to the proper location on your Mac, verify that your team is set correctly in the project and target settings under the General section. As you can see in Figure 16-5, I’ve set mine to my name, which reflects my individual iOS/Mac developer account credentials.
Figure 16-5. Set the project to the team account you are using to create this app
Verify the Build Process
Similar to how we wanted to make sure the iPhone and Watch icons were properly displayed in the Coin Toss project, before we go any further into developing the logic, let’s make sure we can build this project to our device.
First, let’s make a simple change to our LaunchScreen.storyboard so we can see if everything works. We could add something to our Main.storyboard as well, but we’ll get to that shortly. For now, I just put a label at the center of the launch screen so we’ll see something happen when and if the app builds and loads correctly (Figure 16-6).
Figure 16-6. Add a simple label in the center of the launch screen to see if the app at least tries to start properly
You should now expect, because we’ve set up everything and made one very simple change to one storyboard, that the app should build okay, load, and start running on your connected device. Trying this, however, likely yields results similar to those shown in Figure 16-7.
Figure 16-7. You haven’t yet created a provisioning profile for building this app to a device, though we might expect it to use our team profile
Fortunately, this is one of those problems that Xcode has become fairly good at dealing with. Click on Fix Issue, and if you have multiple accounts you’ll be presented with the dialog in Figure 16-8. If so, choose the same team we picked earlier, and things should begin to work. If you have only one team, it may skip this screen and just fix the provisioning issue.
Figure 16-8. If presented with a choice, select the same team we did earlier in the General project settings
Test your work by building, downloading, and executing the project onto your device. You should quickly see the launch screen we modified followed by a completely blank screen.
Create the User Interface
Because all we’re looking to do in this project is control a simple AC-powered device, our disco ball, rather than spend time developing hierarchical table view screens, I’m going to go with a few simple controls and indicators as shown in Figure 16-9. Starting from the top:
Add a label to indicate the status of the device, either on or off
Add a button to turn the device on or off.
Add a label that we’ll use to show the user when we found an accessory and what the accessory is called.
Add a label to show if the accessory is connected to our app.
Add another button to attach the accessory when the app finds it.
Figure 16-9. For this project, because we’re more concerned about using HomeKit, our UI will consist of very simple buttons and simple labels as indicators
Add the actions and outlets for the UI objects to the ViewController.swift file as shown in Figure 16-10. The deviceIndicator label shows whether the disco ball is turned on or not. The switchStatus UIButton outlet allows us to change the label on the ON/OFF button as appropriate. The activateDevice action turns the disco ball on or off—this is the main power switch. The accessoryFound label shows the name of the accessory that the app has located, and accessoryStatus tells us if that accessory is connected to the app or not. Finally, the attachAccessory action will try to connect an accessory that has been found to the app so it can be operated upon.
Figure 16-10. Set up two outlets and an action for our UI
About HomeKit
So, what exactly is HomeKit? Like many terms, HomeKit can be used differently depending on the context of our discussion. For the consumer, HomeKit represents a certification from Apple, or rather, a stamp of approval that the accessory will work with apps developed for HomeKit. To the hardware designer, it represents a set of standards and underlying protocols that allow communications and control with various hardware elements. That is, not just any AC power control or light bulb or garage door opener can work with HomeKit. The underlying hardware and, most importantly, the communications mechanism must be designed and developed according to highly regulated specifications that a company can access only if they are part of Apple’s MFi program .
To us software developers, HomeKit provides the framework we need to communicate with, access, and control these accessory devices. The methods within the framework provide access to a HomeKit database stored within iOS on an Apple device. So, because iOS controls the database, your home’s information is hardware encrypted and secure to the extent that you keep your iPhone secure. Leave it in a bar with no password protection and you may find your lights and thermostat seem to change for no reason.
HomeKit keeps information in a basic hierarchy. At the top is the home, the container for all your devices. Actually, you can have as many homes as you need, e.g., a vacation home, a rental property, etc. Underneath are rooms that contain the third layer, the accessories. Just as a home can contain multiple rooms, a room can contain multiple accessories.
Continuing, each accessory can contain one or more services. This is where things may seem to get a little more complicated, but if you’ve looked at the Bluetooth network standard, it will seem very familiar. A service is pretty much as it sounds—something that the accessory does. A desk lamp accessory would have a lamp service to provide illumination. A garage door would have a motor or door service, but could also have a lamp service and maybe an alarm service.
Each service contains one or more characteristics. These are the various data elements, essentially the leaves of our hierarchy tree. For the garage door, the door service would have an open characteristic indicating whether the door is closed or not. This would be a characteristic that we read; that is, through our HomeKit framework methods, we read whether the door is open or closed. But, this characteristic might also allow write access. We would write to the characteristic whether we want to open or close the door. Characteristics can be read only, write only (less common), or read/write.
To delve deep into HomeKit would require nearly an entire book to itself. What we’ve covered in this section are the very basic elements, but it is enough to begin designing our app around our AC power control for the disco ball.
Our Configuration
I plan to keep things extremely simple in order to get the most out of the HomeKit explanations, so we’ll have a very simple hierarchy. At the top is MyHome. We will use only one home and won’t worry about changes, deletions, additions, and so on. Below that, our home will have one room. Imagine a cabin in the mountains. For our work we’ll simply call it MainRoom. In that room we’ll have our DiscoBall accessory and LightBulb service , as that is a standard option when using the HomeKit Accessory Simulator. Our characteristics for the LightBulb service will be on and outlet in use. We write to the on characteristic in order to control the outlet to which the disco ball is connected. Outlet in Use serves as a status indicating whether the outlet is actually powered on or not.
MyHome ➤ MainRoom ➤ DiscoBall ➤ LightBulb ➤ (on and outlet_in_use)
Problem
We have our iHome Control AC Power switch , as shown earlier in Figure 16-2, but how do we work with it? Moreover, upon opening the box we find the contents include nothing more than the product itself and a simple card telling us to download the associated app. But what we want to do is work with this product.
Solution
Let’s work through building our HomeKit app and see what happens. For early testing, use the Xcode simulator, as this gives you better control over resets, which we’ll need shortly.
First, we have to instantiate an HMHomeManager object to manage our home. Because our app should be extremely simple, we’ll do everything inside the ViewController class viewDidLoad method in the ViewController.swift file.
You need to do four things:
Import the HomeKit framework.
Make sure the class conforms to the HMHomeManagerDelegate protocol.
Instantiate an HMHomeManager object.
Set the manager’s delegate.
All this can be seen clearly in Listing 16-1.
Listing 16-1. Adding an HMHomeManager to Our ViewController
//
// ViewController.swift
// DiscoBall
//
// Created by Molly Maskrey on 12/17/15.
// Copyright © 2015 Global Tek Labs. All rights reserved.
//
import UIKit
import HomeKit
class ViewController: UIViewController, HMHomeManager Delegate{
@IBOutlet weak var deviceIndicator: UILabel!
@IBOutlet weak var switchStatus: UIButton!
@IBAction func activateDevice(sender: AnyObject) {
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//
// Create a home manager
//
let manager = HMHomeManager()
manager.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Let’s build this to the simulator and see what happens. You should see the alert shown in Figure 16-11 asking you, the user, to allow your app to access accessory data. For this you would click OK, as we want to move on from here. However, a common situation happens when you select Don’t Allow because you want to make a few more changes or maybe just by accident. What you will find, in many cases, is that restarting the app does not show this dialog. That is, you’ve selected the option to preclude this app from accessing your home or accessory data and iOS will remember this. Deleting the app, cleaning the project, and even rebooting your iPhone won’t usually fix this. So, what do you do?
Figure 16-11. On initial launch iOS will confirm with you as to whether you want to allow the app to access your home database
Go to the Settings app and scroll down to find the DiscoBall app and select it. You will, of course, need to have the app built and on your device in case you just deleted it. You should see something similar to Figure 16-12 with the switch set to off. Simply set the switch to on (Figure 16-13), and now your app will have access to your home accessory data. Let’s move on to the next step.
Figure 16-12. Home data access is off for your app
Figure 16-13. Home data access is on for your app
Add a Home
Now that we have our manager instantiated, let’s try adding our home. To do so we use the addHomeWithName method as shown in Listing 16-2. However, running this will return the message:
Error happened.. message: Missing entitlement for API
Listing 16-2. Simple Call to addHomeWithName in Our viewDidLoad Method
//
// Add our Home
//
manager.addHomeWithName("MyHome", completionHandler: {(home, error) in
print("Trying to add MyHome")
if error != nil{
print("Error happened.. message: (error!.localizedDescription)")
}
})
Problem
Trying the simplest way to add a home to our HomeKit database returned a Missing Entitlement error.
Solution
Because HomeKit allows access to sensitive personal information, you need to specifically add the HomeKit entitlement to the capabilities of the app target.
In the Xcode Project Navigator, choose the target and go to the Capabilities section (Figure 16-14).
Figure 16-14. Add the HomeKit entitlements to your app in the Capabilities section
Scroll down to HomeKit and flip the switch to ON.
Rebuilding and testing the app now should eliminate the entitlements error. One of the things you might want to be aware of when early testing your app is that after adding a home to the database, trying to add it a second time will return an error.
Problem
After making some corrections to your app, you can no longer add your home to the database. You see the localized error description: “Home with similar name exists.”
Solution
In the short-term while testing, the easiest thing would be to use the “Reset Content and Settings. . .” option of the simulator, as shown in Figure 16-15.
Figure 16-15. “Reset Content and Settings…” provides an easy way to reset everything on your simulator so as to test adding a new home. However, this will require you to reallow the app to access home data as shown earlier
The better solution, of course, is to handle the error condition in the app. And while it is an error in the sense that the call did not add our home because it was already there, it’s not going to crash our app or mess up what we’re trying to do. Remember, all we want to do is to add our home.
That said, let’s take things a couple steps further in our code. We’ll not only add our home, but also add a room to the home MainRoom and make our home the primary. And, of course, we’ll add checks as to what is returned by our attempts to add a home and a room. Listing 16-3 shows the code handling these three simple functions. Note that error code 32 reflects the condition returned by the addHomeWithName method and indicates the home provided is already in the database. Similarly, error code 13 means that the room specified is already in the home. I’ve added numerous print() statements so that you can see what happens in the console as the code executes.
Listing 16-3. Our Complete Code to Add a Room and Our Home, and Make It the Primary
//
// Add our Home
//
func addMyHome() {
manager.addHomeWithName(kmyHome, completionHandler: {(home, error) -> Void in
print("Trying to add MyHome")
if error == nil || error?.code == 32 { // error code 32 means the home is already in the database
if error == nil {
print("MyHome added to database")
self.primaryHome = home
} else {
print("MyHome already in database")
}
print("…either way, we can add a room to the home now")
//
// We've stored the house we added as our primary so we'll
// now add a room to it
//
if self.primaryHome != nil {
self.primaryHome!.addRoomWithName(self.kmainRoom, completionHandler: { (room, error) -> Void in
if error == nil || error?.code == 13 { // error code 13 means room already in home
if error == nil {
print("Room added to Home")
} else {
print("Room ALREADY there")
}
//
// This might seem tricky, but we have to tell HomeKit to make this
// our primary home as well. Previously, we just essentially called it that
// but we have to do it in code so the database is "correct"
//
self.manager.updatePrimaryHome(self.primaryHome!, completionHandler: {(error) -> Void in
if error == nil {
print("(self.primaryHome!.name) now defined as the primary home")
} else {
print("ERROR: setting primary home: (error)")
}
})
} else {
print("ERROR: attempting to add room. (error!.localizedDescription)")
}
})
} else {
print("ERROR: For some reason our global primary home was not set")
}
} else {
print("ERROR: attempting to add home: (error)")
}
})
}
//
// End of setting up our simple one room home
// Note that we didn't do anything with accessories. We will deal with those separately.
//
Hierarchical Differences
Earlier I showed our top-down hierarchy, starting with homes. In our case, the primary home is at the top and lower levels contain with each of the characteristics for a service within our accessory. This structure is pretty straightforward, and we fairly easily grasp the concept. Implementation tells a different story. At first pass, when confronted with a problem like this, we construct this intricate series of if-then-else statements, which works fine for a couple levels, but with a five-tier problem it’s just too unwieldy.
The trick is to logically break it down. Think of it this way: there might be five levels in the hierarchy, but you could classify them into two categories. The “holders of the object” are the top layers: the room and the home. The “objects being held”, i.e., the accessories, comprise the bottom layers: the accessory, the services, and the characteristics. Work on each of these subsections and things will seem a lot clearer.
I wish there were a logical way to deconstruct a HomeKit application that made it clear and simple. Unfortunately, I haven’t found any, and I don’t profess to be brilliant enough to do so. What I’ve tried to do is minimize all the superfluous “stuff,” the noise, if you will, and focus on the key elements. In the previous sections you saw the basics of the top sub-hierarchy, the containers. We’ll dive into the accessory code shortly, but first I want to quickly cover our key delegation issues.
HomeKit Delegation
Listing 16-4 shows the definition line of our ViewController class indicating that it should conform to three different delegate protocols: HomeManager, AccessoryBrowser, and Accessory.
Listing 16-4. Our View Controller Conforms to Three Delegate Protocols
class ViewController: UIViewController, HMHomeManagerDelegate, HMAccessoryBrowserDelegate, HMAccessoryDelegate{
HMHomeManagerDelegate
The HMHomeManagerDelegateprotocol allows you to track changes to your collection of homes with four methods:
1. homeManager(_:didAddHome:)
2. homeManager(_:didRemoveHome:)
3. homeManagerDidUpdateHomes(_:)
4. homeManagerDidUpdatePrimaryHome(_:)
In our code, I’ve implemented three of these and excluded didRemoveHome since we’re not going that far with our example. If you get the first three, the fourth one should be obvious. Listing 16-5 shows two of the methods where all we do is to print to the log when the method is called. This is always a good idea in your early stages of development so you can keep track of what’s going on in case anything funky seems to be happening. Listing 16-6 shows how we handle things when a change to our homes database occurs through our implementation of the homeManagerDidUpdateHomes method. This is a very important method because, after starting your app, until this method is called the homes database shouldn’t be considered as valid.
Listing 16-5. Adding Simple print() Calls to Track What’s Happening with Our Delegate Methods
func homeManagerDidUpdatePrimaryHome(manager: HMHomeManager) {
print("homeManagerDidUpdatePrimaryHome called")
}
func homeManager( manager: HMHomeManager,
didAddHome home: HMHome) {
print("Home Manager added a home")
}
Listing 16-6. Handling Notification of Changes to Our Home Database
func homeManagerDidUpdateHomes(manager: HMHomeManager) {
print("Home Manager updated the homes database")
for home in manager.homes as [HMHome] {
if home.name == kmyHome {
self.primaryHome = home
print("Setting primaryHome to MyHome")
}
}
}
HMAccessoryBrowserDelegate
Similar to how the HMHomeManagerDelegate protocol allows you to track changes in your collection of homes, the HMAccessoryBrowserDelegateprotocol permits tracking of when accessories become available or go missing. This could be when you power on an accessory, initialize a new accessory, remove an accessory, and so on. The protocol offers two methods that should be pretty obvious as to their intent:
1. accessoryBrowser(_:didFindNewAccessory:)
2. accessoryBrowser(_:didRemoveNewAccessory:)
Listing 16-7 shows how we implement the :didFindNewAccessory method in our code. We talk more about this in the section on managing our accessories.
Listing 16-7. Method Called When the Accessory Manager Locates a New Accessory
func accessoryBrowser(browser: HMAccessoryBrowser,
didFindNewAccessory accessory: HMAccessory) {
print("Found a new accessory: (accessory)")
if accessory.name == kdiscoball {
self.discoballAccessory = accessory
self.accessoryFound.text = "Found (accessory.name)"
}
}
HMAccessoryDelegate
While this sounds similar to the last protocol, the HMAccessoryDelegate protocol provides methods associated with monitoring any changes to specific accessories. Once we have an accessory object that we wish to monitor, we set its delegate and use the following methods for monitoring:
1. accessoryDidUpdateName(_:)
2. accessoryDidUpdateReachability(_:)
3. accessoryDidUpdateServices(_:)
4. accessory(_:didUpdateNameForService:)
5. accessory(_:service:didUpdateValueForCharacteristic:)
6. accessory(_:didUpdateAssociatedServiceTypeForService:)
The key method we’re concerned with is the :didUpdateValueForCharacteristic method. So, for a particular characteristic of a particular service in a specific accessory, we get this method call. The provided parameters include enough information to tell us where this change was applied. Listing 16-8 shows how we implemented this method. One important note about this is that if we change the state of the power to the disco ball remotely, as we’ll see in the complete code later, this method may not get called, since the hardware may not supply the status information about the local control interaction actuated remotely. Some devices will do this properly, but the fact is, HomeKit accessories are in the early stages of deployment, so don’t expect perfection in every case. In fact, for me, I never saw it called unless I changed the power on the accessory directly, either through the accessory simulator or on an actual device.
Listing 16-8. Detecting Changes to the Power Supplied to Our Disco Ball
func accessory(accessory: HMAccessory, service: HMService, didUpdateValueForCharacteristic characteristic: HMCharacteristic) {
print("(accessory.name): (characteristic.metadata!.manufacturerDescription!) has changed to (characteristic.value!)")
if accessory.name == "DiscoBall" && characteristic.metadata!.manufacturerDescription == "Power State" {
//
// NOTE: in Apple's documentation, this value is shown in the list as a string, but
// in the details as a Bool. It works as a Bool as you see here...
//
if characteristic.value as! Bool == true {
self.isDiscoBallPowerOn = true
self.deviceIndicator.text = "Disco Ball ON"
self.switchStatus.setTitle("Stop Party", forState: UIControlState.Normal)
self.switchStatus.titleLabel?.textAlignment = NSTextAlignment.Center
} else {
self.isDiscoBallPowerOn = false
self.deviceIndicator.text = "Disco Ball OFF"
self.switchStatus.setTitle("Start Party", forState: UIControlState.Normal)
self.switchStatus.titleLabel?.textAlignment = NSTextAlignment.Center
}
}
}
Accessory Management
Working with any remote hardware device, we’ll need to be able to find the right piece of equipment, get access to it, see what services it offers and figure out what information we can read from and write to the device. We’ll want to be able to securely connect, disconnect, and determine if some interruption occurred while we plan to use the device. This is all part of managing the accessories that we’ll be using.
Problem
How do we find accessories in the first place?
Solution
We use the HMAccessoryBrowser to start and stop our search and the HMAccessoryBrowserDelegate protocol methods to handle changes in the accessory landscape.
As we’re interested in just our disco ball power controller, the first thing we need to do is to find it. So, step one, after making sure we conform to the HMAccessoryBrowserDelegate protocol, is to create an HMAccessoryBrowser:
let browser = HMAccessoryBrowser()
This creates the object that will search for all accessories within our network, but only after we tell it to do so. For that, we tell our browser to start searching for accessories:
browser.startSearchingForNewAccessories()
Because searching with our device’s radio can be a power-hungry operation, we need to limit the amount of time we do this by stopping the search when we’re done, i.e., after we’ve found the accessory we’re looking for. Listing 16-9 shows the helper method I created to stop the accessory search and to conserve power in our mobile device.
Listing 16-9. Stop Searching for Accessories Helper Method
func stopSearching() {
print("stopSearching")
browser.stopSearchingForNewAccessories()
}
But how do we know when to call this method? There are two reasonable answers. The first one is that we call it whenever we’ve found what we’re looking for. In our case, as soon as we locate the disco ball power controller we can stop searching. But what if we don’t find it, or, a more likely scenario, what if we’re looking for all available accessories? We don’t want to stop after the first one. We also don’t want to have to tell our app what or how many to look for. It needs to be dynamic. This is where you have to make a decision and choose a timeout:
NSTimer.scheduledTimerWithTimeInterval(20.0, target: self, selector: "stopSearching", userInfo: nil, repeats: false)
This instruction will start a timer for 20 seconds, and at the end of that will execute the stopSearching() method contained within this code file (target: self). How do you know how long is long enough? You don’t. Trial and error, my friends. I’ve found that 20 seconds works, but many other sections of code that I’ve looked over use a shorter time, about 10 seconds. Again, it’s up to you.
What about if you have a large number of accessories? Setting a few up could run past whatever time you set. The trick I use is to restart the timer after you locate and handle each accessory. One way to do this would be to stop then restart the timer in the didFindNewAccessory delegate method. That way, after the actual last accessory is set up, you give it one more 20 second (or whatever time you choose) try to make sure you’ve exhausted the search.
What about if things change, like an accessory is dropped or removed? Similarly, you may want to create a periodic startSearching() helper method as well that gets called periodically, maybe every few minutes or so. In that case, within that periodic search method, you might only search for 5 seconds to keep radio usage contained in order to prolong battery life.
Problem
We need an accessory to work with. What do we do?
Solution
There are, of course, two options: use a real accessory or use the HomeKit Accessory Simulator. We’ll do both.
HomeKit Accessory Simulator
First, you must download the latest version of the simulator . I’ve found when using older versions of the simulator that the created accessories will not connect with the app, even if they’re both running on your Mac. To get the latest version, go to the capabilities section of your project, as shown in Figure 16-16, scroll down to where we enabled the HomeKit entitlement earlier, and click on the Download HomeKit Simulator… button. Place it in a convenient spot in your filesystem—I put mine in Applications, but then made a shortcut for the desktop.
Figure 16-16. To avoid problems, make sure to download the latest version of the HomeKit Accessory Simulator from the project capabilities section of Xcode
Start the HomeKit Accessory Simulator, click the ‘+’ at the bottom left, and select “New Accessory …” (Figure 16-17). Then configure the accessory, adding the name as DiscoBall (Figure 16-18). For manufacturer and model you can put in anything you want. The serial number will likely be autofilled by the simulator. When complete, it should look something like Figure 16-19. You should see at the top a setup code you’ll need to use when attaching the accessory during execution of your app. iOS will automatically pop up a dialog for you to enter this code at the appropriate time during execution.
Figure 16-17. Add a new accessory to the HomeKit Accessory Simulator
Figure 16-18. Configure the name of the accessory as DiscoBall so we’ll know what to look for in the code
Figure 16-19. After configuring your simulated accessory
Next, we need to add our LightBulb service to the accessory, so click on the Add Service. . . button (Figure 16-20), then go to the drop-down and select “Outlet” since our service will be to allow power to our disco ball or not. Selecting “Outlet” will give us the default characteristics we need in our code. This last step will complete our simulator accessory setup, and you should see something that looks like Figure 16-21. Make sure that the IP slide switch in the upper right-hand side is on, otherwise the accessory won’t be able to be seen on the network.
Figure 16-20. Configure an outlet service named LightBulb for our disco ball accessory
Figure 16-21. Our simulated DiscoBall accessory
Note
An important point to note here is that the simulator allows us to completely configure our accessory from scratch. When using an actual hardware accessory such as the iHome accessory described in the next section, we’ll need to locate and identify all the information that the manufacturer set up in the hardware and firmware of the device. We’ll see how to access that information shortly.
iHome Control SmartPlug
Working with real HomeKit accessories , especially those produced early in the program, can sometimes be a challenge. Many of the accessory features built into the firmware of the device will not work as expected or may be intermittent in their operation. Such was the case with the iHome Control SmartPlug, one of the early HomeKit devices available at the time of this writing. While there were firmware updates available, for my work at least, none of them corrected the issue with that first device. Later, using a second-generation iHome device, I had no such issues. But, with a little effort, I was able to get it to function with minimal changes to the code from what we wrote to work with the HomeKit Accessory Simulator. In fact, I only changed two lines of code and added one, with only one change being absolutely required to get the accessory to function.
Verify Accessory Functions Properly
First, using the supplied instructions and the iHome app, which can be found on the Apple App Store, I verified that the device worked properly. In Figure 16-22 you can see the plug and app in the OFF position. In Figure 16-23 the indicator on the app is darker, indicating that the button was pressed to turn on the device. You can also see that the H indicator on the actual device—in the lower-left bottom—is illuminated, showing that power is flowing.
Figure 16-22. Switch power off using iHome-supplied app
Figure 16-23. Switch power on, with indications shown on the actual device and the button on the iHome native app
Reset Accessory for HomeKit
One of the early issues I discovered was that an actual hardware accessory designed for HomeKit may not be seen by other apps if it has been paired up with a different app. What this meant was that, when I tested the device using the iHome native app, then tried to run our disco ball app without doing anything except for resetting the iPhone simulator, the hardware accessory was never seen. So I had to reset the device.
Resetting the device will be different for each accessory you use. Plug the iHome SmartPlug into an AC outlet, then press and hold the button in the upper right (near the green LED) for at least 12 seconds. The LED will flash, and then you use the iHome app to continue setting it up.
This may seem counterintuitive, since I just said that if the device is connected with the iHome app we might not be able to see it. But, because we reset the actual hardware, it has no wireless connection. So what we do is use the iHome app to set it up, without actually completing the setup. In other words, we exit the iHome app before everything is complete. We just want to get far enough along in the process so the hardware accessory sees our WiFi network. So, after resetting the device and starting the iHome app, you’ll select “Add a New Device ,” as shown in Figure 16-24. At this point you may want to go through the 12-second reset again just to be sure. Tapping Next in the upper right corner of the app starts the search for any compatible accessories (Figure 16-25).
Figure 16-24. After verifying that the accessory works properly with the native app, use the manufacturer’s instructions to reset the device to work with our disco ball HomeKit project
Figure 16-25. The app uses the iPhone hardware to search for compatible accessories within range
Once the HomeKit app finds available devices, it will allow you to select them and set them up, as shown in Figure 16-26. Here’s where things really begin to deviate enough to affect our Swift project code. Note that the accessory is named iHome SmartPlug-XXXXXX, where XXXXXX will be unique to the specific unit, i.e., its serial number. What we’ve done to this point is use the iHome app on our iPhone to locate the hardware accessory. Right now, it’s not connected to any WiFi network, so it can’t know how it’s being configured, much less know about any network password protection. So, we tap Continue and move on with our setup (Figure 16-27).
Figure 16-26. Select the appropriate accessory and tap Continue to proceed
Figure 16-27. Connect the accessory to your local WiFi network
As you can see, I’ve set up a temporary company network called GlobalTekNet to which my Mac is connected and thus, my iPhone simulator is too. Here, of course, we’re working with WiFi networks, but HomeKit also works from your iOS device directly to accessories via Bluetooth Low Energy. This, in fact, was how we began the setup of the accessory.
Note in the middle of the screen in Figure 16-27 you can see the default name of the device. Here is where you would expect to be able to change the name—for example, to DiscoBall—and have it work with our existing code. But, for some reason, this did not work with this version of the iHome app or the latest accessory firmware. We will have to change the code so that the constant that previously pointed to DiscoBall now points to this default name. In a real-life scenario we’d actually just read the name of the accessory returned by the accessory browser and use that from the start, but for now I wanted to keep things as simple as I could, knowing full well that it might seem a little complicated. What I’ve shown is kind of the “tip of the iceberg” when dealing with HomeKit accessories and to thoroughly work with just a few devices, exploring their differences and handling exceptions, would take a complete book of its own.
Normally, this process goes pretty smoothly, and you should be presented with the screen seen in Figure 16-28. You are asked to enter the accessory code, which is usually printed on the actual device (accessory). The code can be entered manually, as we do in our project, or by holding the camera in front of the printed code so the software will automatically translate it to enter the information. HOWEVER, DO NOT ENTER OR SCAN A CODE AT THIS TIME. This is where you actually want to terminate the app. If you were planning to use the iHome app, you would go ahead and enter the code, add the accessory to a home, assign it to a room, and proceed with operation.
Figure 16-28. Screen showing that you need to enter the code , which is usually printed on the actual hardware accessory
At this point, the accessory, the iHome SmartPlug, is set up for us to use with our project and the iPhone simulator.
Problem
How do we access the accessory’s services and characteristics? Similarly, how do we know what services and characteristics the accessory offers?
Solution
The manufacturer may include, either in the packaging material or in online instructions, the information needed to access the device’s services and characteristics. It’s pretty simple if using the HomeKit Accessory Simulator, since we completely defined services and characteristics, including what they were called.
Earlier, in Listing 16-8, I showed how you can see when a characteristic has changed and then take the necessary action. This, as we discussed, applies mainly to changes that are made manually at the accessory device and not changes we perform in our code. Because we were discussing the various delegation aspects of HomeKit, we had not yet gotten to how we found the names of the services and characteristics. We kind of left a “hole” in our knowledge that I will address now.
The way I address this in our project is to create functions that list the services and characteristics of our accessory. In those functions I use a simple print() statement to show me the names of those items. In Listing 16-9 you can see in the addMyAccessory() function the call to the first of these methods. The call, self.listServices(self.discoballAccessory!), begins the process of getting the information we need. Listing 16-10 shows the implementation of that function.
Listing 16-9. Function to List the Services Provided by a Discovered Accessory
func addMyAccessory() {
if self.discoballAccessory != nil {
print("addMyAccessory: discoball: (self.discoballAccessory)")
self.primaryHome?.addAccessory(self.discoballAccessory!, completionHandler: {(error) in
if error != nil {
print("Could not add (self.discoballAccessory!.name) to (self.primaryHome!.name)")
print("error: (error)")
} else {
print("Successfully added (self.discoballAccessory!.name) to (self.primaryHome!.name)")
//
// Set the delegate for the accessory so we can see updates to it
//
self.discoballAccessory!.delegate = self
self.accessoryStatus.text = "(self.discoballAccessory!.name) CONNECTED"
//
// CALL THE FUNCTION TO LIST THE SERVICES FOR THIS ACCESSORY
//
print("Services offerred...")
self.listServices(self.discoballAccessory!)
//
// No need to continue searching
//
self.stopSearching()
}
})
} else {
self.accessoryStatus.text = "No accessory identified to attach"
}
}
Listing 16-10. This Function Lists the Available Services for an Accessory
func listServices(accessory : HMAccessory) {
for service in accessory.services {
//
// Go through any available services, find the Outlet service, which
// is just a HomeKit standard name that we've called our party service,
// and list all the characteristics for our "Party" service.
//
if service.serviceType == HMServiceTypeOutlet {
self.services.append(service as HMService)
print("found (service.name) of type HMServiceTypeOutlet service for (accessory.name)")
self.partyService = service
listCharacteristics(service)
}
}
}
Without showing the large listCharacteristics function, Listing 16-11 shows the key part of the code where we iterate over each characteristic in a service and display its name.
Listing 16-11. Critical Portion of the listCharacteristics Function
func listCharacteristics(service: HMService) {
for characteristic_item in service.characteristics {
characteristics.append(characteristic_item as HMCharacteristic)
print("value (characteristic_item.value!) : (characteristic_item.metadata!.manufacturerDescription!)")
//
// Notification of changes in characteristics are NOT automatically enabled, you have to do this yourself
//
if characteristic_item.properties.contains(HMCharacteristicPropertySupportsEventNotification) {
characteristic_item .enableNotification(true, completionHandler: {(error) in
if error != nil {
print("Error while enabling notification for (characteristic_item.metadata?.manufacturerDescription)")
}
else {
print("(characteristic_item.metadata?.manufacturerDescription) Notification enabled")
}
})
}
Note
The function shown in Listing 16-11 is incomplete for brevity.
Summary
In this somewhat lengthy chapter, we have discussed key elements of using Apple HomeKit with both simulated and actual hardware accessories. We’ve touched on just enough aspects of HomeKit to get you up and running; for example, controlling your own disco ball or anything else. Our focus was on a power control switch, specifically the iHome Smart Plug, but the methods and functions we’ve discussed work with any other type of accessory you might come across in the near future.
Figure 16-29 show the user interface screen our project presents, allowing us to turn our disco party ball on or off, starting or stopping the party.
Figure 16-29. The user interface for turning on or off our accessory. Tapping the “Start Party” button turns on our disco ball while “Stop Party” turns it off.
Listing 16-12 shows the complete code for the ViewController.swift file where we discover and attach our accessory, list our services and methods, and control our actual HomeKit accessory.
Listing 16-12. Complete ViewController.swift Class File
//
// ViewController.swift
// DiscoBall
//
// Created by Molly Maskrey on 12/17/15.
// Copyright © 2015 Global Tek Labs. All rights reserved.
//
import UIKit
import HomeKit
class ViewController: UIViewController, HMHomeManagerDelegate, HMAccessoryBrowserDelegate, HMAccessoryDelegate{
// convenience constants
let kmyHome = "MyHome"
let kmainRoom = "MainRoom"
// If using the simulator...
// let kdiscoball = "DiscoBall"
// If using the actual iHome SmartPlug
// Note that the serial # will likely be different
// on a device that you may use
let kdiscoball = "iHome SmartPlug-1ABCFF"
let kdiscoballDisplayName = "DiscoBall"
// "Global" variables
let manager = HMHomeManager()
let browser = HMAccessoryBrowser()
var accessories = [HMAccessory]()
var services = [HMService]()
var characteristics = [HMCharacteristic]()
var discoBallPowerSwitch: HMCharacteristic?
var isDiscoBallPowerOn : Bool = false
// Some local "testing" variables
var primaryHome : HMHome?
var discoballAccessory : HMAccessory?
var partyService : HMService?
// OUTLET: Indicates if ball is on or off
// if using the HKAccessory Similator this will
// be the "Outlet in Use" characteristic
@IBOutlet weak var deviceIndicator: UILabel!
// OUTLET: Used to change the name on the top
// UI button...the one that controls our disco ball
// e.g., when the ball is on, we want the switch to
// read "OFF" and vice versa
@IBOutlet weak var switchStatus: UIButton!
// ACTION: This method is called when the top button
// is activated, used to turn the disco ball (accessory
// outlet) ON or OFF
@IBAction func activateDevice(sender: AnyObject) {
self.activateDiscoBall()
}
// OUTLET: A simple, mostly diagnostic label we use
// to display information about the located accessory.
// It lets the user know the app has found our disco ball
@IBOutlet weak var accessoryFound: UILabel!
// OUTLET: A simple, mostly diagnostic label used
// to indicate whether or not the accessory is connected
// to the app.
@IBOutlet weak var accessoryStatus: UILabel!
// ACTION: This method is called when the bottom button "ATTACH"
// is pressed to make an attempt to connect the located accessory
// to the app
@IBAction func attachAccessory(sender: AnyObject) {
self.addMyAccessory()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
manager.delegate = self
print("Home Manager created and delegate set to self")
browser.delegate = self
print("Accessory Browser created and delegate set to self")
// Go see if there are any accessories in the area
browser.startSearchingForNewAccessories()
// Only do it for a short period as it will drain the battery
NSTimer.scheduledTimerWithTimeInterval(20.0, target: self, selector: "stopSearching", userInfo: nil, repeats: false)
for home in self.manager.homes as [HMHome] {
if home.name == kmyHome {
print("Found MyHome")
}
}
self.switchStatus.enabled = false
self.switchStatus.titleLabel!.text = "Disabled"
// call the addMyHome convenience method
self.addMyHome()
}
// Our Helper Methods
func stopSearching() {
print("stopSearching")
browser.stopSearchingForNewAccessories()
}
//
// Add our Home
//
func addMyHome() {
manager.addHomeWithName(kmyHome, completionHandler: {(home, error) -> Void in
print("Trying to add MyHome")
if error == nil || error?.code == 32 { // error code 32 means the home is already in the database
if error == nil {
print("MyHome added to database")
self.primaryHome = home
} else {
print("MyHome already in database")
}
print("…either way, we can add a room to the home now")
//
// We've stored the house we added as our primary so we'll
// now add a room to it
//
if self.primaryHome != nil {
self.primaryHome!.addRoomWithName(self.kmainRoom, completionHandler: { (room, error) -> Void in
if error == nil || error?.code == 13 { // error code 13 means room already in home
if error == nil {
print("Room added to Home")
} else {
print("Room ALREADY there")
}
//
// This might seem tricky, but we have to tell HomeKit to make this
// our primary home as well. Previously, we just essentially called it that
// but we have to do it in code so the database is "correct."
//
self.manager.updatePrimaryHome(self.primaryHome!, completionHandler: {(error) -> Void in
if error == nil {
print("(self.primaryHome!.name) now defined as the primary home")
} else {
print("ERROR: setting primary home: (error)")
}
})
} else {
print("ERROR: attempting to add room. (error!.localizedDescription)")
}
})
} else {
print("ERROR: For some reason our global primary home was not set")
}
} else {
print("ERROR: attempting to add home: (error)")
}
})
}
//
// End of setting up our simple one room home
// Note that we didn't do anything with accessories. We will deal with those separately.
//
//
// Stock method - no changes
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func homeManagerDidUpdateHomes(manager: HMHomeManager) {
print("Home Manager updated the homes database")
for home in manager.homes as [HMHome] {
if home.name == kmyHome {
self.primaryHome = home
print("Setting primaryHome to MyHome")
}
}
}
func homeManagerDidUpdatePrimaryHome(manager: HMHomeManager) {
print("homeManagerDidUpdatePrimaryHome called")
}
func homeManager( manager: HMHomeManager,
didAddHome home: HMHome) {
print("Home Manager added a home")
}
func accessoryBrowser(browser: HMAccessoryBrowser,
didFindNewAccessory accessory: HMAccessory) {
print("Found a new accessory: (accessory)")
if accessory.name == kdiscoball {
self.discoballAccessory = accessory
self.accessoryFound.text = "Found (self.kdiscoballDisplayName)"
}
}
func listServices(accessory : HMAccessory) {
for service in accessory.services {
//
// Go through any available services, find the Outlet service, which
// is just a HomeKit standard name that we've called our party service,
// and list all the characteristics for our "Party" service.
//
if service.serviceType == HMServiceTypeOutlet {
self.services.append(service as HMService)
print("found (service.name) of type HMServiceTypeOutlet service for (accessory.name)")
self.partyService = service
listCharacteristics(service)
}
}
}
func listCharacteristics(service: HMService) {
for characteristic_item in service.characteristics {
characteristics.append(characteristic_item as HMCharacteristic)
print("value (characteristic_item.value!) : (characteristic_item.metadata!.manufacturerDescription!)")
//
// Notification of changes in characteristics are NOT automatically enabled; you have to do this yourself
//
if characteristic_item.properties.contains(HMCharacteristicPropertySupportsEventNotification) {
characteristic_item .enableNotification(true, completionHandler: {(error) in
if error != nil {
print("Error while enabling notification for (characteristic_item.metadata?.manufacturerDescription)")
}
else {
print("(characteristic_item.metadata?.manufacturerDescription) Notification enabled")
}
})
}
//
// let's also set our main power switch to a global for access by our UI button
//
if characteristic_item.metadata!.manufacturerDescription == "Power State" {
print("Setting up our global discoBallPowerSwitch")
self.discoBallPowerSwitch = characteristic_item
//
// Read the switch to determine if it is on or not and set the
// power switch and button indicators appropriately, then
// enable the power button
//
// First make sure the characteristic is readable
if self.discoBallPowerSwitch!.properties.contains(HMCharacteristicPropertyReadable) {
// Then prepare the value for reading
self.discoBallPowerSwitch?.readValueWithCompletionHandler({(error) in
if (error) != nil {
print("Error occured reading the value of the Power Switch")
} else {
// Read the value
self.isDiscoBallPowerOn = self.discoBallPowerSwitch!.value as! Bool
print("Power On readback = (self.discoBallPowerSwitch!.value as! Bool)")
print(self.isDiscoBallPowerOn)
}
})
} else {
// Just as an example of the alternative
print("NO HMCharacteristicPropertyReadable PROPERTY")
}
// enable the button
self.switchStatus.enabled = true
// Set the state of the label and power button depending on what we read back
if self.isDiscoBallPowerOn == true {
print("Turning indicator to ON")
self.deviceIndicator.text = "Disco Ball ON"
self.switchStatus.setTitle("Stop Party", forState: UIControlState.Normal)
self.switchStatus.titleLabel?.textAlignment = NSTextAlignment.Center
} else {
print("Turning indicator to OFF")
self.deviceIndicator.text = "Disco Ball OFF"
self.switchStatus.setTitle("Start Party", forState: UIControlState.Normal)
self.switchStatus.titleLabel?.textAlignment = NSTextAlignment.Center
}
}
}
}
func accessoryBrowser(browser: HMAccessoryBrowser,
didRemoveNewAccessory accessory: HMAccessory) {
print("didRemoveNewAccessory: (accessory.name)")
}
// ACCESSORY DELEGATE METHODS
func accessory(accessory: HMAccessory, service: HMService, didUpdateValueForCharacteristic characteristic: HMCharacteristic) {
print("(accessory.name): (characteristic.metadata!.manufacturerDescription!) has changed to (characteristic.value!)")
if accessory.name == kdiscoball && characteristic.metadata!.manufacturerDescription == "Power State" {
//
// NOTE: in Apple's documentation, this value is shown in the list as a string, but
// in the details as a Bool. It works as a Bool as you see here.
//
if characteristic.value as! Bool == true {
self.isDiscoBallPowerOn = true
self.deviceIndicator.text = "Disco Ball ON"
self.switchStatus.setTitle("Stop Party", forState: UIControlState.Normal)
self.switchStatus.titleLabel?.textAlignment = NSTextAlignment.Center
} else {
self.isDiscoBallPowerOn = false
self.deviceIndicator.text = "Disco Ball OFF"
self.switchStatus.setTitle("Start Party", forState: UIControlState.Normal)
self.switchStatus.titleLabel?.textAlignment = NSTextAlignment.Center
}
}
}
// CONVENIENCE METHODS SUPPORTING BUTTON ACTIONS
func addMyAccessory() {
if self.discoballAccessory != nil {
print("addMyAccessory: discoball: (self.discoballAccessory)")
self.primaryHome?.addAccessory(self.discoballAccessory!, completionHandler: {(error) in
if error != nil {
print("Could not add (self.discoballAccessory!.name) to (self.primaryHome!.name)")
print("error: (error)")
} else {
print("Successfully added (self.discoballAccessory!.name) to (self.primaryHome!.name)")
//
// Set the delegate for the accessory so we can see updates to it
//
self.discoballAccessory!.delegate = self
self.accessoryStatus.text = "(self.discoballAccessory!.name) CONNECTED"
//
// CALL THE FUNCTION TO LIST THE SERVICES FOR THIS ACCESSORY
//
print("Services offerred...")
self.listServices(self.discoballAccessory!)
//
// No need to continue searching
//
self.stopSearching()
}
})
} else {
self.accessoryStatus.text = "No accessory identified to attach"
}
}
// Bottom Button
func activateDiscoBall() {
// print("User pressed (self.switchStatus.titleLabel!.text) button")
if self.discoBallPowerSwitch != nil {
if isDiscoBallPowerOn == false {
self.discoBallPowerSwitch?.writeValue(true, completionHandler: { (error) in
if error != nil {
print("Error setting power to Disco Ball: (error)")
} else {
// print("Power to Disco Ball Set Successfully")
self.isDiscoBallPowerOn = true
self.deviceIndicator.text = "Disco Ball ON"
self.switchStatus.setTitle("Stop Party", forState: UIControlState.Normal)
self.switchStatus.titleLabel!.textAlignment = NSTextAlignment.Center
}
})
} else {
self.discoBallPowerSwitch?.writeValue(false, completionHandler: { (error) in
if error != nil {
print("Error turning power OFF to Disco Ball: (error)")
} else {
// print("Power to Disco Ball now OFF")
self.isDiscoBallPowerOn = false
self.deviceIndicator.text = "Disco Ball OFF"
self.switchStatus.setTitle("Start Party", forState: UIControlState.Normal)
self.switchStatus.titleLabel!.textAlignment = NSTextAlignment.Center
}
})
}
}
}
}