Skip to content

Add a device.get-battery-levels AI tool so the assistant can answer battery questions - #2898

Open
Pierre-Gilles wants to merge 2 commits into
masterfrom
claude/ai-battery-tool
Open

Add a device.get-battery-levels AI tool so the assistant can answer battery questions#2898
Pierre-Gilles wants to merge 2 commits into
masterfrom
claude/ai-battery-tool

Conversation

@Pierre-Gilles

Copy link
Copy Markdown
Contributor

Implements feature request: https://community.gladysassistant.com/t/concernant-lia-etat-des-piles/10354

This PR was opened by an automated Claude Code run and needs human review before merging.

Description

The forum request asks the AI to answer questions such as « donne moi l'état en % de toutes les piles » and « quel est l'état de la pile du capteur température salon ».

What was missing: battery levels are ordinary device features (DEVICE_FEATURE_CATEGORIES.BATTERY, type integer, unit percent), but the battery category is not part of sensorFeatures in server/services/mcp/lib/selectFeature.js. As a consequence the AI could not read them at all today: battery features never appear in the schema://home resource, they are absent from the device_type enum of device.get-state, and a device.get-state call filtered on them returns nothing. So neither of the two questions above could be answered — this is not a duplicate of an existing tool.

What this PR adds:

  • isBatteryFeature in selectFeature.js (alongside the other feature predicates) and wired on the MCPHandler prototype.
  • A new device.get-battery-levels MCP tool in buildSchemas.js, registered only when at least one device of the home reports a battery level, like device.set-shutter, device.set-light, sensor.set-state and device.get-energy-consumption are. Categories: device_query and other.
    • Optional device and room parameters (both zod enums built from the home), so the same tool serves the "all batteries" question (no parameter) and the "this one sensor" question.
    • Each entry carries the room, the device name, the feature name, the category, the value, the unit and the age of the value, through the shared formatValue helper — same shape as device.get-state, so the existing prompt rules about age apply unchanged.
    • Fresh values are read with device.getBySelector at call time, like device.get-state, instead of the cached list built when the tools are registered.
  • Error branches modelled on the neighbouring tools, all returning an explicit sentence instead of an empty list (an empty result is the case where a model invents a plausible value): unknown room (with the list of the real rooms, "No room" excluded), unknown device name (with the list of the devices that do report a level), and a scope with no battery powered device.
  • A prompt rule in server/config/prompts/aiChat.prompt.txt pointing the model at device_get_battery_levels for every battery question, telling it that devices absent from the result do not report a level, and what to say when the tool is not available.

Sorting choice: results are sorted by level ascending, lowest battery first, because what a user asking for the state of all their batteries actually wants is the ones to replace — and a model summarizing a long list answers from its head. Devices that have never reported a level (last_value null) are kept at the end rather than sorted as if they were 0.

Deliberately not done: the battery category was not added to isSensorFeature. That would have been the smaller diff, but every battery feature would then be added to the home schema and to every device.get-state answer, where it is noise for questions about temperature, lights or shutters, and it would also have made battery features writable through sensor.set-state for MQTT devices. Only battery (percentage) is covered; battery-low (binary) is left out of scope.

Forum

Forum: https://community.gladysassistant.com/t/concernant-lia-etat-des-piles/10354

Checklist

  • Tests pass: cd server && npm run coverage (Codecov requires 100% coverage on changed lines) and Cypress (npm run cypress:run) if the UI changed
  • Linter and prettier pass on both front and server (npm run eslint, npm run prettier)
  • No undocumented breaking change

Details on what was actually run:

  • npm_config_service=mcp npm run test-service → 144 passing, including 4 new tests: the sorted whole-home listing (with a device without a room and two devices that never reported a level), filtering by device and by room, the three error branches, and the tool not being registered when no device has a battery. Every existing mcpHandler mock in buildSchemas.test.js got isBatteryFeature added, since getAllTools now calls it.
  • c8 over the MCP suite: no uncovered line and no uncovered branch in the new code (selectFeature.js at 100%). The only lines c8 reports as uncovered in buildSchemas.js (798-801, a device.turn-on-off branch) are pre-existing and untouched.
  • npm run eslint in server/: 0 errors (only pre-existing warnings elsewhere in the repo, none in the touched files). npm run prettier-check: clean.
  • test/lib/gateway/gateway.forwardMessageToAiChat.test.js: 43 passing. Not run: the full server suite. test/lib/gateway/gateway.restoreBackupEvent.test.js has 2 failures, verified identical on an untouched checkout of master — unrelated to this change.
  • No front-end change, so Cypress was not run.

