Skip to content

Instantly share code, notes, and snippets.

@drewandre
Created April 12, 2021 10:54
Show Gist options
  • Save drewandre/cc6fd92fe0ee2441badd739fd71e5534 to your computer and use it in GitHub Desktop.
Save drewandre/cc6fd92fe0ee2441badd739fd71e5534 to your computer and use it in GitHub Desktop.
#import <React/RCTBridgeModule.h>
#import <HomeKit/HomeKit.h>
#import "React/RCTEventEmitter.h"
@interface RCT_EXTERN_MODULE(HomeKitModule, RCTEventEmitter)
RCT_EXTERN_METHOD(listHomes: (NSString *)name withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(addAndSetupAccessories: (NSString *)name withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(setCharacteristicValue:(NSString *)uuid value:(NSString *)value withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(addHome:(NSString *)name withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(removeHome:(NSString *)name withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(renameHome:(NSString *)newName oldName:(NSString *)oldName withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(addZone:(NSString *)name toHome:(NSString *)toHome withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(removeZone:(NSString *)name fromHome:(NSString *)fromHome withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(renameZone:(NSString *)oldName oldName:(NSString *)oldName inHome:(NSString *)inHome withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(addRoomToHome:(NSString *)name toHome:(NSString *)toHome withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(addRoomToZone:(NSString *)name toZone:(NSString *)toZone inHome:(NSString *)inHome withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(removeRoomFromZone:(NSString *)name fromZone:(NSString *)fromZone inHome:(NSString *)inHome withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(removeRoomFromHome:(NSString *)name fromHome:(NSString *)fromHome withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(renameRoom:(NSString *)oldName inHome:(NSString *)inHome withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(addAccessoryToHome:(NSString *)accessoryName toHome:(NSString *)toHome withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(removeAccessoryFromHome:(NSString *)accessoryName fromHome:(NSString *)fromHome withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(assignAccessoryToRoom:(NSString *)accessoryName roomName:(NSString *)roomName homeName:(NSString *)homeName withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(renameAccessory:(NSString *)oldName newName:(NSString *)newName withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(startSearchingForNewAccessories)
RCT_EXTERN_METHOD(stopSearchingForNewAccessories)
@end
import HomeKit
@objc(HomeKitModule)
class HomeKitModule: RCTEventEmitter, HMHomeManagerDelegate, HMAccessoryBrowserDelegate, HMAccessoryDelegate {
var homeManager = HMHomeManager()
let accessoryBrowser = HMAccessoryBrowser()
var discoveredAccessories: [HMAccessory] = []
override init() {
self.homeManager = HMHomeManager()
super.init()
self.accessoryBrowser.delegate = self
self.homeManager.delegate = self
}
private func findAccessory(name: String) -> HMAccessory? {
for accessory in accessoryBrowser.discoveredAccessories {
if accessory.name == name {
return accessory
}
}
return nil
}
@objc override static func requiresMainQueueSetup() -> Bool {
return true
}
override func supportedEvents() -> [String]! {
return ["findNewAccessory", "removeNewAccessory", "characteristicNotify"]
}
@objc(listHomes:withResolver:withRejecter:)
func listHomes(name: String, resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void {
self.subscribeToNotifiableCharacteristics()
resolve(HomeKitModule.transformHomes(nhomes: homeManager.homes))
}
func subscribeToNotifiableCharacteristics() -> Void {
NSLog("Subscribing to all available characteristic notifications")
NSLog("Number of homes: \(homeManager.homes.count)")
for home in homeManager.homes {
NSLog("Found home: \(home.name)")
for accessory in home.accessories {
NSLog(" Found accessory: \(accessory.name)")
accessory.delegate = self
for service in accessory.services {
NSLog(" Found service: \(service.name)")
for characteristic in service.characteristics {
if characteristic.isNotificationEnabled {
NSLog(" Found subscribable characteristic: \(characteristic.metadata?.manufacturerDescription ?? characteristic.localizedDescription)")
characteristic.enableNotification(true, completionHandler: { error in
let name = characteristic.metadata?.manufacturerDescription ?? characteristic.localizedDescription
if let error = error {
NSLog(" Error subscribing to \(name) characteristic notifications: \(error.localizedDescription)");
} else {
NSLog(" Subscribed to \(name)");
}
})
} else {
NSLog(" Found characteristic: \(characteristic.metadata?.manufacturerDescription ?? characteristic.localizedDescription)")
}
}
}
}
}
}
@objc(addAndSetupAccessories:withResolver:withRejecter:)
func addAndSetupAccessories(name: String, resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void {
homeManager.homes[0].addAndSetupAccessories(completionHandler: { error in
if let error = error {
reject("error", error.localizedDescription, error);
} else {
resolve("")
}
})
}
@objc(setCharacteristicValue:value:withResolver:withRejecter:)
func setCharacteristicValue(uuid: String, value: String, resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void {
NSLog("Writing \(value) to \(uuid)")
for home in homeManager.homes {
for accessory in home.accessories {
for service in accessory.services {
for characteristic in service.characteristics {
if (characteristic.uniqueIdentifier.uuidString == uuid) {
let numberFormatter = NumberFormatter()
let number = numberFormatter.number(from: value)
var formattedNumber: Any = value
switch characteristic.metadata?.format {
case "float":
formattedNumber = number?.floatValue ?? 0.0
case "int":
formattedNumber = number?.intValue ?? 0
case "bool":
formattedNumber = number?.boolValue ?? false
default:
NSLog("Unknown type: \(characteristic.metadata?.format ?? "unknown")")
formattedNumber = value
}
characteristic.writeValue(formattedNumber, completionHandler: { (error) -> Void in
if error == nil {
NSLog("Wrote \(value) to \(uuid)")
resolve("Wrote \(value) to \(uuid)")
} else {
NSLog("Error writing \(formattedNumber) (of type \(type(of: formattedNumber)) to \(uuid) (\(error.debugDescription)")
reject("error", "Error writing \(formattedNumber) (of type \(type(of: formattedNumber)) to \(uuid) (\(error.debugDescription)", error);
}
})
}
}
}
}
}
}
@objc(addHome:withResolver:withRejecter:)
func addHome(name: String, resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void {
homeManager.addHome(withName: name) { newHome, error in
if error != nil {
reject("error", error?.localizedDescription, error);
} else {
resolve(HomeKitModule.transformHome(home: newHome!))
}
}
}
@objc(removeHome:withResolver:withRejecter:)
func removeHome(name: String, resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void {
guard let home = self.findHome(name: name) else {
reject("error","Home cannot found", nil);
return
}
homeManager.removeHome(home) { error in
if let error = error {
reject("error", error.localizedDescription, error);
} else {
resolve(HomeKitModule.transformHome(home: home))
}
}
}
@objc(renameHome:oldName:withResolver:withRejecter:)
func renameHome(newName: String, oldName: String, resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void {
guard let home = self.findHome(name: oldName) else {
reject("error","Home cannot found", nil);
return
}
home.updateName(newName) { (error) in
if let error = error {
reject("error", error.localizedDescription, error);
} else {
resolve(HomeKitModule.transformHome(home: home))
}
}
}
@objc(addZone:toHome:withResolver:withRejecter:)
func addZone(name: String, toHome:String, resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void {
guard let home = self.findHome(name: toHome) else {
reject("error","Home cannot found", nil);
return
}
home.addZone(withName: name) { zone, error in
if zone != nil {
resolve(name)
}
if let error = error {
reject("error",error.localizedDescription, nil);
}
}
}
@objc(removeZone:fromHome:withResolver:withRejecter:)
func removeZone(name: String, fromHome:String, resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void {
guard let home = self.findHome(name: fromHome) else {
reject("error","Home cannot found", nil);
return
}
guard let zone = self.findZone(name: name, home: home) else {
reject("error","Home cannot found", nil);
return
}
home.removeZone(zone) { (error) in
if let error = error {
reject("error", error.localizedDescription, error);
} else {
resolve(name)
}
}
}
@objc(renameZone:oldName:inHome:withResolver:withRejecter:)
func renameZone(name: String, oldName:String, inHome:String, resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void {
guard let home = self.findHome(name: oldName) else {
reject("error","Home cannot found", nil);
return
}
guard let zone = self.findZone(name: oldName, home: home) else {
reject("error","Zone cannot found", nil);
return
}
zone.updateName(name) { (error) in
if let error = error {
reject("error", error.localizedDescription, error);
} else {
resolve(name)
}
}
}
@objc(addAccessoryToHome:toHome:withResolver:withRejecter:)
func addAccessoryToHome(accessoryName: String, toHome: String ,resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void{
guard let home = self.findHome(name: toHome) else {
reject("error","Home cannot found", nil);
return
}
guard let accessory = self.findAccessory(name: accessoryName) else {
reject("error","Home cannot found", nil);
return
}
home.addAccessory(accessory) { (error) in
if let error = error {
reject("error", error.localizedDescription, error);
} else {
resolve(accessory)
}
}
}
@objc(removeAccessoryFromHome:fromHome:withResolver:withRejecter:)
func removeAccessoryFromHome(accessoryName: String, fromHome: String ,resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void{
guard let home = self.findHome(name: fromHome) else {
reject("error","Home cannot found", nil);
return
}
guard let accessory = self.findAccessoryInHome(name: accessoryName, inHome: home) else {
reject("error","Home cannot found", nil);
return
}
home.removeAccessory(accessory) { (error) in
if let error = error {
reject("error", error.localizedDescription, error);
} else {
resolve(accessory)
}
}
}
@objc(assignAccessoryToRoom:roomName:homeName:withResolver:withRejecter:)
func assignAccessoryToRoom(accessoryName: String, roomName: String , homeName: String , resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void{
guard let home = self.findHome(name: homeName) else {
reject("error","Home cannot found", nil);
return
}
guard let accessory = self.findAccessoryInHome(name: accessoryName, inHome: home) else {
reject("error","Home cannot found", nil);
return
}
guard let room = self.findRoom(name: accessoryName, inHome: home) else {
reject("error","Home cannot found", nil);
return
}
home.assignAccessory(accessory , to: room) { (error) in
if let error = error {
reject("error", error.localizedDescription, error);
} else {
resolve(accessory)
}
}
}
@objc(renameAccessory:newName:withResolver:withRejecter:)
func renameAccessory(oldName: String, newName: String ,resolve: @escaping(RCTPromiseResolveBlock), reject: @escaping(RCTPromiseRejectBlock)) -> Void{
guard let accessory = self.findAccessory(name: oldName) else {
reject("error","Home cannot found", nil);
return
}
accessory.updateName(newName) { (error) in
if let error = error {
reject("error", error.localizedDescription, error);
} else {
resolve(accessory)
}
}
}
@objc(startSearchingForNewAccessories)
func startSearchingForNewAccessories () -> Void {
NSLog("Searching for new accessories!")
self.accessoryBrowser.startSearchingForNewAccessories()
}
@objc(stopSearchingForNewAccessories)
func stopSearchingForNewAccessories () -> Void {
self.accessoryBrowser.stopSearchingForNewAccessories()
}
//Helper functions
private func findHome(name: String) -> HMHome? {
for home in homeManager.homes {
if home.name == name {
return home
}
}
return nil
}
private func findRoom(name: String, inHome: HMHome) -> HMRoom? {
for room in inHome.rooms {
if room.name == name {
return room
}
}
return nil
}
private func findAccessoryInHome(name: String, inHome: HMHome) -> HMAccessory? {
for accessory in inHome.accessories {
if accessory.name == name {
return accessory
}
}
return nil
}
private func findZone(name: String, home:HMHome) -> HMZone? {
for zone in home.zones {
if zone.name == name {
return zone
}
}
return nil
}
private static func transformHome(home: HMHome) -> [String : Any?] {
return [
"uuid": home.uniqueIdentifier.uuidString,
"name" : home.name,
"isPrimary": home.isPrimary,
"rooms": HomeKitModule.transformRooms(nrooms: home.rooms),
"accessories": transformAccessories(naccessories: home.accessories),
"zones": transformZones(nzones: home.zones),
];
}
private static func transformRooms(nrooms: [HMRoom]) -> [Any] {
var rooms: [Any] = []
for room in nrooms {
rooms.append(transformRoom(room: room, skipAccessories:true))
}
return rooms;
}
private static func transformRoom(room: HMRoom, skipAccessories: Bool?) -> [String : Any?] {
return [
"uuid": room.uniqueIdentifier.uuidString,
"name" : room.name,
"accessories": skipAccessories! ? nil : transformAccessories(naccessories: room.accessories)
];
}
private static func transformAccessories(naccessories: [HMAccessory]) -> [Any] {
var accessories: [Any] = []
for accessory in naccessories {
accessories.append(transformAccessory(acc: accessory))
}
return accessories;
}
private static func transformAccessory(acc: HMAccessory) -> [String : Any?] {
return [
"name": acc.name,
"uuid": acc.uniqueIdentifier.uuidString,
"bridged": acc.isBridged,
"services": transformServices(nservices: acc.services),
]
}
private static func transformZones(nzones: [HMZone]) -> [Any] {
var zones: [Any] = []
for zone in nzones {
zones.append(transformZone(zone: zone))
}
return zones;
}
private static func transformZone(zone: HMZone) -> [String : Any] {
return [
"uuid": zone.uniqueIdentifier.uuidString,
"name": zone.name,
"rooms": transformRooms(nrooms: zone.rooms),
]
}
private static func transformServices(nservices: [HMService]) -> [Any] {
var services: [Any] = []
for service in nservices {
services.append(transformService(service: service))
}
return services;
}
private static func transformService(service: HMService) -> [String : Any] {
return [
"uuid": service.uniqueIdentifier.uuidString,
"name": service.name,
"localizedDescription": service.localizedDescription,
"type": service.serviceType,
"characteristics": transformCharacteristics(ncharacteristics: service.characteristics)
]
}
private static func transformHomes(nhomes: [HMHome]) -> [Any] {
var homes = [Any]()
for home in nhomes {
homes.append(HomeKitModule.transformHome(home: home))
}
return homes;
}
private static func transformCharacteristics(ncharacteristics: [HMCharacteristic]) -> [Any] {
var characteristics: [Any] = []
for characteristic in ncharacteristics {
characteristics.append(transformCharacteristic(ncharacteristic:characteristic))
}
return characteristics;
}
private static func transformCharacteristic(ncharacteristic: HMCharacteristic) -> [String : Any] {
let emptyMetadata = [String: String]()
var characteristic = [
"type": ncharacteristic.characteristicType,
"description": ncharacteristic.localizedDescription,
"value": ncharacteristic.value ?? "0",
"localizedDescription": ncharacteristic.localizedDescription,
"properties": ncharacteristic.properties,
"isNotificationEnabled": ncharacteristic.isNotificationEnabled,
"uuid": ncharacteristic.uniqueIdentifier.uuidString,
"metadata": emptyMetadata,
]
if let metadata = ncharacteristic.metadata {
var formattedMetadata = [String: Any]()
formattedMetadata["stepValue"] = metadata.stepValue
formattedMetadata["maxLength"] = metadata.maxLength
formattedMetadata["format"] = metadata.format
formattedMetadata["units"] = metadata.units
formattedMetadata["manufacturerDescription"] = metadata.manufacturerDescription
formattedMetadata["minimumValue"] = metadata.minimumValue
formattedMetadata["maximumValue"] = metadata.maximumValue
characteristic["metadata"] = formattedMetadata;
}
return characteristic
}
// Delegate
// func accessoryBrowser(_ browser: HMAccessoryBrowser, didFindNewAccessory accessory: HMAccessory) {
// self.sendEvent(withName: "findNewAccessory", body:["accessory": HomeKitModule.transformAccessory(acc: accessory)])
//
// }
// func accessoryBrowser(_ browser: HMAccessoryBrowser, didRemoveNewAccessory accessory: HMAccessory) {
// self.sendEvent(withName: "removeNewAccessory", body: ["accessory": HomeKitModule.transformAccessory(acc: accessory)] )
// }
//
func accessory(_ accessory: HMAccessory, service: HMService, didUpdateValueFor characteristic: HMCharacteristic) {
self.sendEvent(withName: "characteristicNotify", body: [
"characteristic": HomeKitModule.transformCharacteristic(ncharacteristic: characteristic),
"homes": HomeKitModule.transformHomes(nhomes: homeManager.homes)
])
NSLog("\(accessory.name) \(characteristic.metadata?.manufacturerDescription ?? characteristic.localizedDescription) value updated to \(characteristic.value ?? "unknown")")
}
func accessoryDidUpdateName(_ accessory: HMAccessory) {
NSLog("accessoryDidUpdateName \(accessory.name)")
}
func accessoryDidUpdateServices(_ accessory: HMAccessory) {
NSLog("accessoryDidUpdateServices \(accessory.name)")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment