Skip to content

Repository files navigation

fanctl

CI Release Downloads License: MIT Python 3.10+

Control Levoit (VeSync) Classic Tower Fans — model LTF-F422S — from your desktop, your browser, or the command line. One Python codebase, a clean UI-agnostic backend, secure token login, and a modern UI.

Unofficial, independent project. "Levoit" and "VeSync" are trademarks of their respective owners; used here only to describe compatibility.

fanctl controlling a Levoit tower fan


Compatibility

⚠️ This app was developed and tested against a single device: the Levoit Classic Tower Fan (LTF-F422S series). It targets that fan's specific VeSync API calls, modes, and state fields.

Other VeSync/Levoit devices — different fans, air purifiers, humidifiers — are not supported out of the box and will likely need additional work: a new controller subclass and possibly different state mapping. The architecture is built for exactly this, though — FanController is a clean extension seam. See ARCHITECTURE.md → Adding a new device. Contributions adding more devices are very welcome.


Features

  • Power, speed (1–12), mode (Normal / Turbo / Auto / Sleep)
  • Oscillation, mute, and display toggles
  • Live room temperature (°F → °C handled for you)
  • Login / logout flow — works with any VeSync account
  • Token-based session — your password is never stored; log in once, auto-connect after
  • Syncing indicator — confirms each command actually took effect on the device
  • Multiple frontends over one backend: Flet (desktop + web), tkinter (native desktop), and a macOS menu bar app
  • Device picker — works with accounts that have more than one fan
  • Fake backend for hardware-free development and demos

Quick start

Run it (developers / from source)

git clone https://github.com/ardacelep/fanctl
cd fanctl
python3 -m venv venv && source venv/bin/activate
pip install -e .            # add [tk] for the tkinter frontend, [dev] for tests

fanctl                      # Flet desktop app (prompts login on first run)
fanctl --web                # open in the browser
fanctl --fake               # in-memory fake fan — no hardware/internet
fanctl --tk                 # alternative tkinter frontend  (needs: pip install -e ".[tk]")
fanctl --menubar            # macOS menu bar app           (needs: pip install -e ".[menubar]")

python3 -m fanctl ... is equivalent. Flags combine, e.g. fanctl --fake --web.

Download it (end users)

No Python needed — grab a prebuilt bundle from the latest release:

OS File
macOS fanctl-macos.zip
Windows fanctl-windows.zip
Linux fanctl-linux.zip

Unzip and launch. The in-app login screen handles credentials — no config files.

First launch — the app is unsigned, so the OS shows a one-time warning. This is expected for a free, independent project (Apple/Microsoft code signing costs a yearly fee). It's a one-time step per download.

macOS (the dialog says "Apple could not verify … is free of malware"): click Done (not Move to Trash), then either —

  • Terminal (most reliable): remove the download quarantine flag, then double-click:
    xattr -dr com.apple.quarantine /path/to/fanctl.app
    Tip: type xattr -dr com.apple.quarantine (with a trailing space) and drag the app onto the Terminal window to fill in the path. A harmless "No such file" note about a symlink may appear — it still works.
  • or System Settings: try to open it once, then go to System Settings → Privacy & Security, scroll down to "fanctl was blocked…"Open Anyway.

(On recent macOS — Sequoia 15, Tahoe 26, and later — the old "right-click → Open" trick no longer bypasses this; use one of the two methods above.)

Windows: SmartScreen shows "Windows protected your PC"More infoRun anyway.

Linux: make it executable if needed (chmod +x) and run it.


Usage

First launch shows a login screen:

  1. Email / password — your VeSync app account.
  2. Region — leave at EU if unsure; a wrong choice still works (VeSync auto-corrects, see ARCHITECTURE.md).
  3. Sign in. The session token is saved, so you won't be asked again.
Control Behavior
Power Toggle on/off (green = on)
Speed slider 1–12; sends the command on release
Mode Normal / Turbo / Auto / Sleep
Oscillation / Mute / Display On-off switches
Refresh Pull fresh state from the cloud
Logout Clears the saved token, returns to login

After each command the header shows "Updating…" with a progress bar until the cloud confirms the new state (e.g. Turbo makes the fan pick its own speed, which appears within ~1.5 s).

Changes made elsewhere — the VeSync mobile app or the physical fan — are picked up automatically too: the app polls the cloud every ~10 s (VeSync has no push API), so external changes appear within a few seconds without needing a manual refresh.


How it works

fanctl separates a pure-async, UI-agnostic backend from swappable frontends. The backend handles auth, serialized device I/O, optimistic updates with a delayed cloud reconcile, and a generation guard so stale reads never overwrite fresher state. Frontends just subscribe to state and call methods.

See ARCHITECTURE.md for the full design — the controller contract, the one-way-rendering rule, the optimistic/reconcile/generation-guard cycle, the VeSync dual-state-field gotcha, and token + cross-region auth.

fanctl/
├── backend/        UI-agnostic device + auth logic
│   ├── state.py        FanState (immutable) + constants
│   ├── controller.py   FanController (ABC) — orchestration policy
│   ├── vesync.py       VeSyncFanController (real device)
│   ├── fake.py         FakeFanController (in-memory)
│   └── paths.py        cross-platform token location
├── ui/             frontends
│   ├── flet_app.py     primary: desktop + web
│   ├── tk_app.py       alternative: native tkinter
│   └── menubar_app.py  alternative: macOS menu bar (rumps)
└── __main__.py     wires a frontend to a backend
tests/              pytest against the fake backend

Packaging

Frontend choice maps to a packaging tool.

Flet (recommended — desktop, web, mobile)

flet build produces native bundles from the same code (requires the Flutter SDK):

flet build macos      # .app
flet build windows    # .exe
flet build linux      # binary
flet build apk        # Android
flet build web        # static web bundle

Code signing is recommended for distribution (otherwise macOS Gatekeeper / Windows SmartScreen warn users): Apple Developer (~$99/yr), Windows cert (~hundreds/yr). For personal/small use, "right-click → Open" bypasses Gatekeeper.

tkinter

Bundle the --tk frontend with PyInstaller or briefcase. Include CustomTkinter's assets (PyInstaller hooks / --add-data).


Requirements

Component Version Notes
Python ≥ 3.10 tkinter frontend needs Tcl/Tk; Flet does not
pyvesync ≥ 3.4 VeSync cloud API (async)
flet ≥ 0.85 primary frontend
platformdirs ≥ 4 cross-platform token storage
customtkinter ≥ 5 only for --tk (pip install -e ".[tk]")
rumps ≥ 0.4 only for --menubar, macOS (pip install -e ".[menubar]")

tkinter / Tcl-Tk: pyenv often builds Python without tkinter. If import tkinter fails and you want the --tk frontend:

brew install tcl-tk@8
CPPFLAGS="-I/opt/homebrew/opt/tcl-tk@8/include" \
LDFLAGS="-L/opt/homebrew/opt/tcl-tk@8/lib" \
pyenv install --force 3.11.9

Use tcl-tk@8 (Tk 8.6); Tcl/Tk 9.x is incompatible with this Python.


Troubleshooting

Symptom Fix
ModuleNotFoundError: _tkinter Rebuild Python with Tk (see above), or just use the default Flet frontend.
"Wrong email or password" Use the same credentials as the VeSync mobile app.
"Couldn't reach the server" Check your internet connection.
Logged in but no fan The account must have an LTF-F422S-series fan registered.
Switch to another account Logout, then sign in again.

Security

  • The password is never stored — used once to authenticate; only the session token is written to the per-OS data dir (platformdirs). Logout deletes it.
  • auth.json / config.json are git-ignored. Never commit credentials.

Contributing

PRs welcome — see CONTRIBUTING.md. The fake backend lets you develop and test with no hardware. A new frontend is just subscribe + render + call set_*.

Acknowledgements

  • pyvesync — the community library that does the heavy lifting of talking to the VeSync cloud. fanctl is a UI on top of it.
  • Flet — the Flutter-powered framework behind the primary UI.

License

MIT.

About

Control Levoit (VeSync) Classic Tower Fans from the desktop, web, or CLI — Flet + tkinter UI over a clean async backend.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages