Skip to content

Commit d7f7c10

Browse files
committed
AccessoryDelegate to handle HAP controller Characteristic get/sets by Accessory implementations
Separate functions to obtain the value of a characteristic for a HAP server and to just get a jason formatted value. Notify device delegate when an accessory changes it's value Add an example OpenWeatherThermostat accessory, which uses the AccessoryDelegate protocol.
1 parent 60d7875 commit d7f7c10

10 files changed

+288
-12
lines changed

Sources/HAP/Base/Accessory.swift

+28-4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public enum AccessoryType: String, Codable {
4949

5050
open class Accessory: JSONSerializable {
5151
public weak var device: Device?
52+
public weak var delegate: AccessoryDelegate?
53+
5254
internal var aid: InstanceID = 0
5355
public let type: AccessoryType
5456
public let info: Service.Info
@@ -125,11 +127,12 @@ open class Accessory: JSONSerializable {
125127

126128
/// Characteristic's value was changed by controller. Used for bubbling up
127129
/// to the device, which will notify the delegate.
128-
func characteristic<T>(_ characteristic: GenericCharacteristic<T>,
129-
ofService service: Service,
130-
didChangeValue newValue: T?) {
130+
internal func characteristic<T>(_ characteristic: GenericCharacteristic<T>,
131+
ofService service: Service,
132+
didChangeValue newValue: T?) {
131133
device?.characteristic(characteristic, ofService: service, ofAccessory: self, didChangeValue: newValue)
132-
}
134+
delegate?.characteristic(characteristic, ofService: service, didChangeValue: newValue)
135+
}
133136

134137
public func serialized() -> [String: JSONValueType] {
135138
return [
@@ -138,3 +141,24 @@ open class Accessory: JSONSerializable {
138141
]
139142
}
140143
}
144+
145+
/// A HAP `Characteristic` calls the methods of this delegate to report
146+
/// set/gets from a HAP controller.
147+
///
148+
/// Implement this protocol in an accessory-specific object (such as a subclass
149+
/// of a given accessory) in order to make the accessory react accordingly.
150+
/// For example, you might want to update the value of certain characteristics
151+
/// if the HAP controller is showing interest or makes a change.
152+
153+
public protocol AccessoryDelegate: class {
154+
/// Characteristic's value was changed by controller. Used for notifying
155+
func characteristic<T>(
156+
_ characteristic: GenericCharacteristic<T>,
157+
ofService: Service,
158+
didChangeValue: T?)
159+
/// Characteristic's value was observed by controller. Used for lazy updating
160+
func characteristic<T>(
161+
_ characteristic: GenericCharacteristic<T>,
162+
ofService: Service,
163+
didGetValue: T?)
164+
}

Sources/HAP/Base/Characteristic.swift

+27-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import Foundation
22

3+
#if os(Linux)
4+
import Dispatch
5+
#endif
6+
37
public struct AnyCharacteristic {
48
let wrapped: Characteristic
59

@@ -17,7 +21,8 @@ protocol Characteristic: class, JSONSerializable {
1721
var iid: InstanceID { get set }
1822
var type: CharacteristicType { get }
1923
var permissions: [CharacteristicPermission] { get }
20-
func getValue() -> JSONValueType?
24+
func jsonValue() -> JSONValueType?
25+
func getValue(fromConnection: Server.Connection?) -> JSONValueType?
2126
func setValue(_: Any?, fromConnection: Server.Connection?) throws
2227
var description: String? { get }
2328
var format: CharacteristicFormat? { get }
@@ -38,7 +43,7 @@ extension Characteristic {
3843

3944
if permissions.contains(.read) {
4045
// TODO: fixit
41-
serialized["value"] = getValue() ?? 0 //NSNull()
46+
serialized["value"] = jsonValue() ?? 0 //NSNull()
4247
}
4348

4449
if let description = description { serialized["description"] = description }
@@ -87,16 +92,34 @@ public class GenericCharacteristic<T: CharacteristicValueType>: Characteristic,
8792
}
8893
}
8994
_value = newValue
90-
if let device = service?.accessory?.device {
95+
if let service = self.service,
96+
let accessory = service.accessory,
97+
let device = accessory.device {
9198
device.notifyListeners(of: self)
99+
device.characteristic(self,
100+
ofService: service,
101+
ofAccessory: accessory,
102+
didChangeValue: _value)
92103
}
93104
}
94105
}
95106

96-
func getValue() -> JSONValueType? {
107+
func jsonValue() -> JSONValueType? {
97108
return value?.jsonValueType
98109
}
99110

111+
// Get Value for HAP controller
112+
func getValue(fromConnection connection: Server.Connection?) -> JSONValueType? {
113+
let currentValue = _value
114+
DispatchQueue.main.async { [weak self] in
115+
if let this = self, let service = this.service {
116+
service.accessory?.delegate?.characteristic(this, ofService: service, didGetValue: currentValue)
117+
}
118+
}
119+
return jsonValue()
120+
}
121+
122+
// Set Value by HAP controller
100123
func setValue(_ newValue: Any?, fromConnection connection: Server.Connection?) throws {
101124
switch newValue {
102125
case let some?:

Sources/HAP/Endpoints/characteristics().swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func characteristics(device: Device) -> Application {
4444
}
4545

4646
var value: Protocol.Value?
47-
switch characteristic.getValue() {
47+
switch characteristic.getValue(fromConnection: connection) {
4848
case let _value as Double: value = .double(_value)
4949
case let _value as Float: value = .double(Double(_value))
5050
case let _value as Int: value = .int(_value)

Sources/HAP/Server/Device.swift

+4
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ public class Device {
228228
configuration.aidForAccessorySerialNumber[serialNumber] = accessory.aid
229229
}
230230
}
231+
delegate?.didChangeAccessoryList()
231232

232233
// Write configuration data to persist updated aid's and notify listeners
233234
updatedConfiguration()
@@ -266,8 +267,11 @@ public class Device {
266267
let serialNumber = accessory.serialNumber
267268
configuration.aidForAccessorySerialNumber.removeValue(forKey: serialNumber)
268269
}
270+
delegate?.didChangeAccessoryList()
271+
269272
// write configuration data to persist updated aid's
270273
updatedConfiguration()
274+
271275
}
272276

273277
// Check if a given serial number is unique amoungst all acessories, except the one being tested

Sources/HAP/Server/DeviceDelegate.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ public protocol DeviceDelegate: class {
5151
///
5252
func didChangePairingState(from: PairingState, to: PairingState)
5353

54+
/// Tells the delegate that one or more Accessories were added or removed.
55+
///
56+
func didChangeAccessoryList()
57+
5458
/// Tells the delegate that the value of a characteristic has changed.
5559
///
5660
/// - Parameters:
@@ -65,7 +69,8 @@ public protocol DeviceDelegate: class {
6569
ofAccessory: Accessory,
6670
didChangeValue: T?)
6771
}
68-
72+
/// Default implementation provides dummy delegate functions
73+
///
6974
public extension DeviceDelegate {
7075
func characteristicListenerDidSubscribe(
7176
_ accessory: Accessory,

Sources/HAP/Server/Server.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ public class Server: NSObject, NetServiceDelegate {
6262
do {
6363
repeat {
6464
let newSocket = try self.listenSocket.acceptClientConnection()
65-
logger.info("Accepted connection from \(newSocket.remoteHostname)")
65+
DispatchQueue.main.async {
66+
logger.info("Accepted connection from \(newSocket.remoteHostname)")
67+
}
6668
self.addNewConnection(socket: newSocket)
6769
} while self.continueRunning
6870
} catch {

Sources/HAP/Utils/Event.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ struct Event {
6262
guard let aid = char.service?.accessory?.aid else {
6363
throw Error.characteristicWithoutAccessory
6464
}
65-
payload.append(["aid": aid, "iid": char.iid, "value": char.getValue() ?? NSNull()])
65+
payload.append(["aid": aid, "iid": char.iid, "value": char.jsonValue() ?? NSNull()])
6666
}
6767
let serialized = ["characteristics": payload]
6868
guard let body = try? JSONSerialization.data(withJSONObject: serialized, options: []) else {

Sources/hap-server/OpenWeather.swift

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//
2+
// OpenWeather.swift
3+
// hap-server
4+
//
5+
6+
import Foundation
7+
import func Evergreen.getLogger
8+
9+
fileprivate let logger = getLogger("openweather")
10+
11+
public class OpenWeather {
12+
13+
public var temperature: Double {
14+
update()
15+
return _temperature
16+
}
17+
18+
public var humidity: Int {
19+
update()
20+
return _humidity
21+
}
22+
23+
public enum Units: String {
24+
case imperial
25+
case metric
26+
}
27+
28+
// swiftlint:disable identifier_name
29+
public struct Measurement: Decodable {
30+
let temp: Double
31+
let pressure: Int
32+
let humidity: Int
33+
let temp_min: Int?
34+
let temp_max: Int?
35+
}
36+
public struct OpenWeatherResponse: Decodable {
37+
let main: Measurement
38+
let name: String
39+
}
40+
41+
let appid: String
42+
let name: String
43+
let lat: String
44+
let lon: String
45+
let units: Units
46+
47+
private var _temperature: Double = 0.0
48+
private var _humidity: Int = 50
49+
50+
private let decoder = JSONDecoder()
51+
52+
private let limit: TimeInterval = 900 // 15 Minutes
53+
private var lastExecutedAt: Date?
54+
private let updateQueue = DispatchQueue(label: "openweather", attributes: [])
55+
56+
private var observers = [(OpenWeather) -> Void]()
57+
58+
public init(name: String, lat: Double, lon: Double, appid: String, units: Units = .metric) {
59+
precondition((lat >= -90.0) && (lat <= 90.0), "Latitude \(lat) is out of range")
60+
precondition((lon >= -180.0) && (lon <= 180.0), "Longitude \(lon) is out of range")
61+
62+
self.name = name
63+
self.appid = appid
64+
self.lat = "\(lat)"
65+
self.lon = "\(lon)"
66+
self.units = units
67+
68+
self.update()
69+
}
70+
71+
public func whenUpdated(closure: @escaping (OpenWeather) -> Void) {
72+
observers.append(closure)
73+
}
74+
75+
func update() {
76+
updateQueue.async {
77+
let now = Date()
78+
79+
// Lookup last executed
80+
let timeInterval = now.timeIntervalSince(self.lastExecutedAt ?? .distantPast)
81+
82+
// Only refresh the values if the last request was older than 'limit'
83+
if timeInterval > self.limit {
84+
// Record execution
85+
self.lastExecutedAt = now
86+
87+
self.updateNow()
88+
}
89+
}
90+
}
91+
92+
func updateNow() {
93+
94+
var urlQuery = URLComponents(string: "https://api.openweathermap.org/data/2.5/weather")!
95+
urlQuery.queryItems = [
96+
URLQueryItem(name: "lat", value: lat),
97+
URLQueryItem(name: "lon", value: lon),
98+
URLQueryItem(name: "APPID", value: appid),
99+
URLQueryItem(name: "units", value: units.rawValue)]
100+
101+
let url = urlQuery.url!
102+
logger.debug("URL: \(url)")
103+
104+
let task = URLSession.shared.dataTask(with: url) { data, response, error in
105+
if let error = error {
106+
logger.debug("OpenWeather connection error \(error)")
107+
return
108+
}
109+
guard let httpResponse = response as? HTTPURLResponse,
110+
(200...299).contains(httpResponse.statusCode) else {
111+
logger.debug("OpenWeather Server error \(response)")
112+
return
113+
}
114+
115+
if let mimeType = httpResponse.mimeType, mimeType == "application/json",
116+
let data = data,
117+
let weatherReport = try? self.decoder.decode(OpenWeatherResponse.self, from: data) {
118+
119+
DispatchQueue.main.sync {
120+
self._temperature = weatherReport.main.temp
121+
self._humidity = weatherReport.main.humidity
122+
for observer in self.observers {
123+
observer(self)
124+
}
125+
}
126+
127+
}
128+
}
129+
task.resume()
130+
131+
}
132+
133+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// OpenWeatherThermometer.swift
3+
// hap-server
4+
//
5+
// Created by Guy Brooker on 03/10/2018.
6+
//
7+
8+
import Foundation
9+
import HAP
10+
import func Evergreen.getLogger
11+
12+
fileprivate let logger = getLogger("openweather")
13+
14+
extension Accessory {
15+
16+
open class OpenWeatherThermometer: Thermometer {
17+
18+
let weather: OpenWeather
19+
20+
public let humiditySensor = Service.HumiditySensor()
21+
22+
public init(_ openWeatherLocation: OpenWeather) {
23+
weather = openWeatherLocation
24+
25+
super.init(info: .init(name:openWeatherLocation.name,
26+
serialNumber:openWeatherLocation.name,
27+
manufacturer:"Open Weather",
28+
model:"API",
29+
firmwareRevision: "1.0"),
30+
additionalServices: [humiditySensor]
31+
)
32+
33+
delegate = self
34+
35+
getLogger("openweather").logLevel = .debug
36+
37+
weather.whenUpdated(closure: { weatherLocation in
38+
self.temperatureSensor.currentTemperature.value = weatherLocation.temperature
39+
self.humiditySensor.currentRelativeHumidity.value = Double(weatherLocation.humidity)
40+
})
41+
updateState()
42+
}
43+
44+
func updateState() {
45+
didGetCurrentTemperature(self.weather.temperature)
46+
}
47+
48+
func didGetCurrentTemperature(_ currentTemp: CurrentTemperature?) {
49+
weather.update()
50+
}
51+
52+
}
53+
}
54+
55+
// swiftlint:disable:next no_grouping_extension
56+
extension Accessory.OpenWeatherThermometer: AccessoryDelegate {
57+
58+
public func characteristic<T>(_ characteristic: GenericCharacteristic<T>,
59+
ofService: Service,
60+
didChangeValue value: T?) {
61+
}
62+
63+
public func characteristic<T>(_ characteristic: GenericCharacteristic<T>,
64+
ofService: Service,
65+
didGetValue value: T?) where T: CharacteristicValueType {
66+
switch characteristic.type {
67+
case .currentTemperature:
68+
// swiftlint:disable:next force_cast
69+
self.didGetCurrentTemperature(value as! CurrentTemperature?)
70+
default:
71+
break
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)