Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "write" operation to HAConnection #55

Merged
merged 7 commits into from
Apr 2, 2024
Merged

Add "write" operation to HAConnection #55

merged 7 commits into from
Apr 2, 2024

Conversation

bgoncal
Copy link
Member

@bgoncal bgoncal commented Mar 12, 2024

No description provided.

@bgoncal bgoncal requested a review from zacwest March 12, 2024 17:13
@bgoncal bgoncal self-assigned this Mar 12, 2024
Copy link

codecov bot commented Mar 12, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 99.87%. Comparing base (48e36a0) to head (476a426).
Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main      #55   +/-   ##
=======================================
  Coverage   99.87%   99.87%           
=======================================
  Files          50       51    +1     
  Lines        1593     1628   +35     
=======================================
+ Hits         1591     1626   +35     
  Misses          2        2           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@bgoncal bgoncal requested a review from robbiet480 March 12, 2024 17:18
@zacwest
Copy link
Member

zacwest commented Mar 12, 2024

This sort of breaks expectations. Is there something the library needs to do that isn't handled right now? Exposing the raw WebSocket for writing data doesn't seem like a good idea to me.

@bgoncal
Copy link
Member Author

bgoncal commented Mar 12, 2024

This sort of breaks expectations. Is there something the library needs to do that isn't handled right now? Exposing the raw WebSocket for writing data doesn't seem like a good idea to me.

To integrate with Assist pipeline I need to be able to write binary data to the websocket

@zacwest
Copy link
Member

zacwest commented Mar 12, 2024

Can you add a request type that can do that?

@bgoncal
Copy link
Member Author

bgoncal commented Mar 12, 2024

Can you add a request type that can do that?

No problem on my side, but what would be the downside of keeping the way it is?

@zacwest
Copy link
Member

zacwest commented Mar 12, 2024

I think the biggest issue is it gives unfettered access to the underneath, and requires consumers to format appropriately. It's like an escape hatch but a dangerous one. I think it also means the callbacks that may trigger are unexpected or confusing to the library, if any happen.

@bgoncal
Copy link
Member Author

bgoncal commented Mar 12, 2024

But consumer will still not be able to do anything else than writting data, if I make a request type for that, the access level will be the same since this request wont have any other information than the binary data itself.

@zacwest
Copy link
Member

zacwest commented Mar 12, 2024

Does the data get decorated in any way to indicate what kind of payload it is or what it's used for?

@bgoncal
Copy link
Member Author

bgoncal commented Mar 12, 2024

The only modification to the data is a prefix of 1 byte which indicates the assist handler id, literally e.g. "01"

@bgoncal
Copy link
Member Author

bgoncal commented Mar 12, 2024

@zacwest check my last commit, I made a proposal, even though I don't think this abstraction is really needed, let me know what you think

@zacwest
Copy link
Member

zacwest commented Mar 12, 2024

Can you share with me an example data write so I can maybe understand? The write needs to contain something like "use this data for purpose X" right?

@bgoncal
Copy link
Member Author

bgoncal commented Mar 12, 2024

Can you share with me an example data write so I can maybe understand? The write needs to contain something like "use this data for purpose X" right?

https://developers.home-assistant.io/docs/voice/pipelines/#sending-speech-data

@zacwest
Copy link
Member

zacwest commented Mar 12, 2024

Is there a mechanism to get responses to that binary data?

My recommendation would be: codify that 'handler id' (the 01 in that example) as a separate field in the 'write data' parameter, so you'd do a 'request with data' + + <here's the data>

@bgoncal
Copy link
Member Author

bgoncal commented Mar 13, 2024

Is there a mechanism to get responses to that binary data?

My recommendation would be: codify that 'handler id' (the 01 in that example) as a separate field in the 'write data' parameter, so you'd do a 'request with data' + + <here's the data>

Just a quick overview of how the flow currently works:

You subscribe to something like:

private func assistByVoiceTypedSubscription() -> HATypedSubscription<AssistResponse> {
        .init(request: .init(type: .webSocket("assist_pipeline/run"), data: [
            "pipeline": preferredPipelineId,
            "start_stage": "stt",
            "end_stage": "tts",
            "input": [
                "sample_rate": audioSampleRate,
            ],
        ]))
    }

Then it will have many events back such as "run-start, stt-start ... and others", when you receive the stt-start, it means you can start writting binary data and you dont get anything back from it, the way the user get's feedback is through events that will be triggered later on such as "stt-end, run-end.. and others". So thats why I don't think we need anything more complex than just writting data directly, if use cases grow then this can be improved. Let me know what do you think

@zacwest
Copy link
Member

zacwest commented Mar 13, 2024

That makes sense. I think of this library as doing whatever is necessary to make HA-specific things happen in the connection. So in this case, I think it is actually quite valuable to say "send a stt request with voice data" without the caller needing to know how to do that. It's not a super heavy wrapper (just adding that handler id prefix and writing data) but it abstracts away a part which could, in theory, change or be made more complex later.

Source/HAConnection.swift Outdated Show resolved Hide resolved
Source/Requests/HARequestType.swift Outdated Show resolved Hide resolved
@bgoncal bgoncal marked this pull request as draft March 14, 2024 10:05
@bgoncal bgoncal force-pushed the write-haconnection branch 4 times, most recently from 97365b2 to bfc15f1 Compare March 14, 2024 10:50
@bgoncal bgoncal marked this pull request as ready for review March 14, 2024 10:54
@bgoncal
Copy link
Member Author

bgoncal commented Mar 14, 2024

That makes sense. I think of this library as doing whatever is necessary to make HA-specific things happen in the connection. So in this case, I think it is actually quite valuable to say "send a stt request with voice data" without the caller needing to know how to do that. It's not a super heavy wrapper (just adding that handler id prefix and writing data) but it abstracts away a part which could, in theory, change or be made more complex later.

Ok I replaced my implementation with some more specific code for sending stt audio data, let me know what you think

Source/HAConnection.swift Outdated Show resolved Hide resolved
@@ -5,14 +5,17 @@ internal struct HARequestControllerAllowedSendKind: OptionSet {

static let webSocket: Self = .init(rawValue: 0b1)
static let rest: Self = .init(rawValue: 0b10)
static let all: Self = [.webSocket, .rest]
static let sttData: Self = .init(rawValue: 0b10)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0b100 - 0b10 is already taken above

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0b11 then right? Why aren't we just using integers here?

Source/Requests/HARequestType.swift Outdated Show resolved Hide resolved
@home-assistant home-assistant bot marked this pull request as draft March 15, 2024 03:38
@home-assistant
Copy link

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Source/Requests/HARequestType.swift Outdated Show resolved Hide resolved
Tests/HAConnectionImpl.test.swift Show resolved Hide resolved
@bgoncal bgoncal marked this pull request as ready for review March 19, 2024 10:50
@home-assistant home-assistant bot requested a review from zacwest March 19, 2024 10:50
@bgoncal bgoncal force-pushed the write-haconnection branch 2 times, most recently from dae3229 to 02693d3 Compare March 19, 2024 12:42
@bgoncal
Copy link
Member Author

bgoncal commented Mar 25, 2024

@zacwest can you check this one again?

Source/Requests/HASttData.swift Outdated Show resolved Hide resolved
]
)
connection.connect()
connection.send(request) { _ in }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean this literal closure here. The completion block for send() - can you test it is invoked?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original implementation of the "send" method didn't have any usage of the completion. Should this be altered?

@@ -401,6 +414,8 @@ extension HAConnectionImpl {
sendWebSocket(identifier: identifier, request: request, command: command)
case let .rest(method, command):
sendRest(identifier: identifier!, request: request, method: method, command: command)
case let .sttData(data):
sendWrite(data.sttBinaryHandlerId, audioDataString: request.data["audioData"] as? String)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this isn't invoking the completion blocks, but it may be useful to either wait to invoke it until the write occurs, or just invoke the completion right away. I know the server isn't ACK-ing this but it's still good form to be able to chain these together for throttling if you want.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Communicating to the response controller isn't enough? like done here:

 private func sendWrite(_ sttBinaryHandlerId: UInt8, audioDataString: String?) {
        // If there is no audioData, handlerID will be the payload alone indicating end of audio
        var audioData = Data(base64Encoded: audioDataString ?? "") ?? Data()

        // Prefix audioData with handler ID so the API can map the binary data
        audioData.insert(sttBinaryHandlerId, at: 0)
        workQueue.async { [connection] in
            connection?.write(data: audioData) { [weak self] in
                self?.responseController.didWrite()
            }
        }
    }

I noticed that when "sending" using websocket or rest, that's the only response, completion is not used for those either

Copy link
Member

@zacwest zacwest Mar 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all of the tests check the invocation of the completion handler, but some do. e.g. testPlainSendSentSuccessful, testPlainSendSentFailurePromise, etc. You're making a new one with a unique code path, so you need to make sure all of the same guarantees happen somewhere.

The response controller only invokes based on identifiers - there's no identifier/callback for this write, so there's no way for it to know what to call back through.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, got it, ok I added the identifier and a test to validate it, I also added this code block inside "sendWrite":

 if let identifier, let request = self.requestController.single(for: identifier) {
                    callbackQueue.async {
                        request.resolve(.success(.empty))
                    }
                    requestController.clear(invocation: request)
                }

Because I can't add identifier to the data dictionary (since I am sending binary in this write) and it will also have no websocket event back from the connection.

Does that make sense?

@bgoncal bgoncal force-pushed the write-haconnection branch 2 times, most recently from d0b8faf to 80d4c99 Compare March 27, 2024 12:30
Copy link
Member

@zacwest zacwest left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm now!

@bgoncal bgoncal merged commit b602e94 into main Apr 2, 2024
5 checks passed
@bgoncal bgoncal deleted the write-haconnection branch April 2, 2024 14:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants