One of the goals of Thuis is optimizing it's own rule engine based on actual activity in the house. Although time is to short to actually start the optimization, with this blog we'll start collecting presence data using iBeacons. As a bonus we solve the Welcome Home use case!
iBeacons monitoring, ranging and indoor location
iBeacons (in my case by Estimote) are little Bluetooth LE devices which on regular intervals broadcast their identifiers. Mobile apps can use this as a way of determining their location. They can be used in several ways. The three most common ways are:
- Monitoring – the app gets a signal whenever it enters the region defined by one or multiple beacons (aka when it receives a packet sent by the them). The app gets a small amount of time to do some work, for example notify the user. This works even when the app is terminated.
- Ranging – When the app is active it can listen to all beacons around and based on the signal strengths make an approximation of the distance between the beacon and the phone.
- Indoor location – Estimote developed another layer on top of ranging. Based on several beacons (at least one per wall) and the sensors in the phone it can determine its location within a space. This works quite precise, but it uses more energy than the other methods. Also it can only be used when the app is in use.
I experimented with all three to see which way is most usable for Thuis. Indoor location was a bit of a hassle to set up (it involves walking lots of circles close to the walls through the house, which is not easy when there is furniture!), but the result is impressive. Not being able to use it in the background made me choose monitoring as the main technique.
ps: for the Android users out here: although I'm talking about iBeacons (by Apple) there are very similar technologies available for other platforms. For Android that's Eddystone, which is the Estimote beacons can broadcast as well.
Presence monitoring
To optimize the Thuis rule engine it needs to have knowledge of what happens around. One of the useful things to now is who is where and when. We'll use presence monitoring to determine who is currently where in the house.
Hardware set up
We want to know who is where on a room-level, so who is in which room. To identify different rooms we'll deploy a iBeacon is every room we're interested in. We're using 3 Estimote Location Beacons and 3 older Estimote Proximity Beacons. In the entrance, living room, bedroom, kitchen and office 1 beacon is installed at the center of the outer wall (as far as possible from other beacons). The signal strength of each of them is tweaked ranging from -30dBm (±1.5 meter) in the smallest rooms to -20dBm (±3.5m) in the bigger rooms. Each beacon is configured to broadcast its ID approximately 3 times per second. These values will likely be optimized further over the coming period.
Initial version
I started by using just region monitoring as this is very easy to implement. To make my life easier I started with some structs describing the deployed beacons together with some helper functions. The interface of the struct looks like:
struct BeaconID : Equatable, CustomStringConvertible, Hashable { let proximityUUID: UUID let major: CLBeaconMajorValue? let minor: CLBeaconMinorValue? let identifier: String? init(proximityUUID: UUID, major: CLBeaconMajorValue?, minor: CLBeaconMinorValue?, identifier: String?) // more init methods var asBeaconRegion: CLBeaconRegion { get } } internal func ==(lhs: BeaconID, rhs: BeaconID) -> Bool extension CLBeacon { var beaconID: BeaconID? { get } }
And the beacons are defined as:
struct BeaconIDs { private static let uuidString = "B9407F30-F5F8-466E-AFF9-25556B57FE6D" static let all = [home, bedroom, office, living, kitchen, entrance] static let home = BeaconID(uuidString: uuidString, identifier: "home") static let bedroom = BeaconID(uuidString: uuidString, major: 23476, minor: 64333, identifier: "bedroom") static let office = BeaconID(uuidString: uuidString, major: 25568, minor: 21134, identifier: "office") static let living = BeaconID(uuidString: uuidString, major: 40474, minor: 19278, identifier: "living") static let kitchen = BeaconID(uuidString: uuidString, major: 16433, minor: 64211, identifier: "kitchen") static let entrance = BeaconID(uuidString: uuidString, major: 16433, minor: 21894, identifier: "entrance") static func of(identifier: String) -> BeaconID? { return all.first(where: { (beaconId) -> Bool in return beaconId.identifier == identifier }) } static func of(proximityUUID: UUID, major: CLBeaconMajorValue, minor: CLBeaconMinorValue) -> BeaconID? { return all.first(where: { (beaconId) -> Bool in return beaconId == BeaconID(proximityUUID: proximityUUID, major: major, minor: minor) }) } }
All code related to beacons takes place in the class BeaconManager
. It sets up monitoring in the init
method and implements the delegate methods for the ESTBeaconManagerDelegate
. It keeps some information about which is the current region for this phone and for each beacon when was the last time this phone entered or left the region. The initial version just logs information to the console based on activity.
class BeaconManager: NSObject, ESTBeaconManagerDelegate { private let beaconManager = ESTBeaconManager() private var currentPresence: BeaconID? private var lastLeftOrEntered: [BeaconID: Date] = [:] override init() { super.init() beaconManager.delegate = self beaconManager.requestAlwaysAuthorization() for beaconID in BeaconIDs.all { beaconManager.startMonitoring(for: beaconID.asBeaconRegion) } } func beaconManager(_ manager: Any, didDetermineState state: CLRegionState, for region: CLBeaconRegion) { guard let beaconID = BeaconIDs.of(identifier: region.identifier) else { return } print("State \(beaconID.identifier!): \(state.rawValue)") } func beaconManager(_ manager: Any, didEnter region: CLBeaconRegion) { guard let beaconID = BeaconIDs.of(identifier: region.identifier) else { return } currentPresence = beaconID lastLeftOrEntered[beaconID] = Date() if let timeIntervalSinceNow = lastLeftOrEntered[beaconID]?.timeIntervalSinceNow { print("Entered \(beaconID.identifier!) since \(timeIntervalSinceNow)") } } func beaconManager(_ manager: Any, didExitRegion region: CLBeaconRegion) { guard let beaconID = BeaconIDs.of(identifier: region.identifier) else { return } print("Left \(beaconID.identifier!)") lastLeftOrEntered[beaconID] = Date() if beaconID.identifier == BeaconIDs.home.identifier { currentPresence = nil } } }
This already works quite well, but as we're working with wireless signals there can be mistakes. For example a packet from another room reaches your phone and therefor your presence is adjusted. Or some packets get lost and cause your phone to think it left the region.
The latter is the reason that for resetting the currentPresence
to outside we use the home
region. This region consists all beacons, so the change of a false positive is smaller. It does however still happen every now and then.
Improving accuracy with ranging
To make presence monitoring more accurate we can combine monitoring with ranging. When the app is in the background and enters a region it's woken up and gets a small amount of time to do some work. We can use this time to start ranging beacons, and with the more detailed data about the beacons around us make a better approximation.
To start ranging we have to adjust the didEnter
method, instead of updating currentPresence
and lastLeftOrEntered
we start ranging: beaconManager.startRangingBeacons(in: BeaconIDs.home.asBeaconRegion)
. To receive the results we'll implement the corresponding delegate method in which we'll update the values:
func beaconManager(_ manager: Any, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) { guard let beaconID = beacons.first(where: {$0.proximity != .unknown})?.beaconID else { return } currentPresence = beaconID lastLeftOrEntered[beaconID] = Date() beaconManager.stopRangingBeacons(in: BeaconIDs.home.asBeaconRegion) }
Notice we'll take the first beacon (with a known proximity) from the given list. The SDK returns them ordered from close to far away, so we'll always use the closest beacon. Our value of currentPresence
got a lot more accurate now!
Based on how it performs I'll do some further optimizations. One thing I still have to do is make the values of currentPresence
and lastLeftOrEntered
persistent, so they will survive a termination of the app.
Publishing presence
As always we want to publish our data through MQTT, so the other Thuis nodes can use it as well. In [Pi IoT] Thuis #10: MQTT User Interface components for iOS we added MQTT to the app, so we can build on this. Whenever the currentPresence
value changes we'll have to publish a message. This means we have to update both the didRangeBeacons
method and the didExitRegion
method.
didRangeBeacons
is updated like this:
func beaconManager(_ manager: Any, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) { // ... if currentPresence != beaconID { MQTT.sharedInstance.publish(beaconID.identifier!, topic: "Thuis/presence/robin", retain: true) currentPresence = beaconID } // ... }
And to detect someone leaving the house we change didExitRegion
, here we only publish when the specific region is home
:
func beaconManager(_ manager: Any, didExitRegion region: CLBeaconRegion) { // ... if beaconID.identifier == BeaconIDs.home.identifier { MQTT.sharedInstance.publish("outside", topic: "Thuis/presence/robin", retain: true) currentPresence = nil } }
Note that the name in the topic is still static, I'll make it configurable in the app later.
Walking around slowly through the house gives the following MQTT messages:
$ mosquitto_sub -t Thuis/presence/# -v Thuis/presence/robin living Thuis/presence/robin entrance Thuis/presence/robin bedroom Thuis/presence/robin entrance Thuis/presence/robin kitchen Thuis/presence/robin office
Welcome home
Based on the same events we can welcome a user home as well and directly give him a useful action. I could directly turn on some lights for example, but I rather give the user the choice. So we'll send the user a notification with an action. We'll start with a single action, but later multiple actions can be added depending on for example the time or person.
When we get home we often watch an episode of a TV series (currently we're watching The Mentalist, very nice show!), so the action of choice will be turning on the home theatre system.
Sending a notification is easy. In the BeaconManager
we create a function for it:
func sendLocalNotification() { let notification: UILocalNotification = UILocalNotification() notification.alertAction = "Watch TV" notification.alertBody = "Welcome home!" notification.soundName = UILocalNotificationDefaultSoundName UIApplication.shared.presentLocalNotificationNow(notification) }
We'll call it from the didEnterRegion
method when we enter the home
region. To avoid getting too many notifications in case of exiting and entering the region by accident we'll add a cool down period of 5 minutes. This looks as follows:
if beaconID.identifier == BeaconIDs.home.identifier && (lastLeftOrEntered[beaconID] == nil || (lastLeftOrEntered[beaconID]?.timeIntervalSinceNow)! < -60*5) { sendLocalNotification() }
The result is you receive this notification when you arrive home:
To make it work there is one more thing to do and that's implementing another delegate method, this time in the AppDelegate
:
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { // ... func application(_ application: UIApplication, didReceive notification: UILocalNotification) { // If the app is already active, don't automatically start TV if application.applicationState == .active { return; } MQTT.sharedInstance.publish("on", topic: "Thuis/scene/homeTheater", retain: false) } }
So when the notification action is used it will publish a MQTT message which enables the Home Theater scene and you can directly start watching your favorite series. How the home theater scene works will be the subject of the next blog!
Top Comments