Local RF control of AOK/Zemismart 433.92 MHz roller blinds from Home Assistant — no cloud, no hub app, just MQTT and a flashed Sonoff RF Bridge.
Many roller blinds sold under the Zemismart brand (and other resellers of AOK OEM tubular
motors) are RF-only: a 433.92 MHz remote is the sole way to control them. This integration gives
each blind — or an arbitrary group of blinds on the same remote — a first-class Home Assistant
cover entity by generating the motors' native RF protocol from scratch, transmitted through one
or more inexpensive Sonoff RF Bridge R2 units.
- Open, close, stop, and set position with assumed-state travel-time position modeling.
- True group commands: a group is one RF transmission, not several colliding commands.
- Calibrate from a single capture: one labeled button press from your remote derives the remote's complete command set (fully reverse-engineered protocol).
- Virtual remotes: mint identities that never existed as hardware and pair motors to them.
- Reliable delivery: correlated bridge acknowledgements, bridge-side STOP deadlines for partial movement, and area-aware multi-bridge failover.
- Guided remote learning: a time-boxed bridge capture identifies the remote, action, and channels automatically during onboarding or reconfiguration.
This project is two cooperating parts:
| Repository | Role |
|---|---|
| zemismart-blinds (this repo) | Home Assistant integration — owns every remote identity, generates Portisch B0 frames, models position, chooses a bridge |
| esphome-rf433-mqtt-bridge | ESPHome firmware — a deliberately dumb MQTT-to-433 MHz beacon with no blind codes and no cover entities |
The two meet at a fixed MQTT topic contract (rf433/<bridge>/...) through whatever MQTT broker
your Home Assistant already uses — the Mosquitto add-on works out of the box, and any other
broker (standalone Mosquitto, EMQX, a NAS container) works identically.
Home Assistant ──(MQTT tx)── broker ──(rf433/<bridge>/tx)── RF Bridge ──📡── blinds
Learn wizard ◀─(MQTT rx)── broker ◀─(rf433/<bridge>/rx)── RF Bridge ◀─📡── remote button
| Requirement | Details |
|---|---|
| Home Assistant | Version 2026.5 or newer (ships Python 3.14, which this integration's syntax requires), with the MQTT integration configured |
| MQTT broker | Any — the Mosquitto add-on is the easiest |
| RF bridge | Sonoff RF Bridge R2 flashed with esphome-rf433-mqtt-bridge (Portisch RF firmware required) |
| Blinds | AOK OEM 433.92 MHz tubular motors — commonly sold as Zemismart; other AOK resellers are expected to be compatible |
See INSTALL.md for the complete guide, including bridge flashing and first-blind calibration.
Quick version (HACS): add this repository as a custom repository in HACS, install Zemismart Blinds, restart Home Assistant, then add the integration from Settings → Devices & services.
Each run of the add-integration flow creates exactly one device with one cover entity. The guided Learn path is the default:
- Enter a name and Home Assistant area, then use the automatically selected online bridge or choose another one.
- Press Up, Down, or Stop on the physical remote during the 30-second capture window. The flow decodes the first valid action frame and detects its prefix, remote ID, channels, and button automatically.
- Confirm the detected identity, then edit the captured channels if needed (
1for one blind, or1,2,3for a group) and enter the full up/down travel times.
Advanced setup can reuse an already-calibrated remote, enter a remote manually from one labeled B0/B1 reference or direct 16-bit action base, or allocate a virtual remote. The optional OEM TRAILER base should be left blank unless captured.
Use Configure on an existing entry to edit channels, timing, area, or RF settings while keeping
the current remote identity. To change the identity or calibration, use Reconfigure → Relearn from
remote. The flow accepts hex with or without the 0x prefix.
Position is estimated, not measured, and a new entity starts unknown. Full OPEN/CLOSE re-anchors at 100/0 after one complete configured travel plus a margin. SET_POSITION requires a known estimate and arms an absolute STOP deadline on the bridge, so a partial move stops even if Home Assistant restarts mid-travel. An acknowledgement timeout makes position unknown instead of pretending a command moved the motor.
If no bridge in the cover's area is online, the integration falls back to the retained default
bridge, then any online bridge, and exposes degraded_bridge: true. One shared worker publishes
one command at a time and waits for the bridge's acknowledgement before the next — with
intelligent coalescing that merges near-simultaneous commands for blinds on the same remote into
a single group frame.
Debug escape hatch: send one complete AAB0...55 frame through a named bridge with optional
repeats.
Returns a fresh remote identity with a complete synthesized calibration:
prefix: "0x5c1a2b"
remote_id: "0x3c"
base_up: "0xf4a1"
base_down: "0xbc69"
base_stop: "0xdc89"To pair one:
- Call the service, then add a manual integration entry using the returned prefix, remote ID, and UP base (calibration action UP).
- Put the motor into its RF pairing mode with its physical program button (confirm the pairing jog; exact button timing varies by motor revision, so keep the motor's own instructions).
- Send OPEN from the new cover while the motor is in pairing mode, exit pairing mode, and verify OPEN, CLOSE, and STOP. Keep the original remote paired until validation is complete.
- Bridge: Sonoff RF Bridge R2 — see esphome-rf433-mqtt-bridge for flashing.
- Motors: AOK OEM tubular roller-shade motors (Zemismart-branded and others).
- 3D-printed tube adapters: printable adapters for fitting these motors to other roller tubes are maintained in joyfulhouse/ZemismartAdapters.
automation:
- alias: "Close blinds at sunset"
trigger:
- platform: sun
event: sunset
action:
- service: cover.close_cover
target:
entity_id: cover.living_room_blinds- Check the bridge is online: the retained
rf433/<bridge_id>/availabilitytopic should beonline(use an MQTT explorer, e.g. MQTT Explorer ormosquitto_sub). - Confirm Home Assistant's MQTT integration is connected to the same broker as the bridges.
- Watch
rf433/<bridge_id>/statuswhile commanding — arejectedstatus includes a reason.
- Re-check the calibration: capture the remote button again and compare the decoded prefix/remote ID with the entry's configuration.
- Increase RF repeats in the entry's Configure dialog (distant or obstructed blinds).
- Verify the blind's channel: a motor paired to remote channel 3 ignores a channel-1 frame.
Travel-time position is an estimate. Re-anchor with a full OPEN or CLOSE, and tune the up/down travel seconds in Configure (motors are often slower upward).
logger:
default: warning
logs:
custom_components.zemismart_blinds: debug- Live state sync requires the paired bridge firmware: when the bridges run the state-sync
firmware contract (continuous idle-listen
/rx, boot id, enriched/status,/cmd disarm), physical remote presses are observed and mirrored into each cover's assumed state. Without that firmware the integration is transmit-only and RF reception is limited to the time-boxed Learn wizard. - Physical takeover of a restored or clamped timed move (deferred): after a Home Assistant
restart, or once a group member reaches its own limit before the group's RF frame ends, HA may no
longer model the command's still-armed bridge fail-safe STOP. A physical remote press that reverses
such a move is not guaranteed to disarm that STOP — the bridge STOP can still halt the reversed
motion — and a displaced restored-timed command may keep a position that should read
unknown. Re-issue the movement if a blind stops unexpectedly after a takeover. - Assumed position: there is no motor feedback; position is modeled from travel time.
- Bridge isolated from MQTT mid-command: a bridge that loses its network link (but not power) keeps executing its already-armed fail-safe STOP locally. With multiple bridges, commands fail over to another bridge meanwhile, and the isolated bridge's late STOP can still reach the motor over the air. One-way RF offers no way to recall it; re-issue the movement if a blind stops unexpectedly after a bridge drops.
- Bridge reboot during an HA restart: a bridge's armed fail-safe STOP lives in its RAM. If the
bridge power-cycles entirely within Home Assistant's own downtime (offline and back online before
HA restores state), a restored in-flight partial move cannot detect that its STOP was lost and
models to its target. Bridges that are offline at restore time, or drop offline afterwards, are
detected and the cover becomes
unknown. - Calibration needs one capture per new physical remote (a one-time step per remote).
git clone https://github.com/joyfulhouse/zemismart-blinds.git
cd zemismart-blinds
uv sync
# Lint, type check, test
uv run ruff check . && uv run ruff format --check .
uv run mypy --strict
uv run pytestThe codec tests pin byte-exact golden vectors (generated with the hardware-validated codec for synthetic remote identities), exhaust all non-empty channel subsets, and cover calibration derivation across opcode-byte carries. See PROTOCOL.md for the full protocol specification.
- Bug reports / feature requests: GitHub Issues
- Questions: GitHub Discussions
This integration is built and maintained in my spare time, with real hardware and tooling costs behind every release. If it's useful to you, consider sponsoring the project or leaving a tip to help offset development and testing — it's genuinely appreciated and helps keep the project moving.
- blark/zemismart-blind-protocol — the starting point for the RF protocol reverse engineering.
- Portisch/RF-Bridge-EFM8BB1 — the RF coprocessor firmware that makes raw B0/B1 capture and transmission possible.
MIT — see LICENSE.