diff --git a/Sources/HAP/Base/Accessory.swift b/Sources/HAP/Base/Accessory.swift index 7cf00ea0..021309dc 100644 --- a/Sources/HAP/Base/Accessory.swift +++ b/Sources/HAP/Base/Accessory.swift @@ -1,10 +1,14 @@ import Foundation -// TODO: SwiftFoundation in Swift 4.0 cannot encode/decode UInt64, -// which is the data-type we wanted to use here. We can change it back -// to UInt64 once the following commit has made it into a release: -// https://github.com/apple/swift-corelibs-foundation/commit/64b67c91479390776c43a96bd31e4e85f106d5e1 -typealias InstanceID = Int +// HAP Specification 2.6.1: Instance IDs +// +// instance IDs are numbers with a range of [1, 18446744073709551615] for IP +// accessories (see ”7.4.4.2 Instance IDs” (page 122) for BLE accessories). +// These numbers are used to uniquely identify HAP accessory objects within an +// HAP accessory server, or uniquely identify services, and characteristics +// within an HAP accessory object. The instance ID for each object must be +// unique for the lifetime of the server/client pairing. +public typealias InstanceID = UInt64 // HAP Specification 2.6.1.1: Accessory Instance IDs // @@ -25,7 +29,7 @@ struct AIDGenerator: Sequence, IteratorProtocol, Codable { } } -open class Accessory: Hashable, JSONSerializable { +open class Accessory: Hashable, JSONSerializable, CustomDebugStringConvertible { public weak var device: Device? internal var aid: InstanceID = 0 public let type: AccessoryType @@ -127,4 +131,8 @@ open class Accessory: Hashable, JSONSerializable { public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } + + public var debugDescription: String { + "#\(aid) \(type) \(info.name.value ?? "Unnamed accessory")" + } } diff --git a/Sources/HAP/Base/Characteristic.swift b/Sources/HAP/Base/Characteristic.swift index 440fb691..3ab0b195 100644 --- a/Sources/HAP/Base/Characteristic.swift +++ b/Sources/HAP/Base/Characteristic.swift @@ -62,14 +62,14 @@ extension Characteristic { } } -public class GenericCharacteristic: Characteristic, JSONSerializable, Hashable, Equatable { +public class GenericCharacteristic: Characteristic, JSONSerializable, Hashable, Equatable, CustomDebugStringConvertible { enum Error: Swift.Error { case valueTypeException } weak var service: Service? - internal var iid: InstanceID = 0 + public var iid: InstanceID = 0 public let type: CharacteristicType internal var _value: T? @@ -179,4 +179,8 @@ public class GenericCharacteristic: Characteristic, internal var device: Device? { service?.accessory?.device } + + public var debugDescription: String { + "characteristic #\(iid) \(description ?? type.description) of service \(service?.debugDescription ?? "no service")" + } } diff --git a/Sources/HAP/Base/CharacteristicValueType.swift b/Sources/HAP/Base/CharacteristicValueType.swift index e56fac25..2d53252f 100644 --- a/Sources/HAP/Base/CharacteristicValueType.swift +++ b/Sources/HAP/Base/CharacteristicValueType.swift @@ -1,4 +1,5 @@ import Foundation +import Logging public protocol CharacteristicValueType: Equatable, JSONValueTypeConvertible { init?(value: Any) @@ -171,11 +172,14 @@ extension Double: CharacteristicValueType { extension Data: CharacteristicValueType, JSONValueTypeConvertible { public init?(value: Any) { - fatalError("How does deserialization of Data work?") + switch value { + case let value as String: self = Data(base64Encoded: value)! + default: fatalError("don't now how to decode \(value)") + } } static public let format = CharacteristicFormat.data public var jsonValueType: JSONValueType { - fatalError("How does serialization of Data work?") + return self.base64EncodedString() } } diff --git a/Sources/HAP/Base/Constants.swift b/Sources/HAP/Base/Constants.swift index f52ceb99..c6bf91e3 100644 --- a/Sources/HAP/Base/Constants.swift +++ b/Sources/HAP/Base/Constants.swift @@ -80,6 +80,7 @@ public enum CharacteristicPermission: String, Codable { // This characteristic is hidden from the user case hidden = "hd" + // This characteristic supports write response case writeResponse = "wr" // Short-hand for "all" permissions. diff --git a/Sources/HAP/Base/Predefined/Characteristics/Characteristic.CharacteristicValueTransitionControl.swift b/Sources/HAP/Base/Predefined/Characteristics/Characteristic.CharacteristicValueTransitionControl.swift index f53ca3c3..29a8b52a 100644 --- a/Sources/HAP/Base/Predefined/Characteristics/Characteristic.CharacteristicValueTransitionControl.swift +++ b/Sources/HAP/Base/Predefined/Characteristics/Characteristic.CharacteristicValueTransitionControl.swift @@ -3,7 +3,7 @@ import Foundation public extension AnyCharacteristic { static func characteristicValueTransitionControl( _ value: Data = Data(), - permissions: [CharacteristicPermission] = [.read, .write], + permissions: [CharacteristicPermission] = [.read, .write, .writeResponse], description: String? = "Characteristic Value Transition Control", format: CharacteristicFormat? = .tlv8, unit: CharacteristicUnit? = nil, @@ -33,7 +33,7 @@ public extension AnyCharacteristic { public extension PredefinedCharacteristic { static func characteristicValueTransitionControl( _ value: Data = Data(), - permissions: [CharacteristicPermission] = [.read, .write], + permissions: [CharacteristicPermission] = [.read, .write, .writeResponse], description: String? = "Characteristic Value Transition Control", format: CharacteristicFormat? = .tlv8, unit: CharacteristicUnit? = nil, diff --git a/Sources/HAP/Base/Service.swift b/Sources/HAP/Base/Service.swift index 33932de0..b7dc6b41 100644 --- a/Sources/HAP/Base/Service.swift +++ b/Sources/HAP/Base/Service.swift @@ -132,6 +132,10 @@ open class Service: NSObject, JSONSerializable { } return json } + + open override var debugDescription: String { + "#\(iid) \(type.description) of accessory \(accessory?.debugDescription ?? "no accessory")" + } } func getOrCreateAppend(type: CharacteristicType, diff --git a/Sources/HAP/Endpoints/characteristics().swift b/Sources/HAP/Endpoints/characteristics().swift index 2f0a5795..9047b8b4 100644 --- a/Sources/HAP/Endpoints/characteristics().swift +++ b/Sources/HAP/Endpoints/characteristics().swift @@ -42,6 +42,9 @@ func characteristics(device: Device, channel: Channel, request: HTTPRequest) -> status: .resourceDoesNotExist)) continue } + + logger.trace("reading value of \(characteristic)") + guard characteristic.permissions.contains(.read) else { logger.info("\(characteristic) has no read permission") responses.append(Protocol.Characteristic(aid: path.aid, iid: path.iid, status: .writeOnly)) @@ -91,6 +94,8 @@ func characteristics(device: Device, channel: Channel, request: HTTPRequest) -> response.ev = characteristic.permissions.contains(.events) } responses.append(response) + + logger.trace("read value: \(value)") } /* HAP Specification 5.7.3.2 diff --git a/Sources/HAP/Server/Device.swift b/Sources/HAP/Server/Device.swift index 89dc2730..7e37c7bf 100644 --- a/Sources/HAP/Server/Device.swift +++ b/Sources/HAP/Server/Device.swift @@ -257,17 +257,17 @@ public class Device { /// Generate uniqueness hash for device configuration, used to determine /// if the configuration number should be updated. func generateStableHash() -> Int { - var hash = 0 + var hash: UInt64 = 0 for accessory in accessories { - hash ^= 17 &* accessory.aid + hash ^= 17 &* UInt64(accessory.aid) for service in accessory.services { - hash ^= 19 &* service.iid + hash ^= 19 &* UInt64(service.iid) for characteristic in service.characteristics { - hash ^= 23 &* characteristic.iid + hash ^= 23 &* UInt64(characteristic.iid) } } } - return hash + return Int(truncatingIfNeeded: hash) } /// Notify the server that the config record has changed diff --git a/Sources/HAP/Server/JSON.swift b/Sources/HAP/Server/JSON.swift index a854021c..0fc36f56 100644 --- a/Sources/HAP/Server/JSON.swift +++ b/Sources/HAP/Server/JSON.swift @@ -18,6 +18,7 @@ extension Int: JSONValueType { } extension UInt8: JSONValueType { } extension UInt16: JSONValueType { } extension UInt32: JSONValueType { } +extension UInt64: JSONValueType { } extension Float: JSONValueType { } extension Double: JSONValueType { } extension NSNull: JSONValueType { } diff --git a/Sources/HAP/Utils/Data+Extensions.swift b/Sources/HAP/Utils/Data+Extensions.swift index 357e843e..edabfd19 100644 --- a/Sources/HAP/Utils/Data+Extensions.swift +++ b/Sources/HAP/Utils/Data+Extensions.swift @@ -26,7 +26,7 @@ extension Data { } extension RandomAccessCollection where Iterator.Element == UInt8 { - var hex: String { + public var hex: String { self.reduce("") { $0 + String(format: "%02x", $1) } } } diff --git a/Sources/HAP/Utils/TLV8.swift b/Sources/HAP/Utils/TLV8.swift index ce5779eb..59746cfd 100644 --- a/Sources/HAP/Utils/TLV8.swift +++ b/Sources/HAP/Utils/TLV8.swift @@ -46,7 +46,7 @@ enum TLV8Error: Swift.Error { case decodeError } -func decode(_ data: Data) throws -> [(Key, Data)] where Key: RawRepresentable, Key.RawValue == UInt8 { +public func decode(_ data: Data) throws -> [(Key, Data)] where Key: RawRepresentable, Key.RawValue == UInt8 { var result = [(Key, Data)]() var index = data.startIndex var currentType: Key? @@ -84,7 +84,7 @@ func decode(_ data: Data) throws -> [(Key, Data)] where Key: RawRepresentab return result } -func encode(_ array: [(Key, Data)]) -> Data where Key: RawRepresentable, Key.RawValue == UInt8 { +public func encode(_ array: [(Key, Data)]) -> Data where Key: RawRepresentable, Key.RawValue == UInt8 { var result = Data() func append(type: UInt8, value: Data.SubSequence) { result.append(Data([type, UInt8(value.count)] + value)) @@ -105,6 +105,30 @@ func encode(_ array: [(Key, Data)]) -> Data where Key: RawRepresentable, Ke return result } +// I think we can avoid duplication of encode methods by having the value be a `TLV8ValueProtocol` thingy, which both Data and [Data] adhere to. +public func encode(_ array: [(Key, [Data])]) -> Data where Key: RawRepresentable, Key.RawValue == UInt8 { + var result = Data() + for (type, value) in array { + var first = true + for (item) in value { + if !first { + result.append(Data([TLV8.delimiter.rawValue, 0])) + } + first = false + result.append(encode([(type, item)])) + } + if first { + // Zero-length array + result.append(Data([type.rawValue, 0])) + } + } + return result +} + +enum TLV8: UInt8 { + case delimiter = 0 +} + // Pair Setup State enum PairSetupStep: UInt8 { case waiting = 0 diff --git a/Sources/HAPDemo/createLogHandler.swift b/Sources/HAPDemo/createLogHandler.swift index 7cc215fe..e7a1f29f 100644 --- a/Sources/HAPDemo/createLogHandler.swift +++ b/Sources/HAPDemo/createLogHandler.swift @@ -7,6 +7,8 @@ func createLogHandler(label: String) -> LogHandler { switch label { case "hap.encryption": handler.logLevel = .info + case "hap.endpoints.characteristics": + handler.logLevel = .trace case "hap", _ where label.starts(with: "hap."): handler.logLevel = .debug diff --git a/Sources/HAPDemo/main.swift b/Sources/HAPDemo/main.swift index 3e6b1779..a677d35f 100644 --- a/Sources/HAPDemo/main.swift +++ b/Sources/HAPDemo/main.swift @@ -30,6 +30,51 @@ if CommandLine.arguments.contains("--recreate") { let livingRoomLightbulb = Accessory.Lightbulb(info: Service.Info(name: "Living Room", serialNumber: "00002")) let bedroomNightStand = Accessory.Lightbulb(info: Service.Info(name: "Bedroom", serialNumber: "00003")) +let brightness = PredefinedCharacteristic.brightness() +let colorTemperature = PredefinedCharacteristic.colorTemperature(maxValue: 400, minValue: 50) +let supportedTransitions = PredefinedCharacteristic.supportedCharacteristicValueTransitionConfiguration() + +let adaptive = Accessory(info: .init(name: "Adaptive", serialNumber: "0001"), type: .lightbulb, services: [ + Service.Lightbulb(characteristics: [ + AnyCharacteristic(brightness), + AnyCharacteristic(colorTemperature), + .characteristicValueTransitionControl(), + .characteristicValueActiveTransitionCount(), + AnyCharacteristic(supportedTransitions) + ]) +]) + +enum SupportedCharacteristicValueTransitionConfigurationsTypes : UInt8 { + case SupportedTransitionConfiguration = 0x01 +} + +enum SupportedValueTransitionConfigurationTypes : UInt8 { + case CharacteristicIid = 0x01 + case TransitionType = 0x02 +} + +enum TransitionType : UInt8 { + case Brightness = 0x01 + case ColorTemperature = 0x02 +} + +supportedTransitions.value = encode([ + (SupportedCharacteristicValueTransitionConfigurationsTypes.SupportedTransitionConfiguration, [ + encode([ + (SupportedValueTransitionConfigurationTypes.CharacteristicIid, brightness.iid.littleEndianBytes), + (SupportedValueTransitionConfigurationTypes.TransitionType, Data([TransitionType.Brightness.rawValue])), + ]), + encode([ + (SupportedValueTransitionConfigurationTypes.CharacteristicIid, colorTemperature.iid.littleEndianBytes), + (SupportedValueTransitionConfigurationTypes.TransitionType, Data([TransitionType.ColorTemperature.rawValue])), + ]), + ]) +]); + +//print("Did encode as: \(supportedTransitions.value!.base64EncodedString())") +//print("Did encode as: \(supportedTransitions.value!.hex)") +//exit(0) + // And a security system with multiple zones and statuses fault and tampered. let securitySystem = Accessory( info: Service.Info(name: "Multi-Zone", serialNumber: "A1803"), @@ -45,11 +90,14 @@ let device = Device( setupCode: "123-44-321", storage: storage, accessories: [ - livingRoomLightbulb, - bedroomNightStand, - securitySystem + adaptive +// livingRoomLightbulb, +// bedroomNightStand, +// securitySystem ]) + + // Attach a delegate that logs all activity. var delegate = LoggingDeviceDelegate(logger: logger) device.delegate = delegate diff --git a/Sources/HAPInspector/Inspector.swift b/Sources/HAPInspector/Inspector.swift index fd309225..9872c6cc 100644 --- a/Sources/HAPInspector/Inspector.swift +++ b/Sources/HAPInspector/Inspector.swift @@ -199,6 +199,10 @@ let defaultTypes: [DefaultType] = [ ] +let additionalPermissions = [ + "characteristic-value-transition-control": CharacteristicInfoPermission.writeResponse +] + struct FileHandlerOutputStream: TextOutputStream { private let fileHandle: FileHandle let encoding: String.Encoding @@ -404,7 +408,7 @@ func inspect(source plistPath: URL, target outputPath: String) throws { format: format, maxValue: dict["MaxValue"] as? NSNumber, minValue: dict["MinValue"] as? NSNumber, - permissions: CharacteristicInfoPermission(rawValue: dict["Properties"] as! Int), + permissions: CharacteristicInfoPermission(rawValue: dict["Properties"] as! UInt32).union(additionalPermissions[name] ?? []), stepValue: dict["StepValue"] as? NSNumber, units: dict["Units"] as? String)) characteristicFormats.insert(format) @@ -874,17 +878,21 @@ struct ServiceInfo { } struct CharacteristicInfoPermission: OptionSet, CustomStringConvertible { - let rawValue: Int + let rawValue: UInt32 static let read = CharacteristicInfoPermission(rawValue: 2) static let write = CharacteristicInfoPermission(rawValue: 4) static let events = CharacteristicInfoPermission(rawValue: 1) + // Custom values to support additional permissions. + static let writeResponse = CharacteristicInfoPermission(rawValue: 1 << 31) + var description: String { var permissions: [String] = [] if contains(.read) { permissions += [".read"] } if contains(.write) { permissions += [".write"] } if contains(.events) { permissions += [".events"] } + if contains(.writeResponse) { permissions += [".writeResponse"] } return "[" + permissions.joined(separator: ", ") + "]" } } diff --git a/Tests/HAPTests/EndpointTests.swift b/Tests/HAPTests/EndpointTests.swift index 94274fa3..6af7fdea 100644 --- a/Tests/HAPTests/EndpointTests.swift +++ b/Tests/HAPTests/EndpointTests.swift @@ -15,7 +15,7 @@ class EndpointTests: XCTestCase { guard let accessory = jsonObject["accessories"]?.first else { return XCTFail("No accessory") } - XCTAssertEqual(accessory["aid"] as? Int, lamp.aid) + XCTAssertEqual(accessory["aid"] as? InstanceID, lamp.aid) guard let services = accessory["services"] as? [[String: Any]] else { return XCTFail("No services") } @@ -77,7 +77,7 @@ class EndpointTests: XCTestCase { return XCTFail("No characteristics") } - guard let nameCharacteristic = characteristics.first(where: { $0["iid"] as? Int == energy.info.name.iid }) else { + guard let nameCharacteristic = characteristics.first(where: { $0["iid"] as? InstanceID == energy.info.name.iid }) else { return XCTFail("No name characteristic") } XCTAssertEqual(nameCharacteristic["value"] as? String, "Energy") @@ -85,7 +85,7 @@ class EndpointTests: XCTestCase { XCTAssertEqual(nameCharacteristic["type"] as? String, "23") XCTAssertEqual(nameCharacteristic["ev"] as? Bool, false) - guard let wattCharacteristic = characteristics.first(where: { $0["iid"] as? Int == energy.service.watt.iid }) else { + guard let wattCharacteristic = characteristics.first(where: { $0["iid"] as? InstanceID == energy.service.watt.iid }) else { return XCTFail("No identify characteristic") } XCTAssertEqual(wattCharacteristic["value"] as? Int, 42) @@ -111,12 +111,12 @@ class EndpointTests: XCTestCase { return XCTFail("No characteristics") } - guard let manufacturerCharacteristic = characteristics.first(where: { $0["iid"] as? Int == lamp.info.manufacturer.iid }) else { + guard let manufacturerCharacteristic = characteristics.first(where: { $0["iid"] as? InstanceID == lamp.info.manufacturer.iid }) else { return XCTFail("No manufacturer characteristic") } XCTAssertEqual(manufacturerCharacteristic["value"] as? String, "Bouke") - guard let nameCharacteristic = characteristics.first(where: { $0["iid"] as? Int == lamp.info.name.iid }) else { + guard let nameCharacteristic = characteristics.first(where: { $0["iid"] as? InstanceID == lamp.info.name.iid }) else { return XCTFail("No name characteristic") } XCTAssertEqual(nameCharacteristic["value"] as? String, "Night stand left") @@ -131,7 +131,7 @@ class EndpointTests: XCTestCase { return XCTFail("No characteristics") } - guard let nameCharacteristic = characteristics.first(where: { $0["iid"] as? Int == lamp.info.name.iid }) else { + guard let nameCharacteristic = characteristics.first(where: { $0["iid"] as? InstanceID == lamp.info.name.iid }) else { return XCTFail("No name characteristic") } XCTAssertEqual(nameCharacteristic["value"] as? String, "Night stand left") @@ -139,7 +139,7 @@ class EndpointTests: XCTestCase { XCTAssertEqual(nameCharacteristic["type"] as? String, "23") XCTAssertEqual(nameCharacteristic["ev"] as? Bool, false) - guard let brightnessCharacteristic = characteristics.first(where: { $0["iid"] as? Int == lamp.lightbulb.brightness!.iid }) else { + guard let brightnessCharacteristic = characteristics.first(where: { $0["iid"] as? InstanceID == lamp.lightbulb.brightness!.iid }) else { return XCTFail("No identify characteristic") } XCTAssertEqual(brightnessCharacteristic["value"] as? Int, 100) @@ -571,15 +571,15 @@ class EndpointTests: XCTestCase { return XCTFail("No characteristics") } - guard let light = characteristics.first(where: { $0["aid"] as? Int == lightsensor.aid }) else { + guard let light = characteristics.first(where: { $0["aid"] as? InstanceID == lightsensor.aid }) else { return XCTFail("Could not get light aid") } - guard let therm = characteristics.first(where: { $0["aid"] as? Int == thermostat.aid }) else { + guard let therm = characteristics.first(where: { $0["aid"] as? InstanceID == thermostat.aid }) else { return XCTFail("Could not get therm aid") } - guard let lampa = characteristics.first(where: { $0["aid"] as? Int == lamp.aid }) else { + guard let lampa = characteristics.first(where: { $0["aid"] as? InstanceID == lamp.aid }) else { return XCTFail("Could not get lampa aid") } @@ -608,8 +608,8 @@ class EndpointTests: XCTestCase { XCTAssertEqual(response.status, .multiStatus) let json = try! JSONSerialization.jsonObject(with: response.body.data ?? Data()) as! [String: [[String: Any]]] XCTAssertEqual(json["characteristics"]![0]["status"]! as! Int, HAPStatusCodes.writeOnly.rawValue) - XCTAssertEqual(json["characteristics"]![0]["aid"]! as! Int, aid) - XCTAssertEqual(json["characteristics"]![0]["iid"]! as! Int, iid) + XCTAssertEqual(json["characteristics"]![0]["aid"]! as! InstanceID, aid) + XCTAssertEqual(json["characteristics"]![0]["iid"]! as! InstanceID, iid) } // trying to read write only access and one with read access