Skip to content

Commit 77209dc

Browse files
committed
Update to use mdns for discovery
1 parent b457f33 commit 77209dc

8 files changed

Lines changed: 417 additions & 14 deletions

File tree

‎README.md‎

Lines changed: 250 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,254 @@
11
Prefab
22
======
33

4-
Prefab is an application that provides a simple http interface to [HomeKit](https://developer.apple.com/documentation/homekit) data. As of this writing the native HomeKit APIs are available on iOS based systems. The goal of this app is to provide HomeKit access to macOS. The Prefab application provides access to data provided by HomeKit while the prefab cli tool provides a simple client to request HomeKit data and provide shell access.
4+
Prefab is an application that provides a simple HTTP interface to [HomeKit](https://developer.apple.com/documentation/homekit) data. As of this writing the native HomeKit APIs are only available on iOS based systems. The goal of this app is to provide HomeKit access to macOS. The Prefab application provides access to data provided by HomeKit while the prefab CLI tool provides a simple client to request HomeKit data and provide shell access.
55

6+
## Requirements
7+
8+
- **Xcode**: Version 15.0 or later
9+
- **macOS**: 14.2 or later (for macOS target)
10+
- **iOS**: 17.2 or later (for iOS target)
11+
- **HomeKit Setup**: Physical HomeKit accessories or HomeKit simulator
12+
- **Apple Developer Account**: Required for HomeKit entitlements and code signing
13+
14+
## Dependencies
15+
16+
This project uses Swift Package Manager through Xcode with the following dependencies:
17+
18+
- **[Hummingbird](https://github.com/hummingbird-project/hummingbird.git)**: Swift HTTP server framework
19+
- **[swift-http-types](https://github.com/apple/swift-http-types.git)**: Modern HTTP types for Swift
20+
- **[swift-argument-parser](https://github.com/apple/swift-argument-parser.git)**: Command-line argument parsing
21+
22+
## Setup and Installation
23+
24+
### 1. Clone the Repository
25+
26+
```bash
27+
git clone https://github.com/kellyp/prefab.git
28+
cd prefab
29+
```
30+
31+
### 2. Open in Xcode
32+
33+
```bash
34+
open prefab.xcodeproj
35+
```
36+
37+
### 3. Resolve Dependencies
38+
39+
Dependencies are automatically resolved by Xcode when you first open the project. If you need to manually resolve them:
40+
41+
1. In Xcode, go to **File** → **Packages** → **Resolve Package Versions**
42+
2. Wait for Xcode to download and resolve all Swift packages
43+
44+
### 4. Configure Code Signing
45+
46+
1. Select the **prefab** project in the navigator
47+
2. Select the **Prefab** target
48+
3. Go to **Signing & Capabilities**
49+
4. Select your development team
50+
5. Ensure the **HomeKit** capability is enabled
51+
52+
## Building
53+
54+
### Build from Xcode
55+
56+
1. Select your target device or simulator
57+
2. Press **⌘+B** to build, or **⌘+R** to build and run
58+
59+
### Build from Command Line
60+
61+
```bash
62+
# Build all targets
63+
xcodebuild -project prefab.xcodeproj -scheme Prefab build
64+
65+
# Build for specific destination
66+
xcodebuild -project prefab.xcodeproj -scheme Prefab -destination 'platform=macOS' build
67+
```
68+
69+
### Build Products
70+
71+
The build creates two main products:
72+
- **Prefab.app**: The main SwiftUI application with HTTP server
73+
- **prefab**: The command-line tool (embedded in the app bundle)
74+
75+
## Testing
76+
77+
### Run Tests in Xcode
78+
79+
1. Press **⌘+U** to run all tests
80+
2. Or use **Product** → **Test** from the menu
81+
82+
### Run Tests from Command Line
83+
84+
```bash
85+
# Run all tests
86+
xcodebuild test -project prefab.xcodeproj -scheme Prefab -destination 'platform=macOS'
87+
88+
# Run specific test plan
89+
xcodebuild test -project prefab.xcodeproj -testPlan Prefab -destination 'platform=macOS'
90+
```
91+
92+
### Test Targets
93+
94+
- **prefabTests**: Unit tests for core functionality
95+
- **prefabUITests**: UI automation tests
96+
97+
## Running
98+
99+
### 1. Run the Main Application
100+
101+
**From Xcode:**
102+
- Select the **Prefab** scheme
103+
- Press **⌘+R** to run
104+
105+
**From Command Line:**
106+
```bash
107+
# Option 1: Build to a specific directory (most predictable)
108+
xcodebuild -project prefab.xcodeproj -scheme Prefab -destination 'platform=macOS' \
109+
-derivedDataPath ./build build
110+
open ./build/Build/Products/Debug-maccatalyst/Prefab.app
111+
112+
# Option 2: Use default derived data path
113+
xcodebuild -project prefab.xcodeproj -scheme Prefab -destination 'platform=macOS' build
114+
open ~/Library/Developer/Xcode/DerivedData/prefab-*/Build/Products/Debug-iphoneos/Prefab.app
115+
116+
# Option 3: Build and run in one command with custom path
117+
xcodebuild -project prefab.xcodeproj -scheme Prefab -destination 'platform=macOS' \
118+
-derivedDataPath ./build build && \
119+
open ./build/Build/Products/Debug-maccatalyst/Prefab.app
120+
```
121+
122+
The application will:
123+
1. Start the HTTP server on `http://localhost:8080` (accessible at `0.0.0.0:8080`)
124+
2. Advertise the service via mDNS/Bonjour as "Prefab HomeKit Server" (`_prefab._tcp.`)
125+
3. Display a SwiftUI interface showing your HomeKit homes
126+
4. Begin serving HomeKit data via REST API
127+
128+
### Service Discovery
129+
130+
The server automatically advertises itself using mDNS (Bonjour) with:
131+
- **Service Type**: `_prefab._tcp.`
132+
- **Service Name**: "Prefab HomeKit Server"
133+
- **Port**: 8080
134+
- **TXT Record**: Contains version info and API details
135+
136+
You can discover the service using:
137+
```bash
138+
# Using dns-sd command-line tool
139+
dns-sd -B _prefab._tcp.
140+
141+
# Or browse all services
142+
dns-sd -B _tcp.
143+
```
144+
145+
### 2. Use the CLI Tool
146+
147+
The CLI tool is embedded within the app bundle. To use it:
148+
149+
```bash
150+
# Navigate to the app bundle
151+
cd build/Build/Products/Debug/
152+
153+
# Or if running from Xcode's DerivedData
154+
./prefab --help
155+
```
156+
157+
**Available CLI commands:**
158+
```bash
159+
# Get all homes
160+
./prefab homes
161+
162+
# Get specific home details
163+
./prefab home --id [HOME_ID]
164+
165+
# Get rooms in a home
166+
./prefab rooms --home-id [HOME_ID]
167+
168+
# Get accessories
169+
./prefab accessories --home-id [HOME_ID]
170+
171+
# Update accessory
172+
./prefab update-accessory --id [ACCESSORY_ID] --value [VALUE]
173+
```
174+
175+
### 3. API Usage
176+
177+
Once the app is running, you can interact with the HTTP API locally or from other devices on your network:
178+
179+
```bash
180+
# Local access
181+
curl http://localhost:8080/homes
182+
183+
# Network access (replace with actual IP)
184+
curl http://192.168.1.100:8080/homes
185+
186+
# Get specific home
187+
curl http://localhost:8080/homes/[HOME_ID]
188+
189+
# Get rooms in a home
190+
curl http://localhost:8080/homes/[HOME_ID]/rooms
191+
192+
# Get accessories in a home
193+
curl http://localhost:8080/homes/[HOME_ID]/accessories
194+
```
195+
196+
**mDNS/Bonjour Discovery**: Other devices can discover the service automatically and connect using the advertised hostname and port.
197+
198+
## Development Workflow
199+
200+
### First-Time Setup
201+
202+
1. Ensure you have HomeKit accessories set up on your iOS device, or use the HomeKit simulator
203+
2. Build and run the app on your Mac
204+
3. Grant HomeKit permissions when prompted
205+
4. Verify the API is responding at `http://localhost:8080/homes`
206+
207+
### Making Changes
208+
209+
1. Edit Swift files in Xcode
210+
2. The HTTP server will restart automatically when you rebuild
211+
3. Use the CLI tool or curl to test API changes
212+
4. Run tests to ensure nothing is broken
213+
214+
### Debugging
215+
216+
- **Console Logs**: Check Console.app for detailed logging output
217+
- **Network Debugging**: Use tools like curl or Postman to test API endpoints
218+
- **HomeKit Debugging**: Use the Home app on iOS to verify HomeKit state
219+
220+
## Common Issues
221+
222+
### HomeKit Permission Denied
223+
224+
If you get `403 Forbidden` responses:
225+
1. Check that HomeKit permission is granted in System Preferences
226+
2. Verify the HomeKit entitlement is properly configured
227+
3. Ensure you're signed with a valid developer certificate
228+
229+
### Build Failures
230+
231+
If dependencies fail to resolve:
232+
1. Clean build folder (**⌘+Shift+K**)
233+
2. Reset package caches: **File** → **Packages** → **Reset Package Caches**
234+
3. Manually resolve packages: **File** → **Packages** → **Resolve Package Versions**
235+
236+
### Server Won't Start
237+
238+
If the HTTP server fails to start:
239+
1. Check that port 8080 is not in use by another process
240+
2. Review console logs for specific error messages
241+
3. Ensure proper code signing for network access
242+
243+
## License
244+
245+
This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.
246+
247+
## Contributing
248+
249+
1. Fork the repository
250+
2. Create a feature branch
251+
3. Make your changes
252+
4. Add tests for new functionality
253+
5. Ensure all tests pass
254+
6. Submit a pull request

‎prefab-client/Client/Client.swift‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,83 @@ import Foundation
99
import HTTPTypes
1010
import HTTPTypesFoundation
1111

12+
public enum ServiceDiscoveryError: Error {
13+
case serviceNotFound
14+
case discoveryTimeout
15+
case invalidServiceData
16+
}
17+
18+
class ServiceDiscoveryDelegate: NSObject, NetServiceBrowserDelegate, NetServiceDelegate {
19+
var foundService: NetService?
20+
var serviceBrowser: NetServiceBrowser
21+
private var isResolving = false
22+
private var discoveryCompletionHandler: ((Result<Client, ServiceDiscoveryError>) -> Void)?
23+
24+
override init() {
25+
serviceBrowser = NetServiceBrowser()
26+
super.init()
27+
serviceBrowser.delegate = self
28+
}
29+
30+
func discoverPrefabService() async throws -> Client {
31+
return try await withCheckedThrowingContinuation { continuation in
32+
self.discoveryCompletionHandler = { result in
33+
continuation.resume(with: result)
34+
}
35+
serviceBrowser.searchForServices(ofType: "_http._tcp.", inDomain: "")
36+
37+
// Set a timeout to prevent hanging indefinitely
38+
DispatchQueue.global().asyncAfter(deadline: .now() + 5.0) {
39+
if self.discoveryCompletionHandler != nil {
40+
self.discoveryCompletionHandler = nil
41+
continuation.resume(returning: Client.initShared(host: "localhost", port: "8080", scheme: "http"))
42+
}
43+
}
44+
}
45+
}
46+
47+
// MARK: - NetServiceBrowserDelegate
48+
49+
func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
50+
// Check if this is a Prefab HomeKit Bridge service
51+
if service.name.contains("Prefab HomeKit Bridge") {
52+
foundService = service
53+
service.delegate = self
54+
isResolving = true
55+
service.resolve(withTimeout: 10.0)
56+
}
57+
}
58+
59+
func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber]) {
60+
serviceBrowser.stop()
61+
discoveryCompletionHandler?(.failure(.serviceNotFound))
62+
discoveryCompletionHandler = nil
63+
}
64+
65+
// MARK: - NetServiceDelegate
66+
67+
func netServiceDidResolveAddress(_ sender: NetService) {
68+
guard isResolving else { return }
69+
isResolving = false
70+
serviceBrowser.stop()
71+
72+
if let hostName = sender.hostName {
73+
discoveryCompletionHandler?(.success(Client.initShared(host: hostName, port: String(sender.port), scheme: "http")))
74+
} else {
75+
discoveryCompletionHandler?(.failure(.invalidServiceData))
76+
}
77+
discoveryCompletionHandler = nil
78+
}
79+
80+
func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber]) {
81+
guard isResolving else { return }
82+
isResolving = false
83+
serviceBrowser.stop()
84+
discoveryCompletionHandler?(.failure(.serviceNotFound))
85+
discoveryCompletionHandler = nil
86+
}
87+
}
88+
1289
public enum HTTPResponseError: Error {
1390
// Throw when a response is not found
1491
case notFound(response: String)
@@ -42,6 +119,11 @@ public class Client {
42119
return shared
43120
}
44121

122+
static func initSharedWithDiscovery() async throws -> Client {
123+
let discovery = ServiceDiscoveryDelegate()
124+
return try await discovery.discoverPrefabService()
125+
}
126+
45127
static func getShared() throws -> Client {
46128
switch "" {
47129
case shared.host:

‎prefab-client/Command/Prefab+Accessories.swift‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ extension Prefab {
2727

2828
mutating func run() async {
2929
do{
30+
let client = try await Prefab.getClient()
3031
let accessories = try await client.getAccessories(home: home, room: room)
3132
print(accessories)
3233
} catch UninitializeClientError.propertyIsNotSet(let property) {
@@ -64,6 +65,7 @@ extension Prefab {
6465

6566
mutating func run() async {
6667
do{
68+
let client = try await Prefab.getClient()
6769
let accData = try await client.getAccessory(name: accessory, home: home, room: room)
6870
print(accData)
6971
} catch UninitializeClientError.propertyIsNotSet(let property) {
@@ -112,6 +114,7 @@ extension Prefab {
112114
// 0D84EB02-914A-5B90-BF23-EF27764F9438 B16D61DB-EFC7-5BA3-BBF0-101860D06D60
113115
mutating func run() async {
114116
do{
117+
let client = try await Prefab.getClient()
115118
let accData = try await client.updateAccessory(name: accessory, home: home, room: room, serviceId: serviceId, characteristicId: characteristicId, value: value)
116119
print(accData)
117120
} catch UninitializeClientError.propertyIsNotSet(let property) {

‎prefab-client/Command/Prefab+Homes.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ extension Prefab {
2121
""")
2222
mutating func run() async {
2323
do{
24+
let client = try await Prefab.getClient()
2425
let homes = try await client.getHomes()
2526
print(homes)
2627
} catch UninitializeClientError.propertyIsNotSet(let property) {
@@ -52,6 +53,7 @@ extension Prefab {
5253
""")
5354
mutating func run() async {
5455
do{
56+
let client = try await Prefab.getClient()
5557
let homes = try await client.getHome(name: home)
5658
print(homes)
5759
} catch UninitializeClientError.propertyIsNotSet(let property) {

0 commit comments

Comments
 (0)