Skip to content

Commit

Permalink
AccessoryDelegate to handle HAP controller Characteristic get/sets by…
Browse files Browse the repository at this point in the history
… 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.
  • Loading branch information
gbrooker committed Feb 23, 2019
1 parent 0655715 commit 594d9d3
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 7 deletions.
28 changes: 26 additions & 2 deletions Sources/HAP/Base/Accessory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ struct AIDGenerator: Sequence, IteratorProtocol, Codable {

open class Accessory: JSONSerializable {
public weak var device: Device?
public weak var delegate: AccessoryDelegate?

internal var aid: InstanceID = 0
public let type: AccessoryType
public let info: Service.Info
Expand Down Expand Up @@ -103,11 +105,12 @@ open class Accessory: JSONSerializable {

/// Characteristic's value was changed by controller. Used for bubbling up
/// to the device, which will notify the delegate.
open func characteristic<T>(_ characteristic: GenericCharacteristic<T>,
internal func characteristic<T>(_ characteristic: GenericCharacteristic<T>,
ofService service: Service,
didChangeValue newValue: T?) {
device?.characteristic(characteristic, ofService: service, ofAccessory: self, didChangeValue: newValue)
}
delegate?.characteristic(characteristic, ofService: service, didChangeValue: newValue)
}

public func serialized() -> [String: JSONValueType] {
return [
Expand All @@ -116,3 +119,24 @@ open class Accessory: JSONSerializable {
]
}
}

/// A HAP `Characteristic` calls the methods of this delegate to report
/// set/gets from a HAP controller.
///
/// Implement this protocol in an accessory-specific object (such as a subclass
/// of a given accessory) in order to make the accessory react accordingly.
/// For example, you might want to update the value of certain characteristics
/// if the HAP controller is showing interest or makes a change.

public protocol AccessoryDelegate: class {
/// Characteristic's value was changed by controller. Used for notifying
func characteristic<T>(
_ characteristic: GenericCharacteristic<T>,
ofService: Service,
didChangeValue: T?)
/// Characteristic's value was observed by controller. Used for lazy updating
func characteristic<T>(
_ characteristic: GenericCharacteristic<T>,
ofService: Service,
didGetValue: T?)
}
27 changes: 24 additions & 3 deletions Sources/HAP/Base/Characteristic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ protocol Characteristic: class, JSONSerializable {
var iid: InstanceID { get set }
var type: CharacteristicType { get }
var permissions: [CharacteristicPermission] { get }
func getValue() -> JSONValueType?
func jsonValue() -> JSONValueType?
func getValue(fromChannel: Channel?) -> JSONValueType?
func setValue(_: Any?, fromChannel: Channel?) throws
var description: String? { get }
var format: CharacteristicFormat? { get }
Expand All @@ -39,7 +40,7 @@ extension Characteristic {

if permissions.contains(.read) {
// TODO: fixit
serialized["value"] = getValue() ?? 0 //NSNull()
serialized["value"] = jsonValue() ?? 0 //NSNull()
}

if let description = description { serialized["description"] = description }
Expand Down Expand Up @@ -82,13 +83,33 @@ public class GenericCharacteristic<T: CharacteristicValueType>: Characteristic,
if let device = service?.accessory?.device {
device.fireCharacteristicChangeEvent(self)
}
if let service = self.service,
let accessory = service.accessory,
let device = accessory.device {
device.characteristic(self,
ofService: service,
ofAccessory: accessory,
didChangeValue: _value)
}
}
}

func getValue() -> JSONValueType? {
func jsonValue() -> JSONValueType? {
return value?.jsonValueType
}

// Get Value for HAP controller
func getValue(fromChannel channel: Channel?) -> JSONValueType? {
let currentValue = _value
DispatchQueue.main.async { [weak self] in
if let this = self, let service = this.service {
service.accessory?.delegate?.characteristic(this, ofService: service, didGetValue: currentValue)
}
}
return jsonValue()
}

// Set Value by HAP controller
func setValue(_ newValue: Any?, fromChannel channel: Channel?) throws {
switch newValue {
case let some?:
Expand Down
2 changes: 1 addition & 1 deletion Sources/HAP/Endpoints/characteristics().swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func characteristics(device: Device) -> Responder {
}

var value: Protocol.Value?
switch characteristic.getValue() {
switch characteristic.getValue(fromChannel: channel) {
case let _value as Double: value = .double(_value)
case let _value as Float: value = .double(Double(_value))
case let _value as UInt8: value = .int(Int(_value))
Expand Down
2 changes: 1 addition & 1 deletion Sources/HAP/Utils/Event.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ struct Event {
guard let aid = char.service?.accessory?.aid else {
throw Error.characteristicWithoutAccessory
}
payload.append(["aid": aid, "iid": char.iid, "value": char.getValue() ?? NSNull()])
payload.append(["aid": aid, "iid": char.iid, "value": char.jsonValue() ?? NSNull()])
}
let serialized = ["characteristics": payload]
guard let body = try? JSONSerialization.data(withJSONObject: serialized, options: []) else {
Expand Down
133 changes: 133 additions & 0 deletions Sources/hap-server/OpenWeather.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//
// OpenWeather.swift
// hap-server
//

import Foundation
import func Evergreen.getLogger

fileprivate let logger = getLogger("openweather")

public class OpenWeather {

public var temperature: Float {
update()
return _temperature
}

public var humidity: Int {
update()
return _humidity
}

public enum Units: String {
case imperial
case metric
}

// swiftlint:disable identifier_name
public struct Measurement: Decodable {
let temp: Float
let pressure: Int
let humidity: Int
let temp_min: Int?
let temp_max: Int?
}
public struct OpenWeatherResponse: Decodable {
let main: Measurement
let name: String
}

let appid: String
let name: String
let lat: String
let lon: String
let units: Units

private var _temperature: Float = 0.0
private var _humidity: Int = 50

private let decoder = JSONDecoder()

private let limit: TimeInterval = 900 // 15 Minutes
private var lastExecutedAt: Date?
private let updateQueue = DispatchQueue(label: "openweather", attributes: [])

private var observers = [(OpenWeather) -> Void]()

public init(name: String, lat: Double, lon: Double, appid: String, units: Units = .metric) {
precondition((lat >= -90.0) && (lat <= 90.0), "Latitude \(lat) is out of range")
precondition((lon >= -180.0) && (lon <= 180.0), "Longitude \(lon) is out of range")

self.name = name
self.appid = appid
self.lat = "\(lat)"
self.lon = "\(lon)"
self.units = units

self.update()
}

public func whenUpdated(closure: @escaping (OpenWeather) -> Void) {
observers.append(closure)
}

func update() {
updateQueue.async {
let now = Date()

// Lookup last executed
let timeInterval = now.timeIntervalSince(self.lastExecutedAt ?? .distantPast)

// Only refresh the values if the last request was older than 'limit'
if timeInterval > self.limit {
// Record execution
self.lastExecutedAt = now

self.updateNow()
}
}
}

func updateNow() {

var urlQuery = URLComponents(string: "https://api.openweathermap.org/data/2.5/weather")!
urlQuery.queryItems = [
URLQueryItem(name: "lat", value: lat),
URLQueryItem(name: "lon", value: lon),
URLQueryItem(name: "APPID", value: appid),
URLQueryItem(name: "units", value: units.rawValue)]

let url = urlQuery.url!
logger.debug("URL: \(url)")

let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
logger.debug("OpenWeather connection error \(error)")
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
logger.debug("OpenWeather Server error \(response)")
return
}

if let mimeType = httpResponse.mimeType, mimeType == "application/json",
let data = data,
let weatherReport = try? self.decoder.decode(OpenWeatherResponse.self, from: data) {

DispatchQueue.main.sync {
self._temperature = weatherReport.main.temp
self._humidity = weatherReport.main.humidity
for observer in self.observers {
observer(self)
}
}

}
}
task.resume()

}

}
75 changes: 75 additions & 0 deletions Sources/hap-server/OpenWeatherThermometer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// OpenWeatherThermometer.swift
// hap-server
//
// Created by Guy Brooker on 03/10/2018.
//

import Foundation
import HAP
import func Evergreen.getLogger

fileprivate let logger = getLogger("openweather")

extension Accessory {

open class OpenWeatherThermometer: Thermometer {

let weather: OpenWeather

public let humiditySensor = Service.HumiditySensor()

public init(_ openWeatherLocation: OpenWeather) {
weather = openWeatherLocation

super.init(info: .init(name:openWeatherLocation.name,
serialNumber:openWeatherLocation.name,
manufacturer:"Open Weather",
model:"API",
firmwareRevision: "1.0"),
additionalServices: [humiditySensor]
)

delegate = self

getLogger("openweather").logLevel = .debug

weather.whenUpdated(closure: { weatherLocation in
self.temperatureSensor.currentTemperature.value = weatherLocation.temperature
self.humiditySensor.currentRelativeHumidity.value = Float(weatherLocation.humidity)
})
updateState()
}

func updateState() {
didGetCurrentTemperature(self.weather.temperature)
}

func didGetCurrentTemperature(_ currentTemp: Float?) {
weather.update()
}
}
}

extension Accessory.OpenWeatherThermometer: AccessoryDelegate {

/// Characteristic's value was changed by controller. Used for notifying
public func characteristic<T>(
_ characteristic: GenericCharacteristic<T>,
ofService: Service,
didChangeValue: T?) {}

/// Characteristic's value was observed by controller. Used for lazy updating
public func characteristic<T>(
_ characteristic: GenericCharacteristic<T>,
ofService: Service,
didGetValue value: T?) {
switch characteristic.type {
case .currentTemperature:
// swiftlint:disable:next force_cast
didGetCurrentTemperature(value as! Float?)
default:
break
}
}
}
11 changes: 11 additions & 0 deletions Sources/hap-server/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ if CommandLine.arguments.contains("--recreate") {
try storage.write(Data())
}

//
// Register for a free account with OpenWeatherMap and obtain a personal appid
//
// https://home.openweathermap.org/api_keys
//
let appid = "033d.........................064"

let weather = OpenWeather(name: "Singapore", lat: 1.35, lon: 103.8, appid: appid)
let openWeatherSensor = Accessory.OpenWeatherThermometer(weather)

let livingRoomLightbulb = Accessory.Lightbulb(info: Service.Info(name: "Living Room", serialNumber: "00002"))
let bedroomNightStand = Accessory.Lightbulb(info: Service.Info(name: "Bedroom", serialNumber: "00003"))

Expand All @@ -28,6 +38,7 @@ let device = Device(
setupCode: "123-44-321",
storage: storage,
accessories: [
openWeatherSensor,
livingRoomLightbulb,
bedroomNightStand
// Accessory.Door(info: Service.Info(name: "Front Door", serialNumber: "00005")),
Expand Down
Binary file added libsodium.23.dylib
Binary file not shown.

0 comments on commit 594d9d3

Please sign in to comment.