A workplace nameplate and presence display for a Raspberry Pi 4 connected to a 480×1920 portrait LCD. Shows your name, job title, and real-time Microsoft Teams availability — pulled live from Microsoft Graph.
![Dark themed display showing name, status, and clock]
- Live presence — reflects your Teams status (Available, Busy, Do Not Disturb, Away, etc.)
- Free After — shows end of current busy block when in a meeting (requires
Calendars.Read) - Ambient glow — background color shifts with your status
- Clock — 12-hour time with blinking colon and full date
- Multi-user — single server instance handles multiple nameplates via
/{username} - Zero interaction — fully automated after boot
| Component | Details |
|---|---|
| SBC | Raspberry Pi 4 |
| Display | 480×1920 portrait LCD via HDMI |
- Python 3.11+
- An Azure app registration with the following application permissions (admin consent required):
User.Read.AllPresence.Read.AllCalendars.Read(optional — enables the "Free After" indicator; omit to skip it)
- Go to Entra ID → App registrations → New registration
- Add API permissions:
User.Read.AllandPresence.Read.All(both Application type) - Optionally add
Calendars.Read(Application type) for the Free After feature - Click Grant admin consent
- Go to Certificates & secrets → New client secret — copy the value immediately
- Note the Application (client) ID and Directory (tenant) ID from the Overview page
cd /opt
git clone https://github.com/denniskoch/pypillar.git
cd pypillar
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtCopy the example env file and fill in your values:
cp .env.example .env.env variables:
| Variable | Description |
|---|---|
PYPILLAR_CLIENT_ID |
Azure app registration client ID |
PYPILLAR_TENANT_ID |
Azure tenant (directory) ID |
PYPILLAR_CLIENT_SECRET |
Client secret value |
PYPILLAR_DOMAIN |
UPN domain suffix, e.g. company.com |
PYPILLAR_COMPANY_NAME |
Company name shown at the top of the display |
PYPILLAR_ALLOWED_SUBNETS |
(optional) Comma-separated CIDRs to restrict access, e.g. 10.0.0.0/8,192.168.1.0/24. Empty = allow all. |
PYPILLAR_TRUST_PROXY |
(optional) Set to 1 if behind a reverse proxy to trust X-Forwarded-For for subnet checks |
A Dockerfile and docker-compose.yml are included for containerised deployments.
cp .env.example .env # fill in your values first
docker compose up -dThis builds the image from source, binds port 8000, loads .env, and restarts automatically unless manually stopped.
To rebuild after a code change:
docker compose up -d --builduvicorn server:app --host 0.0.0.0 --port 8000Open http://localhost:8000/{username} in a browser, where {username} is the UPN prefix (e.g. jsmith for jsmith@company.com). On the Pi, point a fullscreen Chromium window at this address.
Append ?layout=h for the horizontal layout variant.
Create a systemd service at /etc/systemd/system/pypillar.service:
[Unit]
Description=pypillar nameplate
After=network-online.target
Wants=network-online.target
[Service]
User=pi
WorkingDirectory=/home/pi/pypillar
EnvironmentFile=/home/pi/pypillar/.env
ExecStart=/home/pi/pypillar/.venv/bin/uvicorn server:app --host 0.0.0.0 --port 8000
Restart=on-failure
[Install]
WantedBy=multi-user.targetsudo systemctl enable pypillar
sudo systemctl start pypillarFor a fullscreen Chromium kiosk at boot, add to /etc/xdg/lxsession/LXDE-pi/autostart:
@chromium-browser --kiosk --noerrdialogs --disable-infobars http://localhost:8000/jsmith
| Teams status | Display label | Color |
|---|---|---|
| Available | Available | Green |
| Busy | In a Meeting | Red |
| DoNotDisturb | Do Not Disturb | Red |
| BeRightBack | Be Right Back | Amber |
| Away | Away | Amber |
| Offline | Offline | Amber |
| PresenceUnknown | Offline | Amber |
| Out of Office* | Out of Office | Purple |
* OOF is detected via outOfOfficeSettings.isOutOfOffice and takes precedence over the reported availability value, so it displays correctly even when Teams reports a different underlying status (e.g. Offline with OOO left on).
When workLocation is remote, a Remote badge is appended to the label for: Available, Busy, DoNotDisturb, BeRightBack, and Away.
When presence is Busy or DoNotDisturb and Calendars.Read is granted, a Free After time is shown beneath the status label indicating the end of the current contiguous busy block.
pypillar/
├── server.py # FastAPI backend — MSAL auth, Graph polling, routing
├── requirements.txt
├── .env.example
├── Dockerfile
├── docker-compose.yml
└── static/
├── index-v.html # Portrait layout (480×1920)
├── index-h.html # Landscape layout
├── style-v.css # Portrait styles
├── style-h.css # Landscape styles
├── script.js # API polling, clock, name auto-fit
└── error.html # Shown at / when no username is provided