Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5d50dda77 |
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.8.6] - 2025-12-17
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Housekeeper API paths** - Updated all client endpoints to use `/housekeeping/` prefix to match core-api routes
|
||||||
|
- **Housekeeper entity hallucination** - Improved system prompt with critical rule requiring `list_devices()` before any control action to prevent guessing entity IDs
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Housekeeping API spec** - Added `docs/housekeeping-api-spec.md` documenting the core-api home automation interface
|
||||||
|
|
||||||
## [1.8.5] - 2025-12-16
|
## [1.8.5] - 2025-12-16
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
# Home Automation API Interface Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document specifies the expected endpoints for a home automation abstraction layer in core-api. These endpoints will be consumed by the Tatlock Housekeeper agent and potentially other projects (scheduler, dashboards).
|
||||||
|
|
||||||
|
The goal is to provide a simplified, domain-specific interface for home automation that abstracts away the underlying platform (initially Home Assistant, but swappable).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### Device Discovery
|
||||||
|
|
||||||
|
#### `GET /housekeeping/devices`
|
||||||
|
|
||||||
|
List available devices.
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `domain` (optional): Filter by device type (e.g., `light`, `switch`, `climate`, `media_player`)
|
||||||
|
- `area` (optional): Filter by area/room name
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"entity_id": "light.living_room",
|
||||||
|
"name": "Living Room Light",
|
||||||
|
"domain": "light",
|
||||||
|
"area": "Living Room",
|
||||||
|
"state": "on",
|
||||||
|
"attributes": {
|
||||||
|
"brightness": 255,
|
||||||
|
"color_temp": 370
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `GET /housekeeping/devices/{entity_id}`
|
||||||
|
|
||||||
|
Get detailed state of a specific device.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"entity_id": "light.living_room",
|
||||||
|
"name": "Living Room Light",
|
||||||
|
"domain": "light",
|
||||||
|
"area": "Living Room",
|
||||||
|
"state": "on",
|
||||||
|
"attributes": {
|
||||||
|
"brightness": 255,
|
||||||
|
"color_temp": 370,
|
||||||
|
"supported_features": ["brightness", "color_temp"]
|
||||||
|
},
|
||||||
|
"last_changed": "2025-12-16T10:30:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `GET /housekeeping/areas`
|
||||||
|
|
||||||
|
List all areas/rooms.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"areas": [
|
||||||
|
{"id": "living_room", "name": "Living Room"},
|
||||||
|
{"id": "bedroom", "name": "Bedroom"},
|
||||||
|
{"id": "kitchen", "name": "Kitchen"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Device Control
|
||||||
|
|
||||||
|
#### `POST /housekeeping/devices/{entity_id}/control`
|
||||||
|
|
||||||
|
Control a device (turn on, turn off, toggle, or set attributes).
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "turn_on",
|
||||||
|
"brightness": 128,
|
||||||
|
"color_temp": 400
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `action` (required): One of `turn_on`, `turn_off`, `toggle`
|
||||||
|
- Additional attributes vary by device type (brightness, color_temp, rgb_color, etc.)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"entity_id": "light.living_room",
|
||||||
|
"new_state": "on",
|
||||||
|
"message": "Light turned on"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scenes
|
||||||
|
|
||||||
|
#### `GET /housekeeping/scenes`
|
||||||
|
|
||||||
|
List available scenes.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scenes": [
|
||||||
|
{"id": "scene.movie_night", "name": "Movie Night"},
|
||||||
|
{"id": "scene.good_morning", "name": "Good Morning"},
|
||||||
|
{"id": "scene.all_off", "name": "All Off"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `POST /housekeeping/scenes/{scene_id}/activate`
|
||||||
|
|
||||||
|
Activate a scene.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"scene_id": "scene.movie_night",
|
||||||
|
"message": "Scene activated"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
#### `GET /housekeeping/scripts`
|
||||||
|
|
||||||
|
List available scripts/sequences.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": [
|
||||||
|
{"id": "script.bedtime_routine", "name": "Bedtime Routine"},
|
||||||
|
{"id": "script.welcome_home", "name": "Welcome Home"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `POST /housekeeping/scripts/{script_id}/run`
|
||||||
|
|
||||||
|
Execute a script with optional variables.
|
||||||
|
|
||||||
|
**Request Body (optional):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"variables": {
|
||||||
|
"brightness_level": 50,
|
||||||
|
"target_room": "bedroom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"script_id": "script.bedtime_routine",
|
||||||
|
"message": "Script executed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Automations
|
||||||
|
|
||||||
|
#### `GET /housekeeping/automations`
|
||||||
|
|
||||||
|
List automations and their enabled/disabled status.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"automations": [
|
||||||
|
{
|
||||||
|
"id": "automation.motion_lights",
|
||||||
|
"name": "Motion Lights",
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "automation.night_mode",
|
||||||
|
"name": "Night Mode",
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `POST /housekeeping/automations/{automation_id}/toggle`
|
||||||
|
|
||||||
|
Enable or disable an automation.
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"automation_id": "automation.motion_lights",
|
||||||
|
"enabled": true,
|
||||||
|
"message": "Automation enabled"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Utility
|
||||||
|
|
||||||
|
#### `GET /housekeeping/history`
|
||||||
|
|
||||||
|
Get state history for a device.
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `entity_id` (required): Device to get history for
|
||||||
|
- `hours` (optional, default 24): Hours of history to retrieve
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"entity_id": "light.living_room",
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"state": "on",
|
||||||
|
"timestamp": "2025-12-16T10:30:00Z",
|
||||||
|
"attributes": {"brightness": 255}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": "off",
|
||||||
|
"timestamp": "2025-12-16T08:00:00Z",
|
||||||
|
"attributes": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### `GET /housekeeping/health`
|
||||||
|
|
||||||
|
Health check for home automation connection.
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"connected": true,
|
||||||
|
"platform": "home_assistant",
|
||||||
|
"version": "2024.12.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
All endpoints should return consistent error responses:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": true,
|
||||||
|
"code": "DEVICE_NOT_FOUND",
|
||||||
|
"message": "Device light.nonexistent not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Common error codes:
|
||||||
|
- `DEVICE_NOT_FOUND` - Entity ID doesn't exist
|
||||||
|
- `INVALID_ACTION` - Unsupported action for device type
|
||||||
|
- `CONNECTION_ERROR` - Cannot reach home automation platform
|
||||||
|
- `UNAUTHORIZED` - Invalid or missing credentials
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All endpoints require authentication via Bearer token in the `Authorization` header.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consuming Client
|
||||||
|
|
||||||
|
The Tatlock project has an existing client (`src/agents/housekeeper/client.py`) that expects these endpoints. No changes to Tatlock are needed once these endpoints are available.
|
||||||
|
|
||||||
|
Reference: `CoreAPIClient` class in Tatlock expects these exact endpoint patterns.
|
||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tatlock"
|
name = "tatlock"
|
||||||
version = "1.8.5"
|
version = "1.8.6"
|
||||||
description = "OpenAI-compatible API with Ollama backend"
|
description = "OpenAI-compatible API with Ollama backend"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|||||||
@@ -74,21 +74,35 @@ Your role is to help users control and monitor their smart home through Home Ass
|
|||||||
### History Tools
|
### History Tools
|
||||||
- **get_history**: Check a device's state history
|
- **get_history**: Check a device's state history
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
**NEVER GUESS ENTITY IDs.** You do not know what devices exist. Entity IDs vary between
|
||||||
|
installations. You MUST call list_devices() FIRST to discover actual entity_ids before
|
||||||
|
ANY control action (turn_on, turn_off, toggle).
|
||||||
|
|
||||||
|
Wrong approach:
|
||||||
|
User: "Turn off the study lights"
|
||||||
|
You: turn_off("light.study") ← WRONG! You guessed the entity_id
|
||||||
|
|
||||||
|
Correct approach:
|
||||||
|
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
|
## Best Practices
|
||||||
|
|
||||||
1. **Device Discovery First**: If the user asks about devices without being specific,
|
1. **ALWAYS list_devices first** before any control action. No exceptions.
|
||||||
use list_devices to find what's available before acting.
|
Filter by domain and/or area to narrow results.
|
||||||
|
|
||||||
2. **Confirm State After Actions**: After turning something on/off, you can verify
|
2. **Use exact entity_ids** from list_devices results. Never construct or guess them.
|
||||||
with get_device_state if needed.
|
|
||||||
|
|
||||||
3. **Use Entity IDs**: Devices are identified by entity_id (e.g., light.living_room).
|
3. **Area-aware filtering**: Use area parameter when users mention a room.
|
||||||
Always use the exact entity_id from list_devices.
|
Note: Some devices may have area=None but contain the room name in entity_id.
|
||||||
|
|
||||||
4. **Area-Aware**: When users say "living room lights", filter by area="living_room".
|
4. **Verify after actions**: Use get_device_state to confirm state changes if needed.
|
||||||
|
|
||||||
5. **Safety**: For actions affecting multiple devices or automations, summarize
|
5. **Safety for bulk actions**: When affecting multiple devices, summarize first.
|
||||||
what you're about to do.
|
|
||||||
|
|
||||||
## Common Patterns
|
## Common Patterns
|
||||||
|
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ class CoreAPIClient:
|
|||||||
|
|
||||||
logger.debug("core_api_list_devices", domain=domain, area=area)
|
logger.debug("core_api_list_devices", domain=domain, area=area)
|
||||||
|
|
||||||
response = await client.get("/devices", params=params or None)
|
response = await client.get("/housekeeping/devices", params=params or None)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -199,7 +199,7 @@ class CoreAPIClient:
|
|||||||
|
|
||||||
logger.debug("core_api_list_areas")
|
logger.debug("core_api_list_areas")
|
||||||
|
|
||||||
response = await client.get("/areas")
|
response = await client.get("/housekeeping/areas")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -219,7 +219,7 @@ class CoreAPIClient:
|
|||||||
|
|
||||||
logger.debug("core_api_get_state", entity_id=entity_id)
|
logger.debug("core_api_get_state", entity_id=entity_id)
|
||||||
|
|
||||||
response = await client.get(f"/entities/{entity_id}")
|
response = await client.get(f"/housekeeping/devices/{entity_id}")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
return DeviceState(**response.json())
|
return DeviceState(**response.json())
|
||||||
@@ -260,7 +260,7 @@ class CoreAPIClient:
|
|||||||
logger.info("core_api_turn_on", entity_id=entity_id, payload=payload)
|
logger.info("core_api_turn_on", entity_id=entity_id, payload=payload)
|
||||||
|
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"/devices/{entity_id}/control",
|
f"/housekeeping/devices/{entity_id}/control",
|
||||||
json=payload,
|
json=payload,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@@ -288,7 +288,7 @@ class CoreAPIClient:
|
|||||||
logger.info("core_api_turn_off", entity_id=entity_id)
|
logger.info("core_api_turn_off", entity_id=entity_id)
|
||||||
|
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"/devices/{entity_id}/control",
|
f"/housekeeping/devices/{entity_id}/control",
|
||||||
json={"action": "turn_off"},
|
json={"action": "turn_off"},
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@@ -316,7 +316,7 @@ class CoreAPIClient:
|
|||||||
logger.info("core_api_toggle", entity_id=entity_id)
|
logger.info("core_api_toggle", entity_id=entity_id)
|
||||||
|
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"/devices/{entity_id}/control",
|
f"/housekeeping/devices/{entity_id}/control",
|
||||||
json={"action": "toggle"},
|
json={"action": "toggle"},
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@@ -344,7 +344,7 @@ class CoreAPIClient:
|
|||||||
|
|
||||||
logger.debug("core_api_list_scenes")
|
logger.debug("core_api_list_scenes")
|
||||||
|
|
||||||
response = await client.get("/scenes")
|
response = await client.get("/housekeeping/scenes")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -364,7 +364,7 @@ class CoreAPIClient:
|
|||||||
|
|
||||||
logger.info("core_api_activate_scene", scene_id=scene_id)
|
logger.info("core_api_activate_scene", scene_id=scene_id)
|
||||||
|
|
||||||
response = await client.post(f"/scenes/{scene_id}/activate")
|
response = await client.post(f"/housekeeping/scenes/{scene_id}/activate")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -390,7 +390,7 @@ class CoreAPIClient:
|
|||||||
|
|
||||||
logger.debug("core_api_list_scripts")
|
logger.debug("core_api_list_scripts")
|
||||||
|
|
||||||
response = await client.get("/scripts")
|
response = await client.get("/housekeeping/scripts")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -420,7 +420,7 @@ class CoreAPIClient:
|
|||||||
logger.info("core_api_run_script", script_id=script_id)
|
logger.info("core_api_run_script", script_id=script_id)
|
||||||
|
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"/scripts/{script_id}/run",
|
f"/housekeeping/scripts/{script_id}/run",
|
||||||
json=payload or None,
|
json=payload or None,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@@ -448,7 +448,7 @@ class CoreAPIClient:
|
|||||||
|
|
||||||
logger.debug("core_api_list_automations")
|
logger.debug("core_api_list_automations")
|
||||||
|
|
||||||
response = await client.get("/automations")
|
response = await client.get("/housekeeping/automations")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -478,7 +478,7 @@ class CoreAPIClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"/automations/{automation_id}/toggle",
|
f"/housekeeping/automations/{automation_id}/toggle",
|
||||||
json={"enable": enable},
|
json={"enable": enable},
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@@ -515,7 +515,7 @@ class CoreAPIClient:
|
|||||||
logger.debug("core_api_get_history", entity_id=entity_id, hours=hours)
|
logger.debug("core_api_get_history", entity_id=entity_id, hours=hours)
|
||||||
|
|
||||||
response = await client.get(
|
response = await client.get(
|
||||||
"/history",
|
"/housekeeping/history",
|
||||||
params={"entity_id": entity_id, "hours": hours},
|
params={"entity_id": entity_id, "hours": hours},
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@@ -536,7 +536,7 @@ class CoreAPIClient:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = self._ensure_client()
|
client = self._ensure_client()
|
||||||
response = await client.get("/health")
|
response = await client.get("/housekeeping/health")
|
||||||
return response.status_code == 200
|
return response.status_code == 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("core_api_health_check_failed", error=str(e))
|
logger.warning("core_api_health_check_failed", error=str(e))
|
||||||
|
|||||||
Reference in New Issue
Block a user