Skip to content

Add camera, Video Doorbell, cook doorbell, probe sensors, and cook-mode switches - #2

Merged
keithah merged 22 commits into
mainfrom
feature/expanded-homekit-features
Jul 10, 2026
Merged

Add camera, Video Doorbell, cook doorbell, probe sensors, and cook-mode switches#2
keithah merged 22 commits into
mainfrom
feature/expanded-homekit-features

Conversation

@keithah

@keithah keithah commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Adds five opt-in HomeKit features (all default off — no behavior change for existing installs), driven from the Config UI. Protocol details for each were confirmed against a real oven during development.

Features

  • Interior camera — serves the oven's native interior still feed (10011, ~1 fps, 640×480 JPEG). Snapshot works with no extra setup; tap-to-view live streaming spawns system ffmpeg (configurable path) when present and degrades gracefully otherwise.
  • Video Doorbell — enable the camera and doorbell together and the "cook done" notification carries a snapshot of your food to phone + Apple TV.
  • Cook doorbell (plain, if camera is off) — rings on configurable done / ready triggers. (doorOpen toggle exists but is a no-op — no confirmed oven signal.)
  • Food-probe temperature sensors — left/right meat-probe temps from sensor_data.probe[] as HomeKit Temperature Sensors, for "notify at X°F" automations.
  • Cook-mode switches — user-defined list (label + primitive_type + temp); any mode the oven accepts. Mutually exclusive.

Plus a README "Not exposed (and why)" section for the intentionally-omitted timer and cook-progress %.

Verified live against a real oven

  • startMode / cancel command round-trips → ack success
  • Camera snapshot path through compiled code: live 10011latestSnapshot → HTTP 200 → valid JPEG
  • 10011 cadence/format (1 fps still JPEG) and probe telemetry format (sensor_data.probe[]) both captured and matched by the parsers
  • ffmpeg still-sequence → H.264 synthesis (the doorbell video path)

Known limitations

  • HomeKit SRTP live streaming is build-verified only — confirming the encrypted tap-to-fullscreen video needs a real Home hub + iOS device. Snapshots + notifications (the primary use) are verified.
  • Camera is published as a bridged accessory; if it doesn't surface, publishing as an external accessory may be needed.
  • Door-open doorbell trigger is a no-op pending a confirmed signal.

Design + plan: docs/superpowers/specs/2026-07-08-june-expanded-homekit-features-design.md, docs/superpowers/plans/2026-07-08-expanded-homekit-features.md. Lint clean; 16 tests passing.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added opt-in per-oven HomeKit capabilities: configurable cook doorbell triggers, interior camera (snapshot + live view), food-probe temperature sensors, and cook-mode start/cancel switches for multiple modes.
    • Extended configuration and the setup UI to manage these new feature options and names.
  • Bug Fixes
    • Improved mode-switch behavior: enforces mutually exclusive cooking and reverts switch state when start/cancel isn’t confirmed.
  • Documentation
    • Updated the feature list and added expanded plan/design documentation, including what is intentionally not exposed.
  • Tests
    • Added/updated tests for config normalization and probe/camera telemetry parsing.
  • Chores
    • Bumped version to 0.2.0-beta.

keithah and others added 12 commits July 8, 2026 12:57
… modes)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spike A solved via live capture: 10011 delivers a pre-signed still-JPEG URL
(~1fps during a cook) on our own trusted WebSocket, no emulator needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Camera backed by the oven's ~1fps 10011 still feed. Snapshot handler serves
the latest pre-signed JPEG (verified live end-to-end: 10011 -> latestSnapshot
-> HTTP 200 valid JPEG). Streaming spawns system ffmpeg per config.camera.ffmpegPath;
SRTP path is build-verified only (needs a Home hub to confirm). Camera + doorbell
combine into a Video Doorbell.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Confirmed live with probe in a water bowl: 10013 sensor_data.probe is an
array of {id:'left'|'right', value:<milliC>}, not the guessed *_probe fields.
Spike B resolved. Cavity 142F vs water probe 65F verified distinct.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 8, 2026 21:30
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@tenki-reviewer

tenki-reviewer Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Complete

Files Reviewed: 13
Findings: 9

By Severity:

  • 🟠 High: 3
  • 🟡 Medium: 6

This PR adds expanded HomeKit features (camera streaming, doorbell, mode switch, probe sensors) with significant issues including resource leaks, data cross-contamination, and a security vulnerability.

Files Reviewed (13 files)
README.md
config.schema.json
docs/superpowers/plans/2026-07-08-expanded-homekit-features.md
docs/superpowers/specs/2026-07-08-june-expanded-homekit-features-design.md
src/accessories/camera.ts
src/accessories/doorbell.ts
src/accessories/mode-switch.ts
src/accessories/probe-sensor.ts
src/june-client.test.ts
src/june-client.ts
src/platform.ts
src/protocol.test.ts
src/protocol.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds opt-in HomeKit support for a cook doorbell, interior camera, food-probe temperature sensors, and cook-mode switches. It updates configuration normalization, the Homebridge UI/schema, client telemetry parsing, platform discovery, tests, documentation, and the package version.

Changes

Expanded HomeKit accessories

Layer / File(s) Summary
Config contracts and normalization
src/protocol.ts, src/protocol.test.ts, config.schema.json, homebridge-ui/public/index.html
Adds typed configuration, defaults, mode filtering, schema fields, UI controls, and normalization tests for doorbell, camera, probe sensors, and cook modes.
Telemetry and command support
src/june-client.ts, src/june-client.test.ts
Parses probe telemetry and camera frames, stores the latest snapshot, exposes mode-start commands, and tests the new helpers.
Doorbell, probe, and mode accessories
src/accessories/doorbell.ts, src/accessories/probe-sensor.ts, src/accessories/mode-switch.ts
Adds telemetry-driven doorbell and probe services plus mutually exclusive cook-mode switches with start/cancel behavior.
Camera streaming accessory
src/accessories/camera.ts
Adds snapshot fallback, HomeKit stream negotiation, ffmpeg-based RTP streaming, session cleanup, and controller attachment.
Platform registration and compatibility
src/platform.ts, src/platform.test.ts
Registers new accessory kinds from configuration and removes legacy plural probe accessories.
Documentation and release metadata
docs/superpowers/..., README.md, package.json
Adds implementation/design documentation, updates feature and exclusion text, and bumps the version.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant June WebSocket
  participant JuneClient
  participant Homebridge Platform
  participant HomeKit
  participant ffmpeg

  June WebSocket->>JuneClient: Send probe and camera messages
  JuneClient->>JuneClient: Parse telemetry and store latest snapshot
  Homebridge Platform->>JuneClient: Discover configured accessories
  Homebridge Platform->>HomeKit: Register doorbell, probe, mode, and camera services
  HomeKit->>JuneClient: Start configured cook mode
  HomeKit->>Homebridge Platform: Request camera snapshot or stream
  Homebridge Platform->>ffmpeg: Convert JPEG frames to H.264 RTP
  ffmpeg-->>HomeKit: Deliver camera stream
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main new HomeKit features introduced in this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/expanded-homekit-features

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (2)
src/june-client.test.ts (1)

43-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

startMode test only verifies method existence.

