/ PROGRAMMING, IOS, SWIFT, JAVASCRIPT, PYTHON

How I Built My Own Smart Home Application Framework - Part 1

For awhile now, I had been toying with the idea of creating some of my own smart home apps to do things such as, keeping an inventory of my fridge/pantry, displaying useful information about my day, and controlling various household tasks. About a month ago, I took the plunge into seeing what it would really take to develop some of these ideas.

It all started when I had the idea to develop a sort of kiosk to display relevant driving info of the significant locations in my day. As someone who dreads being late to any event, I’m constantly looking up how long it takes for me to get somewhere on Google Maps, this kiosk was going to be my savior for these things by not only displaying the drive time to my relevant locations, but saving me the hassle of having to manually type things into google to figure out my overall travel time. Overall the task seemed pretty easy, I would track my location data with an app on my phone, then sync that data with a cloud database that would then be read and manipulated by a raspberry pi device that I had set up in my living room. However, what if my girlfriend also wanted to sync her location data and travel time? This is where I started building something a bit bigger.

I already had some great ideas for how I wanted the final product to turn out, but I needed to start actually developing something, so I started with the app to sync my location data. I had decided that I wanted to make an app that would actually integrate any smart home applications that I developed, but not be tied specifically to my household, that way if any friends or family want any devices that I developed then I could get them up and running individually. After mocking up some ideas in sketch, I had a name for my app, “HomeSync”.

Within HomeSync, I needed to have some features such as user authentication and data storage, so I started thinking of ways to handle these, but ultimately ended up deciding on using Firebase for most of the parent server-side functionality. I needed to start thinking about how to group some users though, so I came up with a pretty straightforward “household” system. Users could create or join households and invite others using an easily-readable, randomly generated unique ID. An example ID would look something like “12A5EREA”, to achieve this I took the first octet from a UUID generated at account/household creation. Obviously, this might not be the best approach for a large scale application as collision could occur, but I don’t think that I would have that many households anytime soon.

After I had the user authentication and household framework set up, I needed a way to create and integrate the features of the actual applications that I would be writing. I thought about a couple different approaches, but I ended up deciding on having my backend drive most of the logic. For example, application names, icons, and the storyboard/view controllers I stored in Firestore. My “HomeSyncApp” model ending up something like this.

struct HomeSyncApp {
    
    var name: String
    var icon_name: String
    var app_vc: String
    var storyboard: String
    var add_app_vc: String
    var in_app_icon_name: String
    
    var dictionary: [String: Any] {
        return [
            "name": name,
            "icon_name": icon_name,
            "app_vc": app_vc,
            "storyboard": storyboard,
            "add_app_vc": add_app_vc,
            "in_app_icon_name": in_app_icon_name
        ]
    }
}

extension HomeSyncApp: DocumentSerializable {
    init?(dictionary: [String: Any]) {
        guard let name = dictionary["name"] as? String,
            let icon_name = dictionary["icon_name"] as? String,
            let app_vc = dictionary["app_vc"] as? String,
            let storyboard = dictionary["storyboard"] as? String,
            let add_app_vc = dictionary["add_app_vc"] as? String,
            let in_app_icon_name = dictionary["in_app_icon_name"] as? String else {
                return nil
        }
        
        self.init(name: name, icon_name: icon_name, app_vc: app_vc, storyboard: storyboard, add_app_vc: add_app_vc, in_app_icon_name: in_app_icon_name)
    }
}

My home view controller for the app pulls each application document from my Firestore collection and populates a UITableView. In my HomeSyncApp class, “app_vc” refers to the root view controller for the application, “storyboard” refers to the UIStoryboard for the application, and “add_app_vc” refers to the view controller that is used when adding an application to a household. This means that navigation from the home view controller to each “app” is dynamic based on the values stored in Firestore. Each “app” will have its own storyboard as it’s created, so I thought this would be a decent approach to handle multiple applications.

The next thing I needed to do was track some user data, to start, I knew I needed location data for my first HomeSync app, Drivetime. As I mentioned earlier, Drivetime was going to be a kiosk that would display the driving time to significant locations throughout my day, but I needed to track the significant locations in order to display them. Implementing location tracking was pretty straightforward, as most of the logic I stored within the AppDelegate as shown below.

import UIKit
import Firebase
import CoreLocation

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    
    var household_id: String?
    
    static let geoCoder = CLGeocoder()
    let locationManager = CLLocationManager()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        
        locationManager.requestAlwaysAuthorization()
        locationManager.startMonitoringVisits()
        locationManager.delegate = self
        
        return true
    }
}

extension AppDelegate: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) {
        let clLocation = CLLocation(latitude: visit.coordinate.latitude, longitude: visit.coordinate.longitude)
        
        AppDelegate.geoCoder.reverseGeocodeLocation(clLocation) { placemarks, _ in
            if let place = placemarks?.first {
                let description = "\(place)"
                self.newVisitRecieved(visit, description: description)
            }
        }
    }
    
    func newVisitRecieved(_ visit: CLVisit, description: String) {
        let location = Location(visit: visit, descriptionString: description)
        LocationsStorage.shared.saveLocationOnDisk(location)
    }
}

I used a CLGeocoder and CLLocationManager from Core Location to track and geocode the locations that I visit. The location manager tracks each significant CLVisit, then reverse geocodes those coordinates to give me a fully descriptive string of the location that contains the full address, along with the original coordinates. I then translate that string to a custom Location object and encode the object as JSON, saving it as a file to my phone’s local storage using my LocationStorage class.

import Foundation
import CoreLocation

class Location: Codable {
    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .medium
        return formatter
    }()
    
    var coordinates: CLLocationCoordinate2D {
        return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    }
    
    let latitude: Double
    let longitude: Double
    let date: Date
    let dateString: String
    let description: String
    
    init(_ location: CLLocationCoordinate2D, date: Date, descriptionString: String) {
        latitude =  location.latitude
        longitude =  location.longitude
        self.date = date
        dateString = Location.dateFormatter.string(from: date)
        description = descriptionString
    }
    
    convenience init(visit: CLVisit, descriptionString: String) {
        self.init(visit.coordinate, date: visit.arrivalDate, descriptionString: descriptionString)
    }
}
import Foundation
import CoreLocation

class LocationsStorage {
    static let shared = LocationsStorage()
    
    private(set) var locations: [Location]
    private let fileManager: FileManager
    private let documentsURL: URL
    
    init() {
        let fileManager = FileManager.default
        documentsURL = try! fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        self.fileManager = fileManager
        
        let jsonDecoder = JSONDecoder()
        
        let locationFilesURLs = try! fileManager.contentsOfDirectory(at: documentsURL,
                                                                     includingPropertiesForKeys: nil)
        locations = locationFilesURLs.compactMap { url -> Location? in
            guard !url.absoluteString.contains(".DS_Store") else {
                return nil
            }
            guard let data = try? Data(contentsOf: url) else {
                return nil
            }
            return try? jsonDecoder.decode(Location.self, from: data)
            }.sorted(by: { $0.date < $1.date })
    }
    
    func saveLocationOnDisk(_ location: Location) {
        let encoder = JSONEncoder()
        let timestamp = location.date.timeIntervalSince1970
        let fileURL = documentsURL.appendingPathComponent("\(timestamp)")
        
        let data = try! encoder.encode(location)
        try! data.write(to: fileURL)
        
        locations.append(location)
    }
    
    func saveCLLocationToDisk(_ clLocation: CLLocation) {
        let currentDate = Date()
        AppDelegate.geoCoder.reverseGeocodeLocation(clLocation) { placemarks, _ in
            if let place = placemarks?.first {
                let location = Location(clLocation.coordinate, date: currentDate, descriptionString: "\(place)")
                self.saveLocationOnDisk(location)
            }
        }
    }
}

Now that I was saving the locations that I was visiting, I needed a way to sync and store this in my database for my raspberry pi kiosk to eventually pull from. I stood up my Drivetime app within my HomeSync framework and created a manual sync button so that I would be able to control when locations were updated. To start, I decided that I would need to exclusively identify the locations for my home and work, which should be the two top locations that I visit, home being the most visited, and work being the second. Using my LocationsStorage class again, I unloaded my array of locations into a local variable for manipulation, and identified the addresses as shown below.

let locations = LocationsStorage.shared.locations
var counts: [String: Int] = [:]

for loc in locations {
    let desc = loc.description.components(separatedBy: "@")
    counts[desc[0]] = (counts[desc[0]] ?? 0) + 1
}

var homeCount = 0
var homeAddress = ""

//Get home address
for (key, value) in counts {
    print("\(key) occurs \(value) time(s)")
    if(value > homeCount) {
        homeCount = value
        homeAddress = key
    }
}

counts.removeValue(forKey: homeAddress)

I then synced those locations with Firebase to specifically store my home and work locations, other locations I had decided were insignificant at this point in time, but I could implement them later if I found uses for them. Any other locations I figured would be user entered, because having random locations that a user might visit flagged as “significant” might hinder the usefulness of the app. I also decided to add things such as profile color, which updates the color shown for the user card on the Drivetime kiosk and a list that would show the device’s tracked locations.

Overall this part of the project was a pretty great experience as it incorporated a lot of different features like Core Location and User Authentication. Developing the iOS portion took me about 8-9 hours over the course of a week or so from mocking up the design in Sketch to actually coding the app which is nice because it wasn’t a huge time sink. Some future things I could integrate with HomeSync might be Alexa functionality or implementing new features for the smart home hardware I already own, like Philips Hue bulbs. I would absolutely encourage any developers to work on their own smart home applications to see if they could automate tasks or alleviate their daily stresses. If you have any other questions on how HomeSync works or other general questions, please feel free to leave them in the comment box below and I will happily answer them!

This article is part 1 of a multipart series, I am currently writing a follow-up as to how I built my Drivetime kiosk pictured below.

charlemagne

Charles Fager

Charles is the founder and lead developer at Norfare. He spends his days working as a fulltime developer, and enjoys working on new app concepts with Swift. He is also an avid gamer.

Read More