Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions TASKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ to query the voice assistant state via the Home Assistant server.
> The project will not provide general support for Tasker. You should check out their website and
> the `r/tasker` subreddit for support.

## Actions:

- **Wake up satellite**: plays the wake sound (if enabled) and listens to voice commands as if the wake word was detected.
- **Stop ringing**: stops the timer ringing sound, as if "stop" was said.

## State Condition: Ava Activity

The `Ava Activity` state can be used to trigger tasks when the voice satellite enters or exits
Expand Down
20 changes: 20 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- Tasker plugins -->
<activity
android:name=".tasker.ActivityConfigAvaActivity"
android:exported="true"
Expand All @@ -46,6 +48,24 @@
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_CONDITION" />
</intent-filter>
</activity>
<activity
android:name=".tasker.ActivityConfigWakeSatellite"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/tasker_action_wake_satellite">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>
<activity
android:name=".tasker.ActivityConfigStopRinging"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/tasker_action_stop_ringing">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import com.example.ava.server.DEFAULT_SERVER_PORT
import com.example.ava.server.Server
import com.example.ava.server.ServerImpl
import com.example.ava.settings.VoiceSatelliteSettingsStore
import com.example.ava.tasker.StopRingingRunner
import com.example.ava.tasker.WakeSatelliteRunner
import com.example.esphomeproto.api.DeviceInfoResponse
import com.example.esphomeproto.api.VoiceAssistantAnnounceRequest
import com.example.esphomeproto.api.VoiceAssistantConfigurationRequest
Expand Down Expand Up @@ -94,8 +96,11 @@ class VoiceSatellite(
override fun start() {
super.start()
startAudioInput()
}

// Wire up tasker actions
WakeSatelliteRunner.register { scope.launch { wakeSatellite() } }
StopRingingRunner.register { scope.launch { stopTimer() } }
}
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
private fun startAudioInput() = server.isConnected
.flatMapLatest { isConnected ->
Expand Down Expand Up @@ -339,5 +344,7 @@ class VoiceSatellite(
override fun close() {
super.close()
player.close()
WakeSatelliteRunner.unregister()
StopRingingRunner.unregister()
}
}
51 changes: 51 additions & 0 deletions app/src/main/java/com/example/ava/tasker/StopRingingAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.example.ava.tasker

import android.app.Activity
import android.content.Context
import android.os.Bundle
import com.example.ava.R
import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoOutputOrInput
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoOutputOrInput
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigNoInput
import com.joaomgcd.taskerpluginlibrary.input.TaskerInput
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultError
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess

class StopRingingHelper(config: TaskerPluginConfig<Unit>) :
TaskerPluginConfigHelperNoOutputOrInput<StopRingingRunner>(config) {
override val runnerClass get() = StopRingingRunner::class.java

}

class ActivityConfigStopRinging : Activity(), TaskerPluginConfigNoInput {
override val context: Context get() = this
private val taskerHelper by lazy { StopRingingHelper(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
taskerHelper.finishForTasker()
}
}

class StopRingingRunner : TaskerPluginRunnerActionNoOutputOrInput() {
override fun run(context: Context, input: TaskerInput<Unit>): TaskerPluginResult<Unit> {
val cb = callback
?: return TaskerPluginResultError(Exception(context.getString(R.string.tasker_action_error_not_running)))
cb()
return TaskerPluginResultSucess()
}

companion object {
@Volatile
private var callback: (() -> Unit)? = null

fun register(cb: () -> Unit) {
callback = cb
}

fun unregister() {
callback = null
}
}
}
50 changes: 50 additions & 0 deletions app/src/main/java/com/example/ava/tasker/WakeSatelliteAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.example.ava.tasker

import android.app.Activity
import android.content.Context
import android.os.Bundle
import com.example.ava.R
import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoOutputOrInput
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoOutputOrInput
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigNoInput
import com.joaomgcd.taskerpluginlibrary.input.TaskerInput
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultError
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess

class WakeSatelliteHelper(config: TaskerPluginConfig<Unit>) :
TaskerPluginConfigHelperNoOutputOrInput<WakeSatelliteRunner>(config) {
override val runnerClass get() = WakeSatelliteRunner::class.java
}

class ActivityConfigWakeSatellite : Activity(), TaskerPluginConfigNoInput {
override val context: Context get() = this
private val taskerHelper by lazy { WakeSatelliteHelper(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
taskerHelper.finishForTasker()
}
}

class WakeSatelliteRunner : TaskerPluginRunnerActionNoOutputOrInput() {
override fun run(context: Context, input: TaskerInput<Unit>): TaskerPluginResult<Unit> {
val cb = callback
?: return TaskerPluginResultError(Exception(context.getString(R.string.tasker_action_error_not_running)))
cb()
return TaskerPluginResultSucess()
}

companion object {
@Volatile
private var callback: (() -> Unit)? = null

fun register(cb: () -> Unit) {
callback = cb
}

fun unregister() {
callback = null
}
}
}
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,7 @@
<string name="tasker_filter_timer_running_description">At least one timer is counting down</string>
<string name="tasker_filter_timer_paused">Timer Paused</string>
<string name="tasker_filter_timer_paused_description">At least one paused timer remains</string>
<string name="tasker_action_wake_satellite">Wake up satellite</string>
<string name="tasker_action_stop_ringing">Stop ringing</string>
<string name="tasker_action_error_not_running">Satellite service not running</string>
</resources>
121 changes: 121 additions & 0 deletions app/src/test/java/com/example/ava/TaskerPluginsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.example.ava

import android.content.ContextWrapper
import androidx.compose.material3.TimeInput
import com.example.ava.esphome.voicesatellite.AudioResult
import com.example.ava.esphome.voicesatellite.Listening
import com.example.ava.esphome.voicesatellite.VoiceSatellite
import com.example.ava.esphome.voicesatellite.VoiceSatelliteAudioInput
import com.example.ava.esphome.voicesatellite.VoiceSatellitePlayer
import com.example.ava.server.Server
import com.example.ava.stubs.StubAudioPlayer
import com.example.ava.stubs.StubServer
import com.example.ava.stubs.StubVoiceSatelliteAudioInput
import com.example.ava.stubs.StubVoiceSatellitePlayer
import com.example.ava.stubs.StubVoiceSatelliteSettingsStore
import com.example.ava.stubs.stubSettingState
import com.example.ava.tasker.AvaActivityInput
import com.example.ava.tasker.AvaActivityRunner
import com.example.ava.tasker.StopRingingRunner
import com.example.ava.tasker.WakeSatelliteRunner
import com.example.esphomeproto.api.VoiceAssistantRequest
import com.example.esphomeproto.api.VoiceAssistantTimerEvent
import com.example.esphomeproto.api.voiceAssistantTimerEventResponse
import com.joaomgcd.taskerpluginlibrary.input.TaskerInput
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultConditionSatisfied
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultConditionUnsatisfied
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.test.assertEquals

class TaskerPluginsTest {
private val dummyContext = ContextWrapper(null)

private fun TestScope.createSatellite(
server: Server = StubServer(),
audioInput: VoiceSatelliteAudioInput = StubVoiceSatelliteAudioInput(),
player: VoiceSatellitePlayer = StubVoiceSatellitePlayer()
) = VoiceSatellite(
coroutineContext = this.coroutineContext,
"Test Satellite",
server = server,
audioInput = audioInput,
player = player,
settingsStore = StubVoiceSatelliteSettingsStore()
).apply {
start()
advanceUntilIdle()
}

@Test
fun should_handle_wake_satellite_action() = runTest {
val server = StubServer()
val audioInput = StubVoiceSatelliteAudioInput()
val satellite = createSatellite(server = server, audioInput = audioInput)
advanceUntilIdle()

val result = WakeSatelliteRunner().run(dummyContext, TaskerInput(Unit))
assert(result is TaskerPluginResultSucess)
advanceUntilIdle()

assertEquals(Listening, satellite.state.value)
assertEquals(true, audioInput.isStreaming)
assertEquals(1, server.sentMessages.size)
assertEquals(true, (server.sentMessages[0] as VoiceAssistantRequest).start)

satellite.close()
}

@Test
fun should_handle_stop_ringing_action() = runTest {
val server = StubServer()
val ttsPlayer = object : StubAudioPlayer() {
val mediaUrls = mutableListOf<String>()
lateinit var onCompletion: () -> Unit
override fun play(mediaUris: Iterable<String>, onCompletion: () -> Unit) {
this.mediaUrls.addAll(mediaUris)
this.onCompletion = onCompletion
}

var stopped = false
override fun stop() {
stopped = true
}
}
val satellite = createSatellite(
server = server,
player = StubVoiceSatellitePlayer(
ttsPlayer = ttsPlayer,
repeatTimerFinishedSound = stubSettingState(true),
timerFinishedSound = stubSettingState("ring")
)
)

// Make it ring by sending a timer finished event
server.receivedMessages.emit(voiceAssistantTimerEventResponse {
eventType = VoiceAssistantTimerEvent.VOICE_ASSISTANT_TIMER_FINISHED
timerId = "id"
totalSeconds = 60
secondsLeft = 0
isActive = true
})
advanceUntilIdle()

assertEquals(false, ttsPlayer.stopped)
assertEquals(listOf("ring"), ttsPlayer.mediaUrls)
ttsPlayer.mediaUrls.clear()

// Trigger StopRingingAction via its runner
val result = StopRingingRunner().run(dummyContext, TaskerInput(Unit))
assert(result is TaskerPluginResultSucess)
advanceUntilIdle()

// Should no longer be ringing
assertEquals(true, ttsPlayer.stopped)

satellite.close()
}
}
Loading