This test asserts typeof client.startMode === 'function', which TypeScript already guarantees at compile time. Consider testing that it sends the correct command (e.g., by spying on sendCommand or verifying lastCancelled is reset), or remove the test if mocking the WebSocket is too costly for the value provided.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/june-client.test.ts` around lines 43 - 49, The startMode test only checks
that JuneClient.startMode exists, which is redundant and not behavior-focused.
Update the test in the startMode describe block to assert actual behavior by
exercising JuneClient.startMode and verifying it calls sendCommand with the
expected command or resets lastCancelled as intended; if that setup is too
heavy, remove the test rather than keeping a compile-time existence check.
src/june-client.ts (1)

118-126: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

startMode duplicates preheat logic.

Both methods reset lastCancelled and send MC_PREHEAT with the same payload shape. startMode could delegate to preheat to avoid drift:

♻️ Proposed refactor
-  public async startMode(primitiveType: string, tempF: number): Promise<string | null> {
-    this.lastCancelled = false;
-    return this.sendCommand(MC_PREHEAT, { primitive_type: primitiveType, temperature_cavity: fahrenheitToMilliC(tempF) });
-  }
+  public async startMode(primitiveType: string, tempF: number): Promise<string | null> {
+    return this.preheat(primitiveType, tempF);
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/june-client.ts` around lines 118 - 126, The startMode method duplicates
the same reset-and-send flow already implemented in preheat, so refactor it to
delegate to preheat instead of repeating the MC_PREHEAT payload construction.
Keep the existing primitiveType and tempF parameters in startMode, but have it
call preheat with those values so lastCancelled is still reset and the command
shape stays centralized in preheat. Use the preheat and startMode methods in
june-client.ts as the main points to update.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-07-08-expanded-homekit-features.md`:
- Around line 13-18: The plan text still marks camera/ffmpeg as out of scope,
but the rest of the PR docs now treat camera/video-doorbell support as delivered
scope. Update the scope language in the plan document so it matches the shipped
behavior, or clearly label this section as historical planning; keep the wording
consistent with the other docs and the feature summary to avoid conflicting
scope statements.

In `@docs/superpowers/specs/2026-07-08-june-expanded-homekit-features-design.md`:
- Around line 63-67: The probe payload description is inconsistent because it
confirms sensor_data.probe[] and then still references
left_probe/right_probe/probe_temperature as alternatives. Update the spec text
around parseProbeTelemetry to keep only the confirmed sensor_data.probe array
shape and remove the stale fallback language so implementers don’t treat
multiple incompatible contracts as valid.
- Around line 121-136: Align the camera and probe sensor config names in the
June features spec with the runtime schema used elsewhere: update the Feature 2
camera section to include the actual `ffmpegPath` field alongside
`camera.enabled` and `camera.name`, and change the probe sensor naming config
from `probeSensors.names` to the real `probeSensors.leftName` and
`probeSensors.rightName` fields. Use the existing `JuneTelemetry`,
`JuneClient.handleMessage`, and `JuneProbeSensorAccessory` references to keep
the documented contract consistent with implementation.

In `@src/accessories/camera.ts`:
- Around line 100-122: `prepareStream` only logs `socket` errors today, so a
failed `socket.bind` can leave Homebridge waiting forever. Update
`prepareStream` in `CameraAccessory` to invoke the `PrepareStreamCallback` with
an error when the bind fails, and ensure the callback is guarded so it cannot be
called twice if both the bind path and the socket error path fire. Use the
existing `socket`, `callback`, and `sessions` setup to keep the success flow
unchanged while adding proper failure handling.
- Around line 137-187: The `startStream` flow in `camera.ts` leaves stale
sessions active because `proc.on('error')` and `proc.on('exit')` only log
instead of cleaning up. Update `startStream` so that when the ffmpeg child
process emits `error` or exits unexpectedly, it calls `stopSession` for the
current session and clears `session.ffmpeg` before returning/logging. Keep the
existing success path and `callback()` behavior, but ensure the `session` is
always torn down on ffmpeg failure or completion to avoid leaking the UDP socket
and leaving `sessions` stale.

In `@src/accessories/doorbell.ts`:
- Around line 26-31: The `Doorbell.update` logic currently presses on every
telemetry packet while `telemetry.ready` stays true, so add transition tracking
instead of checking the raw flag directly. Update `update(telemetry:
JuneTelemetry)` in `doorbell.ts` to remember the previous `ready` state on the
`Doorbell` instance and only call `press()` when `ready` changes from false to
true, while keeping the existing `done` behavior intact. Use the existing
`update`, `press`, and `JuneTelemetry` symbols to wire this deduplication into
the doorbell state machine.

In `@src/protocol.ts`:
- Around line 47-48: The `modes` input type in `JuneOvenConfig` is too strict
for what `normalizeOvenConfig` actually accepts. Update the type definitions in
`protocol.ts` so the public input for `modes` only requires `primitiveType` and
makes `label` and `tempF` optional, while keeping `normalizeOvenConfig`
responsible for filling defaults. Use a separate input-facing mode type or
adjust `JuneModeConfig` usage so `protocol.test.ts` no longer needs the `as
never` workaround.

---

Nitpick comments:
In `@src/june-client.test.ts`:
- Around line 43-49: The startMode test only checks that JuneClient.startMode
exists, which is redundant and not behavior-focused. Update the test in the
startMode describe block to assert actual behavior by exercising
JuneClient.startMode and verifying it calls sendCommand with the expected
command or resets lastCancelled as intended; if that setup is too heavy, remove
the test rather than keeping a compile-time existence check.

In `@src/june-client.ts`:
- Around line 118-126: The startMode method duplicates the same reset-and-send
flow already implemented in preheat, so refactor it to delegate to preheat
instead of repeating the MC_PREHEAT payload construction. Keep the existing
primitiveType and tempF parameters in startMode, but have it call preheat with
those values so lastCancelled is still reset and the command shape stays
centralized in preheat. Use the preheat and startMode methods in june-client.ts
as the main points to update.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3365ad13-e87b-496e-a7b4-b6553c8ebff6

📥 Commits

Reviewing files that changed from the base of the PR and between f23b636 and 16d8606.

📒 Files selected for processing (13)
  • README.md
  • config.schema.json
  • docs/superpowers/plans/2026-07-08-expanded-homekit-features.md
  • docs/superpowers/specs/2026-07-08-june-expanded-homekit-features-design.md
  • src/accessories/camera.ts
  • src/accessories/doorbell.ts
  • src/accessories/mode-switch.ts
  • src/accessories/probe-sensor.ts
  • src/june-client.test.ts
  • src/june-client.ts
  • src/platform.ts
  • src/protocol.test.ts
  • src/protocol.ts

Comment on lines +13 to +18
- Node floors: `18.20.4+`, `20.19.0+`, `22.12.0+`, or `24+`. Homebridge `>=1.8.0`.
- Every new feature is **opt-in**, default off (doorbell) or empty (modes, probes). Upgrading an existing install must change no behavior.
- Temperature on the wire is milli-°C; convert with existing helpers (`fahrenheitToMilliC`, `milliCToCelsius`). HomeKit temperature characteristics are Celsius.
- No new runtime dependencies. Camera/streaming/ffmpeg are OUT of scope for this plan (deferred to Spike A — see spec).
- Test runner: `npm test` (vitest run). Type-check: `npm run lint` (`tsc --noEmit`).
- Commit after each task with a `feat:`/`docs:` message.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the plan aligned with the shipped scope.

This still says camera/ffmpeg are out of scope, but the rest of the PR docs now describe camera/video-doorbell support as part of the delivered stack. Please either mark this as historical planning or update the scope so the docs don't disagree.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-07-08-expanded-homekit-features.md` around lines
13 - 18, The plan text still marks camera/ffmpeg as out of scope, but the rest
of the PR docs now treat camera/video-doorbell support as delivered scope.
Update the scope language in the plan document so it matches the shipped
behavior, or clearly label this section as historical planning; keep the wording
consistent with the other docs and the feature summary to avoid conflicting
scope statements.

Comment thread docs/superpowers/specs/2026-07-08-june-expanded-homekit-features-design.md Outdated
Comment on lines +121 to +136
- Depends on Spike A. Config (per oven): `camera.enabled` (bool, default false), `camera.name`.
- Implemented as part of the same accessory as the doorbell (feature 1) when both are enabled;
camera-only (no doorbell triggers) is also valid.

## Feature 3 — Food probe temperature sensors

Expose the oven's food probe temperature(s) as HomeKit **Temperature Sensor** services, so users
can build automations like "notify when probe reaches 145 °F."

- Source: `10013` telemetry probe fields (exact path from Spike B). Support up to two probes
(left/right); a sensor is only shown when its probe reports a reading, and reads as inactive /
its last value otherwise.
- Opt-in. Config (per oven): `probeSensors.enabled` (bool, default false). Optionally a
`probeSensors.names` map for left/right display names.
- New `JuneTelemetry` fields (e.g. `probeLeftC`, `probeRightC`, `probePresent`) populated in
`JuneClient.handleMessage` for `10013`; a `JuneProbeSensorAccessory` subscribes to telemetry.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the actual config fields.

The camera config here omits ffmpegPath, and probe sensors are described as probeSensors.names, but the schema/runtime use leftName/rightName. Please align these fields with the config contract used elsewhere in the stack.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-07-08-june-expanded-homekit-features-design.md`
around lines 121 - 136, Align the camera and probe sensor config names in the
June features spec with the runtime schema used elsewhere: update the Feature 2
camera section to include the actual `ffmpegPath` field alongside
`camera.enabled` and `camera.name`, and change the probe sensor naming config
from `probeSensors.names` to the real `probeSensors.leftName` and
`probeSensors.rightName` fields. Use the existing `JuneTelemetry`,
`JuneClient.handleMessage`, and `JuneProbeSensorAccessory` references to keep
the documented contract consistent with implementation.

Comment thread src/accessories/camera.ts
Comment on lines +100 to +122
public prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): void {
const socket = createSocket(request.addressVersion === 'ipv6' ? 'udp6' : 'udp4');
socket.on('error', error => this.platform.log.warn(`June camera RTCP socket error: ${error.message}`));
socket.bind(() => {
const localPort = socket.address().port;
const ssrc = this.platform.api.hap.CameraController.generateSynchronisationSource();
this.sessions.set(request.sessionID, {
socket,
targetAddress: request.targetAddress,
videoPort: request.video.port,
videoSsrc: ssrc,
videoSrtp: Buffer.concat([request.video.srtp_key, request.video.srtp_salt]).toString('base64'),
});
callback(undefined, {
video: {
port: localPort,
ssrc,
srtp_key: request.video.srtp_key,
srtp_salt: request.video.srtp_salt,
},
});
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

prepareStream never calls callback on socket bind error — Homebridge hangs.

If socket.bind fails (e.g., no available ports, network down), the error event fires and is logged, but callback is never invoked. Homebridge blocks indefinitely waiting for stream preparation to complete.

🔒 Proposed fix: guard callback against double-invocation
   public prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): void {
     const socket = createSocket(request.addressVersion === 'ipv6' ? 'udp6' : 'udp4');
+    let settled = false;
+    const finish = (err: Error | undefined, info?: { video: { port: number; ssrc: number; srtp_key: Buffer; srtp_salt: Buffer } }) => {
+      if (settled) return;
+      settled = true;
+      callback(err, info);
+    };
-    socket.on('error', error => this.platform.log.warn(`June camera RTCP socket error: ${error.message}`));
+    socket.on('error', error => {
+      this.platform.log.warn(`June camera RTCP socket error: ${error.message}`);
+      finish(new Error(`RTCP socket error: ${error.message}`));
+    });
     socket.bind(() => {
       const localPort = socket.address().port;
       const ssrc = this.platform.api.hap.CameraController.generateSynchronisationSource();
       this.sessions.set(request.sessionID, {
         socket,
         targetAddress: request.targetAddress,
         videoPort: request.video.port,
         videoSsrc: ssrc,
         videoSrtp: Buffer.concat([request.video.srtp_key, request.video.srtp_salt]).toString('base64'),
       });
-      callback(undefined, {
+      finish(undefined, {
         video: {
           port: localPort,
           ssrc,
           srtp_key: request.video.srtp_key,
           srtp_salt: request.video.srtp_salt,
         },
       });
     });
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): void {
const socket = createSocket(request.addressVersion === 'ipv6' ? 'udp6' : 'udp4');
socket.on('error', error => this.platform.log.warn(`June camera RTCP socket error: ${error.message}`));
socket.bind(() => {
const localPort = socket.address().port;
const ssrc = this.platform.api.hap.CameraController.generateSynchronisationSource();
this.sessions.set(request.sessionID, {
socket,
targetAddress: request.targetAddress,
videoPort: request.video.port,
videoSsrc: ssrc,
videoSrtp: Buffer.concat([request.video.srtp_key, request.video.srtp_salt]).toString('base64'),
});
callback(undefined, {
video: {
port: localPort,
ssrc,
srtp_key: request.video.srtp_key,
srtp_salt: request.video.srtp_salt,
},
});
});
}
public prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): void {
const socket = createSocket(request.addressVersion === 'ipv6' ? 'udp6' : 'udp4');
let settled = false;
const finish = (err: Error | undefined, info?: { video: { port: number; ssrc: number; srtp_key: Buffer; srtp_salt: Buffer } }) => {
if (settled) return;
settled = true;
callback(err, info);
};
socket.on('error', error => {
this.platform.log.warn(`June camera RTCP socket error: ${error.message}`);
finish(new Error(`RTCP socket error: ${error.message}`));
});
socket.bind(() => {
const localPort = socket.address().port;
const ssrc = this.platform.api.hap.CameraController.generateSynchronisationSource();
this.sessions.set(request.sessionID, {
socket,
targetAddress: request.targetAddress,
videoPort: request.video.port,
videoSsrc: ssrc,
videoSrtp: Buffer.concat([request.video.srtp_key, request.video.srtp_salt]).toString('base64'),
});
finish(undefined, {
video: {
port: localPort,
ssrc,
srtp_key: request.video.srtp_key,
srtp_salt: request.video.srtp_salt,
},
});
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/accessories/camera.ts` around lines 100 - 122, `prepareStream` only logs
`socket` errors today, so a failed `socket.bind` can leave Homebridge waiting
forever. Update `prepareStream` in `CameraAccessory` to invoke the
`PrepareStreamCallback` with an error when the bind fails, and ensure the
callback is guarded so it cannot be called twice if both the bind path and the
socket error path fire. Use the existing `socket`, `callback`, and `sessions`
setup to keep the success flow unchanged while adding proper failure handling.

Comment thread src/accessories/camera.ts
Comment on lines +137 to +187
private startStream(request: StartStreamRequest, callback: StreamRequestCallback): void {
const session = this.sessions.get(request.sessionID);
const snapshot = this.client.latestSnapshot;
if (!session) {
callback(new Error('No prepared session'));
return;
}
if (!snapshot) {
this.platform.log.warn('June camera has no frame yet (no active cook) — cannot start live stream.');
callback(new Error('No camera frame available'));
return;
}
const { video } = request;
const args = [
'-loglevel', 'error',
'-loop', '1', '-re', '-i', snapshot.url,
'-an', '-sn', '-dn',
'-codec:v', 'libx264', '-pix_fmt', 'yuv420p', '-profile:v', 'baseline',
'-preset', 'ultrafast', '-tune', 'zerolatency',
'-r', String(video.fps),
'-vf', `scale=${video.width}:${video.height}`,
'-b:v', `${video.max_bit_rate}k`, '-bufsize', `${2 * video.max_bit_rate}k`, '-maxrate', `${video.max_bit_rate}k`,
'-payload_type', String(video.pt),
'-ssrc', String(session.videoSsrc),
'-f', 'rtp',
'-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80',
'-srtp_out_params', session.videoSrtp,
`srtp://${session.targetAddress}:${session.videoPort}?rtcpport=${session.videoPort}&pkt_size=1316`,
];

const ffmpegPath = this.client.config.camera.ffmpegPath;
let proc: ChildProcess;
try {
proc = spawn(ffmpegPath, args, { env: process.env });
} catch (error) {
this.platform.log.error(`June camera: failed to spawn ffmpeg ("${ffmpegPath}"): ${(error as Error).message}`);
callback(new Error('ffmpeg not available'));
return;
}
session.ffmpeg = proc;
proc.on('error', error =>
this.platform.log.error(`June camera ffmpeg error: ${error.message} (is ffmpeg installed at "${ffmpegPath}"?)`),
);
proc.stderr?.on('data', data => this.platform.log.debug(`[june-camera ffmpeg] ${data}`));
proc.on('exit', (code, signal) => {
if (code !== null && code !== 0 && signal !== 'SIGKILL') {
this.platform.log.warn(`June camera ffmpeg exited with code ${code}`);
}
});
callback();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

ffmpeg async errors and exits are not cleaned up — session leaks and stream appears active.

spawn returns successfully even when the ffmpeg binary is missing; the error event fires asynchronously after callback() has already been called, so Homebridge believes the stream started. Additionally, when ffmpeg exits (crash or normal completion), the session socket is never closed unless Homebridge separately sends a STOP request. Both cases leak the UDP socket and leave a stale session in the map.

🧹 Proposed fix: clean up session on ffmpeg error and exit
     session.ffmpeg = proc;
     proc.on('error', error =>
-      this.platform.log.error(`June camera ffmpeg error: ${error.message} (is ffmpeg installed at "${ffmpegPath}"?)`),
+      this.platform.log.error(`June camera ffmpeg error: ${error.message} (is ffmpeg installed at "${ffmpegPath}"?)`),
+      this.stopSession(request.sessionID),
     );
     proc.stderr?.on('data', data => this.platform.log.debug(`[june-camera ffmpeg] ${data}`));
     proc.on('exit', (code, signal) => {
       if (code !== null && code !== 0 && signal !== 'SIGKILL') {
         this.platform.log.warn(`June camera ffmpeg exited with code ${code}`);
       }
+      this.stopSession(request.sessionID);
     });
     callback();

Note: stopSession is idempotent — it no-ops if the session was already removed, so calling it from both the exit handler and a later Homebridge STOP request is safe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/accessories/camera.ts` around lines 137 - 187, The `startStream` flow in
`camera.ts` leaves stale sessions active because `proc.on('error')` and
`proc.on('exit')` only log instead of cleaning up. Update `startStream` so that
when the ffmpeg child process emits `error` or exits unexpectedly, it calls
`stopSession` for the current session and clears `session.ffmpeg` before
returning/logging. Keep the existing success path and `callback()` behavior, but
ensure the `session` is always torn down on ffmpeg failure or completion to
avoid leaking the UDP socket and leaving `sessions` stale.

Comment on lines +26 to +31
private update(telemetry: JuneTelemetry): void {
const triggers = this.client.config.doorbell.triggers;
if ((triggers.done && telemetry.done) || (triggers.ready && telemetry.ready)) {
this.press();
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Doorbell rings on every telemetry update where ready is true — add transition tracking.

The done flag fires once per active→inactive transition, but ready can remain true across multiple telemetry updates (the oven keeps sending temperature readings while near target). Without deduplication, the doorbell will press on every update, flooding the user with HomeKit notifications.

🔔 Proposed fix: ring only on false→true transitions
 export class JuneDoorbellAccessory {
   private readonly service: Service;
+  private rangDone = false;
+  private rangReady = false;
 
   constructor(
     private readonly platform: JunePlatform,
     private readonly accessory: PlatformAccessory,
     private readonly client: JuneClient,
   ) {
     const { Service, Characteristic } = this.platform;
     this.service = accessory.getService(Service.Doorbell) || accessory.addService(Service.Doorbell);
     this.service.setCharacteristic(Characteristic.Name, this.client.config.doorbell.name);
     this.client.on('telemetry', telemetry => this.update(telemetry));
   }
 
   private update(telemetry: JuneTelemetry): void {
     const triggers = this.client.config.doorbell.triggers;
+    // Reset when a new cook starts so the next done/ready event can ring again
+    if (telemetry.active) {
+      this.rangDone = false;
+      this.rangReady = false;
+    }
-    if ((triggers.done && telemetry.done) || (triggers.ready && telemetry.ready)) {
-      this.press();
-    }
+    if (triggers.done && telemetry.done && !this.rangDone) {
+      this.rangDone = true;
+      this.press();
+    }
+    if (triggers.ready && telemetry.ready && !this.rangReady) {
+      this.rangReady = true;
+      this.press();
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/accessories/doorbell.ts` around lines 26 - 31, The `Doorbell.update`
logic currently presses on every telemetry packet while `telemetry.ready` stays
true, so add transition tracking instead of checking the raw flag directly.
Update `update(telemetry: JuneTelemetry)` in `doorbell.ts` to remember the
previous `ready` state on the `Doorbell` instance and only call `press()` when
`ready` changes from false to true, while keeping the existing `done` behavior
intact. Use the existing `update`, `press`, and `JuneTelemetry` symbols to wire
this deduplication into the doorbell state machine.

Comment thread src/protocol.ts
Comment on lines +47 to +48
doorbell?: Partial<Omit<JuneDoorbellConfig, 'triggers'>> & { triggers?: Partial<JuneDoorbellConfig['triggers']> };
modes?: JuneModeConfig[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

modes input type is stricter than the normalization logic.

JuneOvenConfig.modes is typed as JuneModeConfig[], requiring label, primitiveType, and tempF on every entry. But normalizeOvenConfig (line 122) provides defaults for label (m.label || m.primitiveType) and tempF (m.tempF ?? 350), so users should be able to omit them. The test at src/protocol.test.ts:56 uses as never to work around this mismatch.

Consider a separate input type that only requires primitiveType:

🔧 Proposed type fix
 export interface JuneOvenConfig {
   // ...
-  modes?: JuneModeConfig[];
+  modes?: Array<{ primitiveType: string; label?: string; tempF?: number }>;
   // ...
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
doorbell?: Partial<Omit<JuneDoorbellConfig, 'triggers'>> & { triggers?: Partial<JuneDoorbellConfig['triggers']> };
modes?: JuneModeConfig[];
doorbell?: Partial<Omit<JuneDoorbellConfig, 'triggers'>> & { triggers?: Partial<JuneDoorbellConfig['triggers']> };
modes?: Array<{ primitiveType: string; label?: string; tempF?: number }>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/protocol.ts` around lines 47 - 48, The `modes` input type in
`JuneOvenConfig` is too strict for what `normalizeOvenConfig` actually accepts.
Update the type definitions in `protocol.ts` so the public input for `modes`
only requires `primitiveType` and makes `label` and `tempF` optional, while
keeping `normalizeOvenConfig` responsible for filling defaults. Use a separate
input-facing mode type or adjust `JuneModeConfig` usage so `protocol.test.ts` no
longer needs the `as never` workaround.

@tenki-reviewer tenki-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: 🟠 High (72/100) — 3 high findings, 6 medium · 1599 LOC across 13 files


Overview

This PR introduces camera streaming (ffmpeg/RTP/SRTP), doorbell accessory, mode switch for oven control, and probe temperature sensors across 13 files (5 new, 8 modified). The review uncovered 10 findings with severity ranging from medium to high.

Critical Issues

  • Probe telemetry cross-contamination (src/june-client.ts:58): A disconnected left probe can inherit the right probe's temperature due to stale object reference sharing. This provides incorrect temperature data to users.
  • Camera advertises audio but never streams audio (src/accessories/camera.ts:70): The camera's streaming configuration declares audio codecs but no audio track is set up, causing HomeKit clients to incorrectly expect audio capability.

Security

  • SSRF via snapshot URL (src/accessories/camera.ts:88): The snapshot URL received from WebSocket data is passed to ffmpeg without validation, allowing a compromised oven or MITM attacker to cause the host to request arbitrary URLs.

Resource Leaks

  • No shutdown cleanup (src/accessories/camera.ts:47): When the camera streaming session stops, ffmpeg subprocess and UDP sockets are not cleaned up, causing process orphans and socket leaks.
  • UDP socket leak on stream start failure (src/accessories/camera.ts:144): If stream preparation fails after socket creation, the socket is never closed.

Protocol & Concurrency

  • Doorbell repeated rings (src/accessories/doorbell.ts:26): The doorbell fires ProgrammableSwitchEvent on every telemetry poll while the oven is in ready state instead of once per cooking cycle.
  • Mode switch race condition (src/accessories/mode-switch.ts:46): Telemetry-driven setAllOff() races with user-driven setOn(), potentially toggling the mode incorrectly.
  • Side-effect before guard (src/june-client.ts:129): lastCancelled is mutated before sendCommand executes, persisting cancellation state even when the command send fails.
  • Missing error callback (src/accessories/camera.ts:100): prepareStream never invokes the callback when the UDP socket bind fails, leaving the HomeKit stream setup hung indefinitely.

HomeKit Compliance

  • Probe temperature range violation (src/accessories/probe-sensor.ts:21): Probe sensors use CurrentTemperature characteristic without clamping to HAP's valid 0-100°C range, potentially violating HomeKit spec.

Comment thread src/accessories/camera.ts
Comment on lines +47 to +202
private readonly sessions = new Map<string, Session>();

constructor(
private readonly platform: JunePlatform,
private readonly client: JuneClient,
) {
const hap = this.platform.api.hap;
this.controller = new hap.CameraController({
cameraStreamCount: 2,
delegate: this,
streamingOptions: {
supportedCryptoSuites: [hap.SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80],
video: {
resolutions: [
[640, 480, 15],
[640, 480, 2],
[320, 240, 15],
],
codec: {
profiles: [hap.H264Profile.BASELINE, hap.H264Profile.MAIN, hap.H264Profile.HIGH],
levels: [hap.H264Level.LEVEL3_1, hap.H264Level.LEVEL3_2, hap.H264Level.LEVEL4_0],
},
},
audio: {
codecs: [
{ type: hap.AudioStreamingCodecType.AAC_ELD, samplerate: hap.AudioStreamingSamplerate.KHZ_16 },
],
},
},
});
}

public async handleSnapshotRequest(_request: SnapshotRequest, callback: SnapshotRequestCallback): Promise<void> {
const snapshot = this.client.latestSnapshot;
if (!snapshot) {
callback(undefined, PLACEHOLDER_JPEG);
return;
}
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
const response = await fetch(snapshot.url, { signal: controller.signal });
clearTimeout(timer);
if (!response.ok) {
throw new Error(`snapshot fetch ${response.status}`);
}
callback(undefined, Buffer.from(await response.arrayBuffer()));
} catch (error) {
this.platform.log.warn(`June camera snapshot failed: ${(error as Error).message}`);
callback(undefined, PLACEHOLDER_JPEG);
}
}

public prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): void {
const socket = createSocket(request.addressVersion === 'ipv6' ? 'udp6' : 'udp4');
socket.on('error', error => this.platform.log.warn(`June camera RTCP socket error: ${error.message}`));
socket.bind(() => {
const localPort = socket.address().port;
const ssrc = this.platform.api.hap.CameraController.generateSynchronisationSource();
this.sessions.set(request.sessionID, {
socket,
targetAddress: request.targetAddress,
videoPort: request.video.port,
videoSsrc: ssrc,
videoSrtp: Buffer.concat([request.video.srtp_key, request.video.srtp_salt]).toString('base64'),
});
callback(undefined, {
video: {
port: localPort,
ssrc,
srtp_key: request.video.srtp_key,
srtp_salt: request.video.srtp_salt,
},
});
});
}

public handleStreamRequest(request: StreamingRequest, callback: StreamRequestCallback): void {
const { StreamRequestTypes } = this.platform.api.hap;
if (request.type === StreamRequestTypes.START) {
this.startStream(request, callback);
return;
}
if (request.type === StreamRequestTypes.STOP) {
this.stopSession(request.sessionID);
}
// RECONFIGURE is a no-op for a still-based source.
callback();
}

private startStream(request: StartStreamRequest, callback: StreamRequestCallback): void {
const session = this.sessions.get(request.sessionID);
const snapshot = this.client.latestSnapshot;
if (!session) {
callback(new Error('No prepared session'));
return;
}
if (!snapshot) {
this.platform.log.warn('June camera has no frame yet (no active cook) — cannot start live stream.');
callback(new Error('No camera frame available'));
return;
}
const { video } = request;
const args = [
'-loglevel', 'error',
'-loop', '1', '-re', '-i', snapshot.url,
'-an', '-sn', '-dn',
'-codec:v', 'libx264', '-pix_fmt', 'yuv420p', '-profile:v', 'baseline',
'-preset', 'ultrafast', '-tune', 'zerolatency',
'-r', String(video.fps),
'-vf', `scale=${video.width}:${video.height}`,
'-b:v', `${video.max_bit_rate}k`, '-bufsize', `${2 * video.max_bit_rate}k`, '-maxrate', `${video.max_bit_rate}k`,
'-payload_type', String(video.pt),
'-ssrc', String(session.videoSsrc),
'-f', 'rtp',
'-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80',
'-srtp_out_params', session.videoSrtp,
`srtp://${session.targetAddress}:${session.videoPort}?rtcpport=${session.videoPort}&pkt_size=1316`,
];

const ffmpegPath = this.client.config.camera.ffmpegPath;
let proc: ChildProcess;
try {
proc = spawn(ffmpegPath, args, { env: process.env });
} catch (error) {
this.platform.log.error(`June camera: failed to spawn ffmpeg ("${ffmpegPath}"): ${(error as Error).message}`);
callback(new Error('ffmpeg not available'));
return;
}
session.ffmpeg = proc;
proc.on('error', error =>
this.platform.log.error(`June camera ffmpeg error: ${error.message} (is ffmpeg installed at "${ffmpegPath}"?)`),
);
proc.stderr?.on('data', data => this.platform.log.debug(`[june-camera ffmpeg] ${data}`));
proc.on('exit', (code, signal) => {
if (code !== null && code !== 0 && signal !== 'SIGKILL') {
this.platform.log.warn(`June camera ffmpeg exited with code ${code}`);
}
});
callback();
}

private stopSession(sessionID: string): void {
const session = this.sessions.get(sessionID);
if (!session) {
return;
}
session.ffmpeg?.kill('SIGKILL');
try {
session.socket?.close();
} catch {
// socket may already be closed
}
this.sessions.delete(sessionID);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 No cleanup on shutdown — ffmpeg orphans and UDP socket leaks (bug)

The JuneCameraSource class (src/accessories/camera.ts:45-202) has no destroy() or cleanup method. Neither platform.ts nor any caller iterates camera sources to shut them down on plugin unload. When HomeBridge restarts or the plugin is unloaded, any running ffmpeg processes become orphaned (still running with no parent monitoring them), and all bound UDP sockets remain open until the Node.js process exits. The JunePlatform class does call client.stop() on its JuneClient instances (june-client.ts:111) but has no corresponding cleanup for attached camera sources.

💡 Suggestion: Add a public destroy() method to JuneCameraSource that iterates all sessions, kills ffmpeg processes, closes sockets, and clears the sessions map. Store the source reference from attachCamera and invoke destroy() from the platform's shutdown path.

📋 Prompt for AI Agents
  1. In src/accessories/camera.ts, add to JuneCameraSource class:
    public destroy(): void {
    for (const [sessionID] of this.sessions) {
    this.stopSession(sessionID);
    }
    }
  2. Change attachCamera() to return the JuneCameraSource instance.
  3. In src/platform.ts, store camera source references (e.g., in a Map<string, JuneCameraSource>) and iterate them on plugin shutdown, calling source.destroy() on each.

Comment thread src/june-client.ts Outdated
Comment on lines +58 to +59
const left = valueOf(probes.find((p: any) => p?.id === 'left')) ?? valueOf(probes.find((p: any) => valueOf(p) !== undefined));
const right = valueOf(probes.find((p: any) => p?.id === 'right'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Probe telemetry cross-contamination: disconnected left probe inherits right probe's temperature (bug)

In parseProbeTelemetry (src/june-client.ts:58), the left-probe fallback chain uses a nullish coalesce with a find over any probe that has a numeric value. When both probes are reported in the sensor_data.probe array but the left probe's value is undefined/null/absent (indicating a disconnected or malfunctioning probe), the fallback captures the right probe's temperature and assigns it to probeLeftC. Meanwhile the right probe's value is correctly assigned to probeRightC via the direct find at line 59. Result: both probeLeftC and probeRightC reflect the right probe's temperature — a cross-contamination that hides the left probe's disconnected state from the user, which could lead to food safety issues if a probe silently reads the wrong temperature.

💡 Suggestion: Remove the right-hand side of the ?? fallback for the left probe. If no left probe with a valid numeric value is found, probeLeftC should remain undefined. The fallback was intended for single-probe ovens (which always report id='left'), but it backfires dangerously when a dual-probe oven has a disconnected left probe.

📋 Prompt for AI Agents

In src/june-client.ts, line 58, replace:
const left = valueOf(probes.find((p: any) => p?.id === 'left')) ?? valueOf(probes.find((p: any) => valueOf(p) !== undefined));
with:
const left = valueOf(probes.find((p: any) => p?.id === 'left'));

This removes the dangerous fallback. The single-probe case is already handled because single-probe ovens report [{id:'left', value:...}] — the direct find matches. If future protocol versions omit the 'left' id for single probes, a safer alternative is: probes.length === 1 ? valueOf(probes[0]) : valueOf(probes.find(p => p?.id === 'left')).

Comment on lines +26 to +31
private update(telemetry: JuneTelemetry): void {
const triggers = this.client.config.doorbell.triggers;
if ((triggers.done && telemetry.done) || (triggers.ready && telemetry.ready)) {
this.press();
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Doorbell rings repeatedly during ready state instead of once per event (bug)

The doorbell accessory (src/accessories/doorbell.ts) calls press() on every telemetry update where the done or ready trigger is true (line 28), without edge detection or debouncing. The ready field is recomputed from live telemetry data on each 10013 frame (cook_state_data.progress >= 0.995 at june-client.ts:256, refined by temperature check at june-client.ts:293-295). When the oven stays in the ready state, every 10013 frame (~1/s during a cook) re-emits ready: true, causing the doorbell notification to fire repeatedly. For comparison, the existing JuneOccupancySensorAccessory (sensors.ts:30-33) has a similar pattern for ready/done but is resilient because it uses a 30-second timeout to reset OccupancyDetected; the doorbell has no such dampening.

💡 Suggestion: Track the previous state and only call press() when transitioning from false→true. Add private lastReady = false; private lastDone = false; to the class, then in update() check for edge transitions before firing. Alternatively, add a cooldown timer similar to the occupancy sensor's 30-second debounce.

📋 Prompt for AI Agents

In src/accessories/doorbell.ts, modify the JuneDoorbellAccessory class:

  1. Add private fields: private lastReady = false; and private lastDone = false;
  2. In the update() method (lines 26-31), replace the current condition with edge-detection:
const readyEdge = triggers.ready && telemetry.ready && !this.lastReady;
const doneEdge = triggers.done && telemetry.done && !this.lastDone;
if (readyEdge || doneEdge) {
  this.press();
}
this.lastReady = telemetry.ready ?? false;
this.lastDone = telemetry.done ?? false;

Comment thread src/accessories/camera.ts Outdated
Comment on lines +144 to +175
if (!snapshot) {
this.platform.log.warn('June camera has no frame yet (no active cook) — cannot start live stream.');
callback(new Error('No camera frame available'));
return;
}
const { video } = request;
const args = [
'-loglevel', 'error',
'-loop', '1', '-re', '-i', snapshot.url,
'-an', '-sn', '-dn',
'-codec:v', 'libx264', '-pix_fmt', 'yuv420p', '-profile:v', 'baseline',
'-preset', 'ultrafast', '-tune', 'zerolatency',
'-r', String(video.fps),
'-vf', `scale=${video.width}:${video.height}`,
'-b:v', `${video.max_bit_rate}k`, '-bufsize', `${2 * video.max_bit_rate}k`, '-maxrate', `${video.max_bit_rate}k`,
'-payload_type', String(video.pt),
'-ssrc', String(session.videoSsrc),
'-f', 'rtp',
'-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80',
'-srtp_out_params', session.videoSrtp,
`srtp://${session.targetAddress}:${session.videoPort}?rtcpport=${session.videoPort}&pkt_size=1316`,
];

const ffmpegPath = this.client.config.camera.ffmpegPath;
let proc: ChildProcess;
try {
proc = spawn(ffmpegPath, args, { env: process.env });
} catch (error) {
this.platform.log.error(`June camera: failed to spawn ffmpeg ("${ffmpegPath}"): ${(error as Error).message}`);
callback(new Error('ffmpeg not available'));
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 UDP socket leak on stream start failure — session not cleaned up (bug)

In startStream() (src/accessories/camera.ts:137-187), two early-return paths leave the session (with its bound UDP socket created in prepareStream) in this.sessions without cleanup: (1) no snapshot available at lines 144-147, and (2) ffmpeg spawn throws at lines 169-174. In both cases, callback(new Error(...)) is called and the method returns without calling stopSession(request.sessionID). If the HomeKit controller does not send a STOP request after receiving a START error, the socket stays bound indefinitely and the session entry leaks. Over repeated attempts, multiple UDP sockets accumulate.

💡 Suggestion: Call this.stopSession(request.sessionID) before returning the error callback in both early-return paths of startStream. This ensures the bound UDP socket is closed and the session is removed from the map regardless of why streaming fails.

📋 Prompt for AI Agents

In src/accessories/camera.ts, in startStream():

  1. At line 145 (after the log.warn), add this.stopSession(request.sessionID); before the callback on line 146.
  2. At line 172 (after the log.error), add this.stopSession(request.sessionID); before the callback on line 173.
    This ensures the bound UDP socket from prepareStream is always cleaned up on start failure.

Comment thread src/accessories/camera.ts Outdated
Comment on lines +70 to +80
audio: {
codecs: [
{ type: hap.AudioStreamingCodecType.AAC_ELD, samplerate: hap.AudioStreamingSamplerate.KHZ_16 },
],
},
},
});
}

public async handleSnapshotRequest(_request: SnapshotRequest, callback: SnapshotRequestCallback): Promise<void> {
const snapshot = this.client.latestSnapshot;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Camera advertises audio codecs but never negotiates or streams audio (bug)

The JuneCameraSource streaming options declare AAC-ELD audio at 16 kHz (camera.ts:70-75), but the prepareStream callback only returns a video configuration object — no audio port, SSRC, or SRTP parameters (camera.ts:113-120). The ffmpeg command in startStream uses -an to disable audio (camera.ts:153). Per the HAP CameraRTPStreamManagement specification, if audio codecs appear in SupportedRTPConfiguration, the SelectedRTPConfiguration response must include negotiated audio parameters. HomeKit's streaming controller may reject the session when audio is advertised but not provided in the negotiation response, causing live-stream tap-to-view to fail silently on affected controllers.

💡 Suggestion: Remove the audio codec block from streamingOptions (simplest, since this camera has no audio source). This is a still-image-based camera with no microphone input.

📋 Prompt for AI Agents

In src/accessories/camera.ts, remove lines 70-75 (the audio: block from streamingOptions). This still-image-based camera source has no audio input, so advertising audio codecs causes a protocol mismatch with HomeKit controllers. The resulting streamingOptions should only contain supportedCryptoSuites and video. If audio support is added later, implement full audio negotiation in prepareStream (return audio: {port, ssrc, srtp_key, srtp_salt} matching the request) and add audio encoding to the ffmpeg command in startStream.

Comment thread src/accessories/probe-sensor.ts Outdated
Comment on lines +21 to +26
this.left = this.accessory.getServiceById(Service.TemperatureSensor, 'probe-left')
|| this.accessory.addService(Service.TemperatureSensor, cfg.leftName, 'probe-left');
this.left.setCharacteristic(Characteristic.Name, cfg.leftName);
this.right = this.accessory.getServiceById(Service.TemperatureSensor, 'probe-right')
|| this.accessory.addService(Service.TemperatureSensor, cfg.rightName, 'probe-right');
this.right.setCharacteristic(Characteristic.Name, cfg.rightName);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Probe temperature sensors may exceed HAP CurrentTemperature valid range (0–100°C) (bug)

The probe-sensor.ts accessory sets CurrentTemperature on TemperatureSensor services without adjusting the characteristic's valid value range (probe-sensor.ts:21-26). The HAP specification defines CurrentTemperature with default minValue: 0 and maxValue: 100 (°C). The thermostat accessory in the same codebase explicitly widens this to maxValue: 300 via setProps (thermostat.ts:22), but probe-sensor.ts does not. If a food probe reads above 100°C — common when a probe is left dangling in a hot oven cavity or during a sensor fault — the value would be clamped or rejected, displaying incorrect temperature in the Home app.

💡 Suggestion: After creating the TemperatureSensor services, call .getCharacteristic(Characteristic.CurrentTemperature).setProps({minValue: -20, maxValue: 300}) on both left and right probe services, matching the thermostat's approach.

📋 Prompt for AI Agents

In src/accessories/probe-sensor.ts, after line 23 (this.left.setCharacteristic(...)) add:
this.left.getCharacteristic(Characteristic.CurrentTemperature).setProps({minValue: -20, maxValue: 300});
After line 26 (this.right.setCharacteristic(...)) add:
this.right.getCharacteristic(Characteristic.CurrentTemperature).setProps({minValue: -20, maxValue: 300});
This widens the HAP valid range to accommodate food probes reading cavity air or sensor extremes. The -20°C floor handles frozen food; the 300°C ceiling matches the thermostat.

Comment thread src/accessories/camera.ts
Comment on lines +100 to +122
public prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): void {
const socket = createSocket(request.addressVersion === 'ipv6' ? 'udp6' : 'udp4');
socket.on('error', error => this.platform.log.warn(`June camera RTCP socket error: ${error.message}`));
socket.bind(() => {
const localPort = socket.address().port;
const ssrc = this.platform.api.hap.CameraController.generateSynchronisationSource();
this.sessions.set(request.sessionID, {
socket,
targetAddress: request.targetAddress,
videoPort: request.video.port,
videoSsrc: ssrc,
videoSrtp: Buffer.concat([request.video.srtp_key, request.video.srtp_salt]).toString('base64'),
});
callback(undefined, {
video: {
port: localPort,
ssrc,
srtp_key: request.video.srtp_key,
srtp_salt: request.video.srtp_salt,
},
});
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Missing error callback: prepareStream never calls back when socket bind fails (bug)

In JuneCameraSource.prepareStream (camera.ts:100-122), socket.bind() is called with a success callback (lines 103-121) that invokes the HomeKit-provided callback. If createSocket() throws synchronously or socket.bind() fails (emitting 'error'), the HomeKit callback is never invoked. The error handler at line 102 only logs a warning. This causes the HomeKit streaming session to hang indefinitely with no timeout or recovery. The HomeKit callback MUST always be invoked (either with success or error) to complete the streaming negotiation handshake.

💡 Suggestion: Wrap createSocket in try/catch and call callback(err) on failure. In the socket 'error' handler (line 102), also call callback(new Error(...)) after logging. Wrap the bind success callback body in try/catch so any exception also results in callback being called with an error.

📋 Prompt for AI Agents

In src/accessories/camera.ts, modify prepareStream (lines 100-122):

  1. Wrap createSocket in try/catch: if it throws, call callback(err) and return.
  2. Change the socket 'error' handler at line 102 to also call callback(new Error(...)) after logging.
  3. Wrap the body of the bind success callback (lines 104-121) in try/catch so any exception also calls callback(err).
    This ensures the HomeKit streaming negotiation always completes (success or error) rather than hanging indefinitely.

Comment thread src/accessories/camera.ts Outdated
Comment on lines +88 to +152
const response = await fetch(snapshot.url, { signal: controller.signal });
clearTimeout(timer);
if (!response.ok) {
throw new Error(`snapshot fetch ${response.status}`);
}
callback(undefined, Buffer.from(await response.arrayBuffer()));
} catch (error) {
this.platform.log.warn(`June camera snapshot failed: ${(error as Error).message}`);
callback(undefined, PLACEHOLDER_JPEG);
}
}

public prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): void {
const socket = createSocket(request.addressVersion === 'ipv6' ? 'udp6' : 'udp4');
socket.on('error', error => this.platform.log.warn(`June camera RTCP socket error: ${error.message}`));
socket.bind(() => {
const localPort = socket.address().port;
const ssrc = this.platform.api.hap.CameraController.generateSynchronisationSource();
this.sessions.set(request.sessionID, {
socket,
targetAddress: request.targetAddress,
videoPort: request.video.port,
videoSsrc: ssrc,
videoSrtp: Buffer.concat([request.video.srtp_key, request.video.srtp_salt]).toString('base64'),
});
callback(undefined, {
video: {
port: localPort,
ssrc,
srtp_key: request.video.srtp_key,
srtp_salt: request.video.srtp_salt,
},
});
});
}

public handleStreamRequest(request: StreamingRequest, callback: StreamRequestCallback): void {
const { StreamRequestTypes } = this.platform.api.hap;
if (request.type === StreamRequestTypes.START) {
this.startStream(request, callback);
return;
}
if (request.type === StreamRequestTypes.STOP) {
this.stopSession(request.sessionID);
}
// RECONFIGURE is a no-op for a still-based source.
callback();
}

private startStream(request: StartStreamRequest, callback: StreamRequestCallback): void {
const session = this.sessions.get(request.sessionID);
const snapshot = this.client.latestSnapshot;
if (!session) {
callback(new Error('No prepared session'));
return;
}
if (!snapshot) {
this.platform.log.warn('June camera has no frame yet (no active cook) — cannot start live stream.');
callback(new Error('No camera frame available'));
return;
}
const { video } = request;
const args = [
'-loglevel', 'error',
'-loop', '1', '-re', '-i', snapshot.url,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 SSRF via unvalidated snapshot URL from WebSocket data (security)

In src/accessories/camera.ts, the snapshot.url value extracted from June WebSocket frames (message_code 10011 via parseCameraFrame in src/june-client.ts) is used without any validation in two dangerous sinks: (1) fetch(snapshot.url) at line 88 to retrieve the snapshot image, and (2) '-i', snapshot.url at line 152 passed to ffmpeg as input. Since the URL comes from external WebSocket data, an attacker who compromises that data stream could inject URLs targeting internal services (e.g., http://169.254.169.254/latest/meta-data/ for AWS metadata), local files (file:///etc/passwd), or internal network hosts. Neither the scheme nor the host are validated against expected June CDN/S3 domains.

💡 Suggestion: Validate the snapshot URL before use: ensure it uses the https:// scheme and its hostname matches an expected June image CDN domain or S3 bucket. Reject any URL with unexpected schemes (file://, http:// on non-local networks) or suspicious hostnames (localhost, 169.254.x.x, 10.x.x.x, etc.). The most targeted fix is to add validation in parseCameraFrame in src/june-client.ts so both call sites are protected.

📋 Prompt for AI Agents

In src/june-client.ts, modify the parseCameraFrame function (around lines 39-46) to validate the extracted URL before returning it: import the url module from Node.js, parse the candidate URL, reject on non-https schemes, and reject hostnames that resolve to private/localhost/multicast IPs. If validation fails, return null (which the camera will handle via the existing placeholder fallback). This protects both the fetch() sink in camera.ts:88 and the ffmpeg -i sink in camera.ts:152 with a single check at the parsing boundary.

Comment on lines +46 to +62
private async setOn(subtype: string, value: CharacteristicValue): Promise<void> {
const entry = this.services.get(subtype);
if (!entry) {
return;
}
const status = value
? await this.client.startMode(entry.mode.primitiveType, entry.mode.tempF)
: await this.client.cancel();
if (status !== 'success') {
entry.service.updateCharacteristic(this.platform.Characteristic.On, !value);
this.platform.log.warn(`June rejected ${entry.mode.label} command: ${status || 'no ack'}`);
return;
}
if (value) {
this.setAllOff(subtype);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Race condition between telemetry-driven setAllOff() and user-driven setOn() in mode switches (bug)

JuneModeSwitchAccessory registers handlers for both telemetry events (line 29) and HomeKit characteristic onSet callbacks (line 26). The setOn() method is async and awaits client.startMode() or client.cancel() at lines 52-53, yielding to the event loop. While suspended, a telemetry event carrying active===false (e.g., from a stale 10018 device-state frame) can fire synchronously via EventEmitter, invoking update() (line 32) which calls setAllOff() (line 34), writing Characteristic.On=false to every mode switch. When setOn() resumes after the command ack, the switch state it expects may have already been mutated by the telemetry handler. In particular: if the user turns ON mode A, telemetry sets it OFF during the await, then setOn succeeds and calls setAllOff(subtype) — restoring other switches but leaving A OFF even though the cook is active.

💡 Suggestion: Add a per-accessory guard flag to serialize the update() and setOn() critical sections. For example, use a commandInFlight boolean that setOn() sets before the await and clears in a finally block; update() checks this flag and defers state changes until the command completes.

📋 Prompt for AI Agents

In src/accessories/mode-switch.ts, protect the shared switch state:

  1. Add private fields: private commandInFlight = false; and private pendingActiveState: boolean | undefined;
  2. In setOn(), wrap the async body: set commandInFlight = true before the await, and in a finally block set commandInFlight = false then apply any stashed pendingActiveState.
  3. In update(), when commandInFlight is true, stash telemetry.active in pendingActiveState instead of calling setAllOff().
    This serializes access without blocking the event loop.

@keithah
keithah force-pushed the feature/expanded-homekit-features branch from 08a4c5f to 7f55f04 Compare July 8, 2026 22:17
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@keithah
keithah force-pushed the feature/expanded-homekit-features branch from 7f55f04 to 57f6138 Compare July 8, 2026 22:24
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

… the custom Config UI

The plugin uses a custom Config UI (customUi:true), so the schema 'form' is
ignored — the new opt-in features were not editable in the UI. Adds per-oven
controls (doorbell + triggers, camera + ffmpeg path, probe sensor names, and a
dynamic cook-mode list) matching the existing card pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@homebridge-ui/public/index.html`:
- Around line 288-297: The temperature field in modeRowHtml is missing
client-side bounds even though the config schema for tempF enforces a 100–550
range. Update the field-mode-temp input to include matching min and max
attributes so the UI validates the same constraints as the schema. Use the
modeRowHtml helper and the field-mode-temp control as the reference points when
making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7a885586-c4f3-4a93-976c-5f7ab2273426

📥 Commits

Reviewing files that changed from the base of the PR and between 16d8606 and 8057515.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • homebridge-ui/public/index.html
  • package.json
✅ Files skipped from review due to trivial changes (1)
  • package.json

Comment on lines +288 to +297
function modeRowHtml(mode, modeIndex) {
return `
<div class="mode-row" data-mode-index="${modeIndex}" style="display:flex; gap:0.5rem; margin-bottom:0.5rem;">
<input type="text" class="form-control field-mode-label" placeholder="Name (e.g. Broil)" value="${escapeHtml(mode.label ?? '')}">
<input type="text" class="form-control field-mode-type" placeholder="Mode id (e.g. broil)" value="${escapeHtml(mode.primitiveType ?? '')}">
<input type="number" class="form-control field-mode-temp" placeholder="&deg;F" style="max-width:6rem;" value="${mode.tempF ?? 350}">
<button class="btn btn-sm btn-outline-danger remove-mode" type="button">&times;</button>
</div>`;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add min/max attributes to the temperature input to match schema bounds.

The config schema defines tempF with "minimum": 100, "maximum": 550, but the field-mode-temp input has no min/max attributes, allowing out-of-range values. Adding them provides client-side validation consistent with the schema contract.

🛡️ Proposed fix
       <div class="mode-row" data-mode-index="${modeIndex}" style="display:flex; gap:0.5rem; margin-bottom:0.5rem;">
         <input type="text" class="form-control field-mode-label" placeholder="Name (e.g. Broil)" value="${escapeHtml(mode.label ?? '')}">
         <input type="text" class="form-control field-mode-type" placeholder="Mode id (e.g. broil)" value="${escapeHtml(mode.primitiveType ?? '')}">
-        <input type="number" class="form-control field-mode-temp" placeholder="&deg;F" style="max-width:6rem;" value="${mode.tempF ?? 350}">
+        <input type="number" class="form-control field-mode-temp" placeholder="&deg;F" min="100" max="550" style="max-width:6rem;" value="${mode.tempF ?? 350}">
         <button class="btn btn-sm btn-outline-danger remove-mode" type="button">&times;</button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function modeRowHtml(mode, modeIndex) {
return `
<div class="mode-row" data-mode-index="${modeIndex}" style="display:flex; gap:0.5rem; margin-bottom:0.5rem;">
<input type="text" class="form-control field-mode-label" placeholder="Name (e.g. Broil)" value="${escapeHtml(mode.label ?? '')}">
<input type="text" class="form-control field-mode-type" placeholder="Mode id (e.g. broil)" value="${escapeHtml(mode.primitiveType ?? '')}">
<input type="number" class="form-control field-mode-temp" placeholder="&deg;F" style="max-width:6rem;" value="${mode.tempF ?? 350}">
<button class="btn btn-sm btn-outline-danger remove-mode" type="button">&times;</button>
</div>`;
}
function modeRowHtml(mode, modeIndex) {
return `
<div class="mode-row" data-mode-index="${modeIndex}" style="display:flex; gap:0.5rem; margin-bottom:0.5rem;">
<input type="text" class="form-control field-mode-label" placeholder="Name (e.g. Broil)" value="${escapeHtml(mode.label ?? '')}">
<input type="text" class="form-control field-mode-type" placeholder="Mode id (e.g. broil)" value="${escapeHtml(mode.primitiveType ?? '')}">
<input type="number" class="form-control field-mode-temp" placeholder="&deg;F" min="100" max="550" style="max-width:6rem;" value="${mode.tempF ?? 350}">
<button class="btn btn-sm btn-outline-danger remove-mode" type="button">&times;</button>
</div>`;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@homebridge-ui/public/index.html` around lines 288 - 297, The temperature
field in modeRowHtml is missing client-side bounds even though the config schema
for tempF enforces a 100–550 range. Update the field-mode-temp input to include
matching min and max attributes so the UI validates the same constraints as the
schema. Use the modeRowHtml helper and the field-mode-temp control as the
reference points when making the change.

CameraController advertised an AAC-ELD codec but prepareStream returned no audio
block, so HAP threw 'Audio was enabled but not supplied in PrepareStreamResponse'
on every stream request, crash-looping the child bridge. The oven cam is
video-only; audio is optional in CameraStreamingOptions, so drop it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Previously ffmpeg looped the single still captured at stream-start, so the
tapped-in live view was frozen. Now feed the latest 10011 frame into ffmpeg via
image2pipe as new frames arrive (~1/s during a cook), so the live view updates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

The June oven has one probe. The telemetry array's {id:'left'} label implied a
possible 'right', but there's only one — the right sensor was dead weight.
Collapse to a single 'Food Probe' temperature sensor across client, accessory,
config, schema, and custom UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/accessories/probe-sensor.ts (1)

29-36: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Still missing setProps to widen CurrentTemperature valid range.

The previous review flagged that CurrentTemperature defaults to 0–100°C per HAP, but food probes can exceed this (oven air, high-temp cooking, sensor faults). The refactor to a single sensor didn't add the setProps call. Values above 100°C will be clamped or rejected by HomeKit.

🛡️ Proposed fix: widen CurrentTemperature range
    this.service = this.accessory.getService(Service.TemperatureSensor) || this.accessory.addService(Service.TemperatureSensor);
+   this.service.getCharacteristic(Characteristic.CurrentTemperature).setProps({ minValue: -20, maxValue: 300 });
    this.service.setCharacteristic(Characteristic.Name, cfg.name);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/accessories/probe-sensor.ts` around lines 29 - 36, The `ProbeSensor`
setup still needs to widen the HomeKit `CurrentTemperature` range before
publishing readings. In the constructor where `this.service` is created and
`Characteristic.Name` is set, add the missing `setProps` call on
`Characteristic.CurrentTemperature` so the `update` path can accept probe
temperatures above the default 0–100°C range. Use the existing `this.service`
initialization and `this.platform.Characteristic.CurrentTemperature` reference
to locate the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/accessories/probe-sensor.ts`:
- Around line 29-36: The `ProbeSensor` setup still needs to widen the HomeKit
`CurrentTemperature` range before publishing readings. In the constructor where
`this.service` is created and `Characteristic.Name` is set, add the missing
`setProps` call on `Characteristic.CurrentTemperature` so the `update` path can
accept probe temperatures above the default 0–100°C range. Use the existing
`this.service` initialization and
`this.platform.Characteristic.CurrentTemperature` reference to locate the fix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d99c45aa-21d2-4c50-a438-793131bd059a

📥 Commits

Reviewing files that changed from the base of the PR and between 3100969 and d7865c9.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • config.schema.json
  • homebridge-ui/public/index.html
  • package.json
  • src/accessories/camera.ts
  • src/accessories/probe-sensor.ts
  • src/june-client.test.ts
  • src/june-client.ts
  • src/protocol.test.ts
  • src/protocol.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/protocol.test.ts
  • src/june-client.test.ts
  • src/protocol.ts
  • src/accessories/camera.ts
  • homebridge-ui/public/index.html

@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@mvanhorn

mvanhorn commented Jul 9, 2026

Copy link
Copy Markdown

omg.. founder of June here.. this is awesome! Somehow found this by accident right now running last30days on June Oven. THANKS FOR MAKING THIS I'm @mvanhorn on x

@mvanhorn

mvanhorn commented Jul 9, 2026

Copy link
Copy Markdown

you made it possible for me to make a printing press skill for this (claude code / hermes / open claw) THANK YOU! mvanhorn/printing-press-library#1475

@cursor

cursor Bot commented Jul 10, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-07-08-doorbell-probe-polish.md`:
- Line 13: Change the “Task 1: Remove Unsupported Door-Open Trigger” heading
from level 3 to level 2 so it follows the document’s H1 title sequentially and
satisfies MD001.

In `@docs/superpowers/specs/2026-07-08-june-expanded-homekit-features-design.md`:
- Around line 14-16: Rename the “Cook-done doorbell” feature throughout the
specification, including the related section at lines 88-92, to reflect that it
supports both ready and done triggers; use an oven/cook-event doorbell name or
explicitly identify the ready trigger as a preheat notification.
- Around line 122-127: Update the probe sensor specification to match the
confirmed left/right payload contract: define separate left and right sensors,
configuration names such as probeSensors.leftName and probeSensors.rightName,
and per-side telemetry fields and accessory subscriptions. If aggregation is
intended instead, explicitly document that behavior and its naming and telemetry
semantics rather than using singular probe fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ba43c5f4-4e0a-4697-ad86-b18c839ba236

📥 Commits

Reviewing files that changed from the base of the PR and between d7865c9 and 2cbe2f5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • README.md
  • config.schema.json
  • docs/superpowers/plans/2026-07-08-doorbell-probe-polish.md
  • docs/superpowers/specs/2026-07-08-june-expanded-homekit-features-design.md
  • package.json
  • src/accessories/doorbell.ts
  • src/platform.test.ts
  • src/platform.ts
  • src/protocol.test.ts
  • src/protocol.ts
✅ Files skipped from review due to trivial changes (2)
  • package.json
  • README.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/protocol.test.ts
  • src/platform.ts
  • config.schema.json
  • src/accessories/doorbell.ts
  • src/protocol.ts


---

### Task 1: Remove Unsupported Door-Open Trigger

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the Markdown heading hierarchy sequential.

### Task 1 skips directly from the H1 title to H3. Change task headings to ## (or add an H2 section) to satisfy MD001 and preserve document navigation.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 13-13: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-07-08-doorbell-probe-polish.md` at line 13,
Change the “Task 1: Remove Unsupported Door-Open Trigger” heading from level 3
to level 2 so it follows the document’s H1 title sequentially and satisfies
MD001.

Source: Linters/SAST tools

Comment on lines +14 to +16
1. **Cook-done doorbell** — ships as a Doorbell, and becomes a Video Doorbell when the interior
camera is also enabled. Offered as an option alongside the existing ready/done sensors; users can
enable either, both, or neither.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Rename the feature to reflect both triggers.

This is called “Cook-done doorbell,” but ready is also a first-class trigger and fires before cooking completes. Rename it to an oven/cook-event doorbell or explicitly describe the ready trigger as a preheat notification.

Also applies to: 88-92

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-07-08-june-expanded-homekit-features-design.md`
around lines 14 - 16, Rename the “Cook-done doorbell” feature throughout the
specification, including the related section at lines 88-92, to reflect that it
supports both ready and done triggers; use an oven/cook-event doorbell name or
explicitly identify the ready trigger as a preheat notification.

Comment on lines +122 to +127
- Source: `10013` telemetry `sensor_data.probe` array from Spike B. The June oven has a single
food probe; the accessory keeps the last reported value between updates.
- Opt-in. Config (per oven): `probeSensors.enabled` (bool, default false). Optionally a
`probeSensors.name` display name.
- New `JuneTelemetry` fields (`probeC`, `probePresent`) populated in
`JuneClient.handleMessage` for `10013`; a `JuneProbeSensorAccessory` subscribes to telemetry.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Document separate left/right probe sensors.

The confirmed payload is keyed by id: "left"|"right" and the PR objective calls for left/right HomeKit temperature sensors, but this section describes one probe, one probeSensors.name, and singular probeC/probePresent fields. Align the spec with the per-side contract (leftName/rightName and per-side telemetry), or explicitly document an intentional aggregation. This overlaps the earlier probe-configuration review comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-07-08-june-expanded-homekit-features-design.md`
around lines 122 - 127, Update the probe sensor specification to match the
confirmed left/right payload contract: define separate left and right sensors,
configuration names such as probeSensors.leftName and probeSensors.rightName,
and per-side telemetry fields and accessory subscriptions. If aggregation is
intended instead, explicitly document that behavior and its naming and telemetry
semantics rather than using singular probe fields.

@keithah
keithah merged commit 3cd2228 into main Jul 10, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants