Active Host Daemon is a macOS utility that detects whether local or remote input is active and reports host changes to Home Assistant. It runs in the background as a per-user LaunchAgent and uses a Quartz event tap to observe local input.
The transfer gesture is Ctrl plus a deliberate movement to the active display's left edge. After the configured movement and dwell requirements are satisfied, the daemon selects the remote host. An active Screen Sharing connection also selects the remote host and takes priority over Quartz input events. Genuine local input returns control to the local host once Screen Sharing has disconnected; disconnecting alone leaves the remote host selected until fresh local input is observed.
- Native macOS Quartz input monitoring
- Explicit local and remote host states
- Multi-display left-edge detection
- Standard and High Performance Screen Sharing detection with priority over local Quartz input
- Home Assistant webhook notifications on host changes
- Optional display wake when control returns from a Logitech Flow peer
- Automatic startup and restart through launchd
- Event-tap health watchdog and operational diagnostics
- Install, start, stop, restart, status, and uninstall commands
Requirements:
- macOS
- Python 3.10 or newer
- A Home Assistant webhook URL
Clone the repository, create its virtual environment, install dependencies, and run the installer:
cd /path/to/active-host-daemon
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
./install.shThe installer renders the LaunchAgent with the repository's absolute path, writes
it to ~/Library/LaunchAgents, creates ~/Library/Logs, and starts the daemon.
Edit config.yaml before installation:
hosts:
local: "macmini"
remote: "laptop"
poll_interval: 1
screen_sharing_port: 5900
home_assistant:
webhook: "http://home-assistant.local:8123/api/webhook/active_host"
request_timeout: 10
logging:
level: INFOhosts.localis sent when local input is active.hosts.remoteis sent after a successful edge-transfer gesture.poll_intervalcontrols how frequently the daemon loop observes detector state.screen_sharing_portis the base Screen Sharing port. Standard sessions use an established TCP connection; High Performance sessions use connected UDP sockets on this port and the next two ports.home_assistant.webhookmust be an absolute HTTP or HTTPS URL.request_timeoutis the webhook timeout in seconds.display.wake_on_localenables display wake on a remote-to-local transition.display.wake_durationcontrols how longcaffeinateregisters user activity.logging.levelaccepts standard Python logging levels such asINFOorDEBUG.
Display wake is optional and disabled when the display section is omitted. To
enable it, add:
display:
wake_on_local: true
wake_duration: 5This is intended for Logitech Flow users whose destination Mac remains awake while
its display has gone to sleep. When control returns from the remote host to the
local Mac, the daemon runs /usr/bin/caffeinate -u -t 5 asynchronously to register
user activity and wake the display. It never runs during a local-to-remote transfer.
Validate configuration without starting the detector:
.venv/bin/python active_host.py --checkDisplay the installed software version:
.venv/bin/python active_host.py --versionAfter ./install.sh, inspect the service:
./status.shThe initial Home Assistant event is sent when the daemon first observes its host state. Subsequent events are sent only when the detected host changes. Payloads use this format:
{
"host": "macmini",
"source": "active-host-daemon",
"timestamp": 1783856401
}The virtual-environment Python interpreter may require permission under:
System Settings
→ Privacy & Security
→ Accessibility
and, if requested:
System Settings
→ Privacy & Security
→ Input Monitoring
Grant access to .venv/bin/python from this repository. If permissions are
granted after launch, restart the daemon:
./restart.shor:
launchctl kickstart -k gui/$(id -u)/uk.co.lailey.active-host-daemonAfter updating the repository, refresh dependencies and reinstall the LaunchAgent so its rendered definition matches the current project:
git pull
.venv/bin/python -m pip install -r requirements.txt
./install.shThe installer safely unloads an existing copy before loading the replacement.
Start with the formatted status report:
./status.shThe primary logs are:
~/Library/Logs/active-host-daemon.log
~/Library/Logs/active-host-daemon-error.log
Follow them live with:
tail -f ~/Library/Logs/active-host-daemon.log
tail -f ~/Library/Logs/active-host-daemon-error.logCommon checks:
- If the service is not installed, run
./install.sh. - If permission guidance appears, grant Accessibility and Input Monitoring access,
then run
./restart.sh. - If configuration validation fails, correct
config.yamland restart. - If the webhook fails, verify its URL and confirm Home Assistant is reachable.
- For detailed Quartz callback and health diagnostics, temporarily set the logging
level to
DEBUG, then restart the daemon.
Inspect the launchd service directly:
launchctl print gui/$(id -u)/uk.co.lailey.active-host-daemonRestart it directly:
launchctl kickstart -k gui/$(id -u)/uk.co.lailey.active-host-daemonThe installed plist is located at:
~/Library/LaunchAgents/uk.co.lailey.active-host-daemon.plist
Install or replace the LaunchAgent:
./install.shStart a stopped LaunchAgent:
./start.shRestart the daemon and immediately display its status:
./restart.shStop the daemon without uninstalling it:
./stop.shDisplay service, process, path, permission, and recent-log information:
./status.shUnload the service and remove its installed plist while preserving logs:
./uninstall.sh