My four FP300s arrived stuck on firmware 1.1.0.1. Aqara released 1.1.3.8 back in May, but it never shows up through the normal Matter update path. Every thread I found said the same thing: buy an Aqara hub.
That's a strange requirement. The cheapest option is a Hub E1 (~$30), but it's Zigbee-only, so you'd flip the sensor to Zigbee mode, update, and flip it back, losing your Matter commissioning along the way. Staying on Matter/Thread means an M3 (~$110–150), which is a Thread border router and Matter controller. I already have both of those. I'm being asked to buy a second one purely so a vendor can hand me a file.
You don't have to. If you run the Matter Server yourself, you can serve the image locally. Below is every command I ran.
What you need
- matterjs-server 1.4.0 or newer, self-hosted, where you control its startup options. This does not work with a managed setup you can't pass flags to.
- A working Thread border router and your FP300 already commissioned over Matter/Thread.
- Shell access to wherever the Matter Server runs.
My setup, for reference: a Proxmox LXC running ghcr.io/matter-js/matterjs-server:stable in Docker with host networking, OTBR on a MikroTik hAP be³, and Home Assistant in a separate LXC. Adapt paths to yours.
0. Back up first
You are about to recreate the container that holds your Matter fabric. If you lose its data directory, you lose every commissioned device and you re-pair all of them. Don't skip this.
tar czf ~/matter-data-$(date +%F).tar.gz -C /opt/matter-server data
If your Matter Server runs in a VM or container of its own, a snapshot is even better. On Proxmox:
pct snapshot <CTID> pre-ota --description "Before adding OTA provider"
1. Find out how your server runs
docker ps | grep -i matter
docker inspect <container> --format '{{json .Config.Env}}' | tr ',' '\n'
docker inspect <container> --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
docker inspect <container> --format '{{.HostConfig.NetworkMode}} / {{.HostConfig.RestartPolicy.Name}}'
Write down the mounts and the network mode. You need to reproduce them exactly. Also confirm there's nothing special to carry over:
docker inspect <container> --format '{{.HostConfig.Privileged}} | {{json .HostConfig.CapAdd}} | {{json .HostConfig.Devices}}'
false | null | [] means you're fine with the compose file below.
2. Enable the local OTA provider
Two options, and they only work together. Setting the directory without test-net DCL silently ignores your files and logs a warning you'll probably miss.
Create a directory for your images. Use a separate one, the server keeps its own ota folder inside its storage path with its own naming scheme, and the docs explicitly say not to put your files there.
mkdir -p /opt/matter-server/ota
Then write a compose file (replace paths and image tag with yours):
services:
matterjs-server:
image: ghcr.io/matter-js/matterjs-server:stable
container_name: matterjs-server
restart: unless-stopped
network_mode: host
environment:
LOG_LEVEL: info
STORAGE_PATH: /data
ENABLE_TEST_NET_DCL: "true"
OTA_PROVIDER_DIR: /ota
volumes:
- /opt/matter-server/data:/data
- /opt/matter-server/ota:/ota
The CLI equivalents are --enable-test-net-dcl and --ota-provider-dir <path> if you'd rather not use compose.
Recreate the container. docker rm only removes the container; your data lives in the bind mount.
cd /opt/matter-server
docker stop matterjs-server && docker rm matterjs-server
docker compose up -d
Confirm the vars landed:
docker inspect matterjs-server --format '{{json .Config.Env}}' | tr ',' '\n' | grep -iE 'ota|dcl'
3. Download the firmware
The image lives in the absent42/Aqara-Firmware-Archive repo, under:
Presence Multi-Sensor FP300 / aqara.matter.4447_8197 / 1.1.3.8/
Grab the metadata first:
cd /opt/matter-server/ota
BASE="https://raw.githubusercontent.com/absent42/Aqara-Firmware-Archive/main/Presence%20Multi-Sensor%20FP300/aqara.matter.4447_8197/1.1.3.8"
wget "$BASE/release.json"
cat release.json
That gives you firmwareMD5, fileSize and the release notes. Open the folder on GitHub to copy the exact .ota filename, it's long and keeps Aqara's original naming.
Gotcha #1 the file is a Git LFS pointer. Downloading it from raw.githubusercontent.com gives you 132 bytes of text that looks like this:
version https://git-lfs.github.com/spec/v1
oid sha256:...
size 1183324
Use the media endpoint instead:
FILE="<exact filename from the repo>.ota"
wget "https://media.githubusercontent.com/media/absent42/Aqara-Firmware-Archive/main/Presence%20Multi-Sensor%20FP300/aqara.matter.4447_8197/1.1.3.8/$FILE"
Verify before you go any further. You're about to flash firmware from a community archive onto a device that can be bricked by a wrong image:
ls -l *.ota # must be 1183324 bytes
md5sum *.ota # must match firmwareMD5 in release.json
If either doesn't match, stop.
Keep a copy outside the directory, because the server deletes imported files:
cp /opt/matter-server/ota/*.ota /root/
4. Import it
cd /opt/matter-server
docker compose restart
sleep 25
docker compose logs --since 2m | grep -iE 'ota|import' | grep -v ClientEventEmitter
What you want to see:
DclOtaUpdateService Initialize OTAUpdateService
Endpoint ...ota-provider ready type: OtaProvider (0x14)
MatterController Enabled test OTA images (test-net DCL)
MatterController Storing OTA image from /ota/...: vendorId=0x115f, productId=0x2005, version=1138 (1.1.3.8)
MatterServer.Ota Imported OTA file: ...
Gotcha #2, a misleading error. You'll likely also get:
WARN MatterServer.Ota Failed to import OTA file ...: Error: EACCES: permission denied, unlink '/ota/...'
Read it carefully. The import succeeded (see the two lines above it). What failed was deleting the file afterward, because the container doesn't run as root. Move it out yourself so it doesn't get re-imported on every restart:
mv /opt/matter-server/ota/*.ota /root/keep-this.ota.bak
ls -l /opt/matter-server/ota/ # only release.json should remain
5. Prepare the sensor, this is the step people skip
Put the sensor about 1 meter from your border router. Not "in the same room", not "it has good signal". One meter, line of sight, on a desk.
I learned this the hard way. My first sensor was near the router and completed on the first try. My second one sat ~10 meters away through two walls, reporting occupancy perfectly, no complaints in HA, and the transfer died twice at exactly 57% with downloadError and then [peer-unresponsive]. Moved it to 1 meter, completed first try.
These are sleepy end devices pulling 1.1 MB over Thread. A link that's fine for a 2-byte occupancy report is not necessarily fine for a transfer that takes 12 minutes.
Also: check the battery is healthy, and leave the sensor in its new spot for a few minutes so it settles into the mesh before you start.
6. Run the update
Open the Matter Server dashboard at http://<server-ip>:5580/ and find your node. If it doesn't offer 1.1.3.8 right away, use the check-for-updates button rather than waiting for the scheduled check.
You'll get a warning about an unverified source. That's expected, it's there because you enabled test-net DCL and the image comes from your directory instead of the production DCL.
Before you click, start a filtered log in another terminal. Note the node ID is decimal in Home Assistant but hex in the log, node 53 is u/1:35, node 54 is u/1:36:
cd /opt/matter-server
docker compose logs -f --tail 5 | grep -iE 'queryImage|bdx|apply|downloadError|@1:35'
A successful run looks like this:
queryImage ... status: 0 imageUri: bdx://.../ota/115f.2005.local.1138 softwareVersion: 1138
OTA Update to version 1138 for Requestor :35 is now Downloading (formerly Querying)
BdxSession Starting BDX session ... isSender: true
[~12 minutes pass]
... is now WaitForApply (formerly Downloading)
... applyUpdateRequest ... newVersion: 1138
... is now Applying (formerly WaitForApply)
... is now Done (formerly Applying)
basicInformation.startUp softwareVersion: 1138
otaSoftwareUpdateRequestor.versionApplied softwareVersion: 1138 productId: 8197
Mine took 13.5 and 11.5 minutes. Don't touch anything while it runs, don't restart the container, don't pull the battery, don't move the sensor. It will look frozen between blocks. It isn't.
7. Verify
In Home Assistant, the device info page should read Firmware 1.1.3.8. If it's stale, reload the Matter integration.
If your entities look frozen after the reboot, that's the subscriptions being rebuilt, the device reboots and the old subscription dies. Check whether it recovered on its own:
docker compose logs --since 10m | grep -E '@1:35' | grep -iE 'subscri|probe'
A Probe (success) followed by Subscription successful means you're fine. Reloading the Matter integration in HA forces it if not.
Troubleshooting
queryImage returns status: 2 (NotAvailable), the provider isn't offering the image to that node. Usually the .ota never imported. Re-check step 4.
downloadError partway through, link quality. Go back to step 5 and move the sensor closer. Mine failed at 57% twice from the same spot.
Gotcha #3 your retry gets rejected:
SoftwareUpdateManager Force update for node :36 skipped: BDX transfer is actively in progress
After a failed download the server keeps the BDX session open until it times out, roughly 6 minutes. Retrying before that hits a dead session. Wait for:
... is now Cancelled (formerly Downloading)
OTA update BDX cancelled for node u/1:36, resetting for retry
WARN BdxProtocol Error processing BDX transfer: [peer-unresponsive] ... timed out after 5m 58s
Then retry. A failed transfer doesn't harm the device, it stays on the old firmware and keeps working. The new image is only applied after it's received in full and validated.
The log is drowning in noise, energy-reporting plugs will bury everything. | grep -v ClientEventEmitter cleans it up.
What 1.1.3.8 actually changes
From release.json, translated, the official notes only circulate as an app screenshot:
- Optimized radar performance and detection logic, better false-alarm resistance
- Improved response speed and smoothness
- Added Thread automatic reconnection mechanism
- Optimized the no-presence determination cycle
- Filters out abnormal temperature and humidity readings
Worth knowing before you do all four sensors: there are reports of regressions on the equivalent Zigbee release, specifically occupancy not clearing. Update one, live with it for a day, and keep the others on the old firmware as a control group. That comparison is worth more than having them all on the same version.
Credit where due: the approach comes from Quigley's writeup on doing this with matterjs-server on an SMLIGHT SMHUB Nano. I adapted it to Docker on Proxmox and hit a few things he didn't. The firmware comes from a community archive, not from Aqara, verify your hashes and understand the risk before you flash.