Skip to content

Commit 5ab417c

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 0655715 commit 5ab417c

File tree

8 files changed

+273
-6
lines changed

8 files changed

+273
-6
lines changed

Sources/HAP/Base/Accessory.swift

+25-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ struct AIDGenerator: Sequence, IteratorProtocol, Codable {
2727

2828
open class Accessory: JSONSerializable {
2929
public weak var device: Device?
30+
public weak var delegate: AccessoryDelegate?
31+
3032
internal var aid: InstanceID = 0
3133
public let type: AccessoryType
3234
public let info: Service.Info
@@ -107,7 +109,8 @@ open class Accessory: JSONSerializable {
107109
ofService service: Service,
108110
didChangeValue newValue: T?) {
109111
device?.characteristic(characteristic, ofService: service, ofAccessory: self, didChangeValue: newValue)
110-
}
112+
delegate?.characteristic(characteristic, ofService: service, didChangeValue: newValue)
113+
}
111114

112115
public func serialized() -> [String: JSONValueType] {
113116
return [
@@ -116,3 +119,24 @@ open class Accessory: JSONSerializable {
116119
]
117120
}
118121
}
122+
123+
/// A HAP `Characteristic` calls the methods of this delegate to report
124+
/// set/gets from a HAP controller.
125+
///
126+
/// Implement this protocol in an accessory-specific object (such as a subclass
127+
/// of a given accessory) in order to make the accessory react accordingly.
128+
/// For example, you might want to update the value of certain characteristics
129+
/// if the HAP controller is showing interest or makes a change.
130+
131+
public protocol AccessoryDelegate: class {
132+
/// Characteristic's value was changed by controller. Used for notifying
133+
func characteristic<T>(
134+
_ characteristic: GenericCharacteristic<T>,
135+
ofService: Service,
136+
didChangeValue: T?)
137+
/// Characteristic's value was observed by controller. Used for lazy updating
138+
func characteristic<T>(
139+
_ characteristic: GenericCharacteristic<T>,
140+
ofService: Service,
141+
didGetValue: T?)
142+
}

Sources/HAP/Base/Characteristic.swift

+28-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import Foundation
22
import NIO
33

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

@@ -18,7 +22,8 @@ protocol Characteristic: class, JSONSerializable {
1822
var iid: InstanceID { get set }
1923
var type: CharacteristicType { get }
2024
var permissions: [CharacteristicPermission] { get }
21-
func getValue() -> JSONValueType?
25+
func jsonValue() -> JSONValueType?
26+
func getValue(fromChannel: Channel?) -> JSONValueType?
2227
func setValue(_: Any?, fromChannel: Channel?) throws
2328
var description: String? { get }
2429
var format: CharacteristicFormat? { get }
@@ -39,7 +44,7 @@ extension Characteristic {
3944

4045
if permissions.contains(.read) {
4146
// TODO: fixit
42-
serialized["value"] = getValue() ?? 0 //NSNull()
47+
serialized["value"] = jsonValue() ?? 0 //NSNull()
4348
}
4449

4550
if let description = description { serialized["description"] = description }
@@ -82,13 +87,33 @@ public class GenericCharacteristic<T: CharacteristicValueType>: Characteristic,
8287
if let device = service?.accessory?.device {
8388
device.fireCharacteristicChangeEvent(self)
8489
}
90+
if let service = self.service,
91+
let accessory = service.accessory,
92+
let device = accessory.device {
93+
device.characteristic(self,
94+
ofService: service,
95+
ofAccessory: accessory,
96+
didChangeValue: _value)
97+
}
8598
}
8699
}
87100

88-
func getValue() -> JSONValueType? {
101+
func jsonValue() -> JSONValueType? {
89102
return value?.jsonValueType
90103
}
91104

105+
// Get Value for HAP controller
106+
func getValue(fromChannel channel: Channel?) -> JSONValueType? {
107+
let currentValue = _value
108+
DispatchQueue.main.async { [weak self] in
109+
if let this = self, let service = this.service {
110+
service.accessory?.delegate?.characteristic(this, ofService: service, didGetValue: currentValue)
111+
}
112+
}
113+
return jsonValue()
114+
}
115+
116+
// Set Value by HAP controller
92117
func setValue(_ newValue: Any?, fromChannel channel: Channel?) throws {
93118
switch newValue {
94119
case let some?:

Sources/HAP/Endpoints/characteristics().swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func characteristics(device: Device) -> Responder {
4747
}
4848

4949
var value: Protocol.Value?
50-
switch characteristic.getValue() {
50+
switch characteristic.getValue(fromChannel: channel) {
5151
case let _value as Double: value = .double(_value)
5252
case let _value as Float: value = .double(Double(_value))
5353
case let _value as UInt8: value = .int(Int(_value))

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: Float {
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: Float
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: Float = 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, AccessoryDelegate {
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 = Float(weatherLocation.humidity)
40+
})
41+
updateState()
42+
}
43+
44+
func updateState() {
45+
didGetCurrentTemperature(self.weather.temperature)
46+
}
47+
48+
func didGetCurrentTemperature(_ currentTemp: Float?) {
49+
weather.update()
50+
}
51+
52+
53+
/// Characteristic's value was changed by controller. Used for notifying
54+
open override func characteristic<T>(
55+
_ characteristic: GenericCharacteristic<T>,
56+
ofService: Service,
57+
didChangeValue: T?) {}
58+
59+
/// Characteristic's value was observed by controller. Used for lazy updating
60+
public func characteristic<T>(
61+
_ characteristic: GenericCharacteristic<T>,
62+
ofService: Service,
63+
didGetValue value: T?) {
64+
switch characteristic.type {
65+
case .currentTemperature:
66+
// swiftlint:disable:next force_cast
67+
didGetCurrentTemperature(value as! Float?)
68+
default:
69+
break
70+
}
71+
}
72+
73+
}
74+
}

Sources/hap-server/main.swift

+11
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ if CommandLine.arguments.contains("--recreate") {
2020
try storage.write(Data())
2121
}
2222

23+
//
24+
// Register for a free account with OpenWeatherMap and obtain a personal appid
25+
//
26+
// https://home.openweathermap.org/api_keys
27+
//
28+
let appid = "033d.........................064"
29+
30+
let weather = OpenWeather(name: "Singapore", lat: 1.35, lon: 103.8, appid: appid)
31+
let openWeatherSensor = Accessory.OpenWeatherThermometer(weather)
32+
2333
let livingRoomLightbulb = Accessory.Lightbulb(info: Service.Info(name: "Living Room", serialNumber: "00002"))
2434
let bedroomNightStand = Accessory.Lightbulb(info: Service.Info(name: "Bedroom", serialNumber: "00003"))
2535

@@ -28,6 +38,7 @@ let device = Device(
2838
setupCode: "123-44-321",
2939
storage: storage,
3040
accessories: [
41+
openWeatherSensor,
3142
livingRoomLightbulb,
3243
bedroomNightStand
3344
// Accessory.Door(info: Service.Info(name: "Front Door", serialNumber: "00005")),

libsodium.23.dylib

494 KB
Binary file not shown.

0 commit comments

Comments
 (0)