Skip to content

Commit 591ecdf

Browse files
committed
Implement events
1 parent aa396b7 commit 591ecdf

13 files changed

+484
-333
lines changed

HomeKitDotNet/AccessoryInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ public struct AccessoryInfo
1616
{
1717
public AccessoryInfo(byte[] id, byte[] ltpk)
1818
{
19-
PairingID = id;
19+
ID = id;
2020
LTPK = ltpk;
2121
}
22-
public byte[] PairingID { get; set; }
22+
public byte[] ID { get; set; }
2323
public byte[] LTPK { get; set; }
2424
}
2525
}

HomeKitDotNet/Connection.cs

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System.Net.Sockets;
1717
using System.Security.Cryptography;
1818
using System.Text;
19+
using System.Threading.Channels;
1920

2021
namespace HomeKitDotNet
2122
{
@@ -26,6 +27,9 @@ public class Connection : IDisposable
2627
EncryptedStreamReader reader;
2728
readonly string host;
2829
SemaphoreSlim semaphore = new SemaphoreSlim(1);
30+
CancellationTokenSource token = new CancellationTokenSource();
31+
Channel<HttpResponseMessage> responses = Channel.CreateUnbounded<HttpResponseMessage>();
32+
public event EventHandler<HttpResponseMessage>? EventReceived;
2933

3034
public Connection(IPEndPoint ep)
3135
{
@@ -35,18 +39,26 @@ public Connection(IPEndPoint ep)
3539
client.Connect(ep.Address, ep.Port);
3640
writer = new EncryptedStreamWriter(client.GetStream());
3741
reader = new EncryptedStreamReader(client.GetStream());
42+
Task.Factory.StartNew(ReceiveLoop, token.Token);
3843
}
3944

4045
public void Dispose()
4146
{
47+
token.Cancel();
48+
token.Dispose();
4249
client.Dispose();
4350
}
4451

4552
public void EnableEncryption(byte[] writeKey, byte[] readKey)
4653
{
54+
token.Cancel();
55+
token.Dispose();
4756

4857
writer.EnableEncryption(new ChaCha20Poly1305(writeKey));
4958
reader.EnableDecryption(new ChaCha20Poly1305(readKey));
59+
60+
token = new CancellationTokenSource();
61+
Task.Factory.StartNew(ReceiveLoop, token.Token);
5062
}
5163

5264
public async Task<HttpResponseMessage> Get(string path)
@@ -57,7 +69,7 @@ public async Task<HttpResponseMessage> Get(string path)
5769
public async Task<HttpResponseMessage> Put(string path, byte[] json)
5870
{
5971
ByteArrayContent content = new ByteArrayContent(json);
60-
content.Headers.ContentType = new MediaTypeHeaderValue("application/hap+json\r\n");
72+
content.Headers.ContentType = new MediaTypeHeaderValue("application/hap+json");
6173
return await SendAsync(HttpMethod.Put, path, content);
6274
}
6375

@@ -73,6 +85,54 @@ public async Task<HttpResponseMessage> Post(string path, params TLVValue[] tlvs)
7385
return await SendAsync(HttpMethod.Post, path, content);
7486
}
7587

88+
public async Task ReceiveLoop()
89+
{
90+
try
91+
{
92+
while (!token.IsCancellationRequested)
93+
{
94+
string? line = await reader.ReadLineAsync(token.Token);
95+
if (line == null)
96+
throw new EndOfStreamException();
97+
string[] parts = line.Split(' ', 3);
98+
if (parts.Length != 3 || !int.TryParse(parts[1], out int status))
99+
throw new HttpRequestException("Invalid Response: " + line);
100+
HttpResponseMessage response = new HttpResponseMessage((HttpStatusCode)status);
101+
response.ReasonPhrase = parts[2];
102+
string protocol = parts[0];
103+
int contentLen = 0;
104+
string type = "";
105+
while (line != "" && line != null)
106+
{
107+
parts = line.Split(':', StringSplitOptions.TrimEntries);
108+
if (parts.Length == 2 && parts[0].Equals("content-length", StringComparison.InvariantCultureIgnoreCase))
109+
contentLen = int.Parse(parts[1]);
110+
if (parts.Length == 2 && parts[0].Equals("content-type", StringComparison.InvariantCultureIgnoreCase))
111+
type = parts[1];
112+
line = await reader.ReadLineAsync(token.Token);
113+
}
114+
if (line == null)
115+
throw new EndOfStreamException();
116+
if (contentLen != 0)
117+
{
118+
byte[] contentBytes = new byte[contentLen];
119+
await reader.ReadBytesAsync(contentBytes, token.Token);
120+
response.Content = new ByteArrayContent(contentBytes);
121+
response.Content.Headers.ContentType = new MediaTypeHeaderValue(type);
122+
}
123+
if (protocol == "EVENT/1.0")
124+
EventReceived?.Invoke(this, response);
125+
else
126+
responses.Writer.TryWrite(response);
127+
}
128+
}
129+
catch (OperationCanceledException) { } // Ignore
130+
catch (Exception e)
131+
{
132+
Console.WriteLine("Error: " + e.ToString());
133+
}
134+
}
135+
76136
protected async Task<HttpResponseMessage> SendAsync(HttpMethod method, string path, ByteArrayContent? content)
77137
{
78138
Stream? contentStream = null;
@@ -87,9 +147,7 @@ protected async Task<HttpResponseMessage> SendAsync(HttpMethod method, string pa
87147
if (content != null)
88148
{
89149
msg.Append($"Content-Length: {contentStream!.Length}\r\n");
90-
msg.Append("Content-Type: ");
91-
msg.Append(content.Headers.ContentType);
92-
msg.Append("\r\n");
150+
msg.Append($"Content-Type: {content.Headers.ContentType}\r\n");
93151
}
94152
msg.Append("\r\n");
95153
await semaphore.WaitAsync();
@@ -98,36 +156,7 @@ protected async Task<HttpResponseMessage> SendAsync(HttpMethod method, string pa
98156
if (contentStream != null)
99157
await writer.WriteAsync(contentStream);
100158
await writer.FlushAsync();
101-
102-
string? line = await reader.ReadLineAsync();
103-
if (line == null)
104-
throw new EndOfStreamException();
105-
string[] parts = line.Split(' ', 3);
106-
if (parts.Length != 3 || !int.TryParse(parts[1], out int status))
107-
throw new HttpRequestException("Invalid Response: " + line);
108-
HttpResponseMessage response = new HttpResponseMessage((HttpStatusCode)status);
109-
response.ReasonPhrase = parts[2];
110-
int contentLen = 0;
111-
string type = "";
112-
while (line != "" && line != null)
113-
{
114-
parts = line.Split(':', StringSplitOptions.TrimEntries);
115-
if (parts.Length == 2 && parts[0].Equals("content-length", StringComparison.InvariantCultureIgnoreCase))
116-
contentLen = int.Parse(parts[1]);
117-
if (parts.Length == 2 && parts[0].Equals("content-type", StringComparison.InvariantCultureIgnoreCase))
118-
type = parts[1];
119-
line = await reader.ReadLineAsync();
120-
}
121-
if (line == null)
122-
throw new EndOfStreamException();
123-
if (contentLen != 0)
124-
{
125-
byte[] contentBytes = new byte[contentLen];
126-
await reader.ReadBytesAsync(contentBytes);
127-
response.Content = new ByteArrayContent(contentBytes);
128-
response.Content.Headers.ContentType = new MediaTypeHeaderValue(type);
129-
}
130-
return response;
159+
return await responses.Reader.ReadAsync();
131160
}
132161
finally
133162
{

HomeKitDotNet/EncryptedStreamReader.cs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
// You should have received a copy of the GNU Affero General Public License
1111
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1212

13-
using System;
1413
using System.Buffers.Binary;
15-
using System.IO;
1614
using System.Security.Cryptography;
1715
using System.Text;
1816

@@ -31,11 +29,11 @@ public EncryptedStreamReader(Stream stream)
3129
this.stream = stream;
3230
}
3331

34-
public async Task<string> ReadLineAsync()
32+
public async Task<string> ReadLineAsync(CancellationToken token)
3533
{
3634
int newLine = -1;
3735
if (decrypter != null && buffEnd <= readPos)
38-
await ReadEncrypted();
36+
await ReadEncrypted(token);
3937
if (buffEnd > 0)
4038
newLine = FindReturn(buffer.Slice(readPos, buffEnd - readPos).Span);
4139
if (decrypter == null)
@@ -49,7 +47,7 @@ public async Task<string> ReadLineAsync()
4947
buffEnd -= readPos;
5048
readPos = 0;
5149
}
52-
buffEnd += await stream.ReadAsync(buffer.Slice(buffEnd));
50+
buffEnd += await stream.ReadAsync(buffer.Slice(buffEnd), token);
5351
newLine = FindReturn(buffer.Slice(readPos, buffEnd - readPos).Span);
5452
}
5553
}
@@ -62,10 +60,10 @@ public async Task<string> ReadLineAsync()
6260
return ret;
6361
}
6462

65-
public async Task ReadBytesAsync(byte[] bytes)
63+
public async Task ReadBytesAsync(byte[] bytes, CancellationToken token)
6664
{
6765
if (decrypter == null)
68-
await ReadBytesStream(bytes);
66+
await ReadBytesStream(bytes, token);
6967
else
7068
{
7169
int len, writePos = 0;
@@ -78,7 +76,7 @@ public async Task ReadBytesAsync(byte[] bytes)
7876
}
7977
while (writePos < bytes.Length)
8078
{
81-
await ReadEncrypted();
79+
await ReadEncrypted(token);
8280
len = Math.Min(bytes.Length - writePos, buffEnd);
8381
buffer.Slice(0, buffEnd).Span.CopyTo(bytes.AsSpan().Slice(writePos));
8482
writePos += len;
@@ -87,7 +85,7 @@ public async Task ReadBytesAsync(byte[] bytes)
8785
}
8886
}
8987

90-
private async Task ReadBytesStream(byte[] bytes)
88+
private async Task ReadBytesStream(byte[] bytes, CancellationToken token)
9189
{
9290
int pos = 0;
9391
if (readPos != buffEnd)
@@ -98,18 +96,17 @@ private async Task ReadBytesStream(byte[] bytes)
9896
readPos = 0;
9997
}
10098
while (pos < bytes.Length - 1)
101-
pos += await stream.ReadAsync(bytes, pos, bytes.Length - pos);
99+
pos += await stream.ReadAsync(bytes, pos, bytes.Length - pos, token);
102100
}
103101

104-
private async Task ReadEncrypted() //read line
102+
private async Task ReadEncrypted(CancellationToken token) //read line
105103
{
106-
await stream.ReadExactlyAsync(buffer.Slice(0, 2));
104+
await stream.ReadExactlyAsync(buffer.Slice(0, 2), token);
107105
ushort len = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(0, 2).Span);
108-
await stream.ReadExactlyAsync(buffer.Slice(2, len + 16));
106+
await stream.ReadExactlyAsync(buffer.Slice(2, len + 16), token);
109107
byte[] nonce = new byte[12];
110108
BinaryPrimitives.WriteUInt64LittleEndian(nonce.AsSpan().Slice(4), counter++);
111109
decrypter!.Decrypt(nonce.AsSpan(), buffer.Slice(2, len).Span, buffer.Slice(2 + len, 16).Span, buffer.Slice(0, len).Span, buffer.Slice(0, 2).Span);
112-
Console.WriteLine("Decrypted " + len);
113110
buffEnd = len;
114111
readPos = 0;
115112
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// HomeKitDotNet Copyright (C) 2024
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU Affero General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or any later version.
6+
// This program is distributed in the hope that it will be useful,
7+
// but WITHOUT ANY WARRANTY, without even the implied warranty of
8+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9+
// See the GNU Affero General Public License for more details.
10+
// You should have received a copy of the GNU Affero General Public License
11+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
12+
13+
using System.Text.Json;
14+
using System.Text.Json.Serialization;
15+
16+
namespace HomeKitDotNet.JSON
17+
{
18+
public record CharacteristicEventJSON
19+
{
20+
[JsonPropertyName("aid")]
21+
public int AccessoryID { get; init; }
22+
[JsonPropertyName("iid")]
23+
public int InstanceID { get; init; }
24+
[JsonPropertyName("value")]
25+
public JsonElement? Value { get; init; }
26+
}
27+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// HomeKitDotNet Copyright (C) 2024
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU Affero General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or any later version.
6+
// This program is distributed in the hope that it will be useful,
7+
// but WITHOUT ANY WARRANTY, without even the implied warranty of
8+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
9+
// See the GNU Affero General Public License for more details.
10+
// You should have received a copy of the GNU Affero General Public License
11+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
12+
13+
using System.Text.Json.Serialization;
14+
15+
namespace HomeKitDotNet.JSON
16+
{
17+
public record CharacteristicNotificationsJSON
18+
{
19+
public CharacteristicNotificationsJSON(int aid, int iid)
20+
{
21+
this.AccessoryID = aid;
22+
this.InstanceID = iid;
23+
}
24+
25+
[JsonPropertyName("aid")]
26+
public int AccessoryID { get; init; }
27+
[JsonPropertyName("iid")]
28+
public int InstanceID { get; init; }
29+
[JsonPropertyName("ev")]
30+
public bool EventNotifications { get; set; }
31+
}
32+
}

HomeKitDotNet/JSON/CharacteristicWriteJSON.cs renamed to HomeKitDotNet/JSON/CharacteristicValueJSON.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,18 @@
1515

1616
namespace HomeKitDotNet.JSON
1717
{
18-
public record CharacteristicWriteJSON
18+
public record CharacteristicValueJSON
1919
{
20-
public CharacteristicWriteJSON(int aid, int iid)
20+
public CharacteristicValueJSON(int aid, int iid)
2121
{
2222
this.AccessoryID = aid;
2323
this.InstanceID = iid;
2424
}
25-
2625
[JsonPropertyName("aid")]
2726
public int AccessoryID { get; init; }
2827
[JsonPropertyName("iid")]
2928
public int InstanceID { get; init; }
3029
[JsonPropertyName("value")]
3130
public JsonElement? Value { get; set; }
32-
[JsonPropertyName("ev")]
33-
public bool? EventNotifications { get; set; }
3431
}
3532
}

HomeKitDotNet/Models/Accessory.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace HomeKitDotNet.Models
1616
{
1717
public class Accessory
1818
{
19+
private Dictionary<int, CharacteristicBase> instanceTable = new Dictionary<int, CharacteristicBase>();
1920
internal Accessory(HomeKitEndPoint endPoint, AccessoryJSON json)
2021
{
2122
this.EndPoint = endPoint;
@@ -31,7 +32,7 @@ internal Accessory(HomeKitEndPoint endPoint, AccessoryJSON json)
3132
}
3233

3334
public Service[] Services { get; init; }
34-
public int ID { get; init;}
35+
public int ID { get; init; }
3536
public HomeKitEndPoint EndPoint { get; init; }
3637

3738
protected IService? GetService(string type, ServiceJSON service)
@@ -187,5 +188,16 @@ internal Accessory(HomeKitEndPoint endPoint, AccessoryJSON json)
187188
}
188189
return null;
189190
}
191+
192+
internal void RegisterCharacteristic(int instance, CharacteristicBase characteristic)
193+
{
194+
instanceTable.TryAdd(instance, characteristic);
195+
}
196+
197+
internal void InvokeEvent(CharacteristicEventJSON json)
198+
{
199+
if (instanceTable.TryGetValue(json.InstanceID, out CharacteristicBase? characteristic))
200+
characteristic.FireUpdate(json.Value);
201+
}
190202
}
191203
}

0 commit comments

Comments
 (0)