Skip to content

Mod Store Development

KNOOP edited this page Jun 29, 2026 · 2 revisions

Mod Development Guide

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


Architecture

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

Manifest Specification

Root Fields

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)

Minimal Manifest

{
  "id": "my-mod",
  "name": "My Mod",
  "version": "1.0.0",
  "entities": [
    {
      "type": "switch",
      "id": "power",
      "name": "Power"
    }
  ]
}

Full Manifest Example

{
  "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"
    }
  ]
}

Entity Types

Common Entity Fields

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"

switch

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_sensor

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)

sensor

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)

number

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

button

Trigger action on press.

Field Description
press Action method name (e.g. reboot)

select

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

Text input field.

Field Description
set Method to set value (called as set:value)
read Method to read current value

text_sensor

Read-only text state sensor.

Field Description
read Method to read value (returns String/Number/Boolean)
refresh_interval_ms Polling interval in ms

Config Items

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.


Action Format

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:1manager.setPower(1)
  • setPower:truemanager.setPower(true)
  • setRGB:255,0,0manager.setRGB("255", "0", "0") (multi-arg not auto-typed; use String)
  • rebootmanager.reboot()

Manager Class

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;
    }
}

Optional Methods

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

State Listener

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), or String ("true"/"on"/"1" = true)
  • sensor: NumbertoFloat()
  • text_sensor: String, Number, or BooleantoString()

State Persistence

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.


Device Compatibility Hooks

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.


Voice Pipeline API

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.

Opt-in (both required)

  1. Set "voice_pipeline": true in manifest.json
  2. Manager implements onVoicePipelineEvent(Context, String, Bundle)

Manifest

{
  "id": "echo-dot-led",
  "name": "Echo Dot LED",
  "voice_pipeline": true,
  "manager": "com.example.EchoDotLedManager",
  "libs": ["libs/echo-dot-led.jar"]
}

Java Manager

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 Reference

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

External App (Broadcast Only)

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.


Permissions

Manifest Permission Tokens

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.

Permission Categories

  • 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.


Build

Requirements

  • Android SDK (platform 34+)
  • Java 11+
  • d8 tool (in Android SDK build-tools)

Steps

cd sources/features/your-mod/
chmod +x build.sh
./build.sh

The build script:

  1. Compiles Java sources with javac
  2. Converts .class to DEX format using d8
  3. 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/

Native Libraries

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.


Testing

  1. Copy manifest.json and libs/ to device: /data/data/com.example.ava/files/mods/your-mod/
  2. Enable in Ava settings (Settings → Advanced → Mod Store → Installed)
  3. Grant required permissions
  4. Check Home Assistant for entities
  5. Monitor logcat: adb logcat -s ModManager ModEntityFactory ModVoicePipeline

Publishing

  1. Create source files in sources/devices/ or sources/features/
  2. Build and copy release package to mods/devices/ or mods/features/
  3. Add or update the entry in store.json
  4. Submit Pull Request to ava-mods

Store Index Format (store.json)

{
  "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)

Headless Mod Control (Broadcast)

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.

Actions

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)

Extras

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_ENABLEDtrue to enable, false to disable
no_restart Boolean No (default: false) If true, skip voice satellite restart after mod change

ADB Examples

# 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

Behavior

  • SET_MOD_ENABLED refreshes the registry from disk first, so manual adb push of mod files is picked up.
  • RELOAD_MOD disables and re-enables the mod to force DexClassLoader refresh. 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.

Self-Update API

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");

TWRP Provisioning

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.

Zip Structure

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

Provisioning JSON

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.

Mod Files

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/

First-Boot Script

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"

What Happens After

  • Settings merge into DataStore JSON files at /data/data/com.example.ava/files/*_settings.json — survives reboot
  • player.enableAutoRestart: true means BootReceiver auto-starts the voice satellite on every boot
  • Mods load from registry.json on service start
  • Re-running the script without the marker is safe — ACTION APPLY SETTINGS is a merge, not a replace

Gaps

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

Clone this wiki locally