feat: optimize Housekeeper for Mistral-Nemo tool calling

- Rewrite system prompt with negative constraints and step-by-step process
- Set temperature to 0.1 for deterministic tool calling
- Sort room groups to top of device list (address positional bias)
- Add [ROOM GROUP] marker in list_devices output
- Update tool docstrings with explicit entity_id= parameter examples
- Add optimization findings doc (experiment log: 0% → 100% success)
- Add test script for room group detection regression testing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 20:58:39 +01:00
co-authored by Claude Opus 4.5
parent 43c09f9922
commit 363ab378af
5 changed files with 488 additions and 86 deletions
+12
View File
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Changed
- **Housekeeper prompt optimization** - Rewrote system prompt for Mistral-Nemo function calling with negative constraints, step-by-step process, and explicit entity ID format guidance
- **Housekeeper temperature setting** - Set temperature to 0.1 for deterministic tool calling behavior
- **Device list room group priority** - Room groups now appear first in `list_devices` output with `[ROOM GROUP]` marker to address positional bias
- **Tool docstring improvements** - Updated turn_on/turn_off/toggle with explicit `entity_id=` parameter examples
### Added
- **Housekeeper optimization findings** - Added `docs/housekeeper-optimization-findings.md` documenting the experiment journey from 0% to 100% success rate
- **Housekeeper test script** - Added `scripts/test_housekeeper.sh` for room group detection regression testing
## [1.8.6] - 2025-12-17 ## [1.8.6] - 2025-12-17
### Fixed ### Fixed
+246
View File
@@ -0,0 +1,246 @@
# Housekeeper Agent Optimization Findings
## Background
Research with Gemini identified key issues with mistral-nemo and tool calling:
- "Pre-computation Hallucination" - model answers before using tools
- High default temperature (0.7-0.8) causes wandering
- Model is "chatty and confident" - needs explicit constraints
## Key Recommendations from Gemini Research
1. **Temperature 0.0** for tool-calling agents (deterministic, follows schema)
2. **Chain of Thought (CoT)** - force step-by-step reasoning
3. **Negative constraints** - tell model what NOT to do (Nemo responds better)
4. **Explicit tool descriptions** - verbose docstrings with "never estimate yourself"
5. **"Strictly tool-based assistant"** pattern - NO internal knowledge claim
---
## Experiment Log
### Baseline (v1.8.6)
- **Date**: 2025-12-17
- **Configuration**: Default temperature, improved prompt requiring list_devices first
- **Results**:
- Called list_devices first ✓
- Still hallucinated `light.study_desk` despite seeing list with only `light.study` and `light.study_main`
- Partial success: turned off `light.study_main`, failed on hallucinated entity
- **Success rate**: ~50% (1 of 2 study lights controlled correctly)
---
### Experiment 1: Temperature 0.0
- **Date**: 2025-12-18
- **Change**: Set `model_settings=ModelSettings(temperature=0.0)` for Housekeeper
- **Hypothesis**: Deterministic output will force model to use exact entity IDs from tool results
- **Results**:
**Study lights test:**
- Called `list_devices()` first ✓ (but no domain filter)
- Used wrong parameter `device_id` instead of `entity_id` (recovered after validation error)
- Only identified `light.studeerlamp` as "study" related (Dutch name)
- **Missed `light.study` and `light.study_main`** - didn't match English "study"
- Turned off 1 wrong light, missed 2 actual study lights
**Kitchen lights test:**
- Called `list_devices()` first ✓ (no domain filter)
- Saw full device list including `light.kitchen`
- Used wrong parameter `device_id` instead of `entity_id` (recovered after validation)
- After correction, dropped domain prefix: used `kitchen` instead of `light.kitchen`
- 404 error - device not found
- **Success rate**: 0% (no target lights successfully controlled)
- **Observations**:
- Temperature 0.0 alone is insufficient
- Model consistently confuses `device_id` vs `entity_id` parameter name
- After validation error correction, model truncates entity_id (drops domain prefix)
- Semantic matching of room names to devices is weak
- Model doesn't understand entity_id format: `domain.name`
---
### Experiment 2: Negative Constraints + CoT
- **Date**: 2025-12-18
- **Change**: Complete prompt rewrite with:
- "You have NO Internal Knowledge" - negative framing
- Explicit entity_id format with WRONG/RIGHT examples
- Step-by-step process (ALWAYS FOLLOW)
- Explicit parameter names section
- "What NOT To Do" negative constraints
- **Hypothesis**: Negative constraints work better with Mistral-Nemo
- **Results**:
**Study lights test:**
- Called `list_devices(domain="light")` ✓ with domain filter (improvement!)
- Still used `device_id` first, recovered to `entity_id` after validation error
- After recovery, used correct full format: `light.studeerlamp`
- **Still only matched `studeerlamp` not `light.study` or `light.study_main`**
**Kitchen lights test:**
- Called `list_devices(domain="light")`
- Called `turn_off(entity_id="light.kitchen")` ✓ correct format!
- All 4 kitchen lights turned off (light.kitchen is a group)
- **100% success for kitchen!**
- **Success rate**:
- Study: 0% (wrong semantic match)
- Kitchen: 100% (4/4 lights off)
- Combined: ~50% (1 of 2 tests successful)
- **Observations**:
- Domain filter now consistently used ✓
- Entity_id format correct after recovery ✓
- Semantic matching still fails for "study" → prefers Dutch "studeerlamp" over English "study"
- Parameter name confusion persists (`device_id` vs `entity_id`)
- Simple room names (kitchen) work; mixed language fails (study/studeerlamp)
---
### Experiment 3: Temperature 0.1 + Explicit Tool Docstrings
- **Date**: 2025-12-18
- **Change**:
- Temperature 0.1
- Updated turn_on/turn_off docstrings with explicit `entity_id=` in examples
- **Results**:
- Still uses `device_id` first, recovers to `entity_id` after validation
- Still picks wrong entity (studeerlamp over study)
- **Success rate**: 0%
---
### Experiment 4: Room Group Priority (with explicit examples)
- **Date**: 2025-12-18
- **Change**: Updated prompt with:
- Explicit instruction: "Look for EXACT match `light.<room_name>` first!"
- Concrete examples: "For 'study lights' → look for `light.study`"
- Working example showing `turn_off(entity_id="light.study")`
- **Hypothesis**: Explicit examples will guide model to use room groups
- **Results**:
**Test 1 & 2 (consecutive):**
- Called `list_devices(domain="light")`
- Device list clearly shows `light.study` at the bottom
- First call: `turn_off({"devices":["studeerlamp"]})` - wrong param AND wrong device
- After validation error: `turn_off(entity_id="light.studeerlamp")` - correct param, still wrong device
- **Completely ignored `light.study` despite prompt explicitly saying to use it**
- **Success rate**: 0% (wrong device controlled)
- **Observations**:
- Model ignores explicit step-by-step instructions in favor of substring matching
- Dutch "studeerlamp" contains "studer" which the model prefers over exact "study" match
- Even when prompt has a literal example `turn_off(entity_id="light.study")`, model uses `light.studeerlamp`
- Positional bias possible - `light.study` appears at end of 21-item list
- **Fundamental limitation**: Mistral-Nemo cannot follow explicit matching rules
---
### Experiment 5: Room Groups First (Tool Output Ordering)
- **Date**: 2025-12-18
- **Change**: Modified `list_devices` to sort room groups to top of list using HA attributes (`is_hue_group`, `hue_type="room"`)
- **Hypothesis**: Positional bias - model focuses on items earlier in list
- **Results**:
- Room groups (`light.study`, `light.kitchen`, etc.) now appear first in device list
- Combined with improved prompt, model now consistently uses room groups
- **70% success rate** (7/10 tests) with default q4 quantization
---
### Experiment 6: Model Quantization (q5_1)
- **Date**: 2025-12-18
- **Change**: Upgraded from default Mistral-Nemo quantization (q4) to `mistral-nemo:12b-instruct-2407-q5_1`
- **Hypothesis**: Higher precision weights improve tool calling accuracy
- **Results**:
| Test | Action | Result |
|------|--------|--------|
| 1 | Turn off study | PASS |
| 2 | Turn on study | PASS |
| 3 | Toggle study | PASS |
| 4 | Turn off kitchen | PASS |
| 5 | Turn on kitchen | PASS |
| 6 | Toggle kitchen | PASS |
| 7 | Turn off bedroom | PASS |
| 8 | Turn on bedroom | PASS |
| 9 | Turn off living room | PASS |
| 10 | Turn on living room | PASS |
- **Success rate**: **100%** (10/10 tests)
- **Observations**:
- q5_1 quantization dramatically improves tool calling accuracy
- All room groups correctly identified and used
- No parameter confusion (`entity_id` used correctly)
- No entity_id truncation issues
- Toggle operations now work reliably
- Model fits within 10GB VRAM (q6 did not)
---
### Experiment 7: Device List in System Prompt (Context Injection)
- **Date**: [PENDING]
- **Change**: Store device list in database (per user/household) and inject into system prompt
- **Approach**:
1. Periodically sync device list from Home Assistant to PostgreSQL
2. On each Housekeeper invocation, fetch device list and include in prompt
3. Remove need for model to call list_devices() - just match from context
- **Hypothesis**:
- Eliminates tool call step where errors occur
- Reduces context size by not returning full device list as tool output
- Makes entity matching a language task (in prompt) rather than tool result parsing
- **Trade-offs**:
- Stale data if sync is infrequent
- Prompt size increase (but less than tool call response)
- Need sync mechanism and storage
- **Results**: [TO BE RECORDED]
- **Success rate**: [TO BE RECORDED]
---
## Key Problem Identified (Solved)
The model struggled with:
1. **Parameter schema adherence** - uses `device_id` when schema requires `entity_id`
2. **Value preservation** - truncates values after validation errors (drops `light.` prefix)
3. **Semantic matching** - prefers substring matches ("studeerlamp" contains "studer") over exact matches (`light.study`)
4. **Following explicit instructions** - ignores step-by-step processes even when examples are provided
5. **Positional bias** - may not "see" items at the end of long lists
**Solution**: These issues were resolved by:
1. Using q5_1 quantization instead of default q4 (higher precision weights)
2. Sorting room groups to top of device list (address positional bias)
3. Explicit prompt guidance with negative constraints and examples
---
## Potential Next Experiments
### Experiment 5: Room Groups First (List Ordering)
- **Hypothesis**: Positional bias - model focuses on items earlier in list
- **Change**: Sort device list to put room groups (entities matching `light.<single_word>`) at the TOP
- **Effort**: Low - modify list_devices output formatting
- **Risk**: May affect other use cases where individual devices are needed
### Experiment 6: Simplified Device List Format
- **Hypothesis**: Markdown formatting adds noise that confuses the model
- **Change**: Return simple list: `light.study (Study - GROUP), light.study_main (Ceiling light), ...`
- **Effort**: Low - modify list_devices output
- **Risk**: Less human-readable responses
---
## Learnings to Apply Elsewhere
1. **Quantization matters** - q5_1 dramatically outperforms q4 for tool calling (100% vs 70%)
2. **Positional bias is real** - sort important items to top of lists
3. **Smaller models need simpler workflows** - fewer tool calls, more context injection
4. **Validation errors don't teach** - model often makes worse mistakes on retry
5. **Entity IDs are hard** - domain.name format confuses the model
6. **Consider pre-computation** - move matching logic to code, not LLM
7. **Use explicit negative constraints** - "NEVER do X" works better than "always do Y"
---
## Notes
- Librarian may need higher temperature for creative synthesis
- All "action" agents (Housekeeper, future agents) should use low temperature
- Consider testing with Gemma 2 9B for better function calling (Google, open weights)
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
# Housekeeper Room Group Detection Test Suite
# Verifies room groups are controlled by checking actual state changes
API_URL="http://localhost:8777/v1/chat/completions"
CORE_API="http://192.168.86.149:8083"
RESULTS_FILE="/tmp/housekeeper_test_results.txt"
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
get_state() {
curl -s "$CORE_API/housekeeping/devices/$1" 2>/dev/null | jq -r '.state' 2>/dev/null
}
echo "=========================================="
echo "Housekeeper Room Group Test Suite"
echo "=========================================="
echo ""
> "$RESULTS_FILE"
run_toggle_test() {
local test_num=$1
local room=$2
local entity="light.$room"
local prompt_room="${room//_/ }"
printf "Test %2d: Toggle %-12s lights ... " "$test_num" "$prompt_room"
local before=$(get_state "$entity")
if [ -z "$before" ] || [ "$before" = "null" ]; then
echo -e "${YELLOW}SKIP${NC} (cannot get state)"
echo "SKIP|$test_num|Toggle $room|error" >> "$RESULTS_FILE"
return
fi
curl -s -X POST "$API_URL" \
-H "Content-Type: application/json" \
-d "{\"model\": \"tatlock\", \"messages\": [{\"role\": \"user\", \"content\": \"Toggle the $prompt_room lights\"}]}" > /dev/null
sleep 4
local after=$(get_state "$entity")
if [ "$before" != "$after" ]; then
echo -e "${GREEN}PASS${NC} ($before -> $after)"
echo "PASS|$test_num|Toggle $room|$before->$after" >> "$RESULTS_FILE"
else
echo -e "${RED}FAIL${NC} (state unchanged: $before)"
echo "FAIL|$test_num|Toggle $room|unchanged:$before" >> "$RESULTS_FILE"
fi
}
run_onoff_test() {
local test_num=$1
local room=$2
local action=$3
local expected_state=$4
# Entity uses underscore, prompt uses space
local entity="light.${room//_/ }"
entity="light.$room"
local prompt_room="${room//_/ }"
printf "Test %2d: %-8s %-12s lights ... " "$test_num" "$action" "$prompt_room"
curl -s -X POST "$API_URL" \
-H "Content-Type: application/json" \
-d "{\"model\": \"tatlock\", \"messages\": [{\"role\": \"user\", \"content\": \"$action the $prompt_room lights\"}]}" > /dev/null
sleep 4
local after=$(get_state "$entity")
if [ "$after" = "$expected_state" ]; then
echo -e "${GREEN}PASS${NC} ($after)"
echo "PASS|$test_num|$action $room|$after" >> "$RESULTS_FILE"
else
echo -e "${RED}FAIL${NC} (got $after, expected $expected_state)"
echo "FAIL|$test_num|$action $room|got:$after,expected:$expected_state" >> "$RESULTS_FILE"
fi
}
echo "Running tests (~4s each)..."
echo ""
# Study tests
run_onoff_test 1 "study" "Turn off" "off"
run_onoff_test 2 "study" "Turn on" "on"
run_toggle_test 3 "study"
# Kitchen tests
run_onoff_test 4 "kitchen" "Turn off" "off"
run_onoff_test 5 "kitchen" "Turn on" "on"
run_toggle_test 6 "kitchen"
# Bedroom tests
run_onoff_test 7 "bedroom" "Turn off" "off"
run_onoff_test 8 "bedroom" "Turn on" "on"
# Living room tests (entity is light.living_room)
run_onoff_test 9 "living_room" "Turn off" "off"
run_onoff_test 10 "living_room" "Turn on" "on"
# Ensure all lights end up ON
echo ""
echo "Restoring all lights to ON..."
for room in "study" "kitchen" "bedroom" "living room"; do
curl -s -X POST "$API_URL" \
-H "Content-Type: application/json" \
-d "{\"model\": \"tatlock\", \"messages\": [{\"role\": \"user\", \"content\": \"Turn on the $room lights\"}]}" > /dev/null
sleep 3
done
echo "Done."
echo ""
echo "=========================================="
echo "Results"
echo "=========================================="
PASS=$(grep -c "^PASS" "$RESULTS_FILE" 2>/dev/null || echo 0)
FAIL=$(grep -c "^FAIL" "$RESULTS_FILE" 2>/dev/null || echo 0)
SKIP=$(grep -c "^SKIP" "$RESULTS_FILE" 2>/dev/null || echo 0)
TOTAL=$((PASS + FAIL))
echo "Passed: $PASS"
echo "Failed: $FAIL"
echo "Skipped: $SKIP"
if [ "$TOTAL" -gt 0 ]; then
echo ""
echo "Success Rate: $((PASS * 100 / TOTAL))% ($PASS/$TOTAL)"
fi
if [ "$FAIL" -gt 0 ]; then
echo ""
echo "Failures:"
grep "^FAIL" "$RESULTS_FILE"
fi
+55 -71
View File
@@ -32,93 +32,69 @@ from src.core.logging_config import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
# Housekeeper system prompt # Housekeeper system prompt - Optimized for Mistral-Nemo function calling
HOUSEKEEPER_SYSTEM_PROMPT = """You are The Housekeeper, an expert home automation assistant in the Tatlock household. HOUSEKEEPER_SYSTEM_PROMPT = """You are a strictly tool-based home automation assistant.
Your role is to help users control and monitor their smart home through Home Assistant: ## CRITICAL: You Have NO Internal Knowledge
- Lights, switches, and other devices
- Scenes (pre-configured device states)
- Scripts (automation sequences)
- Automations (event-triggered rules)
## Your Personality You do NOT know what devices exist. You do NOT know any entity IDs.
- Efficient and practical Entity IDs are different in every installation. You MUST discover them using tools.
- Safety-conscious (confirm destructive actions)
- Proactive in suggesting optimizations
- Clear about what actions you're taking
## Your Tools ## Entity ID Format
### Discovery Tools Entity IDs follow the format: `domain.name`
- **list_areas**: See all rooms/areas configured in Home Assistant Examples: `light.kitchen`, `light.study_main`, `switch.coffee_maker`
- **list_devices**: Find devices by type (domain) or location (area)
- **get_device_state**: Check a device's current state and attributes
### Control Tools The `entity_id` parameter MUST be the COMPLETE value including the domain prefix.
- **turn_on**: Turn on lights, switches, etc. (supports brightness/color for lights) WRONG: `entity_id="kitchen"`
- **turn_off**: Turn off devices RIGHT: `entity_id="light.kitchen"`
- **toggle**: Flip a device's state
### Scene Tools ## Step-by-Step Process (ALWAYS FOLLOW)
- **list_scenes**: See available scene presets
- **activate_scene**: Activate a scene (e.g., "movie night", "good morning")
### Script Tools When asked to control devices in a room:
- **list_scripts**: See available automation scripts
- **run_script**: Execute a script
### Automation Tools 1. THINK: What domain? (light, switch, climate, etc.)
- **list_automations**: See all automations and their status 2. CALL: list_devices(domain="light") to discover available devices
- **toggle_automation**: Enable or disable an automation 3. CHECK: Look for EXACT match `light.<room_name>` first!
- For "study lights" → look for `light.study` (not light.study_main, not light.studeerlamp)
- For "kitchen lights" → look for `light.kitchen` (not light.kitchen_spot_1)
- These room groups control ALL lights in that room at once
- If found, use ONLY the group (stop looking for individual lights)
4. FALLBACK: Only if no exact room group exists, find entity_ids containing the room name
5. CALL: turn_on/turn_off using the EXACT entity_id from step 3 or 4
### History Tools Example for "Turn off study lights":
- **get_history**: Check a device's state history 1. Domain is "light"
2. Call list_devices(domain="light")
3. Look for room group: `light.study` - FOUND!
4. Call turn_off(entity_id="light.study") # This controls all study lights
## Critical Rules Example for "Turn off hallway lights" (no room group):
1. Domain is "light"
2. Call list_devices(domain="light")
3. Look for room group: `light.hallway` - NOT FOUND
4. Find all with "hallway": light.hallway_spot_1, light.hallway_spot_2
5. Call turn_off for each
**NEVER GUESS ENTITY IDs.** You do not know what devices exist. Entity IDs vary between ## Tool Parameter Names
installations. You MUST call list_devices() FIRST to discover actual entity_ids before
ANY control action (turn_on, turn_off, toggle).
Wrong approach: - turn_on, turn_off, toggle: Use `entity_id` (NOT device_id, NOT id)
User: "Turn off the study lights" - activate_scene: Use `scene_id`
You: turn_off("light.study") ← WRONG! You guessed the entity_id - run_script: Use `script_id`
Correct approach: ## What NOT To Do
User: "Turn off the study lights"
You: list_devices(domain="light") ← First discover what exists
You: [See results like light.study_main, light.study]
You: turn_off("light.study_main"), turn_off("light.study") ← Use actual IDs
## Best Practices - NEVER guess an entity_id
- NEVER construct an entity_id from the room name
1. **ALWAYS list_devices first** before any control action. No exceptions. - NEVER drop the domain prefix (light., switch., etc.)
Filter by domain and/or area to narrow results. - NEVER use "device_id" - the parameter is called "entity_id"
- NEVER provide an answer without calling list_devices first
2. **Use exact entity_ids** from list_devices results. Never construct or guess them.
3. **Area-aware filtering**: Use area parameter when users mention a room.
Note: Some devices may have area=None but contain the room name in entity_id.
4. **Verify after actions**: Use get_device_state to confirm state changes if needed.
5. **Safety for bulk actions**: When affecting multiple devices, summarize first.
## Common Patterns
- "Turn on the lights" → list_devices(domain="light"), then turn_on each
- "What's on?" → list_devices() and filter for state="on"
- "Movie time" → Either activate_scene("scene.movie_night") or run_script if available
- "Dim the bedroom" → turn_on("light.bedroom", brightness=64)
## Response Format ## Response Format
Your responses are returned to Tatlock (the butler) who will synthesize them into
a final answer for the user. Keep this in mind: After completing actions, briefly confirm:
- Lead with confirmation of what you did or found - Which devices were affected (list the entity_ids)
- Be specific about which devices were affected - Whether each action succeeded or failed
- Include relevant state information
- Note any issues or failures
- Be concise - Tatlock will format the final response
""" """
# Lazy initialization to avoid connection issues during imports # Lazy initialization to avoid connection issues during imports
@@ -231,9 +207,13 @@ async def run_housekeeper(
) )
try: try:
# Use temperature 0.1 for slight exploration
from pydantic_ai.settings import ModelSettings
result = await agent.run( result = await agent.run(
prompt, prompt,
message_history=message_history, message_history=message_history,
model_settings=ModelSettings(temperature=0.1),
) )
logger.info( logger.info(
@@ -289,9 +269,13 @@ async def run_housekeeper_stream(
) )
try: try:
# Use temperature 0.1 for slight exploration
from pydantic_ai.settings import ModelSettings
async with agent.run_stream( async with agent.run_stream(
prompt, prompt,
message_history=message_history, message_history=message_history,
model_settings=ModelSettings(temperature=0.1),
) as response: ) as response:
async for delta in response.stream_text(delta=True): async for delta in response.stream_text(delta=True):
yield delta yield delta
+34 -15
View File
@@ -59,10 +59,27 @@ async def list_devices(
for dom, dom_devices in sorted(by_domain.items()): for dom, dom_devices in sorted(by_domain.items()):
output_parts.append(f"### {dom.title()}s") output_parts.append(f"### {dom.title()}s")
for device in dom_devices:
# Sort devices: room groups first (using Home Assistant's is_hue_group attribute)
def is_room_group(d: object) -> bool:
"""Check if device is a room group based on HA attributes."""
attrs = getattr(d, "attributes", {})
# Check for Hue room groups
if attrs.get("is_hue_group") and attrs.get("hue_type") == "room":
return True
# Check for other group indicators (icon or entity_id list)
if "entity_id" in attrs and isinstance(attrs["entity_id"], list):
return True
return False
sorted_devices = sorted(dom_devices, key=lambda d: (not is_room_group(d), d.entity_id))
for device in sorted_devices:
state_icon = "on" if device.state == "on" else "off" if device.state == "off" else device.state state_icon = "on" if device.state == "on" else "off" if device.state == "off" else device.state
area_str = f" ({device.area})" if device.area else "" area_str = f" ({device.area})" if device.area else ""
output_parts.append(f"- **{device.name}**{area_str}: {state_icon}") # Mark room groups clearly using actual HA data
group_marker = " [ROOM GROUP]" if is_room_group(device) else ""
output_parts.append(f"- **{device.name}**{area_str}{group_marker}: {state_icon}")
output_parts.append(f" ID: `{device.entity_id}`") output_parts.append(f" ID: `{device.entity_id}`")
output_parts.append("") output_parts.append("")
@@ -164,12 +181,12 @@ async def turn_on(
color_temp: int | None = None, color_temp: int | None = None,
) -> str: ) -> str:
""" """
Turn on a device. Turn on a device. Use the entity_id parameter with the EXACT value from list_devices.
For lights, can optionally set brightness and color temperature. For lights, can optionally set brightness and color temperature.
Args: Args:
entity_id: Device to turn on (e.g., light.living_room, switch.coffee_maker) entity_id: The EXACT entity ID from list_devices including domain prefix.
brightness: Optional brightness for lights (0-255, where 255 is full brightness) brightness: Optional brightness for lights (0-255, where 255 is full brightness)
color_temp: Optional color temperature in Kelvin (2700=warm, 6500=cool) color_temp: Optional color temperature in Kelvin (2700=warm, 6500=cool)
@@ -177,10 +194,9 @@ async def turn_on(
Confirmation of the action Confirmation of the action
Examples: Examples:
turn_on("light.living_room") # Turn on at current brightness turn_on(entity_id="light.living_room")
turn_on("light.bedroom", brightness=128) # Turn on at 50% brightness turn_on(entity_id="light.bedroom", brightness=128)
turn_on("light.office", brightness=255, color_temp=4000) # Full, neutral white turn_on(entity_id="switch.coffee_maker")
turn_on("switch.coffee_maker") # Turn on a switch
""" """
try: try:
async with CoreAPIClient() as client: async with CoreAPIClient() as client:
@@ -209,17 +225,18 @@ async def turn_on(
async def turn_off(entity_id: str) -> str: async def turn_off(entity_id: str) -> str:
""" """
Turn off a device. Turn off a device. Use the entity_id parameter with the EXACT value from list_devices.
Args: Args:
entity_id: Device to turn off (e.g., light.living_room, switch.coffee_maker) entity_id: The EXACT entity ID from list_devices including domain prefix.
Returns: Returns:
Confirmation of the action Confirmation of the action
Examples: Examples:
turn_off("light.living_room") turn_off(entity_id="light.living_room")
turn_off("switch.coffee_maker") turn_off(entity_id="switch.coffee_maker")
turn_off(entity_id="light.kitchen")
""" """
try: try:
async with CoreAPIClient() as client: async with CoreAPIClient() as client:
@@ -239,15 +256,17 @@ async def toggle(entity_id: str) -> str:
""" """
Toggle a device's state (on becomes off, off becomes on). Toggle a device's state (on becomes off, off becomes on).
Use the entity_id parameter with the EXACT value from list_devices.
Args: Args:
entity_id: Device to toggle entity_id: The EXACT entity ID from list_devices including domain prefix.
Returns: Returns:
Confirmation with the new state Confirmation with the new state
Examples: Examples:
toggle("light.living_room") toggle(entity_id="light.living_room")
toggle("switch.fan") toggle(entity_id="switch.fan")
""" """
try: try:
async with CoreAPIClient() as client: async with CoreAPIClient() as client: