- JavaScript 98.3%
- VBScript 0.9%
- Batchfile 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Webex WDM started rejecting device creation with 400 "Missing Model". Send model and localizedModel, and log the response body on device list/create failures. |
||
| .gitattributes | ||
| .gitignore | ||
| config.example.json | ||
| LICENSE | ||
| presence-bridge.mjs | ||
| README.md | ||
| run-bridge.cmd | ||
| run-bridge.vbs | ||
Webex Presence + Status (Home Assistant → Webex)
One small Windows service that keeps your Webex availability and custom status in sync with Home Assistant, watches its own health, and records your presence over time. Runs hidden at logon, restarts itself if it dies. Node 18+, no npm dependencies.
Personal project, not affiliated with Cisco/Webex. It talks to some undocumented Webex endpoints (presence "apheleia" events and the Mercury websocket) that may change without notice.
Presence (availability)
| Home Assistant state | Webex availability | Webex shows |
|---|---|---|
personEntity = home (and houseModeEntity ≠ asleepState) |
active |
Active (green) |
| anything else, or house mode = asleep | dnd |
Do Not Disturb / Busy (red) |
While away/asleep the custom status is cleared too.
Status ticker (custom status)
The 75-char custom status line, chosen in priority order:
- Away/asleep → nothing (the presence half clears it).
- In a call/meeting/presenting → nothing (the YouTube Music Webex plugin clears it during calls).
- A track is playing (status starts with
🎵) → always show the info line and prepend the track:🎵 <track> · <info>. If the track is too long, it is shortened with…so the info line always survives. - Otherwise → the info line:
🏠 Springfield, IL · ⛅89° · ↑6:18a↓6:23p
Info line parts:
- City from
geoEntity(e.g. the HA companion app's geocoded location sensor — follows you when you travel). - Weather from
weatherEntity(condition emoji + temperature). - Sun from
sunEntity, rendered in the current location's timezone: the phone's own timezone sensor (tzEntity) if it reports one, else a coordinate→IANA lookup (cached), else thetimeZonefallback.
If you use a YouTube Music Webex plugin that writes 🎵 statuses, it keeps ownership of that
line; this service only fills the gaps and enhances the track line when it fits.
Watchdog
Every hour the service checks itself and raises (or clears) a Home Assistant notification:
- Home Assistant reachable,
- Webex presence API responding,
- a successful Webex write within
watchdogMaxSilenceMinutes(default 120).
Notifications go to persistent_notification and notify.<notifyService> (both configurable).
Alerts (Webex messages → lights)
A real-time Mercury websocket listener (the same transport the Webex client uses) watches your incoming messages. When someone senior messages you, it flashes the lights.
- Dynamic allowlist by job title: the sender's
title(from the Webex directory) is matched againstalertTitleRegex— by default Manager / Lead / Director / Staff Engineer / Engineer III / President (word-boundary match, so "Network Engineer II" and "Leadership Coach" do not match). - Triggers on 1:1 direct messages only by default (
alertOnDMs). SetalertOnMentions: trueto also trigger on @mentions in group spaces. - Your own messages and bots are ignored.
- Action:
lightEntitygoes fulllightColorforlightFlashSeconds, then the previous on/off/colour is restored. A globalalertCooldownSecondsprevents repeat flashing.
Titles and room types are cached for titleCacheHours. The listener registers a Webex device
named mercuryDeviceName (reused across restarts) and reconnects automatically.
Analytics
Every analyticsIntervalSeconds (default 60) the composed presence is sampled:
presence-history.jsonl— one line per state transition (t,status,category,from).presence-today.json— seconds spent in each category today; rolls over at midnight and logs a daily summary topresence-bridge.log.
Setup
-
Clone the repo somewhere permanent and install Node 18+.
-
Config: copy
config.example.jsontoconfig.jsonand fill in:haUrlandhaToken— a Home Assistant long-lived access token (HA → Profile → Security → Long-lived access tokens).- The entity IDs (
personEntity,houseModeEntity,geoEntity,tzEntity,weatherEntity,notifyService,lightEntity) to match your Home Assistant.config.jsonis git-ignored — never commit it.
-
Webex credentials: the service does not do its own OAuth login. It looks for a Webex OAuth access/refresh token (and client id/secret for refreshing) in, in order:
tokens.jsonnext to the script (its own copy, written after a refresh),- the YouTube Music desktop app's Webex plugin config (
%APPDATA%\YouTube Music\config.json,plugins.webex), ~/webex-messaging-mcp-server/.webex-tokens.jsonand that folder's.env(WEBEX_CLIENT_ID/WEBEX_CLIENT_SECRET).
The simplest route for a fresh install is to create a Webex integration at developer.webex.com, complete an OAuth flow once, and write the result to
tokens.json(access_token,refresh_token,expires_atin ms,client_id,client_secret). -
Try it in a console:
node presence-bridge.mjsand watchpresence-bridge.log. -
Run at logon: copy
run-bridge.vbsinto your Startup folder (shell:startup), and editBRIDGE_DIRin the copy to point at your clone. If Node isn't atC:\Program Files\nodejs\node.exe, editrun-bridge.cmdtoo.
How it runs
run-bridge.vbs(copied to the Startup folder) launches the supervisor hidden at logon.run-bridge.cmdis a supervisor that restartspresence-bridge.mjsif it exits.presence-bridge.mjspolls HA every 60 s (presence), 20 s (ticker), 60 s (analytics), 1 h (watchdog).
Files
| File | Purpose |
|---|---|
presence-bridge.mjs |
The service. |
config.example.json |
Template for config.json. |
config.json |
(ignored) HA URL + long-lived token, entities, and tuning. |
tokens.json |
(ignored) Created on first refresh; the service's own Webex token copy. |
presence-bridge.log |
(ignored) Append-only activity log. |
presence-history.jsonl |
(ignored) Presence transition log. |
presence-today.json |
(ignored) Per-day time-in-state totals. |
run-bridge.cmd / run-bridge.vbs |
Hidden supervisor + logon launcher. |
Tuning (config.json)
| Key | Default | Meaning |
|---|---|---|
pollSeconds |
60 | Presence poll interval. |
awayTtlSeconds |
900 | DND validity; re-asserted every poll while away. |
activeTtlSeconds / activeReassertSeconds |
3600 / 300 | Active event TTL and re-assert cadence. |
clearCustomStatusWhenAway |
true | Clear 🎵 … while away/asleep. |
tickerEnabled / tickerPollSeconds |
true / 20 | Info-line ticker on/off and cadence. |
tickerTtlSeconds / tickerReassertSeconds |
900 / 600 | Status TTL and refresh cadence. |
timeZone / tzEntity / tzCacheHours |
UTC / sensor.phone_current_time_zone / 6 |
Sun-time timezone: phone sensor → coordinate lookup → this fallback. |
geoEntity / weatherEntity / sunEntity |
sensor.phone_geocoded_location / weather.forecast_home / sun.sun |
Sources. |
personEntity / houseModeEntity / asleepState |
person.me / input_select.house_mode / Asleep |
Presence sources. |
watchdogEnabled / watchdogIntervalSeconds / watchdogMaxSilenceMinutes |
true / 3600 / 120 | Watchdog. |
notifyService / notifyPersistent |
mobile_app_phone / true |
Where alerts go. |
analyticsEnabled / analyticsIntervalSeconds |
true / 60 | Presence sampling. |
alertsEnabled / mercuryEnabled |
true / true | Webex message → lights alerts. |
alertTitleRegex |
\b(manager|lead|director|staff engineer|engineer iii|president)\b |
Titles that trigger. |
alertOnDMs / alertOnMentions |
true / false | What counts as directed at you. |
lightEntity / lightColor / lightBrightness / lightFlashSeconds |
light.office / [255,0,0] / 255 / 15 |
Flash action. |
alertCooldownSeconds / titleCacheHours |
60 / 24 | Rate limit and cache TTL. |
Testing / forcing a state
Stop the running service first (Task Manager → the node.exe running presence-bridge.mjs
and the cmd.exe supervisor), then from the repo folder:
$env:BRIDGE_FORCE='away'; node presence-bridge.mjs # forces DND
$env:BRIDGE_FORCE='home'; node presence-bridge.mjs # forces Active
(BRIDGE_FORCE only affects presence; the ticker still follows HA/Webex state.)
Disabling / removing
- Temporarily: kill the
presence-bridge.mjsnode process and thecmd.exesupervisor. - Permanently: delete your copy of
run-bridge.vbsfrom the Startup folder.
Notes / caveats
- HA token: use a dedicated long-lived token so you can revoke it independently (HA → Profile → Security).
- Webex token: if the service refreshes, it saves to
tokens.jsonand mirrors into the MCP server's token file. Webex may rotate refresh tokens, so an independent refresh could require other apps sharing the same token (e.g. the YouTube Music plugin) to reconnect; it only refreshes when no valid access token exists. - Manual overrides: while home, a DND you set by hand is cleared within ~5 min.
- If
personEntityisunknown/unavailable, presence is left unchanged rather than flipping to DND. - Sun times use
sun.sun's next rise/set; late in the day those roll to tomorrow's events. - Startup launchers must keep Windows (CRLF) line endings — LF-only
.cmdfiles silently fail..gitattributesenforces this on checkout.
License
The Do-Not-Disturb License — MIT with jokes. Do what you like; beverages appreciated.