The AFA Systems Presence Detection API provides RESTful endpoints for managing beacons, settings, and real-time WebSocket communication for live updates.
http://localhost:8080
Currently, the API does not implement authentication. This should be added for production deployments.
Retrieves a list of all registered beacons with their current status and location information.
GET /api/beacons
Response:
{
"beacons": [
{
"name": "Conference Room Beacon",
"beacon_id": "beacon_001",
"beacon_type": "ingics",
"beacon_location": "conference_room",
"last_seen": 1703078400,
"distance": 2.5,
"location_confidence": 85,
"hs_button_counter": 42,
"hs_button_battery": 85,
"hs_button_random": "abc123",
"hs_button_mode": "normal"
}
]
}
Registers a new beacon in the system.
POST /api/beacons
Content-Type: application/json
Request Body:
{
"name": "Meeting Room Beacon",
"beacon_id": "beacon_002",
"beacon_type": "eddystone",
"beacon_location": "meeting_room",
"hs_button_counter": 0,
"hs_button_battery": 100
}
Response:
{
"message": "Beacon created successfully",
"beacon": {
"name": "Meeting Room Beacon",
"beacon_id": "beacon_002",
"beacon_type": "eddystone",
"beacon_location": "meeting_room",
"last_seen": 0,
"distance": 0,
"location_confidence": 0,
"hs_button_counter": 0,
"hs_button_battery": 100
}
}
Updates an existing beacon’s information.
PUT /api/beacons/{id}
Content-Type: application/json
Path Parameters:
id (string): The beacon ID to updateRequest Body:
{
"name": "Updated Conference Room Beacon",
"beacon_location": "main_conference",
"location_confidence": 90
}
Response:
{
"message": "Beacon updated successfully",
"beacon": {
"name": "Updated Conference Room Beacon",
"beacon_id": "beacon_001",
"beacon_type": "ingics",
"beacon_location": "main_conference",
"last_seen": 1703078400,
"distance": 2.5,
"location_confidence": 90,
"hs_button_counter": 42,
"hs_button_battery": 85
}
}
Removes a beacon from the system.
DELETE /api/beacons/{id}
Path Parameters:
id (string): The beacon ID to deleteResponse:
{
"message": "Beacon deleted successfully"
}
Retrieves current system configuration settings.
GET /api/settings
Response:
{
"settings": {
"location_confidence": 80,
"last_seen_threshold": 300,
"beacon_metrics_size": 10,
"ha_send_interval": 60,
"ha_send_changes_only": true,
"rssi_min_threshold": -90,
"enforce_rssi_threshold": true
}
}
Updates system configuration settings.
POST /api/settings
Content-Type: application/json
Request Body:
{
"location_confidence": 85,
"last_seen_threshold": 600,
"beacon_metrics_size": 15,
"ha_send_interval": 30,
"rssi_min_threshold": -85
}
Response:
{
"message": "Settings updated successfully",
"settings": {
"location_confidence": 85,
"last_seen_threshold": 600,
"beacon_metrics_size": 15,
"ha_send_interval": 30,
"ha_send_changes_only": true,
"rssi_min_threshold": -85,
"enforce_rssi_threshold": true
}
}
Retrieves current location information for all beacons.
GET /api/locations
Response:
{
"beacons": [
{
"method": "location_update",
"previous_confident_location": "reception",
"distance": 3.2,
"id": "beacon_001",
"location": "conference_room",
"last_seen": 1703078450
},
{
"method": "location_update",
"previous_confident_location": "office_a",
"distance": 1.8,
"id": "beacon_002",
"location": "meeting_room",
"last_seen": 1703078440
}
]
}
Retrieves location information for a specific beacon.
GET /api/locations/{id}
Path Parameters:
id (string): The beacon IDResponse:
{
"method": "location_update",
"previous_confident_location": "reception",
"distance": 3.2,
"id": "beacon_001",
"location": "conference_room",
"last_seen": 1703078450
}
Check if the API server is running and basic systems are operational.
GET /api/health
Response:
{
"status": "healthy",
"timestamp": "2024-12-20T10:30:00Z",
"services": {
"database": "connected",
"kafka": "connected",
"redis": "connected"
}
}
Connect to the WebSocket endpoint for real-time updates.
ws://localhost:8080/ws/broadcast
{
"type": "beacon_update",
"data": {
"method": "location_update",
"beacon_info": {
"name": "Conference Room Beacon",
"beacon_id": "beacon_001",
"beacon_type": "ingics",
"distance": 2.5
},
"name": "conference_room",
"beacon_name": "Conference Room Beacon",
"previous_location": "reception",
"new_location": "conference_room",
"timestamp": 1703078450
}
}
{
"type": "button_event",
"data": {
"beacon_id": "beacon_001",
"button_counter": 43,
"button_mode": "normal",
"timestamp": 1703078460
}
}
{
"type": "battery_alert",
"data": {
"beacon_id": "beacon_002",
"battery_level": 15,
"alert_level": "warning",
"timestamp": 1703078470
}
}
{
"type": "fall_detection",
"data": {
"beacon_id": "beacon_001",
"event_type": "fall_detected",
"confidence": 92,
"timestamp": 1703078480
}
}
{
"type": "system_status",
"data": {
"active_beacons": 12,
"total_locations": 8,
"kafka_status": "connected",
"redis_status": "connected",
"timestamp": 1703078490
}
}
interface Beacon {
name: string;
beacon_id: string;
beacon_type: "ingics" | "eddystone" | "minew_b7" | "ibeacon";
beacon_location: string;
last_seen: number; // Unix timestamp
distance: number; // Distance in meters
previous_location?: string;
previous_confident_location?: string;
expired_location?: string;
location_confidence: number; // 0-100
location_history: string[];
beacon_metrics: BeaconMetric[];
// Handshake/Button specific fields
hs_button_counter: number;
hs_button_prev: number;
hs_button_battery: number;
hs_button_random: string;
hs_button_mode: string;
}
interface BeaconMetric {
location: string;
distance: number;
rssi: number;
timestamp: number;
}
interface Settings {
location_confidence: number; // Minimum confidence level (0-100)
last_seen_threshold: number; // Seconds before beacon considered offline
beacon_metrics_size: number; // Number of RSSI measurements to keep
ha_send_interval: number; // Home Assistant update interval (seconds)
ha_send_changes_only: boolean; // Only send updates on changes
rssi_min_threshold: number; // Minimum RSSI for detection
enforce_rssi_threshold: boolean; // Filter weak signals
}
interface LocationChange {
method: string; // "location_update" | "beacon_added" | "beacon_removed"
beacon_ref: Beacon; // Complete beacon information
name: string; // Beacon name
beacon_name: string; // Beacon name (duplicate)
previous_location: string; // Previous location
new_location: string; // New location
timestamp: number; // Unix timestamp
}
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": {
"field": "beacon_id",
"reason": "Beacon ID is required"
}
}
}
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR |
400 | Request data validation failed |
NOT_FOUND |
404 | Resource not found |
CONFLICT |
409 | Resource already exists |
INTERNAL_ERROR |
500 | Internal server error |
SERVICE_UNAVAILABLE |
503 | Required service is unavailable |
POST /api/beacons
Content-Type: application/json
Invalid Request:
{
"name": "",
"beacon_type": "invalid_type"
}
Response:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": {
"name": "Name cannot be empty",
"beacon_type": "Invalid beacon type. Must be one of: ingics, eddystone, minew_b7, ibeacon"
}
}
}
Currently, the API does not implement rate limiting. Consider implementing rate limiting for production deployments:
The API server is configured with CORS enabled for development. Production deployments should restrict CORS origins to specific domains.
class PresenceAPIClient {
private baseURL: string;
constructor(baseURL: string = 'http://localhost:8080') {
this.baseURL = baseURL;
}
async getBeacons(): Promise<Beacon[]> {
const response = await fetch(`${this.baseURL}/api/beacons`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data.beacons;
}
async createBeacon(beacon: Partial<Beacon>): Promise<Beacon> {
const response = await fetch(`${this.baseURL}/api/beacons`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(beacon),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'Failed to create beacon');
}
const data = await response.json();
return data.beacon;
}
connectWebSocket(onMessage: (message: any) => void): WebSocket {
const ws = new WebSocket(`${this.baseURL.replace('http', 'ws')}/ws/broadcast`);
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
onMessage(message);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('WebSocket connection closed');
};
return ws;
}
}
// Usage example
const client = new PresenceAPIClient();
// Get all beacons
const beacons = await client.getBeacons();
console.log('Active beacons:', beacons);
// Create a new beacon
const newBeacon = await client.createBeacon({
name: 'Test Beacon',
beacon_id: 'test_beacon_001',
beacon_type: 'eddystone',
beacon_location: 'test_room'
});
// Connect to WebSocket for real-time updates
const ws = client.connectWebSocket((message) => {
switch (message.type) {
case 'beacon_update':
console.log('Beacon location updated:', message.data);
break;
case 'button_event':
console.log('Button pressed:', message.data);
break;
case 'battery_alert':
console.log('Low battery warning:', message.data);
break;
}
});
import requests
import websocket
import json
from typing import List, Dict, Any
class PresenceAPIClient:
def __init__(self, base_url: str = "http://localhost:8080"):
self.base_url = base_url
self.ws_url = base_url.replace("http", "ws")
def get_beacons(self) -> List[Dict[str, Any]]:
"""Get all registered beacons."""
response = requests.get(f"{self.base_url}/api/beacons")
response.raise_for_status()
data = response.json()
return data["beacons"]
def create_beacon(self, beacon_data: Dict[str, Any]) -> Dict[str, Any]:
"""Create a new beacon."""
response = requests.post(
f"{self.base_url}/api/beacons",
json=beacon_data
)
response.raise_for_status()
data = response.json()
return data["beacon"]
def update_beacon(self, beacon_id: str, beacon_data: Dict[str, Any]) -> Dict[str, Any]:
"""Update an existing beacon."""
response = requests.put(
f"{self.base_url}/api/beacons/{beacon_id}",
json=beacon_data
)
response.raise_for_status()
data = response.json()
return data["beacon"]
def delete_beacon(self, beacon_id: str) -> None:
"""Delete a beacon."""
response = requests.delete(f"{self.base_url}/api/beacons/{beacon_id}")
response.raise_for_status()
def get_settings(self) -> Dict[str, Any]:
"""Get system settings."""
response = requests.get(f"{self.base_url}/api/settings")
response.raise_for_status()
return response.json()["settings"]
def update_settings(self, settings_data: Dict[str, Any]) -> Dict[str, Any]:
"""Update system settings."""
response = requests.post(
f"{self.base_url}/api/settings",
json=settings_data
)
response.raise_for_status()
return response.json()["settings"]
# Usage example
client = PresenceAPIClient()
# Get all beacons
beacons = client.get_beacons()
print(f"Found {len(beacons)} beacons")
# Create a new beacon
new_beacon = client.create_beacon({
"name": "Python Test Beacon",
"beacon_id": "python_test_001",
"beacon_type": "eddystone",
"beacon_location": "python_room"
})
print(f"Created beacon: {new_beacon['name']}")
# Update settings
settings = client.update_settings({
"location_confidence": 85,
"ha_send_interval": 30
})
print(f"Updated settings: {settings}")
package api_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetBeacons(t *testing.T) {
// Setup test server
router := setupTestRouter()
req, _ := http.NewRequest("GET", "/api/beacons", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Contains(t, response, "beacons")
}
func TestCreateBeacon(t *testing.T) {
router := setupTestRouter()
beaconData := map[string]interface{}{
"name": "Test Beacon",
"beacon_id": "test_001",
"beacon_type": "eddystone",
"beacon_location": "test_room",
}
jsonData, _ := json.Marshal(beaconData)
req, _ := http.NewRequest("POST", "/api/beacons", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Contains(t, response, "beacon")
}
For production deployments, consider implementing:
The current API is version 1. Future versions will be:
/api/v1/... (current, implied)/api/v2/... (future breaking changes)Backward compatibility will be maintained within major versions.