A Vue.js-based overlay for displaying fitness tracking data from Home Assistant on your webcam during video meetings.
This application connects to a Home Assistant instance via WebSockets and displays real-time fitness metrics (steps, speed, distance, heart rate) as a transparent overlay. It's designed to be used with OBS Studio's virtual camera feature in video conferencing apps (Zoom, Teams, Meet, etc.) so you can work out on a treadmill or exercise bike during meetings while showing colleagues you're actively engaged.
- Real-time Data: Connects to Home Assistant via WebSockets for instant fitness metrics
- Meeting-Appropriate: Visible enough to show activity, subtle enough not to distract
- Widget System: Modular display of steps, speed, distance, heart rate with color-coded zones
- Virtual Camera Ready: Designed to work as a browser source in OBS Studio's virtual camera
- Transparent Background: Renders only the overlay elements, no camera feed (use OBS to composite)
- Configurable: Easily customize connection settings via environment variables
- Node.js 18+ and pnpm
- Home Assistant instance with long-lived access token
- OBS Studio (for virtual camera functionality)
- Video conferencing app (Zoom, Teams, Meet, etc.)
-
Clone this repository:
git clone https://github.com/yourusername/fitness-tracker-overlay.git cd fitness-tracker-overlay -
Install dependencies:
pnpm install
-
Configure environment variables:
- Copy the example environment file:
cp .env.example .env
- Edit
.envand add your Home Assistant access token and other configuration:VITE_HA_TOKEN=your_long_lived_access_token_here VITE_HA_DEV_HOST=192.168.0.13 VITE_HA_DEV_PORT=8123
- Copy the example environment file:
-
Start the development server:
pnpm dev
-
Build for production:
pnpm build
This overlay is designed to be composited with your webcam feed in OBS, then output as a virtual camera to your video conferencing app.
- Add your webcam as a "Video Capture Device" source in OBS
- Add the overlay as a "Browser" source above your webcam:
- For local development: use
http://localhost:5174/(Vite dev server) - For production: host the built files on a web server or use "Local File" pointing to
dist/index.html
- For local development: use
- Set the Browser source width and height to match your OBS canvas size (e.g., 1920x1080)
- Important: Check "Enable transparency" in the Browser Source properties
- Start Virtual Camera in OBS (Tools → Start Virtual Camera)
- In your video conferencing app (Zoom/Teams/Meet), select "OBS Virtual Camera" as your camera source
Tip: If the overlay background isn't transparent, add this in the Browser source Custom CSS field:
body {
background-color: rgba(0, 0, 0, 0) !important;
margin: 0px auto;
overflow: hidden;
}| Variable | Description | Default |
|---|---|---|
VITE_HA_TOKEN |
Home Assistant long-lived access token | - |
VITE_HA_DEV_HOST |
Host IP/domain for development | 192.168.0.13 |
VITE_HA_DEV_PORT |
Dev server websocket port | 8123 |
For production builds the overlay now reads app-config.json at runtime.
Create public/app-config.json (or copy public/app-config.example.json) with:
{
"haToken": "your_long_lived_access_token_here"
}When deploying via Woodpecker the pipeline writes this file automatically using the HA_TOKEN secret.
If the file is missing, the overlay falls back to the compile-time VITE_HA_TOKEN env variable (useful for local dev).
When in development mode (pnpm dev), a Development Panel appears (press u to toggle). It allows you to:
- Mock step and heart rate data without a real Home Assistant connection
- Manually control connection state
- Toggle special overlays (Be Right Back, Heart Rate visualization, Compact steps mode)
- Monitor current metric values
This makes it easy to test and develop the overlay without needing a running treadmill or Home Assistant instance.
Which Home Assistant entities feed the overlay, and which widgets appear where, is declared in
public/overlay-config.json — a committed file deployed alongside the build (fetched relative to
the current page, so it keeps working under Home Assistant's /local/). No entity ID or widget
layout is hard-coded in src/.
{
"maxHeartRate": 185,
"heartRate": {
"anchor": "top-left",
"offset": { "x": 20, "y": 20 }
},
"entities": {
"steps": "sensor.ksmb_v1_7aed_current_step_count",
"distance": "sensor.ksmb_v1_7aed_current_distance",
"speed": "number.ksmb_v1_7aed_speed_level",
"heartRate": "sensor.galaxy_watch5_rrry_heart_rate",
"brbToggle": "input_boolean.janis_vco_brb",
"heartToggle": "input_boolean.janis_vco_heart",
"compactToggle": "input_boolean.janis_vco_compact"
},
"widgets": [
{
"id": "steps-display",
"type": "steps",
"anchor": "bottom-right",
"offset": { "x": 30, "y": 30 },
"size": { "width": 240, "height": 190 }
}
]
}entitiesmaps every Home Assistant entity the overlay reads to its entity ID. Point the overlay at a different treadmill or watch by editing these values.widgetsis a list of widgets to render. Each entry has anid, atype(steps,temperature, orsensor), ananchor(top-left,top-right,bottom-left, orbottom-right), anoffsetfrom that corner, asize, and type-specificprops(entityId,unit,displayName— not needed forsteps, which reads from the entities above). Add atemperatureorsensorentry to light up those widgets; no code change is required. Astepsentry may also setcompactSize({width, height}), used instead of the built-in 600×72 default while compact mode is on; omit it to keep that default.maxHeartRateconfigures the heart-rate zone thresholds (seeDESIGN.md).heartRatecontrols where the BPM display and its pulse-wave animation render — ananchorandoffset, same shape as a widget's. They always move together, anchored to the same corner. The full-screen pulsing screen border is unaffected and always stays inset to the viewport. IfheartRateis missing or fails validation, the overlay still renders,HeartRatefalls back totop-left,{x: 20, y: 20}, and one warning is logged to the console — unlikeentities/widgets, an invalidheartRatedoesn't take down the rest of the overlay.
If the file is missing, isn't valid JSON, or is missing/mistypes a required entities or
widgets field (including a malformed compactSize), the overlay renders nothing and logs one
error to the console rather than running with partial or stale configuration. See
docs/adr/0001-overlay-config-file.md and docs/adr/0002-heart-rate-position-and-steps-compact-size.md
for the full rationale.
These input_boolean helpers can be flipped live from Home Assistant to control the overlay without a redeploy:
| Entity | Effect |
|---|---|
input_boolean.janis_vco_brb |
Shows the "Be Right Back" overlay |
input_boolean.janis_vco_heart |
Enables the heart-rate pulse visualization |
input_boolean.janis_vco_compact |
Collapses the steps widget into a compact, header-less row |
If a helper doesn't exist in Home Assistant, its toggle simply has no effect; the overlay behaves as if it were off.
- Connection Issues: Check that your Home Assistant instance is accessible and that your token is valid.
- No Data Showing: Verify that the entity IDs in
public/overlay-config.jsonmatch your Home Assistant entity IDs. - Virtual Camera Not Appearing: Make sure OBS Virtual Camera is started (Tools → Start Virtual Camera).
- Overlay Not Transparent in OBS: Verify "Enable transparency" is checked in Browser source properties.
- Video App Can't See OBS Camera: Some apps require you to restart them after OBS Virtual Camera is started.
MIT License - See LICENSE file for details.
- Built with Vue.js and Vite
- Styling with Tailwind CSS