{"_id":"@caseywebb/homebridge-free-sleep","_rev":"4-45d81f403f9860d692e33d095260e9b2","name":"@caseywebb/homebridge-free-sleep","dist-tags":{"latest":"0.3.1"},"versions":{"0.1.0":{"name":"@caseywebb/homebridge-free-sleep","version":"0.1.0","keywords":["homebridge-plugin","supports-hap","eight-sleep","free-sleep","homekit","thermostat","local"],"license":"MIT","_id":"@caseywebb/homebridge-free-sleep@0.1.0","maintainers":[{"name":"caseywebb","email":"notcaseywebb@gmail.com"}],"homepage":"https://github.com/caseyWebb/homebridge-free-sleep#readme","bugs":{"url":"https://github.com/caseyWebb/homebridge-free-sleep/issues"},"dist":{"shasum":"c2b5e80b1c83f128ed6d840631a65d9af2b8ceb3","tarball":"https://registry.npmjs.org/@caseywebb/homebridge-free-sleep/-/homebridge-free-sleep-0.1.0.tgz","fileCount":45,"integrity":"sha512-H/E9zEvaXu+FjulO2Wnt6Joljk4wmcoiEE4hgL5syBe6gOm+TSCtSdxgs/uvJ2PurFxgCn5GAm57ONvIil64MQ==","signatures":[{"sig":"MEUCIFskxIN9c2jK9Cg4UOqJeMkDrQW3bW3UEFrFG9qRh8i3AiEAlI40RlWLbNoSfYyVCjRQTJAgq+qv26xXvDFztxARhQY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":312216},"main":"dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":"^22 || ^24 || ^26","homebridge":"^2.0.0"},"scripts":{"lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","smoke":"node --experimental-strip-types scripts/smoke.ts","watch":"tsc -p tsconfig.build.json --watch","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"caseywebb","email":"notcaseywebb@gmail.com"},"repository":{"url":"git+https://github.com/caseyWebb/homebridge-free-sleep.git","type":"git"},"_npmVersion":"11.19.0","description":"Control an Eight Sleep Pod running free-sleep from Apple Home, locally and without the Eight Sleep cloud.","directories":{},"displayName":"Free Sleep","_nodeVersion":"26.8.1","dependencies":{"zod":"^4.5.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^10.10.0","vitest":"^3.2.0","@eslint/js":"^10.0.1","homebridge":"^2.4.0","typescript":"^5.9.0","@types/node":"^22.10.0","typescript-eslint":"^8.69.0","@homebridge/hap-nodejs":"2.2.2"},"_npmOperationalInternal":{"tmp":"tmp/homebridge-free-sleep_0.1.0_1788741474869_0.5716029902590487","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@caseywebb/homebridge-free-sleep","version":"0.2.0","keywords":["homebridge-plugin","supports-hap","eight-sleep","free-sleep","homekit","thermostat","local"],"license":"MIT","_id":"@caseywebb/homebridge-free-sleep@0.2.0","maintainers":[{"name":"caseywebb","email":"notcaseywebb@gmail.com"}],"homepage":"https://github.com/caseyWebb/homebridge-free-sleep#readme","bugs":{"url":"https://github.com/caseyWebb/homebridge-free-sleep/issues"},"dist":{"shasum":"92f6d766da38efb8321ebce072a3d2373b70afef","tarball":"https://registry.npmjs.org/@caseywebb/homebridge-free-sleep/-/homebridge-free-sleep-0.2.0.tgz","fileCount":51,"integrity":"sha512-AfxcjWQOA07Qz5s11UxnG2bonpHDFBPaxrGjMi72XAt5XIA1JlN1jvOHpFKSjuK0MnQ66iKw4NmX4ddXEqlDqg==","signatures":[{"sig":"MEUCIQD8n9sv+Ih1G4L83/NnyqPmIk/nv4yVhWL/cPhmEONPSgIgQ8AuqDdPMjWMAViBGvuCnqrlvibvMpzAjWC4/E54XVw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCK3BMiCV4D80w5yooNpij94d8/AMRx8FAbdF3/cdKvUwIgUjQrMDMmHZ4Osp0CP2VV7k+iNhocTL4s37mgHU6fH2Q=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":385641},"main":"dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":"^22 || ^24 || ^26","homebridge":"^2.0.0"},"scripts":{"lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","smoke":"node --experimental-strip-types scripts/smoke.ts","watch":"tsc -p tsconfig.build.json --watch","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"caseywebb","email":"notcaseywebb@gmail.com"},"repository":{"url":"git+https://github.com/caseyWebb/homebridge-free-sleep.git","type":"git"},"_npmVersion":"11.19.0","description":"Control an Eight Sleep Pod running free-sleep from Apple Home, locally and without the Eight Sleep cloud.","directories":{},"displayName":"Free Sleep","_nodeVersion":"26.8.1","dependencies":{"zod":"^4.5.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^10.10.0","vitest":"^3.2.0","@eslint/js":"^10.0.1","homebridge":"^2.4.0","typescript":"^5.9.0","@types/node":"^22.10.0","typescript-eslint":"^8.69.0","@homebridge/hap-nodejs":"2.2.2"},"_npmOperationalInternal":{"tmp":"tmp/homebridge-free-sleep_0.2.0_1788751733873_0.7600333653918123","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@caseywebb/homebridge-free-sleep","version":"0.3.0","keywords":["homebridge-plugin","supports-hap","eight-sleep","free-sleep","homekit","thermostat","local"],"license":"MIT","_id":"@caseywebb/homebridge-free-sleep@0.3.0","maintainers":[{"name":"caseywebb","email":"notcaseywebb@gmail.com"}],"homepage":"https://github.com/caseyWebb/homebridge-free-sleep#readme","bugs":{"url":"https://github.com/caseyWebb/homebridge-free-sleep/issues"},"dist":{"shasum":"55c9c03a94a6d67d35c5e74e72a10b0a99d66efe","tarball":"https://registry.npmjs.org/@caseywebb/homebridge-free-sleep/-/homebridge-free-sleep-0.3.0.tgz","fileCount":84,"integrity":"sha512-iW+WqZ7vAQQRn5okhiy+5zrRjRbiZRUXUKYYmfefasqc2S1nsBb3pHr43iey3iRb5hEshs6viQlCGziLC2/7qw==","signatures":[{"sig":"MEYCIQC85M+80Gd3oAoHz4aFd3I1ReXGCYJ3fnQ1cbRQJz5DZgIhAMDljYeoJlIdsfLmS0OW3yAUZQO2NBLaijb+MUKc3PRT","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIGbW5S/JTQvSwIYegqP9mgeHRH+huLB5V4yeTESHtIZuAiEAsu5rj0sCJcRrVcKnwEduNVt8yY2Jp48VTNm3rq6H/GU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":764898},"main":"dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":"^22 || ^24 || ^26","homebridge":"^2.0.0"},"scripts":{"lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","smoke":"node --experimental-strip-types scripts/smoke.ts","watch":"tsc -p tsconfig.build.json --watch","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"caseywebb","email":"notcaseywebb@gmail.com"},"repository":{"url":"git+https://github.com/caseyWebb/homebridge-free-sleep.git","type":"git"},"_npmVersion":"11.19.0","description":"Control an Eight Sleep Pod running free-sleep from Apple Home, locally and without the Eight Sleep cloud.","directories":{},"displayName":"Free Sleep","_nodeVersion":"26.8.1","dependencies":{"zod":"^4.5.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^10.10.0","vitest":"^3.2.0","@eslint/js":"^10.0.1","homebridge":"^2.4.0","typescript":"^5.9.0","@types/node":"^22.10.0","typescript-eslint":"^8.69.0","@homebridge/hap-nodejs":"2.2.2"},"_npmOperationalInternal":{"tmp":"tmp/homebridge-free-sleep_0.3.0_1788802520193_0.9775920369702378","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"_id":"@caseywebb/homebridge-free-sleep@0.3.1","bugs":{"url":"https://github.com/caseyWebb/homebridge-free-sleep/issues"},"dist":{"shasum":"abc9a2022fc1e9e76172484e3504b2a483b82ae5","tarball":"https://registry.npmjs.org/@caseywebb/homebridge-free-sleep/-/homebridge-free-sleep-0.3.1.tgz","fileCount":87,"integrity":"sha512-07Sbc+A7jn7tTG0aonUfRZAgTHHCjiag91Gpo/krZmotN/E+IdRiYKtWISTBlxzLT0eh+9Fc/rk44YfbeVGsNA==","signatures":[{"sig":"MEUCIQCRpnyyHWfBZ5kZFmbweQH4UvKrHdfjD7GHwqBkxClKjAIgBKm93Yi9zchxTNsq4t7RAOBHr78RYdJgDkM2oDt57as=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDgJLO1AxcROimuIQQebyeo5MKU1MeZ8zt3/JQjzFOMFAIgdUdCwM7jxJiFVDFJzRLfsWN77EPjruDGh4Cesi3EwP4="}],"unpackedSize":781005},"main":"dist/index.js","name":"@caseywebb/homebridge-free-sleep","type":"module","types":"./dist/index.d.ts","engines":{"node":"^22 || ^24 || ^26","homebridge":"^2.0.0"},"license":"MIT","scripts":{"lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","smoke":"node --experimental-strip-types scripts/smoke.ts","watch":"tsc -p tsconfig.build.json --watch","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build"},"version":"0.3.1","_npmUser":{"name":"caseywebb","email":"notcaseywebb@gmail.com"},"homepage":"https://github.com/caseyWebb/homebridge-free-sleep#readme","keywords":["homebridge-plugin","supports-hap","eight-sleep","free-sleep","homekit","thermostat","local"],"repository":{"url":"git+https://github.com/caseyWebb/homebridge-free-sleep.git","type":"git"},"_npmVersion":"11.19.0","description":"Control an Eight Sleep Pod running free-sleep from Apple Home, locally and without the Eight Sleep cloud.","directories":{},"displayName":"Free Sleep","maintainers":[{"name":"caseywebb","email":"notcaseywebb@gmail.com"}],"_nodeVersion":"26.8.1","dependencies":{"zod":"^4.5.4"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^10.10.0","vitest":"^3.2.0","@eslint/js":"^10.0.1","homebridge":"^2.4.0","typescript":"^5.9.0","@types/node":"^22.10.0","typescript-eslint":"^8.69.0","@homebridge/hap-nodejs":"2.2.2"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/homebridge-free-sleep_0.3.1_1788809134597_0.4383365844606675"}}},"time":{"created":"2026-09-07T00:37:54.772Z","modified":"2026-09-07T19:25:34.850Z","0.1.0":"2026-09-07T00:37:55.040Z","0.2.0":"2026-09-07T03:28:53.961Z","0.3.0":"2026-09-07T17:35:20.290Z","0.3.1":"2026-09-07T19:25:34.692Z"},"bugs":{"url":"https://github.com/caseyWebb/homebridge-free-sleep/issues"},"license":"MIT","homepage":"https://github.com/caseyWebb/homebridge-free-sleep#readme","keywords":["homebridge-plugin","supports-hap","eight-sleep","free-sleep","homekit","thermostat","local"],"repository":{"url":"git+https://github.com/caseyWebb/homebridge-free-sleep.git","type":"git"},"description":"Control an Eight Sleep Pod running free-sleep from Apple Home, locally and without the Eight Sleep cloud.","maintainers":[{"name":"caseywebb","email":"notcaseywebb@gmail.com"}],"readme":"# homebridge-free-sleep\n\nControl an Eight Sleep Pod from Apple Home — **locally**, with no Eight Sleep account and no\ncloud.\n\n## What this is\n\n[free-sleep](https://github.com/throwaway31265/free-sleep) replaces the firmware on an Eight\nSleep Pod with an open-source server that runs on the Pod itself and exposes a local REST\nAPI. This plugin bridges that API into HomeKit.\n\nEvery other Eight Sleep HomeKit plugin authenticates against Eight Sleep's **cloud** with\nyour account credentials. That is exactly what free-sleep exists to avoid, and it does not\nwork at all on a Pod that has been firewalled off from the internet — which is the\nrecommended free-sleep setup. This plugin talks only to your Pod, on your LAN.\n\n## Status: 0.3.0 published, M1–M4 complete — M5 (release) in progress\n\nThe full feature set is implemented and shipped to npm. Every side of the bed is a HomeKit\n**Thermostat** (Off/Auto, 55–110°F) driven from a live cache of the Pod's status, plus:\n\n- **Alarm events** — a per-side \"Alarm\" event (fires \"When … is pressed\" in the Home app's\n  automation picker) and a \"Dismiss Alarm\" switch, backed by a scheduler that briefly polls the\n  Pod faster around each predicted alarm instant so the short-lived vibration event is not missed\n  (`alarmEvents`, on by default).\n- **Away Mode** and **Skip Next Alarm** switches per side (`awayModeSwitch`/`skipAlarmSwitch`,\n  both on by default), debounced and rate-limited against the Pod's expensive settings write.\n- **Occupancy sensor** per side, opt-in via `occupancySource: 'presence' | 'vitals'` (default\n  `'none'` — zero extra polling until you opt in), with proof-of-life `StatusActive` semantics so\n  a dead detection stream can never masquerade as \"Not Occupied\".\n- **Hub accessory**: a \"Pod Connection\" contact sensor that reports reachability and handles the\n  Pod's daily reboot gracefully (last-known values served through the outage; nothing throws \"No\n  Response\" for a routine restart), an always-published \"Water Level\" sensor (contact or leak\n  style, `waterLowSensorType`), and four opt-in extras each gated by its own config key —\n  \"Prime\" (a switch), \"LED\" (a lightbulb), per-side \"Test Alarm\" switches, and a \"Server Fault\"\n  sensor.\n- **(Unreleased)** Every HAP service on every accessory now carries its own distinct tile label\n  in the Home app instead of a shared accessory-name fallback — see the ConfiguredName section\n  of [docs/HOMEKIT.md](docs/HOMEKIT.md) and the `[Unreleased]` entry in\n  [CHANGELOG.md](CHANGELOG.md).\n\nImplemented and tested against **free-sleep v2.1.5**. See\n[docs/ROADMAP.md](docs/ROADMAP.md) for the milestone history and what remains for M5, and\n[CHANGELOG.md](CHANGELOG.md) for what changed in each release.\n\n### Honesty caveat — read this before installing\n\nThis plugin has been extensively tested against a behavioral mock of the Pod's API and against\nreal API responses captured from a physical Pod 3. Core thermostat control has been paired and\nverified end-to-end against a real Home app (the archived `thermostat-and-offline` change,\n[#9](https://github.com/caseyWebb/homebridge-free-sleep/issues/9)/[#11](https://github.com/caseyWebb/homebridge-free-sleep/issues/11)).\n\n**Everything shipped since then is implemented and covered by the test suite, but the specific\nreal-hardware confirmations below are still open** — tracked in\n[#36](https://github.com/caseyWebb/homebridge-free-sleep/issues/36):\n\n- **Offline handling**: a free-sleep restart on the Pod flips the connection sensor exactly\n  twice; the same across the Pod's real daily reboot; whether a long (>10 min) outage's \"No\n  Response\" escalation recovers promptly on reconnection or latches until the Home app is\n  force-quit (this decides whether `noResponseAfterMs` should default to `0`); whether\n  `StatusFault`/`StatusActive` surface anywhere in the Home app or only in Eve-class clients.\n- **Slider drag**: a full-range (55→110°F) continuous drag on the real device never visibly\n  snaps back mid-drag or after release (CI proves this against a mock with a virtual clock; this\n  is the real-world confirmation).\n- **Occupancy**: presence/vitals transitions flip the sensor within the expected window and\n  `StatusActive` latches correctly on first real detection; the sensor stays inactive-but-quiet\n  when biometrics is disabled on the Pod.\n- **Hub extras**: a real prime cycle tracks and self-corrects on the tile; LED brightness slider\n  feel (~5s settle is expected); each per-side Test Alarm fires only its own side; water-low\n  reads correctly against the actual tank state; the server-fault sensor stays quiet in normal\n  operation.\n- **Alarm timing**: a real scheduled alarm produces a single press within ~3s of vibration start\n  and Dismiss stops it; the documented DST caveat (our skipped-wall-clock resolution differs from\n  `moment-timezone`'s by one hour for an alarm scheduled inside the transition hour — pinned by\n  regression tests, at most one shifted prediction per transition).\n- **Settings switches**: Away Mode toggled from the Home app agrees with the Pod's own UI, and\n  with `awayModeTurnsSideOff` the side actually powers down first; Skip Next Alarm suppresses a\n  real alarm and drops back to off afterward, and is refused in the dead window between a fired\n  alarm and the following noon.\n- **Service ConfiguredName** (unreleased, #49): every tile shows a distinct label rather than\n  the accessory-name fallback, and a Home-app rename survives a plugin restart — traced through\n  the installed HAP-NodeJS/Homebridge source, but confirmation on a real paired install is a\n  post-merge follow-up, not a blocker.\n\nNone of the above are known bugs — they are real-hardware confirmations of behavior already\ncovered by the automated test suite against a behavioral mock. Treat the plugin as **stable but\nnot yet fully hardware-confirmed** until #36 closes.\n\n## Requirements\n\n- An Eight Sleep Pod running [free-sleep](https://github.com/throwaway31265/free-sleep)\n  (tested against Pod 3 / free-sleep 2.1.5; Pod 1 and 2 are not supported by free-sleep).\n- Node >=22 and Homebridge 2.x, on **separate always-on hardware** — a NAS, a Mac mini, a Pi.\n  Not the Pod: it reboots daily and is memory-constrained.\n- The Pod reachable on your LAN. A **static DHCP reservation for the Pod is strongly\n  recommended** — this plugin identifies the Pod by the configured hostname/IP, and an address\n  that changes underneath it will look like a permanent outage.\n\n## Install\n\n```sh\nnpm install -g @caseywebb/homebridge-free-sleep\n```\n\nOr, from the Homebridge UI's **Plugins** search box, type the exact scoped package name:\n\n```\n@caseywebb/homebridge-free-sleep\n```\n\nThe UI matches a scoped package name (`@scope/homebridge-*`) directly against the npm registry,\nbypassing its own search index entirely — so this works immediately after publish. Generic\nterms like \"free sleep\" or \"eight sleep\" will **not** find it yet: that is npm's own\nsearch-index lag catching up to a new package (typically hours, no fixed SLA), not something a\n`keywords`/metadata change can fix. If you've already typed a generic term with no luck, try\nthe exact name above instead of waiting.\n\n## Configuration\n\nMinimal config — everything but `host` is optional:\n\n```json\n{\n  \"platforms\": [\n    {\n      \"platform\": \"FreeSleep\",\n      \"host\": \"192.168.1.50\"\n    }\n  ]\n}\n```\n\n`sides` lets you publish only one side if the other is unused:\n\n```json\n{\n  \"platforms\": [\n    {\n      \"platform\": \"FreeSleep\",\n      \"host\": \"192.168.1.50\",\n      \"sides\": \"left\"\n    }\n  ]\n}\n```\n\n### Config keys\n\n| Key | Type | Default | Notes |\n|---|---|---|---|\n| `host` | string | *(required)* | Pod's LAN hostname or IP. Normalized to lowercase. |\n| `sides` | `'both'` \\| `'left'` \\| `'right'` | `'both'` | Which side accessories to publish. |\n| `pollIntervals.*` | object | see `config.schema.json` | Advanced poll/write timing overrides (base/fast/slow poll intervals, backoff ceiling, write debounce, plus `deviceWriteDebounceMs` below). Most installs should leave these alone. |\n| `pollIntervals.deviceWriteDebounceMs` | number (ms) | `500` | Write debounce for the device-wide lane specifically (currently only the LED lightbulb writes on it), independent of the side lanes' own write debounce. Minimum `500`. |\n| `pollIntervals.alarmPollIntervalMs` | number (ms) | `3000` | The `deviceStatus` polling interval used while a fast-poll window is active around a predicted alarm instant (see `alarmEvents` below). Minimum `3000`. |\n| `writeSettleMs` | number (ms) | `15000` | How long a write's optimistic value is protected from being overwritten by an in-flight poll. |\n| `noResponseAfterMs` | number (ms) | `600000` | How long the Pod must be unreachable before thermostat reads start throwing instead of serving last-known values. `0` disables escalation. |\n| `occupancySource` | `'none'` \\| `'presence'` \\| `'vitals'` | `'none'` | Publishes an `OccupancySensor` per enabled side, driven by the chosen source. `'none'` (default) publishes no sensor at all. Both sources require biometrics enabled on the Pod; `StatusActive` reports inactive until the source has proven itself live this launch (a real presence transition, or any vitals row ever), so the sensor is never confidently, permanently wrong. Disabling biometrics on the Pod itself takes up to `pollIntervals.slowPollIntervalMs` (~5 minutes by default) to take effect here, since that's how often this plugin re-checks it. See `docs/HOMEKIT.md`'s \"Occupancy\" section for the trust-signal detail per source. |\n| `waterLowSensorType` | `'contact'` \\| `'leak'` | `'contact'` | Chooses which HomeKit service type represents the hub's water-low sensor — a `ContactSensor` (default) or a `LeakSensor`. The sensor itself is always published, regardless of this setting or any other config key. |\n| `primeSwitch` | boolean | `false` | Publishes a \"Pod Prime\" switch on the hub. Turning it on starts a prime cycle; the Pod has no stop command, so turning it off is refused (surfaces as \"not allowed\" in the Home app) rather than sent. |\n| `ledLightbulb` | boolean | `false` | Publishes a \"Pod LED\" lightbulb (on/off + brightness) on the hub. Every write re-posts the Pod's full device-settings object (`v`/`gainLeft`/`gainRight`/`ledBrightness`), debounced by `pollIntervals.deviceWriteDebounceMs` (default 500ms) — the plugin re-reads the Pod's device status immediately before dispatch so the `gainLeft`/`gainRight` it carries along are as fresh as possible, not whatever was cached when the Home app write started. **Trade-off:** unlike the thermostat's target temperature, an LED write has no optimistic feedback — after debouncing, the slider can take up to roughly the fast-poll interval (default ~5s) to visibly settle at the written value in the Home app. This reads as \"a little slow,\" not stuck or reverted: nothing pushes a disagreeing value in the interim. A small residual race remains even with the pre-dispatch refresh: a gain changed externally in the narrow window between that refresh completing and the write actually landing is still clobbered — bounded by one request's round trip, not a full ~30s poll interval. |\n| `testAlarmSwitch` | boolean | `false` | Publishes two momentary switches on the hub, \"Test Alarm Left\" and \"Test Alarm Right\" — one boolean gates both. Turning either on triggers the Pod's alarm vibration on that side only, immediately, overriding away mode and power state; each tile self-resets to off after about a second regardless of outcome, while the physical vibration itself lasts at least 10 real seconds. (Split into two per-side switches rather than one both-sides switch — a hub-level trigger firing on both sides risked vibrating a sleeping partner's side as a side effect of testing the other.) |\n| `serverFaultSensor` | boolean | `false` | Publishes a \"Pod Server Fault\" sensor on the hub, reflecting the Pod's own self-reported subsystem health (`GET /api/serverStatus`). Polled on the slow cadence, and only while this is enabled — the endpoint is not free (a real SQLite round-trip on every call upstream). |\n| `alarmEvents` | boolean | `true` | Publishes each side's alarm-press `StatelessProgrammableSwitch` (\"When … is pressed\" in the Home app's automation picker) and its \"Dismiss Alarm\" switch, and runs the scheduler that briefly polls the Pod faster (`pollIntervals.alarmPollIntervalMs`) around each predicted alarm instant so the short-lived (as little as 10s) vibration event is not missed by the base ~30s poll. On by default — the extra polling only ever runs near an actually-enabled alarm and has zero effect on an alarm-free Pod. Set to `false` to opt out entirely (no services, no scheduler, no extra polling). |\n| `keepAlive` | boolean | `true` | While a side is on, periodically re-posts its remaining time so the Pod's 12-hour `isOn` duration never silently expires. `false` disables the component entirely — no timer, no writes. |\n| `keepAliveMs` | number (ms) | `43200000` (12h) | The duration re-posted as a side's remaining time when it is re-armed, matching the Pod's own 12-hour duration. |\n| `keepAliveThresholdMs` | number (ms) | `1800000` (30min) | A side is re-armed once its remaining time drops below this. Must be strictly less than `keepAliveMs`; the config UI cannot enforce that, so an invalid combination fails loudly at Homebridge startup instead. Minimum `120000` (2min) — below that, the plugin's own internally-derived check cadence can no longer guarantee it catches every side before it expires. |\n| `awayModeWritePolicy` | `'mirror'` \\| `'block'` | `'mirror'` | Governs a write to one side while either side has away mode on (the Pod itself always applies such a write to both sides). `'mirror'` (default) lets the write through and issues a second real `POST /api/deviceStatus` to the other side, updating its cached state to match, so HomeKit shows the truth immediately. `'block'` refuses the write before it reaches the Pod, surfacing \"not allowed\" in the Home app instead. Either way, an away-mode change made outside this plugin (e.g. free-sleep's own web UI) is only detected on the next settings poll (default every 300s), not sooner. |\n| `awayModeSwitch` | boolean | `true` | Publishes a per-side \"Away Mode\" switch, bound to `settings.{side}.awayMode` — pauses that side's schedules and alarms, the same field free-sleep's own web UI toggles. Writes are debounced locally (at least 2s) and rate-limited to at most one settings write per side per 10s window. |\n| `skipAlarmSwitch` | boolean | `true` | Publishes a per-side \"Skip Next Alarm\" switch. Turning it on computes that side's next scheduled alarm occurrence (the alarm scheduled for the current *sleep day* — the calendar day, in the side's time zone, that began 12 hours ago — applied to today's date if it's before noon, otherwise tomorrow's, plus 2 minutes) and writes `scheduleOverrides.alarm.expiresAt` so free-sleep's own recurring alarm job skips it; turning it off clears the override. Self-clears (reads back off) once the computed time passes, with no write, and also proactively pushes to HomeKit at that exact instant, or when a poll observes the override changed by some other means (e.g. free-sleep's own web UI) — neither push needs a read. Writes are debounced locally (at least 2s) and rate-limited to at most one settings write per side per 10s window, same as Away Mode. Turning the switch on between an alarm ringing and the following noon — a \"dead window\" where the noon rule would otherwise target that already-elapsed alarm — is refused instead of writing an override that would skip nothing. |\n| `awayModeTurnsSideOff` | boolean | `false` | When `awayModeSwitch` is enabled, turning a side's Away Mode switch on first turns that side off (confirmed), then enables away mode, as two sequenced writes — the reverse order would let the Pod's own both-sides mirroring turn the *other* side off too, which enabling away mode alone should not do. Turning Away Mode off never touches power, regardless of this setting. If the power-off pre-step is refused because the partner side is already away (`awayModeWritePolicy: 'block'`), the toggle still proceeds straight to the `awayMode: true` write — the Pod applies a settings write to both sides whenever either is away anyway, so aborting here would make Away Mode itself unreachable while the partner stays away. Separately, under the default `'mirror'` policy: enabling Away Mode with the partner side already away also turns the partner's side off, an unavoidable side effect of the Pod's own both-sides mirroring (`awayModeWritePolicy` above) — not something this option can prevent. |\n\nEvery key above is live and consumed — none are placeholders. An unrecognized top-level key\n(almost always a typo) is logged as a warning rather than silently ignored.\n\n## Security\n\nfree-sleep's API has **no authentication**. Anything on your LAN can control the bed. This\nplugin does not change that — do not expose port 3000 to the internet.\n\n## Documentation\n\n- [docs/POD-API.md](docs/POD-API.md) — source-verified notes on free-sleep's API and the\n  constraints that shape this plugin.\n- [docs/HOMEKIT.md](docs/HOMEKIT.md) — HAP service/characteristic modeling decisions and the\n  HomeKit-side gotchas they work around.\n- [docs/ROADMAP.md](docs/ROADMAP.md) — milestones, what's not built yet, and non-goals.\n- [CHANGELOG.md](CHANGELOG.md) — what changed in each release.\n- `openspec/` — spec-driven change proposals. `openspec list` to see what is in flight.\n\n## Licence\n\nMIT\n","readmeFilename":"README.md"}