A smart bulb outside your work room that lets people nearby know when you're free β automatically synced to your Teams status.
Place a smart bulb outside your work room. PresenceBeam keeps it in sync with your Microsoft Teams presence so that anyone nearby β family, roommates, colleagues β can see at a glance when it's a good time to knock. Glows green when you're free. Dims when you're away. Blinks red when you're in a call.
Built with an extensible, provider-agnostic architecture β adding support for Slack, Zoom, TP-Link Tapo, Govee, or any other platform/bulb is as simple as adding a single class.
π hushensavani.github.io/PresenceBeam
- Real-time status sync β Bulb color changes within seconds of your status changing
- Adaptive polling β Polls faster during non-critical statuses (e.g. Available), slower during meetings to reduce API calls
- Blink support β Bulb blinks for attention-grabbing statuses like "In a Call" or "Presenting"
- Quiet hours β Automatically turns off the bulb and pauses polling during configurable hours (e.g. 9 PM to 10 AM)
- Token caching β Microsoft authentication is cached to disk; authenticate once, run forever
- Dockerized β Run as a lightweight background container with
docker compose up - Extensible β Swap providers or light controllers by changing two lines in
main.py
The current implementation (Microsoft Teams β Philips WiZ) maps statuses as follows:
| Teams Status | Bulb Color | Brightness | Effect |
|---|---|---|---|
| Available | π’ Green | 180 | Solid |
| AvailableIdle | π’ Green | 100 | Solid (dimmed) |
| Busy | π΄ Red | 200 | Solid |
| BusyIdle | π΄ Red | 130 | Solid (dimmed) |
| InAMeeting | π΄ Red | 200 | Solid |
| InACall | π΄ Red | 200 | Blinking |
| DoNotDisturb | π΄ Red | 200 | Blinking |
| Presenting | π΄ Red | 255 | Fast blinking (0.4s) |
| Away | π‘ Yellow | 150 | Solid |
| BeRightBack | π‘ Yellow | 100 | Solid (dimmed) |
| Offline | β« Off | β | Bulb off |
| PresenceUnknown | π΄ Red | 200 | Blinking |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β main.py β
β Instantiates provider, controller, engine and calls run() β
ββββββββββββββββ¬ββββββββββββββββββββββββββ¬ββββββββββββββββββββ
β β
βΌ βΌ
ββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββ
β BasePresenceProviderβ β BaseLightController β
β (ABC β core/) β β (ABC β core/) β
β β β β
β + authenticate() β β + apply_status(status) async β
β + get_status() β β + turn_off() async β
β + on_token_expired()β β + cleanup() async β
ββββββββββββ¬ββββββββββββ ββββββββββββββββ¬βββββββββββββββββ
β β
βΌ βΌ
TeamsPresenceProvider WizLightController
(providers/ms_teams/) (lights/wiz/)
β β
ββββββββββββ¬ββββββββββββββββββββ
βΌ
βββββββββββββββββ
β SyncEngine β
β (engine.py) β
βββββββββββββββββ
The SyncEngine is fully provider-agnostic. It knows nothing about Microsoft Teams or Philips WiZ. It only interacts with the abstract base classes, making it trivial to swap implementations.
PresenceBeam/
βββ src/
β βββ config.py # Environment-driven configuration + timestamp logging
β βββ engine.py # Core polling loop (provider/controller agnostic)
β βββ main.py # Entry point β wires provider + controller β engine
β βββ core/
β β βββ base_presence_provider.py # Abstract base class for presence providers
β β βββ base_light_controller.py # Abstract base class for light controllers
β βββ providers/
β β βββ ms_teams/
β β βββ teams_presence_provider.py # Microsoft Teams via Graph API + MSAL
β βββ lights/
β βββ wiz/
β βββ wiz_light_controller.py # Philips WiZ via pywizlight (UDP)
βββ data/ # Runtime data (token cache); volume-mounted in Docker
βββ Dockerfile # python:3.11-slim container
βββ docker-compose.yml # Production-ready compose file
βββ requirements.txt # Python dependencies
βββ .env.example # Configuration template
βββ docs/
β βββ index.html # GitHub Pages landing page
βββ AGENTS.md # Architecture context for AI agents and contributors
βββ CONTRIBUTING.md # How to add new providers and light controllers
βββ LICENSE # MIT
βββ README.md # This file
| Component | Implementation | Technology |
|---|---|---|
| Presence Provider | Microsoft Teams | Microsoft Graph API + MSAL (device-flow auth) |
| Light Controller | Philips WiZ Neo 12W B22 | pywizlight library (UDP, local network) |
- Python 3.11+ (for manual run)
- Docker & Docker Compose (for containerized run)
- A Philips WiZ Neo 12W B22 smart bulb (Wi-Fi & Bluetooth) connected to your local network
- A Microsoft 365 work/school account with Teams
- An Azure App Registration (free β setup instructions below)
You need to register an app in Azure AD to get permission to read your Teams presence status via the Graph API.
- Go to portal.azure.com
- Navigate to Azure Active Directory β App registrations β New registration
- Name it anything (e.g.
PresenceBeam), leave all other defaults - Click Register
- Note down the Application (client) ID and Directory (tenant) ID β you'll need these later
- In your app registration, go to API Permissions β Add a permission
- Select Microsoft Graph β Delegated permissions
- Search for
Presence.Readand check it - Click Add permissions
- If you're an admin, click Grant admin consent. If not, ask your IT admin to grant it
This allows the app to authenticate via device flow (interactive browser login) without needing a client secret.
- In your app registration, click Authentication in the left menu
- Click Add a platform β Mobile and desktop applications
- Check
https://login.microsoftonline.com/common/oauth2/nativeclientβ Click Configure - Scroll down to Advanced settings
- Set "Allow public client flows" β Yes
- Click Save
cp .env.example .envEdit .env with your values:
# IP Address of the WiZ Bulb (find it in your router's DHCP table or the WiZ app)
BULB_IP=192.168.1.100
# Azure App Registration details (from Step 1)
CLIENT_ID=your_client_id_here
TENANT_ID=your_tenant_id_here| Variable | Required | Default | Description |
|---|---|---|---|
BULB_IP |
β | β | IP address of the smart bulb on the local network |
CLIENT_ID |
β | β | Azure App Registration client ID |
TENANT_ID |
β | β | Azure tenant ID |
POLL_FAST |
β | 5 |
Polling interval (seconds) for non-critical statuses |
POLL_SLOW |
β | 30 |
Polling interval (seconds) for critical/idle statuses |
DEFAULT_BLINK_INTERVAL |
β | 0.7 |
Seconds per on/off cycle for blinking statuses |
ENABLE_QUIET_HOURS |
β | False |
Enable the quiet hours feature (True / False) |
QUIET_HOURS_START |
β | 21:00 |
Start of quiet window (24-hour HH:MM format) |
QUIET_HOURS_END |
β | 10:00 |
End of quiet window (24-hour HH:MM format) |
Note:
ENABLE_QUIET_HOURSacceptstrue,1, oryes(case-insensitive) as truthy values. Anything else disables it.
# Build and start the container in detached mode
docker compose up -d --build
# Watch logs β important on first run for the Microsoft auth prompt
docker compose logs -fOn the first run, you'll see a Microsoft device-flow prompt in the logs:
ACTION REQUIRED: Authenticate with Microsoft
==================================================
To sign in, use a web browser to open the page https://microsoft.com/devicelogin
and enter the code XXXXXXXX to authenticate.
==================================================
- Visit the URL shown
- Enter the code displayed
- Log in with your Microsoft work/school account
The token is cached to ./data/.presence_beam_token_cache.json (volume-mounted from the container). Subsequent runs authenticate silently β no user interaction needed.
Useful Docker commands:
docker compose logs -f # View live logs
docker compose restart # Restart (e.g. after changing .env)
docker compose down # Stop and remove the container# Install dependencies
pip install -r requirements.txt
# Create the token cache file with secure permissions
touch ~/.presence_beam_token_cache.json
chmod 600 ~/.presence_beam_token_cache.json
# Run
python src/main.pyOn the first run, the same Microsoft device-flow prompt will appear directly in your terminal. Follow the instructions to authenticate.
The token is cached to ~/.presence_beam_token_cache.json. Subsequent runs authenticate silently.
To run in the background:
nohup python src/main.py > sync.log 2>&1 &When enabled, the daemon automatically turns off the bulb and stops polling the Teams API during a configurable time window. This is useful for after-work hours so the bulb isn't glowing while you sleep.
How it works:
- When the current time enters the quiet window, the bulb is turned off once
- No further API calls or bulb commands are made until the window ends
- When quiet hours end, syncing resumes immediately
Configuration:
ENABLE_QUIET_HOURS=True
QUIET_HOURS_START=21:00
QUIET_HOURS_END=10:00Note: Windows that cross midnight are fully supported (e.g.
21:00to10:00means "quiet from 9 PM until 10 AM the next day").
PresenceBeam is designed to be extended. Adding support for a new communication platform (Slack, Zoom, Google Chatβ¦) or a new smart bulb (TP-Link Tapo, Govee, LIFXβ¦) requires implementing a single class and wiring it up in two lines.
See CONTRIBUTING.md for full step-by-step guides with complete code examples.
| Package | Purpose |
|---|---|
pywizlight |
UDP control of Philips WiZ smart bulbs |
msal |
Microsoft Authentication Library for device-flow auth + token caching |
requests |
HTTP client for Microsoft Graph API calls |
python-dotenv |
Load .env file into environment variables |
This project is licensed under the MIT License.
Contributions are welcome! Whether it's a new presence provider (Slack, Zoom, Google Chat), a new light controller (TP-Link, Govee, LIFX), or improvements to the core engine β PRs are appreciated.
See CONTRIBUTING.md for full contribution guides with code examples, and AGENTS.md for architecture context.
Hushen Savani linkedin.com/in/hushensavani
