diff --git a/extra-addons/open_workshop/open_workshop_mqtt/API.md b/extra-addons/open_workshop/open_workshop_mqtt/API.md index 26451a3..abb729e 100644 --- a/extra-addons/open_workshop/open_workshop_mqtt/API.md +++ b/extra-addons/open_workshop/open_workshop_mqtt/API.md @@ -1,638 +1,508 @@ -# Odoo IoT Event API Reference +# Open Workshop MQTT - API Reference -**Base URL:** `http://localhost:9018` (development) / `https://your-odoo-instance.com` (production) -**Endpoint:** `POST /ows/iot/event` -**Protocol:** JSON-RPC 2.0 -**Module:** `open_workshop_mqtt` -**Version:** 1.0.0 +**Auto-generated from Python docstrings** +Last Updated: 2026-02-15 13:39:24 --- -## Overview +## Table of Contents -This API receives IoT events from the Bridge service and processes them to: -- Track device online/offline status -- Detect work sessions -- Record device state transitions -- Calculate session statistics +### Controllers +- [iot_api.py](#controller-iot_api) -**Auto-called by:** IoT Bridge when MQTT messages are received and processed +### Models +- [iot_event.py](#model-iot_event) +- [mqtt_device.py](#model-mqtt_device) +- [mqtt_session.py](#model-mqtt_session) --- -## Authentication +## Controllers -**Method:** Odoo Session Cookie or API Key (optional) +### 📄 iot_api.py -Currently configured with `auth='none'` for Bridge-to-Odoo communication. For production: +**File:** `controllers/iot_api.py` -```python -# Option 1: API Key (recommended) -headers = { - 'Content-Type': 'application/json', - 'X-Openerp-Session-Id': 'your-api-key-here' +#### Class: `IotApiController` + +REST API for IoT Bridge integration + +##### Method: `get_iot_config()` + +Get IoT device configuration for Bridge + +Returns JSON with all active devices and their session configs + +Example Response: +{ +``` +"devices": [ + { + "device_id": "shellypmminig3-48f6eeb73a1c", + "mqtt_topic": "shaperorigin/status/pm1:0", + "parser_type": "shelly_pm_mini_g3", + "machine_name": "Shaper Origin", + "session_config": { + "strategy": "power_threshold", + "standby_threshold_w": 20, + "working_threshold_w": 100, + "start_debounce_s": 3, + "stop_debounce_s": 15, + "message_timeout_s": 20, + "heartbeat_interval_s": 300 + } + } +] +``` } -# Option 2: Session-based (after login) -# Odoo automatically validates session cookie +##### Method: `receive_iot_event()` + +Receive IoT event from Bridge and process device/session state changes. + +This is the main API endpoint that receives events from the IoT Bridge microservice. +All events are idempotent via event_uid to prevent duplicate processing. + +Protocol: JSON-RPC 2.0 (params must be wrapped in "params" object) + +Args: +``` +event_uid (str): Unique event identifier (UUID format, required) + Used for idempotency - duplicate submissions return 409 Conflict +event_type (str): Type of event (required), one of: + - 'device_online': Device came online → sets state='idle' + - 'device_offline': Device went offline → sets state='offline' + - 'session_started': Work session started → sets state='active', creates ows.mqtt.session + - 'session_stopped': Session ended → sets state='idle', finalizes ows.mqtt.session + - 'session_heartbeat': Periodic update during session (every 300s default) + - 'session_timeout': Session terminated due to message timeout +device_id (str): Device identifier, must exist in ows.mqtt.device (required) +session_id (str): Session identifier (required for session_* events, optional otherwise) +timestamp (str): ISO 8601 timestamp in UTC (required) +payload (dict): Device-specific data (optional), may contain: + - power_w (float): Current power consumption in watts + - state (str): Device state from Bridge + - avg_power_w (float): Average power for session + - peak_power_w (float): Peak power for session + - total_energy_wh (float): Total energy consumed + - total_duration_s (int): Session duration in seconds +**kw: Additional JSON-RPC parameters (ignored) ``` ---- +Returns: +``` +dict: Response dictionary with keys: + - status (str): 'success', 'duplicate', or 'error' + - event_id (int): Database ID of created/existing event + - event_uid (str): Echo of input event_uid + - timestamp (str): Processing timestamp + - message (str): Additional info (for duplicates/errors) +``` -## Request Format (JSON-RPC 2.0) +HTTP Status Codes: +``` +- 201 Created: Event successfully created and processed +- 409 Conflict: Duplicate event_uid (idempotent, no changes made) +- 400 Bad Request: Missing required fields or validation error +- 404 Not Found: device_id does not exist in ows.mqtt.device +- 500 Internal Server Error: Unexpected processing error +``` -**Critical:** Use JSON-RPC 2.0 format with `params` object! +Side Effects: +``` +- Creates iot.event record +- May update ows.mqtt.device.state (offline/idle/active) +- May create/update ows.mqtt.session record +- Updates ows.mqtt.device.last_message_time +``` -```json +Example Request (JSON-RPC 2.0): +``` { - "jsonrpc": "2.0", - "params": { + "jsonrpc": "2.0", + "params": { + "event_uid": "550e8400-e29b-41d4-a716-446655440000", + "event_type": "session_started", + "device_id": "shellypmminig3-48f6eeb73a1c", + "session_id": "session-abc123", + "timestamp": "2026-02-15T10:30:45.123456Z", + "payload": {"power_w": 125.5, "state": "working"} + } +} +``` + +Example Response (Success): +``` +{ + "status": "success", + "event_id": 42, "event_uid": "550e8400-e29b-41d4-a716-446655440000", - "event_type": "session_started", - "device_id": "shellypmminig3-48f6eeb73a1c", - "session_id": "abc-123-def-456", - "timestamp": "2026-02-15T10:30:45.123456Z", - "payload": { - "power_w": 125.5, - "state": "working" - } - } + "timestamp": "2026-02-15T10:30:45.234567" } ``` -### Parameters - -| Field | Type | Required | Constraints | Description | -|-------|------|----------|-------------|-------------| -| `jsonrpc` | String | ✅ Yes | Must be "2.0" | JSON-RPC protocol version | -| `params` | Object | ✅ Yes | - | Parameter container (Odoo convention) | -| `params.event_uid` | String (UUID) | ✅ Yes | Valid UUID format | Unique event identifier for idempotency | -| `params.event_type` | String (Enum) | ✅ Yes | See Event Types below | Type of event | -| `params.device_id` | String | ✅ Yes | Must exist in `mqtt.device` | Device identifier | -| `params.session_id` | String | ⚠️ Conditional | Required for session events | Session identifier | -| `params.timestamp` | String (ISO 8601) | ✅ Yes | Valid ISO 8601 format | Event timestamp (UTC) | -| `params.payload` | Object | ❌ Optional | Valid JSON | Device-specific data | - ---- - -## Event Types - -### 1. `device_online` - -**Purpose:** Notify Odoo that a device came online (MQTT connection established) - -**Effect:** -- Sets `mqtt.device.state = 'idle'` -- Updates `mqtt.device.last_message_time` -- Creates `iot.event` record (type: device_online) - -**Example Request:** -```json +Example Response (Duplicate): +``` { - "jsonrpc": "2.0", - "params": { - "event_uid": "f47ac10b-58cc-4372-a567-0e02b2c3d479", - "event_type": "device_online", - "device_id": "shellypmminig3-48f6eeb73a1c", - "timestamp": "2026-02-15T10:00:00.000000Z", - "payload": {} - } + "status": "duplicate", + "event_id": 42, + "event_uid": "550e8400-e29b-41d4-a716-446655440000", + "message": "Event already processed" } ``` -**Example Response (201 Created):** -```json -{ - "status": "success", - "event_id": 42, - "event_uid": "f47ac10b-58cc-4372-a567-0e02b2c3d479", - "event_type": "device_online", - "timestamp": "2026-02-15T10:00:00.123456" -} +See Also: +``` +- API.md: Complete API documentation with all event types +- models/mqtt_device.py: Device model with state field +- models/mqtt_session.py: Session model +- _process_event(): Internal event processing logic ``` -**Use Case:** Bridge detects first MQTT message from device after startup or reconnection +##### Method: `_process_event()` ---- +Process IoT event: create or update mqtt.session -### 2. `device_offline` - -**Purpose:** Notify Odoo that a device went offline (MQTT timeout or disconnect) - -**Effect:** -- Sets `mqtt.device.state = 'offline'` -- Updates `mqtt.device.last_message_time` -- Creates `iot.event` record (type: device_offline) -- If session active: Auto-stops session (calls session_stopped logic) - -**Example Request:** -```json -{ - "jsonrpc": "2.0", - "params": { - "event_uid": "a8d3c4f1-9b2e-4d7c-8a1e-2f3b4c5d6e7f", - "event_type": "device_offline", - "device_id": "shellypmminig3-48f6eeb73a1c", - "timestamp": "2026-02-15T12:00:00.000000Z", - "payload": {} - } -} +Args: ``` - -**Example Response (201 Created):** -```json -{ - "status": "success", - "event_id": 43, - "event_uid": "a8d3c4f1-9b2e-4d7c-8a1e-2f3b4c5d6e7f", - "event_type": "device_offline", - "timestamp": "2026-02-15T12:00:00.234567" -} -``` - -**Use Case:** Bridge detected message timeout (20s default) or MQTT disconnect - ---- - -### 3. `session_started` - -**Purpose:** Notify Odoo that a work session started (power crossed working threshold) - -**Effect:** -- Sets `mqtt.device.state = 'active'` -- Updates `mqtt.device.last_message_time` -- Creates `mqtt.session` record with: - - `session_id` (external identifier from Bridge) - - `start_time` (from event timestamp) - - `device_id` (link to mqtt.device) -- Creates `iot.event` record (type: session_started) - -**Example Request:** -```json -{ - "jsonrpc": "2.0", - "params": { - "event_uid": "b9e4d5f2-0c3f-5e8d-9b2f-3a4c5d6e7f8g", - "event_type": "session_started", - "device_id": "shellypmminig3-48f6eeb73a1c", - "session_id": "session-1739624845-abc123", - "timestamp": "2026-02-15T10:15:30.000000Z", - "payload": { - "power_w": 125.5, - "state": "working", - "voltage": 230.2 - } - } -} -``` - -**Example Response (201 Created):** -```json -{ - "status": "success", - "event_id": 44, - "event_uid": "b9e4d5f2-0c3f-5e8d-9b2f-3a4c5d6e7f8g", - "event_type": "session_started", - "session_id": "session-1739624845-abc123", - "timestamp": "2026-02-15T10:15:30.345678" -} -``` - -**Validation:** -- `session_id` must be provided -- Device must exist and be online/idle (not already in session) - -**Use Case:** Bridge's session detector confirms power > working_threshold_w for start_debounce_s duration - ---- - -### 4. `session_stopped` - -**Purpose:** Notify Odoo that a work session ended (power dropped below standby threshold) - -**Effect:** -- Sets `mqtt.device.state = 'idle'` -- Updates `mqtt.device.last_message_time` -- Updates `mqtt.session` record: - - Sets `end_time` (from event timestamp) - - Calculates `duration` (end_time - start_time) - - Updates statistics from payload (avg_power_w, peak_power_w, etc.) -- Creates `iot.event` record (type: session_stopped) - -**Example Request:** -```json -{ - "jsonrpc": "2.0", - "params": { - "event_uid": "c0f5e6g3-1d4g-6f9e-0c3g-4b5d6e7f8g9h", - "event_type": "session_stopped", - "device_id": "shellypmminig3-48f6eeb73a1c", - "session_id": "session-1739624845-abc123", - "timestamp": "2026-02-15T11:45:30.000000Z", - "payload": { - "power_w": 3.2, - "state": "idle", - "total_duration_s": 5400, - "avg_power_w": 118.3, - "peak_power_w": 145.7, - "total_energy_wh": 177.45 - } - } -} -``` - -**Example Response (201 Created):** -```json -{ - "status": "success", - "event_id": 45, - "event_uid": "c0f5e6g3-1d4g-6f9e-0c3g-4b5d6e7f8g9h", - "event_type": "session_stopped", - "session_id": "session-1739624845-abc123", - "timestamp": "2026-02-15T11:45:30.456789" -} -``` - -**Validation:** -- `session_id` must be provided -- Session must exist in `mqtt.session` with matching `session_id` -- Session must not already be stopped (end_time = False) - -**Use Case:** Bridge's session detector confirms power < standby_threshold_w for stop_debounce_s duration - ---- - -### 5. `session_heartbeat` - -**Purpose:** Periodic update during active session (aggregated power statistics) - -**Effect:** -- Updates `mqtt.device.last_message_time` -- Updates `mqtt.session` statistics: - - `avg_power_w` (running average) - - `peak_power_w` (maximum seen) - - `total_energy_wh` (accumulated) - - `message_count` (number of MQTT messages) -- Creates `iot.event` record (type: session_heartbeat) - -**Example Request:** -```json -{ - "jsonrpc": "2.0", - "params": { - "event_uid": "d1g6f7h4-2e5h-7g0f-1d4h-5c6e7f8g9h0i", - "event_type": "session_heartbeat", - "device_id": "shellypmminig3-48f6eeb73a1c", - "session_id": "session-1739624845-abc123", - "timestamp": "2026-02-15T10:20:30.000000Z", - "payload": { - "power_w": 110.3, - "state": "working", - "avg_power_w": 115.2, - "peak_power_w": 130.1, - "total_energy_wh": 9.6, - "message_count": 60 - } - } -} -``` - -**Example Response (201 Created):** -```json -{ - "status": "success", - "event_id": 46, - "event_uid": "d1g6f7h4-2e5h-7g0f-1d4h-5c6e7f8g9h0i", - "event_type": "session_heartbeat", - "session_id": "session-1739624845-abc123", - "timestamp": "2026-02-15T10:20:30.567890" -} -``` - -**Frequency:** Configured via `session_config.heartbeat_interval_s` (default: 300s) - -**Use Case:** Bridge sends periodic updates to track long-running sessions without creating too many events - ---- - -### 6. `session_timeout` - -**Purpose:** Session terminated due to MQTT message timeout (device went silent) - -**Effect:** Same as `session_stopped` but indicates abnormal termination - -**Example Request:** -```json -{ - "jsonrpc": "2.0", - "params": { - "event_uid": "e2h7g8i5-3f6i-8h1g-2e5i-6d7f8g9h0i1j", - "event_type": "session_timeout", - "device_id": "shellypmminig3-48f6eeb73a1c", - "session_id": "session-1739624845-abc123", - "timestamp": "2026-02-15T11:30:00.000000Z", - "payload": { - "timeout_duration_s": 20, - "last_power_w": 95.3 - } - } -} -``` - -**Use Case:** No MQTT messages received for `message_timeout_s` duration (default: 20s) during active session - ---- - -## Error Responses - -### 400 Bad Request - Missing Required Fields - -```json -{ - "status": "error", - "error": "Missing required fields: event_uid, device_id", - "code": 400 -} -``` - -**Cause:** One or more required parameters missing from `params` object - ---- - -### 404 Not Found - Device Not Found - -```json -{ - "status": "error", - "error": "Device not found: unknown-device-123", - "code": 404 -} -``` - -**Cause:** `device_id` does not exist in `mqtt.device` table - -**Solution:** Create device in Odoo first, config will be pushed to Bridge - ---- - -### 409 Conflict - Duplicate Event - -```json -{ - "status": "duplicate", - "event_id": 42, - "event_uid": "f47ac10b-58cc-4372-a567-0e02b2c3d479", - "message": "Event already processed" -} -``` - -**Cause:** `event_uid` already exists in `iot.event` table - -**Behavior:** Idempotent - original event_id returned, no changes made - -**Use Case:** Bridge retry after network failure - ---- - -### 500 Internal Server Error - Processing Failed - -```json -{ - "status": "error", - "error": "Exception message here", - "code": 500 -} -``` - -**Cause:** Unexpected error during event processing (check Odoo logs) - -**Debugging:** -```bash -docker logs odoo-dev | grep "iot_api.py" | tail -20 +event: ows.iot.event record ``` --- -## Idempotency +## Models -The API is **idempotent via `event_uid`**: +### 📄 iot_event.py -✅ **Same `event_uid` submitted twice:** -- First request: 201 Created (event processed) -- Second request: 409 Conflict (duplicate detected, original event_id returned) -- No side effects on second request (no state changes) +**File:** `models/iot_event.py` -**Why it matters:** -- Bridge can safely retry failed requests (network errors, timeouts) -- No duplicate events in database -- No double-counting of sessions/statistics +#### Class: `IotEvent` -**Implementation:** -```python -# In controllers/iot_api.py -existing_event = request.env['iot.event'].sudo().search([ - ('event_uid', '=', event_uid) -], limit=1) +IoT Event Log - Records all events received from IoT Bridge. -if existing_event: - return { - 'status': 'duplicate', - 'event_id': existing_event.id, - 'event_uid': event_uid, - 'message': 'Event already processed' - } +This model stores every event submitted via POST /ows/iot/event endpoint. +Events are immutable (readonly=True) and serve as audit trail for device +activity and session detection. + +Idempotency: +``` +All events are uniquely identified by event_uid (UUID). Duplicate +submissions with same event_uid are rejected with 409 Conflict, +ensuring Bridge can safely retry failed requests. +``` + +Event Types: +``` +- device_online: Device connected to MQTT → state='idle' +- device_offline: Device disconnected → state='offline' +- session_started: Work session detected → state='active', creates ows.mqtt.session +- session_stopped: Session ended → state='idle', finalizes ows.mqtt.session +- session_heartbeat: Periodic update (300s) → updates session stats +- session_timeout: Session lost due to message timeout → abnormal end +``` + +Event Flow: +``` +1. Bridge detects MQTT message pattern +2. Bridge runs session detection state machine +3. Bridge sends event via POST /ows/iot/event (JSON-RPC 2.0) +4. Odoo creates iot.event record (this model) +5. Odoo processes event (_process_event in controllers/iot_api.py) +6. Odoo updates ows.mqtt.device and/or ows.mqtt.session +``` + +Payload Structure: +``` +The payload field (JSON) contains device-specific data: +- power_w: Current power consumption (watts) +- voltage: Supply voltage +- state: Bridge state (idle/starting/working/stopping) +- avg_power_w: Average power for session +- peak_power_w: Peak power for session +- total_energy_wh: Accumulated energy (watt-hours) +- total_duration_s: Session duration (seconds) +- message_count: Number of MQTT messages +``` + +Related Models: +``` +- ows.mqtt.device: Device that generated this event +- ows.mqtt.session: Session associated with this event (for session events) +``` + +Example: +``` +# Query recent events for device +events = env['ows.iot.event'].search([ + ('device_id', '=', 'shellypmminig3-xxx'), + ('timestamp', '>', '2026-02-15 00:00:00') +], order='timestamp desc') +``` + +``` +# Check event payload +payload = events[0].get_payload_dict() +print(f"Power: {payload.get('power_w')}W") +``` + +SQL Constraints: +``` +- event_uid_unique: Prevents duplicate events (enforces idempotency) +``` + +Fields Overview: +``` +- event_uid: Unique UUID from Bridge (indexed for fast lookup) +- event_type: Type of event (session_started, device_online, etc.) +- device_id: Device identifier (string, not Many2one for flexibility) +- session_id: Session identifier (optional, for session events) +- timestamp: Event timestamp from Bridge (UTC) +- payload: JSON data with device-specific measurements +- create_date: Odoo record creation time +``` + +##### Method: `create()` + +Create event and auto-link to ows.mqtt.device and ows.mqtt.session + +##### Method: `get_payload_dict()` + +Return payload as Python dict + +##### Method: `mark_processed()` + +Mark event as processed (or failed) + +--- + +### 📄 mqtt_device.py + +**File:** `models/mqtt_device.py` + +#### Class: `MqttDevice` + +MQTT IoT Device Model - Represents a physical device monitored via MQTT. + +This model stores device configuration and current state for IoT devices +monitored through the Bridge microservice. It supports automatic config +push to the Bridge when devices are created, updated, or deleted. + +State Machine: +``` +offline → idle (device_online event) +idle → active (session_started event) +active → idle (session_stopped event) +idle → offline (device_offline event) +``` + +Config Push Architecture: +``` +- CREATE: Automatically pushes new device config to Bridge +- WRITE: Pushes updated config when relevant fields change +- UNLINK: Removes device from Bridge config +- Manual: User can click "Push Config to Bridge" button +``` + +Related Models: +``` +- mqtt.session: Work sessions detected by this device +- iot.event: Event history for this device +``` + +Bridge Integration: +``` +Configuration is pushed to Bridge via POST /config endpoint. +Bridge URL is configured in system parameter: open_workshop_mqtt.bridge_url +``` + +Example: +``` +device = env['mqtt.device'].create({ + 'name': 'CNC Mill Workshop 1', + 'device_id': 'shellypmminig3-48f6eeb73a1c', + 'topic_pattern': 'shellypmminig3-48f6eeb73a1c/status/pm1:0', + 'parser_type': 'shelly_pm', + 'standby_threshold_w': 5.0, + 'working_threshold_w': 50.0, +}) +# Config automatically pushed to Bridge +``` + +Fields Overview: +``` +- name: Human-readable device name +- device_id: Unique external identifier (used by Bridge) +- state: Current device state (offline/idle/active) +- topic_pattern: MQTT topic to subscribe to +- parser_type: Parser for MQTT messages (shelly_pm, tasmota, etc.) +- Session thresholds: standby_threshold_w, working_threshold_w +- Session timing: start_debounce_s, stop_debounce_s, message_timeout_s +- Statistics: last_message_time, session_count +``` + +##### Method: `write()` + +Auto-subscribe when device is added to running connection +or when topic_pattern changes + +DEPRECATED: Auto-subscribe disabled - use standalone IoT Bridge container + +##### Method: `action_view_sessions()` + +Open sessions view for this device + +##### Method: `action_start_manual_session()` + +Manually start a session + +##### Method: `action_stop_manual_session()` + +Manually stop a session + +##### Method: `get_strategy_config_dict()` + +Parse strategy_config JSON and return as dict + +##### Method: `create()` + +Override create to push config to bridge after device creation + +##### Method: `write()` + +Override write to push config to bridge after device update + +##### Method: `unlink()` + +Override unlink to push config to bridge after device deletion + +##### Method: `action_push_config_to_bridge()` + +Manual action to push config to bridge. +Called from UI button. + +--- + +### 📄 mqtt_session.py + +**File:** `models/mqtt_session.py` + +#### Class: `MqttSession` + +MQTT Device Work Session - Represents a detected work session from IoT device. + +Sessions are automatically created by the IoT Bridge when power consumption +crosses the working threshold and are finalized when power drops below +standby threshold. The Bridge implements a 5-state state machine with +debounce timers to avoid false triggers. + +Session Lifecycle: +``` +1. IDLE: Device idle, power < standby_threshold_w +2. STARTING: Power rising, start_debounce_s timer active +3. WORKING: Power > working_threshold_w, session active +4. STOPPING: Power falling, stop_debounce_s timer active +5. IDLE: Session ended, record finalized +``` + +Events that create/update sessions: +``` +- session_started: Creates new session, sets start_time +- session_heartbeat: Updates statistics during session (every 300s) +- session_stopped: Finalizes session, sets end_time and duration +- session_timeout: Abnormal termination (no MQTT messages) +``` + +Statistics Tracked: +``` +- Duration: Total session time (end_time - start_time) +- Energy: Total energy consumed in Wh (avg_power_w * duration) +- Power: Average and peak power consumption in watts +- Messages: Number of MQTT messages received during session +``` + +Related Models: +``` +- mqtt.device: Parent device that generated this session +- iot.event: Individual events during this session +``` + +Example: +``` +# Created automatically by Bridge via session_started event +session = env['mqtt.session'].search([ + ('device_id.device_id', '=', 'shellypmminig3-xxx'), + ('end_time', '=', False) # Active session +]) +``` + +``` +# Statistics available +print(f"Duration: {session.duration_hours}h") +print(f"Energy: {session.total_energy_wh}Wh") +print(f"Avg Power: {session.avg_power_w}W") +``` + +Fields Overview: +``` +- session_id: Unique identifier from Bridge +- device_id: Link to mqtt.device +- start_time/end_time: Session timestamps +- duration: Calculated in seconds/minutes/hours +- start_power_w/end_power_w: Power at start/end +- avg_power_w/peak_power_w: Power statistics +- total_energy_wh: Accumulated energy +- status: running/completed/timeout +- message_count: Number of MQTT messages +``` + +##### Method: `get_metadata_dict()` + +Parse metadata JSON and return as dict + +##### Method: `create_or_update_session()` + +Create or update session from MQTT event + +Args: +``` +session_data: Dict with session information + - session_id: str (required) + - device_id: int (required, Odoo ID) + - event_type: 'session_start' | 'session_end' + - start_time: datetime + - end_time: datetime (for session_end) + - total_duration_s: int + - standby_duration_s: int + - working_duration_s: int + - start_power_w: float + - end_power_w: float + - end_reason: str + - metadata: dict (optional) +``` + +Returns: +``` +mqtt.session record +``` + +##### Method: `get_running_sessions()` + +Get all running sessions, optionally filtered by device + +Args: +``` +device_id: int (optional) - Filter by device ID +``` + +Returns: +``` +List of session dicts for state recovery ``` --- -## State Machine: Device States - -``` -offline ──device_online──> idle ──session_started──> active - ▲ ▲ │ - │ │ │ - └────device_offline──────┴───────session_stopped────┘ -``` - -| State | Description | Triggered By | -|-------|-------------|--------------| -| `offline` | Device not connected to MQTT | Initial state, device_offline event | -| `idle` | Device online but not working | device_online, session_stopped events | -| `active` | Device online and session active | session_started event | - -**Note:** session_timeout also transitions active → idle (same as session_stopped) - --- -## Testing +*Generated from source code docstrings* -### Manual Test (curl) - -```bash -# 1. Device comes online -curl -X POST http://localhost:9018/ows/iot/event \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "params": { - "event_uid": "550e8400-e29b-41d4-a716-446655440001", - "event_type": "device_online", - "device_id": "testshelly-simulator", - "timestamp": "2026-02-15T10:00:00Z", - "payload": {} - } - }' - -# 2. Session starts -curl -X POST http://localhost:9018/ows/iot/event \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "params": { - "event_uid": "550e8400-e29b-41d4-a716-446655440002", - "event_type": "session_started", - "device_id": "testshelly-simulator", - "session_id": "test-session-001", - "timestamp": "2026-02-15T10:05:00Z", - "payload": {"power_w": 120.0} - } - }' - -# 3. Heartbeat -curl -X POST http://localhost:9018/ows/iot/event \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "params": { - "event_uid": "550e8400-e29b-41d4-a716-446655440003", - "event_type": "session_heartbeat", - "device_id": "testshelly-simulator", - "session_id": "test-session-001", - "timestamp": "2026-02-15T10:10:00Z", - "payload": {"power_w": 125.5, "avg_power_w": 122.8} - } - }' - -# 4. Session stops -curl -X POST http://localhost:9018/ows/iot/event \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc": "2.0", - "params": { - "event_uid": "550e8400-e29b-41d4-a716-446655440004", - "event_type": "session_stopped", - "device_id": "testshelly-simulator", - "session_id": "test-session-001", - "timestamp": "2026-02-15T11:00:00Z", - "payload": {"power_w": 3.5, "total_duration_s": 3300} - } - }' -``` - -### Automated Tests - -```bash -cd iot_bridge/tests -python3 test_e2e_push_architecture.py - -# Expected output: -# Device Status Monitoring.......................... ✓ PASSED -``` - -**Test Coverage:** -- ✅ JSON-RPC 2.0 format validation -- ✅ event_uid requirement -- ✅ All event types (online, offline, session_started, stopped, heartbeat) -- ✅ State transitions (offline → idle → active → idle → offline) -- ✅ Idempotency (duplicate event_uid returns 409) - ---- - -## Monitoring & Observability - -### Odoo Logs - -```bash -# View all IoT API calls -docker logs odoo-dev | grep "iot_api.py" - -# View only errors -docker logs odoo-dev | grep "ERROR.*iot_api" - -# View specific device events -docker logs odoo-dev | grep "shellypmminig3-48f6eeb73a1c" -``` - -### Database Queries - -```sql --- Latest events -SELECT - id, event_uid, event_type, device_id, - timestamp, create_date -FROM iot_event -ORDER BY timestamp DESC -LIMIT 10; - --- Device states -SELECT - device_id, machine_name, state, - last_message_time -FROM mqtt_device; - --- Active sessions -SELECT - id, session_id, device_id, - start_time, end_time, duration, - avg_power_w, peak_power_w -FROM mqtt_session -WHERE end_time IS NULL; - --- Recent sessions -SELECT - id, session_id, device_id, - start_time, end_time, duration, - avg_power_w, total_energy_wh -FROM mqtt_session -ORDER BY start_time DESC -LIMIT 10; -``` - -### Odoo UI - -Navigate to: **IoT Bridge → Events** / **Sessions** / **Devices** - ---- - -## Rate Limiting - -**Current:** No rate limiting implemented - -**Recommendation for Production:** -- Implement rate limiting in nginx/reverse proxy -- Limit: 100 events/second per device_id -- Burst: 200 events/second -- Block excessive retry storms from misconfigured Bridge - ---- - -## Changelog - -| Version | Date | Changes | -|---------|------|---------| -| 1.0.0 | 2026-02-15 | Initial implementation with JSON-RPC 2.0, event_uid for idempotency, state transitions | - ---- - -## Related Documentation - -- [Bridge API Documentation](../../../iot_bridge/API.md) - Config push endpoint (Odoo → Bridge) -- [DOCUMENTATION_STRATEGY.md](../../../DOCUMENTATION_STRATEGY.md) - Overall API documentation approach -- [IMPLEMENTATION_PLAN.md](../../../IMPLEMENTATION_PLAN.md) - Project architecture and design decisions -- [README.md](../../../iot_bridge/README.md) - Bridge deployment and testing - ---- - -**Maintainer:** Open Workshop Team -**Contact:** lotz.matthias@gmail.com -**Last Updated:** 2026-02-15 +*Run `./build_docs.sh` to regenerate after code changes* \ No newline at end of file diff --git a/extra-addons/open_workshop/open_workshop_mqtt/README.md b/extra-addons/open_workshop/open_workshop_mqtt/README.md new file mode 100644 index 0000000..fbe5615 --- /dev/null +++ b/extra-addons/open_workshop/open_workshop_mqtt/README.md @@ -0,0 +1,151 @@ +# Open Workshop MQTT + +**IoT Device Integration with MQTT** + +## Overview + +This module provides seamless integration between Odoo 18 and IoT devices via MQTT protocol. It enables real-time monitoring of industrial equipment with intelligent work session detection based on power consumption patterns. + +## Key Features + +### Real-time Device Monitoring +Track IoT devices (Shelly PM, Tasmota) via MQTT with automatic status updates. + +### Intelligent Session Detection +Automatic work session detection using configurable power thresholds and debounce timers. Session Detection is implemented in the IoT Bridge microservice, which sends events to Odoo when sessions start/stop. + +### Device State Management +Track device states: offline → idle → active → idle → offline + +### REST API +JSON-RPC 2.0 API for event submission from IoT Bridge with idempotency support. + +### Automatic Config Push +Bridge receives configuration updates instantly when devices are created/modified. + +### Session Analytics +Track average power, peak power, duration, and total energy for each work session. + +## Architecture + +The module works in conjunction with the IoT Bridge microservice: + +1. **IoT Device** publishes MQTT messages (power, voltage, state) +2. **IoT Bridge** subscribes to topics, detects sessions, sends events to Odoo +3. **Odoo Module** receives events via REST API, tracks devices/sessions +4. **Automatic Config Push** updates Bridge when devices change + +## Installation + +1. Copy the module to your Odoo addons directory +2. Update the apps list +3. Install `Open Workshop MQTT` module +4. Configure system parameter: `open_workshop_mqtt.bridge_url` (e.g., http://iot-bridge:8080) +5. Deploy IoT Bridge microservice (Docker Compose recommended) + +### Command Line Installation + +```bash +# Install via command line +./odoo-bin -d DATABASE -i open_workshop_mqtt --stop-after-init + +# Or via Odoo Apps UI +Settings → Apps → Update Apps List → Search "mqtt" → Install +``` + +## Configuration + +### 1. Bridge Connection + +Set system parameter for Bridge URL: + +``` +Key: open_workshop_mqtt.bridge_url +Value: http://iot-bridge:8080 +``` + +### 2. Create Device + +Add IoT device: +- Device ID: `shellypmminig3-xxx` +- MQTT Topic: `shellypmminig3-xxx/status/pm1:0` +- Parser: `shelly_pm` +- Thresholds: Standby 5W, Working 50W + +> **⚠️ Important:** Config is automatically pushed to Bridge on device create/update/delete. No manual sync required! + +## Usage + +### Device Management +Navigate to **IoT Bridge → Devices** + +- View list of configured devices +- Check current state (offline/idle/active) +- See last message timestamp +- Manual config push button + +### Session Monitoring +Navigate to **IoT Bridge → Sessions** + +- View work sessions with start/end times +- See duration and energy consumption +- Track average and peak power +- Filter by device or date range + +### Event History +Navigate to **IoT Bridge → Events** + +- View all received events (device_online, session_started, etc.) +- Inspect event payloads (JSON) +- Check event processing status + +## API Reference + +### POST /ows/iot/event + +Submit IoT event from Bridge (JSON-RPC 2.0) + +```json +{ + "jsonrpc": "2.0", + "params": { + "event_uid": "550e8400-e29b-41d4-a716-446655440000", + "event_type": "session_started", + "device_id": "shelly-pm-001", + "session_id": "abc-123", + "timestamp": "2026-02-15T10:30:45Z", + "payload": {"power_w": 125.5} + } +} +``` + +**Event Types:** device_online, device_offline, session_started, session_stopped, session_heartbeat, session_timeout + +For complete API documentation, see: **[API.md](API.md)** (auto-generated from source code) + +## Technical Details + +### Dependencies +- Odoo 18.0 +- Python 3.10+ +- IoT Bridge microservice + +### Database Models +- `ows.mqtt.device` - Device registry +- `ows.mqtt.session` - Work sessions +- `ows.iot.event` - Event history + +## Support & Documentation + +- **Module Documentation:** `README.md` +- **API Reference:** [API.md](API.md) (auto-generated from source code docstrings) +- **Implementation Plan:** `IMPLEMENTATION_PLAN.md` +- **Bridge Documentation:** `iot_bridge/README.md` + +*Note: To update documentation after code changes, run: `./build_docs.sh`* + +For issues and feature requests, please contact the development team. + +## License + +This module is licensed under MIT License. diff --git a/extra-addons/open_workshop/open_workshop_mqtt/build_docs.py b/extra-addons/open_workshop/open_workshop_mqtt/build_docs.py old mode 100644 new mode 100755 index bd4b14c..938823a --- a/extra-addons/open_workshop/open_workshop_mqtt/build_docs.py +++ b/extra-addons/open_workshop/open_workshop_mqtt/build_docs.py @@ -1,7 +1,19 @@ #!/usr/bin/env python3 """ Build Documentation Script -Generates HTML documentation from Python docstrings without importing modules + +Generates documentation for Odoo module: +1. Converts README.md → index.html (module overview for Odoo Apps UI) +2. Extracts Python docstrings → API.md (API reference in Markdown) + +No external dependencies - uses only Python stdlib. + +Usage: + python3 build_docs.py + +Output: + static/description/index.html (from README.md) + API.md (from Python docstrings) """ import os import sys @@ -12,12 +24,187 @@ from datetime import datetime # Paths MODULE_DIR = Path(__file__).parent -DOCS_OUTPUT = MODULE_DIR / "static" / "description" / "api_reference.html" +README_FILE = MODULE_DIR / "README.md" +INDEX_OUTPUT = MODULE_DIR / "static" / "description" / "index.html" +API_OUTPUT = MODULE_DIR / "API.md" + + +# ============================================================================ +# MARKDOWN → HTML CONVERTER (for index.html from README.md) +# ============================================================================ + +def markdown_to_html(md_content): + """Convert Markdown to HTML with Odoo styling.""" + lines = md_content.split('\n') + html_lines = [] + in_code_block = False + in_list = False + code_lang = "" + + for i, line in enumerate(lines): + # Code blocks + if line.startswith('```'): + if not in_code_block: + code_lang = line[3:].strip() + in_code_block = True + html_lines.append('
')
+ else:
+ in_code_block = False
+ html_lines.append('')
+ continue
+
+ if in_code_block:
+ html_lines.append(line.replace('&', '&').replace('<', '<').replace('>', '>'))
+ continue
+
+ # Empty lines
+ if not line.strip():
+ if in_list:
+ html_lines.append('' if isinstance(in_list, str) and in_list == 'ul' else '')
+ in_list = False
+ html_lines.append('')
+ continue
+
+ # Headers
+ if line.startswith('# '):
+ html_lines.append(f'{format_inline(content)}') + + # Regular paragraphs + else: + if in_list: + html_lines.append('
{format_inline(line)}
') + + # Close open lists + if in_list: + html_lines.append('' if in_list == 'ul' else '') + + return '\n'.join(html_lines) + + +def format_inline(text): + """Format inline Markdown (bold, italic, code, links).""" + # Links [text](url) + text = re.sub(r'\[([^\]]+)\]\(([^\)]+)\)', r'\1', text) + + # Bold **text** + text = re.sub(r'\*\*([^\*]+)\*\*', r'\1', text) + + # Italic *text* + text = re.sub(r'\*([^\*]+)\*', r'\1', text) + + # Inline code `code` + text = re.sub(r'`([^`]+)`', r'\1', text)
+
+ # Arrows →
+ text = text.replace('→', '→')
+
+ return text
+
+
+def build_index_html():
+ """Convert README.md to index.html for Odoo Apps UI."""
+ print("📄 Converting README.md to index.html...")
+
+ if not README_FILE.exists():
+ print(f"⚠️ README.md not found at {README_FILE}")
+ return False
+
+ md_content = README_FILE.read_text(encoding='utf-8')
+ body_html = markdown_to_html(md_content)
+
+ # Wrap in Odoo-styled HTML template
+ html = f"""
+
+
+
+
+ Generated from README.md | Run ./build_docs.sh to update after changes
+
\1', html)
-
- # Bold (Args:, Returns:, etc.)
- html = re.sub(r'^(Args?:|Returns?:|Raises?:|Example?:|Note?:|See Also?:|Side Effects?:|HTTP Status Codes?:|Protocol?:|Event Types?:|State Machine?:|Session Lifecycle?:|Statistics Tracked?:|Related Models?:|Bridge Integration?:|Config Push Architecture?:|Payload Structure?:|Event Flow?:|Idempotency?:|SQL Constraints?:|Fields Overview?:)(.*)$',
- r'\1\2', html, flags=re.MULTILINE)
-
- # Paragraphs
- html = '' + html.replace('\n\n', '
') + '
' - - # Line breaks - html = html.replace('\n', '
-
- Generated from source code docstrings
- Run ./build_docs.sh to regenerate after code changes
-
-
REST API for IoT Bridge integration
Get IoT device configuration for Bridge
Returns JSON with all active devices and their session configs
Example Response:
{
Receive IoT event from Bridge and process device/session state changes.
This is the main API endpoint that receives events from the IoT Bridge microservice.
All events are idempotent via event_uid to prevent duplicate processing.
Protocol: JSON-RPC 2.0 (params must be wrapped in "params" object)
Args:
Returns:
HTTP Status Codes:
Side Effects:
Example Request (JSON-RPC 2.0):
Example Response (Success):
Example Response (Duplicate):
See Also:
Process IoT event: create or update mqtt.session
Args:
IoT Event Log - Records all events received from IoT Bridge.
This model stores every event submitted via POST /ows/iot/event endpoint.
Events are immutable (readonly=True) and serve as audit trail for device
activity and session detection.
Idempotency:
Event Types:
Event Flow:
Payload Structure:
Related Models:
Example:
SQL Constraints:
Fields Overview:
Create event and auto-link to mqtt.device and mqtt.session
Return payload as Python dict
Mark event as processed (or failed)
MQTT IoT Device Model - Represents a physical device monitored via MQTT.
This model stores device configuration and current state for IoT devices
monitored through the Bridge microservice. It supports automatic config
push to the Bridge when devices are created, updated, or deleted.
State Machine:
Config Push Architecture:
Related Models:
Bridge Integration:
Example:
Fields Overview:
Auto-subscribe when device is added to running connection
or when topic_pattern changes
DEPRECATED: Auto-subscribe disabled - use standalone IoT Bridge container
Open sessions view for this device
Manually start a session
Manually stop a session
Parse strategy_config JSON and return as dict
Override create to push config to bridge after device creation
Override write to push config to bridge after device update
Override unlink to push config to bridge after device deletion
Manual action to push config to bridge.
Called from UI button.
MQTT Device Work Session - Represents a detected work session from IoT device.
Sessions are automatically created by the IoT Bridge when power consumption
crosses the working threshold and are finalized when power drops below
standby threshold. The Bridge implements a 5-state state machine with
debounce timers to avoid false triggers.
Session Lifecycle:
Events that create/update sessions:
Statistics Tracked:
Related Models:
Example:
Fields Overview:
Parse metadata JSON and return as dict
Create or update session from MQTT event
Args:
Returns:
Get all running sessions, optionally filtered by device
Args:
Returns:
-
- Generated from source code docstrings
- Run ./build_docs.sh to regenerate after code changes
-
-
- This module provides seamless integration between Odoo 18 and IoT devices via MQTT protocol. - It enables real-time monitoring of industrial equipment with intelligent work session detection - based on power consumption patterns. -
-IoT Device Integration with MQTT
-Track IoT devices (Shelly PM, Tasmota) via MQTT with automatic status updates.
-Automatic work session detection using configurable power thresholds and debounce timers.
-Track device states: offline → idle → active → idle → offline
-JSON-RPC 2.0 API for event submission from IoT Bridge with idempotency support.
-Bridge receives configuration updates instantly when devices are created/modified.
-Track average power, peak power, duration, and total energy for each work session.
-
- - The module works in conjunction with the IoT Bridge microservice: -
-This module provides seamless integration between Odoo 18 and IoT devices via MQTT protocol. It enables real-time monitoring of industrial equipment with intelligent work session detection based on power consumption patterns.
-
- 1. Copy the module to your Odoo addons directory
- 2. Update the apps list
- 3. Install Open Workshop MQTT module
- 4. Configure system parameter: open_workshop_mqtt.bridge_url (e.g., http://iot-bridge:8080)
- 5. Deploy IoT Bridge microservice (Docker Compose recommended)
-
+Key Features
+ +Real-time Device Monitoring
+Track IoT devices (Shelly PM, Tasmota) via MQTT with automatic status updates.
+ +Intelligent Session Detection
+Automatic work session detection using configurable power thresholds and debounce timers. Session Detection is implemented in the IoT Bridge microservice, which sends events to Odoo when sessions start/stop.
+ +Device State Management
+Track device states: offline → idle → active → idle → offline
+ +REST API
+JSON-RPC 2.0 API for event submission from IoT Bridge with idempotency support.
+ +Automatic Config Push
+Bridge receives configuration updates instantly when devices are created/modified.
+ +Session Analytics
+Track average power, peak power, duration, and total energy for each work session.
+ +Architecture
+ +The module works in conjunction with the IoT Bridge microservice:
+ ++
+ +- IoT Device publishes MQTT messages (power, voltage, state)
+- IoT Bridge subscribes to topics, detects sessions, sends events to Odoo
+- Odoo Module receives events via REST API, tracks devices/sessions
+- Automatic Config Push updates Bridge when devices change
+Installation
+ ++
+ +- Copy the module to your Odoo addons directory
+- Update the apps list
+- Install
+Open Workshop MQTTmodule- Configure system parameter:
+open_workshop_mqtt.bridge_url(e.g., http://iot-bridge:8080)- Deploy IoT Bridge microservice (Docker Compose recommended)
+Command Line Installation
+ +# Install via command line ./odoo-bin -d DATABASE -i open_workshop_mqtt --stop-after-init # Or via Odoo Apps UI Settings → Apps → Update Apps List → Search "mqtt" → Install --
Set system parameter for Bridge URL:
-Key: open_workshop_mqtt.bridge_url -Value: http://iot-bridge:8080-
Add IoT device:
-shellypmminig3-xxxshellypmminig3-xxx/status/pm1:0shelly_pmNavigate to IoT Bridge → Devices
-Navigate to IoT Bridge → Sessions
-Set system parameter for Bridge URL:
-Navigate to IoT Bridge → Events
-+Key: open_workshop_mqtt.bridge_url +Value: http://iot-bridge:8080 +-
Submit IoT event from Bridge (JSON-RPC 2.0)
-+2. Create Device
+ +Add IoT device:
+
shellypmminig3-xxxshellypmminig3-xxx/status/pm1:0shelly_pmNavigate to IoT Bridge → Devices
+ +Navigate to IoT Bridge → Sessions
+ +Navigate to IoT Bridge → Events
+ +Submit IoT event from Bridge (JSON-RPC 2.0)
+ +
{
"jsonrpc": "2.0",
"params": {
@@ -183,55 +158,50 @@ Value: http://iot-bridge:8080
"payload": {"power_w": 125.5}
}
}
-
- Event Types: device_online, device_offline, session_started, session_stopped, session_heartbeat, session_timeout
-For full API documentation, see: API.md in module directory
mqtt.device - Device registrymqtt.session - Work sessionsiot.event - Event historyEvent Types: device_online, device_offline, session_started, session_stopped, session_heartbeat, session_timeout
-README.mdIMPLEMENTATION_PLAN.mdiot_bridge/README.mdNote: To update API documentation after code changes, run: ./build_docs.sh
For issues and feature requests, please contact the development team.
-For complete API documentation, see: API.md (auto-generated from source code)
-- This module is licensed under MIT License. -
-ows.mqtt.device - Device registryows.mqtt.session - Work sessionsows.iot.event - Event historyREADME.mdIMPLEMENTATION_PLAN.mdiot_bridge/README.mdNote: To update documentation after code changes, run: ./build_docs.sh
For issues and feature requests, please contact the development team.
+ +This module is licensed under MIT License.
+ +
+ Generated from README.md | Run ./build_docs.sh to update after changes
+