HTTP API for picture settings (backlight, picture mode, light sensor, …) on MediaTek-based Fire TV smart TVs.
A tiny sideloaded Android app that binds to the TV's own picture-quality service — the same one the Settings UI talks to — and exposes it as JSON over HTTP on port 8765. No root, no ADB at runtime, no UI automation.
$ curl http://192.168.1.50:8765/backlight/30
{"ok":true,"key":"Backlight","requested":"30","before":"51","after":"30"}Important
Verified on one TV only: Hisense 55" U6 Mini-LED, model 55U65QF (2025), Fire OS 8
(Android 11), MediaTek board codename coyote-ch.
Other Fire TV smart TVs built on MediaTek boards may ship the same service, possibly under different names or transaction codes. If it doesn't work on yours, How this was found walks through finding the right values. Please open an issue with your model either way.
- Quick check: is your TV compatible?
- Install
- API
- Caveats
- Home Assistant
- How it works
- How this was found
- Building from source
With ADB connected (see below):
adb shell pm list packages | grep customerpq
# package:com.mediatek.customerpq <- good sign
adb shell getprop ro.product.device
# coyote-ch <- identical to the tested setIf com.mediatek.customerpq isn't there, this app won't work as-is, but the
investigation steps may still find the equivalent on your set.
This differs from Fire TV sticks and most online guides:
- The TV must be registered to an Amazon account and online. If it's offline or not signed in, the settings tree renders incomplete and Developer Options never appears, no matter how many times you press. Let it update and reboot first.
- Go to Settings → Device & Software → About. On sticks and cubes it's "My Fire TV" instead, which isn't on the TVs.
- Highlight the top row (the device name, e.g. "Your TV") and press Select about 7 times, quickly. Slow presses reset the counter.
- Press Back once. Developer Options is now in the menu. Turn on ADB debugging.
- Note the TV's IP (Settings → Network) and give it a DHCP reservation on your router.
Since early 2024 Fire OS blocks apps on the TV itself from opening local ADB connections, so you need a computer on the same network. After installation, PQ Bridge doesn't use ADB at all.
Download pqbridge-<version>.apk from Releases, then:
adb connect <tv-ip>:5555 # accept the prompt on the TV the first time
adb install -r pqbridge-<version>.apk
adb shell am start -n com.pqbridge.app/.MainActivityThe last command is needed once. Android won't deliver the boot broadcast to an app that has never been opened. It also starts the service right away.
From then on it starts on every boot. From a cold power-on it takes about 60–90 s to respond (most of that is Fire OS booting).
curl http://<tv-ip>:8765/health # {"bound":true}Uninstall / restart / logs
adb uninstall com.pqbridge.app
adb shell am start-foreground-service com.pqbridge.app/.BridgeService # restart without the activity
adb logcat -s PqBridge/Service PqBridge/PqClient PqBridge/HttpIf you switch between a release APK and your own build, uninstall first: adb install -r
refuses to replace an app signed with a different key.
All endpoints are GET and return JSON.
| Endpoint | Does |
|---|---|
/ |
List endpoints |
/health |
{"bound":true} if the binder connection to the PQ service is alive |
/state |
Backlight, Picture_Mode, Light_Sensor, LD_MODE, Color_Temperature, Gamma, Peaking_Brightness, Low_Latency |
/get?key=K |
Read any key |
/set?key=K&value=V |
Write any key, then read it back |
/backlight |
Current backlight (0–100) |
/backlight/N or /backlight?value=N |
Set backlight |
/applyall |
Copy current input's settings to all inputs (see caveats) |
curl http://<tv-ip>:8765/state
curl "http://<tv-ip>:8765/set?key=Light_Sensor&value=Off"
curl "http://<tv-ip>:8765/get?key=Picture_Mode"Always check ok on a set. The TV silently ignores unknown keys and invalid values
(logcat: invalid <...> setting, ABORT!), so the bridge reads the value back and reports
"ok": false if it didn't take. A 200 alone doesn't mean it worked.
Case-sensitive, identical to the TV's internal names. Seen on the 55U65QF:
Backlight (0–100), Picture_Mode, Light_Sensor (On/Off), LD_MODE, Gamma,
Color_Temperature, Peaking_Brightness, Low_Latency, BFI, Live_Tone, DV_IQ_LS,
Color_Temperature_{Red,Green,Blue}_{Gain,Offset}, 11_Point_N_{Red,Green,Blue}.
To see every key on your set, watch logcat and move any picture slider:
adb logcat | grep onNotifyPqSettingInfo- No authentication. Anyone on your LAN can change picture settings. For a home display
this is probably fine. If it isn't, block port 8765 at your router, or build from source
with
TOKENset inHttpServer.javaso every request needs?token=…. - Settings are per-context. The TV stores each value separately per
<input, SDR/HDR10/Dolby Vision, picture mode, colour temperature>, e.g.<HDMI4, SDR, Filmmaker, Warm>. A set only affects the context that's active right now. When a Dolby Vision title starts, you see a different stored backlight./applyallcopies across inputs only, not across HDR types or picture modes. Values persist across power cycles. - Turn the light sensor off, or it will override your backlight:
/set?key=Light_Sensor&value=Off. (Untested whether that setting is global or per-context.) - Unofficial interface. This uses an internal MediaTek service, not a public API. Menu
redesigns won't break it, but a firmware update that renames the service or reorders its
methods would. The names and transaction codes are constants at the top of
PqClient.java. - Fire OS may kill it under memory pressure despite the foreground service. If
/healthstops answering, restart the service (see above) or power-cycle the TV.
examples/home-assistant/tv_backlight.yaml is a
complete package that creates a
number.tv_backlight slider:
- A
rest:sensor polls/stateevery 60 s. The slider greys out (unavailable) when the TV is off. - The slider updates immediately when moved. A trigger-based template sensor takes the
value from a
tv_backlight_setevent fired at set time, and the next poll reconciles it. (homeassistant.update_entityon arest:sensor is throttled, so polling alone feels laggy.) - Sets are debounced (400 ms,
mode: restart) and turnLight_Sensoroff first if it's on.
Replace 192.168.1.50 with your TV's IP. Example dashboard card that hides while the TV is off:
type: tile
entity: number.tv_backlight
features:
- type: numeric-input
style: slider
visibility:
- condition: state
entity: number.tv_backlight
state_not: unavailableThe rest: integration doesn't create entities until its first successful fetch. If HA
starts while the TV is unplugged, the entities show up a minute or so after the TV comes back.
Amazon Settings UI (com.amazon.tv.settings.mediatek)
│ IPqService.setPqParameter
▼
com.mediatek.customerpq / CustomerPqService ◄── PQ Bridge binds here
│ ICustomerPqService.setToPqCache(key, value)
▼
PQ cache + SQLite ─► vendor.mediatek.hardware.pq@1.0::IPq (HIDL HAL) ─► panel
│
└─► side effect: Settings.System.screen_brightness ◄── a mirror nobody reads
CustomerPqService is declared android:exported="true" with no android:permission,
so any installed app may bind to it. setToPqCache validates the key, stores the value for
the current context, and pushes it to the HAL. That's exactly what moving the slider does.
PqClient.java sends raw Parcels to the
binder using the transaction codes from the decompiled stub, instead of reconstructing the
AIDL file (which would drag in SettingItem/ColorItem parcelables we don't need):
| Package / class | com.mediatek.customerpq / .CustomerPqService |
| Intent action | CustomerPqService:remote |
| Interface descriptor | com.mediatek.extservice.pqutils.ICustomerPqService |
| tx 1 | String getFromPqCache(String key) |
| tx 2 | void setToPqCache(String key, String value) |
| tx 3 | void applyToAllSourcesToPQCache() |
| tx 4 | void resetPqCache(List<String> keys) (unused) |
| tx 5–8 | updateSettingItems / updateColorItems / register / unregister callback (unused) |
The rest is a foreground service holding the binder, a hand-rolled HTTP/1.0 server, and a boot receiver. About 300 lines of Java with zero dependencies.
This is the part that carries over to other models. If your TV has different package names or transaction codes, these are the steps to find them.
Diff system settings before and after moving the Backlight slider:
adb shell settings list system > before.txt
# move the Backlight slider on the TV
adb shell settings list system > after.txt
diff before.txt after.txt< screen_brightness=130
> screen_brightness=143
< screen_brightness_float=0.507874
> screen_brightness_float=0.5590551
So you try adb shell settings put system screen_brightness 50. The value sticks
(settings get reads it back), and the panel doesn't change at all.
Logcat while moving the real slider shows why:
adb logcat -c && adb logcat | grep -iE 'backlight|pqcache|brightness'PqCache: set(Backlight, 51), get lock
PqCache: <HDMI4, SDR, Filmmaker, Warm> set(Backlight, 51)
PqCache: set backlight with Settings(130/255) <-- the PQ service WRITES the setting
MTSPQ-OEMService: defaultSetCall, successfully set : Backlight : 51
hwcomposer: setDisplayBrightness success, brightness = 0.503346
The picture-quality service writes screen_brightness as a side effect
(round(backlight × 2.55), float = (int − 1) / 254), and nothing reads it back. It's a
mirror, not a control. This is the first thing everyone tries, which is why it's worth
spelling out.
The same log gives you the names to follow: PqCache, MTSPQ-OEMService, and the
per-context tuple.
| Tried | Result |
|---|---|
adb shell cmd display set-brightness 0.2 |
No effect. dumpsys display shows mScreenBrightness=1.0 regardless; DisplayManager isn't in the chain. |
adb shell service list | grep -i pq |
Nothing. The PQ components aren't registered with servicemanager, so service call can't reach them. |
adb shell lshal | grep -i pq |
vendor.mediatek.hardware.pq@1.0::IPq/default. A HIDL HAL in hwservicemanager, which service call can't reach either. |
MediaTek ContentProviders (com.mediatek.tis/*SettingsProvider) |
Tuner/input settings (HDMI, composite, analog), not picture quality. |
Hisense RemoteNow MQTT (port 36669), hisensetv libs, HA Vidaa integrations |
Only exist on VIDAA / Hisense-Android firmware. Fire OS TVs don't run that broker. |
input keyevent UI automation |
Works (quick settings = keyevent 176), but is brittle. Kept as a last resort only. |
adb shell pm list packages -f | grep -iE 'pq|picture|mediatek'
# package:/product/app/CustomerPqService_coyote-ch/…apk=com.mediatek.customerpq
adb shell dumpsys package com.mediatek.customerpq | grep -A3 -i 'service'
# shows CustomerPqService and its intent action "CustomerPqService:remote"Pull the APK, along with the Settings app that calls it, for decompiling:
adb pull "$(adb shell pm path com.mediatek.customerpq | cut -d: -f2 | tr -d '\r')" customerpq.apk
adb pull "$(adb shell pm path com.amazon.tv.settings.mediatek | cut -d: -f2 | tr -d '\r')" tvsettings.apkOpen the APK in jadx (brew install jadx, then jadx-gui customerpq.apk).
AndroidManifest.xml: find the<service>entry. What matters isandroid:exported="true"with noandroid:permissionattribute. If a permission is set, a sideloaded app can't bind to it and this approach is dead on your firmware.- Search for
Stub: the AIDL-generatedI…Service$Stubclass holds everything a raw binder client needs:DESCRIPTOR = "com.mediatek.extservice.pqutils.ICustomerPqService"TRANSACTION_getFromPqCache = 1,TRANSACTION_setToPqCache = 2, …- the
onTransactswitch, which shows the argument order and types for each code (readString()×2 for set, etc.)
- In the Settings APK, search for the descriptor or
setToPqCacheto confirm this really is the path the UI takes, and to see which key strings it uses.
Update the constants at the top of PqClient.java
(DESCRIPTOR, PKG, CLS, ACTION, TX_*) and the package name in the <queries> block of
AndroidManifest.xml. Package visibility rules require it
before you can bind to another app's service. Build, install, and test /get?key=Backlight
first, since a wrong read can't hurt anything.
If bindService returns false or /health stays {"bound":false}, check
adb logcat -s PqBridge/PqClient and look for ActivityManager errors about the bind.
Android Studio (easiest): File → Open this folder, wait for the Gradle sync, then
Build → Build App Bundle(s) / APK(s) → Build APK(s). Studio bundles the JDK and SDK.
Output: app/build/outputs/apk/debug/app-debug.apk.
Command line: needs a JDK 17–21 and the Android SDK (platform 34). Point JAVA_HOME at the
JDK and either set ANDROID_HOME or let Android Studio write local.properties.
./gradlew assembleDebugThe first run downloads Gradle 8.7 and the Android Gradle Plugin via the wrapper; nothing else needs installing.
settings.gradle.kts declares the project and its one module, :app
build.gradle.kts pins the Android Gradle Plugin version (8.5.2)
gradle/wrapper/, gradlew the Gradle wrapper: fetches the right Gradle on first run
gradle.properties JVM/Gradle flags
app/build.gradle.kts SDK versions, app id, signing, build types
app/src/main/
AndroidManifest.xml permissions, components, boot receiver, <queries>
java/com/pqbridge/app/
PqClient.java raw-Parcel binder client
HttpServer.java GET-only HTTP/1.0 server
BridgeService.java foreground service owning both
BootReceiver.java starts the service on BOOT_COMPLETED
MainActivity.java launch once so Android allows boot start
res/values/strings.xml app name
targetSdk is deliberately 30 (Android 11, matching Fire OS 8). Targeting 34 would require
foregroundServiceType declarations and permissions that don't help on this device.
compileSdk 34 only controls which APIs the compiler knows about.
Maintainers: see RELEASING.md.
MIT. Not affiliated with Hisense, Amazon, or MediaTek. Changing picture settings through an internal interface is at your own risk.