Generated by Claude Code

…uestions

Battery features (category `battery`) were not part of `isSensorFeature`, so
neither the `schema://home` resource nor `device.get-state` could see them: the
AI had no way to answer "donne moi l'état en % de toutes les piles" or "quel est
l'état de la pile du capteur température salon".

Add a dedicated `device.get-battery-levels` tool, registered only when at least
one device reports a battery level. It returns every battery feature with its
room, device name, level and value age, optionally filtered by device or by
room, sorted from the lowest level to the highest so the batteries to replace
come first. Devices that never reported a level are kept last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yxguVaLdJ8ZKmw5x3HePT
@github-actions github-actions Bot added area:server Node.js server code area:integration Services and integrations (server/services/**) type:feature New user-facing feature or improvement labels Aug 15, 2026
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Pierre-Gilles, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 97ac2d8e-0631-4aa6-9ab7-73408f43f244

📥 Commits

Reviewing files that changed from the base of the PR and between 7b5c639 and f3431f9.

📒 Files selected for processing (5)
  • server/config/prompts/aiChat.prompt.txt
  • server/services/mcp/lib/buildSchemas.js
  • server/services/mcp/lib/index.js
  • server/services/mcp/lib/selectFeature.js
  • server/test/services/mcp/lib/buildSchemas.test.js

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 15, 2026

Copy link
Copy Markdown

Deploying gladys-plus with  Cloudflare Pages  Cloudflare Pages

Latest commit: f3431f9
Status: ✅  Deploy successful!
Preview URL: https://2a634ff6.gladys-plus.pages.dev
Branch Preview URL: https://claude-ai-battery-tool.gladys-plus.pages.dev

View logs

@github-actions

Copy link
Copy Markdown
Contributor

🐳 A Docker image has been built for this branch and pushed to the GitHub Container Registry.

You can test this pull request (AMD64 only) by pulling the image below:

ghcr.io/gladysassistant/gladys-preview:claude-ai-battery-tool

For example, run it with:

sudo docker run -d \
  --log-driver json-file \
  --log-opt max-size=10m \
  --cgroupns=host \
  --restart=always \
  --privileged \
  --network=host \
  --name gladys-claude-ai-battery-tool \
  -e NODE_ENV=production \
  -e SERVER_PORT=80 \
  -e TZ=Europe/Paris \
  -e SQLITE_FILE_PATH=/var/lib/gladysassistant/gladys-production.db \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /var/lib/gladysassistant:/var/lib/gladysassistant \
  -v /dev:/dev \
  -v /run/udev:/run/udev:ro \
  ghcr.io/gladysassistant/gladys-preview:claude-ai-battery-tool

This comment and the image are automatically updated on every new commit pushed to this pull request.

Need an ARM64 image (Raspberry Pi, Apple Silicon, …)? Comment /build-arm64 on this pull request.

@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.51%. Comparing base (2f7ef52) to head (f3431f9).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff            @@
##           master    #2898    +/-   ##
========================================
  Coverage   99.51%   99.51%            
========================================
  Files        1235     1235            
  Lines       88064    88270   +206     
========================================
+ Hits        87638    87844   +206     
  Misses        426      426            

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cursor
cursor Bot requested a review from atrovato August 15, 2026 17:23
@Pierre-Gilles Pierre-Gilles added the needs:human-review Automated review is not confident, maintainer must take a look label Aug 15, 2026 — with Cursor

@cursor cursor 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.

Stale comment

This is a well-scoped read-only MCP tool for device piles (DEVICE_FEATURE_CATEGORIES.BATTERY). Keeping battery out of isSensorFeature is the right call: it would pollute every device.get-state answer and the home schema, and it would make MQTT battery features writable via sensor.set-state. No new categories or types. Tests cover listing, sort (null last), device/room filters, the three error branches, and not registering the tool when nothing reports a level.

Change requested: device.get-state was fixed in #2852 so a house name passed as room means the whole home. This callback copies that tool's unknown-room error but not the house fallback, even though the comment says it follows the same reasoning. Whole-home French phrasing ("piles de la maison") is exactly the production case #2852 hit. houses is already in scope — please reuse that branch and add a test.

The system-prompt rule should also be narrowed from "any question about batteries" so it does not steal EV (electrical-vehicle-battery) or home-storage (battery-storage) questions and then claim those devices do not report a level.

Not risk:high: additive MCP read path, no taxonomy/migration/auth change.

Asking for human review of the product choices (dedicated tool vs isSensorFeature, omitting battery-low, not using DEVICE_BATTERY_LEVEL_WARNING_THRESHOLD for "which batteries are low"). Same class as #2852 / #2853.

Residual, not blocking: the intent-router device_query blurb still does not mention battery levels (energy was added explicitly); a missing featureLastState after getBySelector can throw, same as device.get-state.

Open in Web View Automation 

Sent by Cursor Automation: Automatic PR review

Comment thread server/services/mcp/lib/buildSchemas.js Outdated
Comment thread server/config/prompts/aiChat.prompt.txt Outdated
Comment thread server/services/mcp/lib/buildSchemas.js
Handle a house name passed as `room`: the chat gateway calls the callback
with the raw arguments of the model, so "l'état des piles de la maison"
reached the tool as a room and was answered with "not a room of this home".
Fall back to the house, like `device.get-state` does since #2852, and keep
every battery device of its rooms plus the unassigned ones.

Surface `DEVICE_BATTERY_LEVEL_WARNING_THRESHOLD`, the battery warning
threshold of the instance already used by `device.checkBatteries` and the
weekly digest, together with a `below_warning_threshold` flag on each level,
so "which batteries are low" means the same thing in the AI chat as in the
rest of Gladys instead of being left to the model.

Scope the prompt rule and the tool description to battery powered devices:
`battery-storage` and `electrical-vehicle-battery` are separate categories
that this tool does not read, and a device that only publishes a binary low
battery alert has no level to report.

Autofix-Pass: 1

@cursor cursor 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.

Follow-up f3431f9 addresses the three changes requested on e6b8ae0b.

House name as room: the callback now mirrors device.get-state (#2852). If findBySimilarity(rooms, room) misses, it retries against houses and keeps every battery device of that house’s rooms plus unassigned devices. The unknown-room error is only returned when neither matches. Covered by should read device.get-battery-levels of a whole house passed as a room.

Prompt scope: the rule and the tool description are limited to battery-powered sensors/remotes/door sensors. EV (electrical-vehicle-battery) and home storage (battery-storage) are called out as different categories, and the result must not be used to answer them.

“Which batteries are low”: DEVICE_BATTERY_LEVEL_WARNING_THRESHOLD is read at call time. When it is a finite number, the payload carries warning_threshold and each numeric level gets below_warning_threshold (value < threshold, same comparison as device.checkBatteries / the weekly digest). Null levels are left unflagged. When the variable is unset, neither key is emitted. The prompt tells the model to use that flag instead of inventing a limit.

Keeping battery out of isSensorFeature is still the right call (it would pollute every device.get-state answer and make MQTT battery features writable via sensor.set-state). No new DEVICE_FEATURE_CATEGORIES / TYPES. Not risk:high: additive MCP read path, same class as #2852 / #2853. CI is green, including codecov/patch.

Keeping needs:human-review for the product choices a maintainer should still own: dedicated tool vs folding into device.get-state, and leaving battery-low out of this tool. atrovato is already requested.

Residuals, not blocking:

  • The intent-router device_query blurb still does not mention battery levels (energy was added explicitly). The tool is also tagged other, so a misclassify-as-other still keeps it in the list; only a classify-as-weather/scenes-only drop would hide it, and FORCE_TOOL_CHOICE would then not apply.
  • Unchecked featureLastState after getBySelector can throw if the feature disappeared — same as device.get-state.
  • EV / storage still have no dedicated tool. The prompt fallback “if this tool is not available, say that no device reports a battery level” can still misfire on an EV-only home; that is a follow-up, not a merge blocker for this feature request.
Open in Web View Automation 

Sent by Cursor Automation: Automatic PR review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:integration Services and integrations (server/services/**) area:server Node.js server code needs:human-review Automated review is not confident, maintainer must take a look type:feature New user-facing feature or improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants