-
-
Notifications
You must be signed in to change notification settings - Fork 13
Mod Store Development
Complete reference for building Ava mods. Covers manifest format, entity types, manager API, voice pipeline hooks, device compatibility, build, and publishing.
Source: github.com/knoop7/ava-mods
Mods are lightweight Java modules loaded at runtime via DexClassLoader. They run inside Ava's process and communicate with Home Assistant through ESPHome protocol entities. No APK rebuild required.
ava-mods/
├── store.json # Store index consumed by Ava
├── mods/ # Release packages (downloaded by Ava)
│ ├── devices/ # Device-specific mods
│ └── features/ # Feature-oriented mods
├── sources/ # Source code and build scripts
│ ├── devices/
│ └── features/
├── docs/ # Documentation
└── examples/ # Minimal example manifests
-
mods/— release-ready packages only (manifest.json + libs/) -
sources/— source code, build scripts, build output -
devices/— model-specific or hardware-specific mods -
features/— reusable functional mods not tied to one device family
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique identifier (lowercase, hyphens, max 64 chars, [A-Za-z0-9._-]) |
name |
string | Yes | Display name |
version |
string | Yes | Semantic version (e.g. "1.0.0") |
author |
string | No | Author name |
description |
string | No | Short description |
icon |
string | No | MDI icon name (default: mdi:puzzle) |
libs |
array | No | JAR/SO files to load (e.g. ["libs/mydevice.jar"]) |
manager |
string | No | Manager class fully-qualified name |
voice_pipeline |
boolean | No | Opt-in to voice pipeline events (default: false) |
permissions |
array | No | Android permission tokens (aliases or full names) |
config |
array | No | Configuration items (see Config Items below) |
entities |
array | Yes | Entity definitions (see Entity Types below) |
jar_hash |
string | No | MD5 hash of JAR file (used for update detection) |
{
"id": "my-mod",
"name": "My Mod",
"version": "1.0.0",
"entities": [
{
"type": "switch",
"id": "power",
"name": "Power"
}
]
}{
"id": "my-device-mod",
"name": "My Device Controller",
"version": "1.0.0",
"author": "Your Name",
"description": "Control my custom device",
"icon": "mdi:chip",
"libs": ["libs/mydevice.jar"],
"manager": "com.mydevice.DeviceManager",
"voice_pipeline": true,
"permissions": ["camera", "android.permission.READ_LOGS"],
"entities": [
{
"type": "switch",
"id": "power",
"name": "Power",
"icon": "mdi:power",
"on": "setPower:1",
"off": "setPower:0",
"read": "isPowerOn"
},
{
"type": "binary_sensor",
"id": "motion",
"name": "Motion",
"icon": "mdi:motion-sensor",
"class": "motion",
"gpio": 4,
"read": "isMotionDetected"
},
{
"type": "sensor",
"id": "temperature",
"name": "Temperature",
"icon": "mdi:thermometer",
"unit": "°C",
"class": "temperature",
"accuracy_decimals": 1,
"read": "getTemperature",
"refresh_interval_ms": 5000
},
{
"type": "number",
"id": "brightness",
"name": "Brightness",
"icon": "mdi:brightness-6",
"min": 0,
"max": 100,
"step": 1,
"set": "setBrightness",
"read": "getBrightness"
},
{
"type": "button",
"id": "reboot",
"name": "Reboot",
"icon": "mdi:restart",
"press": "reboot"
},
{
"type": "select",
"id": "mode",
"name": "Mode",
"icon": "mdi:format-list-bulleted",
"options": ["auto", "manual", "off"],
"set": "setMode",
"read": "getMode"
},
{
"type": "text",
"id": "label",
"name": "Label",
"icon": "mdi:form-textbox",
"set": "setLabel",
"read": "getLabel"
},
{
"type": "text_sensor",
"id": "status",
"name": "Status",
"icon": "mdi:text-box-outline",
"category": "diagnostic",
"read": "getStatusText"
}
],
"config": [
{
"type": "switch",
"key": "enable_feature_x",
"label": "Feature X",
"description": "Enable feature X entities",
"defaultValue": "false"
},
{
"type": "select",
"key": "mode",
"label": "Operating Mode",
"defaultValue": "auto",
"options": ["auto", "manual"]
},
{
"type": "number",
"key": "threshold",
"label": "Threshold",
"defaultValue": "50",
"min": 0,
"max": 100,
"step": 1
},
{
"type": "text",
"key": "api_key",
"label": "API Key",
"defaultValue": "",
"placeholder": "Paste key here"
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | Entity type (see below) |
id |
string | Yes | Unique entity ID within mod |
name |
string | Yes | Display name |
icon |
string | No | MDI icon name |
category |
string | No |
config, diagnostic, or empty (default: none) |
enabledWhen |
string | No | Config key that must be "true" for entity to appear |
enabledByConfig |
string | No | Same as enabledWhen — entity only shows when config key is "true"
|
On/off control with optional state read.
| Field | Description |
|---|---|
on |
Action for turning on (e.g. setPower:1) |
off |
Action for turning off (e.g. setPower:0) |
read |
Method name to read current state (returns boolean/number/string) |
Switch state is persisted across restarts via ModStateStore.
Binary state sensor (motion, door, etc).
| Field | Description |
|---|---|
class |
Device class: motion, door, window, smoke, moisture, occupancy, illuminance
|
gpio |
GPIO pin number (informational) |
read |
Method to read state (returns boolean/number/string) |
Numeric or text value sensor.
| Field | Description |
|---|---|
unit |
Unit of measurement (e.g. °C, lx, %) |
class |
HA device class (e.g. temperature, illuminance) |
accuracy_decimals |
Decimal places (default: 1) |
read |
Method to read value (returns Number) |
refresh_interval_ms |
Polling interval in ms (default: 30000, min: 50) |
Adjustable numeric value.
| Field | Description |
|---|---|
min |
Minimum value (default: 0) |
max |
Maximum value (default: 100) |
step |
Step size (default: 1) |
unit |
Unit of measurement |
set |
Method to set value (called as set:value) |
read |
Method to read current value |
Trigger action on press.
| Field | Description |
|---|---|
press |
Action method name (e.g. reboot) |
Dropdown option selector.
| Field | Description |
|---|---|
options |
Array of string options |
set |
Method to set value (called as set:value) |
read |
Method to read current selection |
Text input field.
| Field | Description |
|---|---|
set |
Method to set value (called as set:value) |
read |
Method to read current value |
Read-only text state sensor.
| Field | Description |
|---|---|
read |
Method to read value (returns String/Number/Boolean) |
refresh_interval_ms |
Polling interval in ms |
Configuration items appear in Ava's mod settings UI. Users interact with them before the voice service restarts to apply changes.
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes |
switch, select, number, or text
|
key |
string | Yes | Unique config key (used in enabledWhen/enabledByConfig and passed to applyConfig) |
label |
string | Yes | Display label |
description |
string | No | Help text |
dialogHint |
string | No | Hint text in input dialog |
placeholder |
string | No | Placeholder for text input |
defaultValue |
string | No | Default value (defaults: switch="true", select=first option, number=min, text="") |
enabledWhen |
string | No | Config key that must be "true" for this item to be editable |
options |
array | No | For select type — available options |
min |
float | No | For number type — minimum |
max |
float | No | For number type — maximum |
step |
float | No | For number type — step size |
Config values are resolved as defaults + userOverrides, sanitized per type, and passed to the manager via applyConfig.
Actions use methodName:arg1,arg2,arg3 format. Ava coerces the argument type based on the value:
| Argument | Java Method Signature |
|---|---|
| (none) | method() |
true / false
|
method(boolean) |
Integer (e.g. 42) |
method(int) |
Float (e.g. 3.14) |
method(float) |
| Other string | method(String) |
Examples:
-
setPower:1→manager.setPower(1) -
setPower:true→manager.setPower(true) -
setRGB:255,0,0→manager.setRGB("255", "0", "0")(multi-arg not auto-typed; use String) -
reboot→manager.reboot()
The manager is a Java class loaded via DexClassLoader. It must follow the singleton pattern:
public class MyManager {
private static MyManager instance;
public static MyManager getInstance(Context context) {
if (instance == null) instance = new MyManager();
return instance;
}
}| Method | Signature | Description |
|---|---|---|
applyConfig |
(Context, String key, String value) or (String key, String value)
|
Called for each config item on load |
registerStateListener |
(String entityId, ModStateCallback callback) |
Register push-based state updates |
setLastError |
(String message) |
Report errors to Ava (shown in UI) |
onDestroy |
() |
Called when mod is disabled/uninstalled — cleanup resources |
For push-based updates instead of polling, implement registerStateListener:
public boolean registerStateListener(String entityId, ModStateCallback callback) {
// Store callback, invoke callback.onStateChanged(value) when state changes
// Return true to indicate listener registered (Ava will skip initial read)
return true;
}ModStateCallback is an abstract class:
@Keep
public abstract class ModStateCallback {
@Keep
public abstract void onStateChanged(Object value);
}Value type handling:
-
switch/binary_sensor:
Boolean,Number(0=false), orString("true"/"on"/"1" = true) -
sensor:
Number→toFloat() -
text_sensor:
String,Number, orBoolean→toString()
Switch states are automatically persisted by ModStateStore. On restart, the last known state is restored before the manager's read method is called. No action needed from the mod author.
Mods with a manager class can optionally expose device-level hooks consumed by Ava core. These are useful for model-specific behavior without modifying the main APK.
| Method | Signature | Description |
|---|---|---|
isSupported |
() or (Context) → boolean
|
Whether this mod applies to the current device |
getMinBrightness |
() or (Context) → int
|
Override minimum brightness for the display |
isLowEndBleChip |
() or (Context) → boolean
|
Flag for BLE tuning on low-end chips |
grantOverlayPermissionIfNeeded |
() or (Context) → boolean
|
Root-based overlay permission grant |
onKeyDown |
(Context, int keyCode, KeyEvent) → boolean
|
Intercept physical key presses |
onKeyUp |
(Context, int keyCode, KeyEvent) → boolean
|
Intercept physical key releases |
All methods are optional. Mods that only expose entities do not need to implement them. Ava checks for method existence by name — if none of these methods exist on the manager class, the mod is skipped for device support.
Opt-in hook for reacting to satellite lifecycle events (e.g. LED ring on wake, TTS playback). Zero cost when unused: no ClassLoader load, no broadcast, no thread.
- Set
"voice_pipeline": truein manifest.json - Manager implements
onVoicePipelineEvent(Context, String, Bundle)
{
"id": "echo-dot-led",
"name": "Echo Dot LED",
"voice_pipeline": true,
"manager": "com.example.EchoDotLedManager",
"libs": ["libs/echo-dot-led.jar"]
}public class EchoDotLedManager {
private static EchoDotLedManager instance;
public static EchoDotLedManager getInstance(Context context) {
if (instance == null) instance = new EchoDotLedManager();
return instance;
}
public void onVoicePipelineEvent(Context context, String event, Bundle extras) {
switch (event) {
case "wake_detected":
// extras: wake_word, wake_word_id, wake_confidence, synthetic_wake
break;
case "listening_started":
// extras: accent_color
break;
case "stt_vad_start":
case "stt_vad_end":
case "stt_end":
// extras: stt_text
break;
case "processing_started":
case "responding":
break;
case "tts_start":
// extras: tts_text (optional)
break;
case "tts_playback_started":
case "tts_finished":
break;
case "session_ended":
case "run_start":
case "run_end":
break;
case "pipeline_error":
// extras: error_code, error_message
break;
}
}
}| Event | Extras | Description |
|---|---|---|
wake_detected |
wake_word, wake_word_id, wake_confidence, synthetic_wake
|
Wake word triggered |
listening_started |
accent_color |
Recording started |
stt_vad_start |
— | VAD detected speech start |
stt_vad_end |
— | VAD detected speech end |
stt_end |
stt_text |
Speech-to-text completed |
processing_started |
— | HA processing intent |
responding |
— | Response generation started |
tts_start |
tts_text (optional) |
TTS synthesis started |
tts_playback_started |
— | TTS audio playback started |
tts_finished |
— | TTS playback completed |
session_ended |
— | Voice session fully ended |
run_start |
— | Pipeline run started |
run_end |
— | Pipeline run ended |
pipeline_error |
error_code, error_message
|
Error occurred |
Other installed apps can receive voice pipeline events via broadcast without a mod:
<receiver android:name=".AvaVoiceReceiver" android:exported="true">
<intent-filter>
<action android:name="com.example.ava.VOICE_PIPELINE_EVENT" />
</intent-filter>
</receiver>Intent extras: event (string) plus event-specific keys. Ava checks for external receivers via PackageManager.queryBroadcastReceivers — if none found, no broadcast is sent.
Use aliases or full Android permission names in the permissions array:
| Alias | Resolved Permissions |
|---|---|
gps / location
|
ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION
|
fine_location |
ACCESS_FINE_LOCATION |
coarse_location |
ACCESS_COARSE_LOCATION |
background_location |
ACCESS_BACKGROUND_LOCATION |
camera |
CAMERA |
microphone / record_audio
|
RECORD_AUDIO |
bluetooth |
BLUETOOTH_SCAN, BLUETOOTH_CONNECT (API 31+) or ACCESS_FINE_LOCATION (older) |
bluetooth_scan |
BLUETOOTH_SCAN |
bluetooth_connect |
BLUETOOTH_CONNECT |
bluetooth_advertise |
BLUETOOTH_ADVERTISE |
notifications / post_notifications
|
POST_NOTIFICATIONS |
Full names (e.g. android.permission.READ_LOGS) are also accepted. Unknown tokens are logged and skipped.
-
Install-time (auto-granted):
INTERNET,ACCESS_NETWORK_STATE,WAKE_LOCK,FOREGROUND_SERVICE*,RECEIVE_BOOT_COMPLETED,VIBRATE -
Runtime (prompt user):
CAMERA,RECORD_AUDIO,ACCESS_FINE_LOCATION, etc. -
Privileged (require ADB/root):
READ_LOGS,WRITE_SECURE_SETTINGS,DUMP,PACKAGE_USAGE_STATS
If a mod requires a runtime permission that hasn't been granted, it cannot be enabled. If denied permanently, the user must grant it via Android app settings.
- Android SDK (platform 34+)
- Java 11+
-
d8tool (in Android SDK build-tools)
cd sources/features/your-mod/
chmod +x build.sh
./build.shThe build script:
- Compiles Java sources with
javac - Converts
.classto DEX format usingd8 - Packages DEX into a JAR in
libs/
Copy the built JAR to the release directory:
cp libs/your-manager.jar ../../mods/features/your-mod/libs/Mods can include .so files for native code. List them in libs alongside JARs:
{
"libs": [
"libs/mymanager.jar",
"libs/jni/arm64-v8a/libnative.so",
"libs/jni/armeabi-v7a/libnative.so"
]
}Ava sets the native library path automatically when creating the DexClassLoader.
- Copy
manifest.jsonandlibs/to device:/data/data/com.example.ava/files/mods/your-mod/ - Enable in Ava settings (Settings → Advanced → Mod Store → Installed)
- Grant required permissions
- Check Home Assistant for entities
- Monitor logcat:
adb logcat -s ModManager ModEntityFactory ModVoicePipeline
- Create source files in
sources/devices/orsources/features/ - Build and copy release package to
mods/devices/ormods/features/ - Add or update the entry in
store.json - Submit Pull Request to ava-mods
{
"version": 1,
"baseUrl": "https://raw.githubusercontent.com/knoop7/ava-mods/main/",
"mods": [
{
"id": "my-mod",
"name": "My Mod",
"version": "1.0.0",
"author": "Your Name",
"description": "Description here",
"path": "mods/features/my-mod/",
"jar_hash": "md5hashofjarfile"
}
]
}| Field | Description |
|---|---|
id |
Unique mod ID (must match manifest id) |
name |
Display name |
version |
Current version |
author |
Author name |
description |
Short description |
path |
Relative path to mod directory (trailing /) |
jar_hash |
MD5 hash of JAR file (used for update detection — if hash matches, download is skipped) |
External apps or ADB can enable/disable/reload mods via broadcast, without opening the Ava UI. This is useful for automation scripts, provisioning, and headless deployments.
| Action | Description |
|---|---|
com.example.ava.ACTION_SET_MOD_ENABLED |
Enable or disable a specific mod |
com.example.ava.ACTION_RELOAD_MOD |
Reload one mod (or all enabled mods if mod_id is omitted) |
| Extra | Type | Required | Description |
|---|---|---|---|
mod_id |
String | Yes for SET_MOD_ENABLED, optional for RELOAD_MOD
|
Mod ID to control. If omitted in RELOAD_MOD, all enabled mods are reloaded. |
mod_enabled |
Boolean | No (default: true) |
For SET_MOD_ENABLED — true to enable, false to disable |
no_restart |
Boolean | No (default: false) |
If true, skip voice satellite restart after mod change |
# Enable a mod
adb shell am broadcast -a com.example.ava.ACTION_SET_MOD_ENABLED --es mod_id echo_dot_led --ez mod_enabled true
# Disable a mod without restarting satellite
adb shell am broadcast -a com.example.ava.ACTION_SET_MOD_ENABLED --es mod_id echo_dot_led --ez mod_enabled false --ez no_restart true
# Reload a specific mod
adb shell am broadcast -a com.example.ava.ACTION_RELOAD_MOD --es mod_id echo_dot_led
# Reload all enabled mods
adb shell am broadcast -a com.example.ava.ACTION_RELOAD_MOD-
SET_MOD_ENABLEDrefreshes the registry from disk first, so manualadb pushof mod files is picked up. -
RELOAD_MODdisables and re-enables the mod to forceDexClassLoaderrefresh. Only enabled mods are reloaded; disabled mods are left unchanged. - Unless
no_restart=true, the voice satellite service is restarted automatically to apply changes. - If the service is not running, mod state applies on next start.
Mods can trigger self-updates via reflection on ModManager. Available methods:
| Method | Returns | Description |
|---|---|---|
updateModSync(modId) |
"ok" or error message |
Refreshes store, downloads latest version |
reloadModSync(modId) |
"ok" or error message |
Disables and re-enables mod to force ClassLoader refresh |
updateAndReloadModSync(modId) |
"ok" or error message |
Downloads + reloads in one call |
ModManager modManager = ModManager.getInstance(context);
String result = modManager.updateAndReloadModSync("my-mod");Bundle Ava APK, mods, and configuration in a single TWRP zip for factory deployment. No new API needed — Ava persists all settings to DataStore via ACTION APPLY SETTINGS broadcast.
twrp-ava-echo.zip
├── system/priv-app/Ava/Ava.apk
├── ava_mods/
│ ├── echo-show-support/
│ │ ├── manifest.json
│ │ └── libs/echo-show-support.jar
│ └── (other mods...)
├── ava_provision.json
└── META-INF/com/google/android/update-binary
The ava_provision.json file is a partial JSON patch merged into Ava's DataStore. Only include keys you want to set — missing keys keep their defaults.
Supported stores: microphone, player, experimental, sendspin, voice_channel, screensaver, voice_satellite
{
"microphone": {
"wakeWord": "okay_nabu",
"wakeWords": ["okay_nabu"],
"micGainDb": 12,
"muted": false
},
"voice_satellite": {
"name": "echo_show_crown",
"serverPort": 6503,
"serverPortUserConfigured": true
},
"player": {
"enableAutoRestart": true
},
"screensaver": {
"darkOffEnabled": true
},
"experimental": {
"environmentSensorEnabled": true
},
"sendspin": {
"enabled": false
}
}File path whitelist: /sdcard/, /storage/emulated/0/, app data dir, app filesDir. The file must be in one of these or the broadcast will be rejected.
Copy mod files directly to Ava's data directory (same structure as UI install):
/data/data/com.example.ava/files/mods/
├── registry.json
└── echo-show-support/
├── manifest.json
└── libs/echo-show-support.jar
registry.json:
{
"version": 1,
"mods": [
{ "id": "echo-show-support", "version": "1.1.0", "enabled": true }
]
}Fix ownership after copying (get UID from dumpsys package com.example.ava):
AVA_UID=$(dumpsys package com.example.ava | grep userId= | head -1 | cut -d= -f2 | tr -d ' ')
chown -R ${AVA_UID}:${AVA_UID} /data/data/com.example.ava/files/mods/
chmod -R 755 /data/data/com.example.ava/files/mods/Place in /data/adb/service.d/ (Magisk) or post-fs-data.d. Idempotent — runs once, marks completion.
#!/system/bin/sh
MARKER=/data/local/ava_provisioned
[ -f "$MARKER" ] && exit 0
# Wait for boot
while [ "$(getprop sys.boot_completed)" != "1" ]; do sleep 2; done
while ! pm path com.example.ava >/dev/null 2>&1; do sleep 2; done
sleep 5
# Permissions
am broadcast -a com.example.ava.ACTION_GRANT_OVERLAY com.example.ava
am broadcast -a com.example.ava.ACTION_GRANT_BLUETOOTH com.example.ava
am broadcast -a com.example.ava.ACTION_GRANT_RECORD_AUDIO com.example.ava
# Apply settings (persists to DataStore)
am broadcast -a com.example.ava.ACTION_APPLY_SETTINGS \
--es settings_file /sdcard/ava_provision.json com.example.ava
# Enable mods
am broadcast -a com.example.ava.ACTION_SET_MOD_ENABLED \
--es mod_id echo-show-support --ez mod_enabled true com.example.ava
# Start service
am broadcast -a com.example.ava.ACTION_START_SERVICE com.example.ava
touch "$MARKER"- Settings merge into DataStore JSON files at
/data/data/com.example.ava/files/*_settings.json— survives reboot -
player.enableAutoRestart: truemeansBootReceiverauto-starts the voice satellite on every boot - Mods load from
registry.jsonon service start - Re-running the script without the marker is safe —
ACTION APPLY SETTINGSis a merge, not a replace
| Item | Status | Workaround |
|---|---|---|
| Bluetooth Proxy toggle | Stored in bluetooth_settings SharedPreferences, not in applier |
Write XML directly: /data/data/com.example.ava/shared_prefs/bluetooth_settings.xml with detect_enabled boolean |
| iptables / WiFi / priv-app | Not Ava's responsibility | Handle in TWRP zip install script |
Back to Mod Store