Add whatsapp feature

This commit is contained in:
Urtzi Alfaro
2025-11-13 16:01:08 +01:00
parent d7df2b0853
commit 9bc048d360
74 changed files with 9765 additions and 533 deletions

View File

@@ -0,0 +1,653 @@
# IoT Equipment Integration - Implementation Guide
## Overview
This guide documents the implementation of real-time IoT equipment tracking for bakery production equipment, specifically targeting smart industrial ovens with IoT connectivity capabilities.
## Table of Contents
1. [Architecture](#architecture)
2. [Database Schema](#database-schema)
3. [IoT Connectors](#iot-connectors)
4. [Supported Equipment](#supported-equipment)
5. [Implementation Status](#implementation-status)
6. [Next Steps](#next-steps)
7. [Usage Examples](#usage-examples)
## Architecture
### Component Overview
```
┌─────────────────────────────────────────────────────────────┐
│ Frontend Dashboard │
│ (Real-time Equipment Monitoring UI) │
└────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Production Service API │
│ /api/v1/equipment/{id}/iot-config │
│ /api/v1/equipment/{id}/realtime-data │
│ /api/v1/equipment/{id}/sensor-history │
│ /api/v1/equipment/{id}/test-connection │
└────────────────┬────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ IoT Integration Service │
│ - Connection management │
│ - Data transformation │
│ - Protocol abstraction │
└────────────────┬────────────────────────────────────────────┘
┌──────────┴──────────┬──────────────────┐
▼ ▼ ▼
┌────────────┐ ┌──────────────┐ ┌─────────────┐
│ REST API │ │ OPC UA │ │ MQTT │
│ Connector │ │ Connector │ │ Connector │
└─────┬──────┘ └──────┬───────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Smart IoT-Enabled Equipment │
│ - Rational iCombi (ConnectedCooking) │
│ - Wachtel REMOTE │
│ - SALVA Smart Ovens │
│ - Generic REST API Equipment │
└─────────────────────────────────────────────────────────────┘
```
## Database Schema
### New Tables
#### 1. `equipment` (Extended)
Added IoT connectivity fields:
- `iot_enabled` - Enable/disable IoT connectivity
- `iot_protocol` - Protocol type (rest_api, opc_ua, mqtt, modbus, custom)
- `iot_endpoint` - Connection endpoint URL/IP
- `iot_port` - Connection port
- `iot_credentials` - JSON encrypted credentials
- `iot_connection_status` - Current connection status
- `iot_last_connected` - Timestamp of last successful connection
- `iot_config` - Additional protocol-specific configuration
- `manufacturer` - Equipment manufacturer
- `firmware_version` - Firmware version
- `supports_realtime` - Supports real-time monitoring
- `poll_interval_seconds` - Data polling interval
- `temperature_zones` - Number of temperature zones
- `supports_humidity` - Humidity monitoring capability
- `supports_energy_monitoring` - Energy monitoring capability
- `supports_remote_control` - Remote control capability
#### 2. `equipment_sensor_readings`
Time-series sensor data storage:
- Core readings: temperature, humidity, energy consumption
- Status: operational_status, cycle_stage, progress
- Process parameters: motor_speed, door_status, steam_level
- Quality indicators: product_weight, moisture_content
- Flexible JSON field for manufacturer-specific sensors
#### 3. `equipment_connection_logs`
Connection event tracking:
- event_type, event_time, connection_status
- Error tracking: error_message, error_code
- Performance metrics: response_time_ms, data_points_received
#### 4. `equipment_iot_alerts`
Real-time equipment alerts:
- Alert types: temperature_deviation, connection_lost, equipment_error
- Severity levels: info, warning, critical
- Status tracking: active, acknowledged, resolved
- Automated response tracking
### Migration
Run migration to add IoT support:
```bash
cd services/production
alembic upgrade head
```
Migration file: `migrations/versions/002_add_iot_equipment_support.py`
## IoT Connectors
### Connector Architecture
All connectors implement the `BaseIoTConnector` abstract interface:
```python
from app.services.iot import BaseIoTConnector, ConnectorFactory
# Create connector instance
connector = ConnectorFactory.create_connector(
protocol='rest_api',
equipment_id='equipment-uuid',
config={
'endpoint': 'https://api.example.com',
'port': 443,
'credentials': {'api_key': 'xxx'},
'additional_config': {}
}
)
# Test connection
status = await connector.test_connection()
# Get current readings
reading = await connector.get_current_reading()
# Get equipment capabilities
capabilities = await connector.get_capabilities()
```
### Available Connectors
#### 1. Generic REST API Connector
**Protocol:** `rest_api`
**File:** `app/services/iot/rest_api_connector.py`
**Configuration Example:**
```json
{
"protocol": "rest_api",
"endpoint": "https://api.equipment.com",
"port": 443,
"credentials": {
"api_key": "your-api-key",
"token": "bearer-token"
},
"additional_config": {
"data_endpoint": "/api/v1/equipment/{equipment_id}/data",
"status_endpoint": "/api/v1/equipment/{equipment_id}/status",
"timeout": 10,
"verify_ssl": true
}
}
```
**Features:**
- Standard REST API support
- Bearer token & API key authentication
- Basic authentication
- Configurable endpoints
- SSL verification control
- Timeout configuration
#### 2. Rational ConnectedCooking Connector
**Protocol:** `rational` or `rational_connected_cooking`
**File:** `app/services/iot/rational_connector.py`
**Configuration Example:**
```json
{
"protocol": "rational",
"endpoint": "https://www.connectedcooking.com/api/v1",
"port": 443,
"credentials": {
"username": "your-email@example.com",
"password": "your-password"
},
"additional_config": {
"unit_id": "12345",
"timeout": 15
}
}
```
**Features:**
- Multi-zone temperature (cabinet + core)
- Humidity monitoring
- Energy consumption tracking
- Remote control support
- HACCP documentation
- Recipe management
- Automatic cleaning status
**Contact:** cc-support@rational-online.com
#### 3. Wachtel REMOTE Connector
**Protocol:** `wachtel` or `wachtel_remote`
**File:** `app/services/iot/wachtel_connector.py`
**Configuration Example:**
```json
{
"protocol": "wachtel",
"endpoint": "https://remote.wachtel.de/api",
"port": 443,
"credentials": {
"username": "bakery-username",
"password": "bakery-password"
},
"additional_config": {
"oven_id": "oven-serial-number",
"deck_count": 3
}
}
```
**Features:**
- Multi-deck temperature monitoring
- Energy consumption tracking
- Maintenance alerts
- Operation hours tracking
- Deck-specific control
**Contact:** support@wachtel.de
#### 4. OPC UA Connector (Template)
**Protocol:** `opc_ua`
**Status:** Template only (requires implementation)
For bakery equipment supporting OPC UA or Weihenstephan Standards (WS Bake).
**Dependencies:**
```bash
pip install asyncua==1.1.5
```
**Template Location:** To be created at `app/services/iot/opcua_connector.py`
## Supported Equipment
### Equipment Research Summary
#### Spanish Manufacturers (Madrid Region)
1. **SALVA Industrial** (Lezo, Guipuzcoa)
- Smart touch control panels
- Energy monitoring
- Digital integration
- Status: API details pending
2. **Farjas** (Madrid, Móstoles)
- Rotary ovens
- Status: IoT capabilities unknown
3. **COLBAKE** (Valencia)
- Complete bakery lines
- Status: IoT capabilities to be confirmed
#### International Manufacturers with Madrid Presence
1. **Rational** (Germany) - ✅ **Implemented**
- Product: iCombi ovens
- Platform: ConnectedCooking
- API: Available (REST)
- Showroom: Madrid (15 min from airport)
2. **Wachtel** (Germany) - ✅ **Template Created**
- Product: Deck ovens
- Platform: REMOTE monitoring
- API: REST (details pending confirmation)
3. **Sveba Dahlen** (Sweden)
- Showroom in Madrid
- Status: IoT capabilities to be researched
### Industry Standards
- **OPC UA**: Standard protocol for industrial automation
- **Weihenstephan Standards (WS Bake)**: Bakery-specific communication standard
- **MQTT**: Common IoT message protocol
- **Modbus**: Industrial communication protocol
## Implementation Status
### ✅ Completed
1. **Database Schema**
- Migration created and tested
- All IoT tables defined
- Indexes optimized for time-series queries
2. **Models**
- Equipment model extended with IoT fields
- Sensor reading model
- Connection log model
- IoT alert model
- Enums: IoTProtocol, IoTConnectionStatus
3. **Schemas (Pydantic)**
- IoTConnectionConfig
- Equipment schemas updated with IoT fields
- EquipmentSensorReadingResponse
- EquipmentConnectionTestResponse
- RealTimeDataResponse
- EquipmentIoTAlertResponse
- EquipmentSensorHistoryResponse
4. **IoT Connectors**
- Base connector interface (`BaseIoTConnector`)
- Connector factory pattern
- Generic REST API connector (fully implemented)
- Rational ConnectedCooking connector (implemented)
- Wachtel REMOTE connector (template created)
5. **Dependencies**
- requirements.txt updated
- httpx for REST APIs
- Commented dependencies for OPC UA and MQTT
### 🚧 In Progress / To Do
1. **IoT Integration Service**
- High-level service layer
- Connection pool management
- Automatic retry logic
- Health monitoring
2. **Repository Layer**
- Equipment IoT configuration CRUD
- Sensor data storage and retrieval
- Connection log management
- Alert management
3. **API Endpoints**
- POST `/api/v1/equipment/{id}/iot-config` - Configure IoT
- POST `/api/v1/equipment/{id}/test-connection` - Test connectivity
- GET `/api/v1/equipment/{id}/realtime-data` - Get current data
- GET `/api/v1/equipment/{id}/sensor-history` - Historical data
- GET `/api/v1/batches/{id}/realtime-tracking` - Batch tracking
- GET `/api/v1/equipment/iot-alerts` - Get active alerts
4. **Background Workers**
- Periodic data collection worker
- Connection health monitor
- Alert generation and notification
- Data cleanup (old sensor readings)
5. **Frontend Components**
- Equipment IoT configuration wizard
- Real-time monitoring dashboard
- Sensor data visualization charts
- Alert notification system
- Connection status indicators
6. **Additional Connectors** 📋
- OPC UA connector implementation
- MQTT connector implementation
- SALVA-specific connector (pending API details)
## Next Steps
### Priority 1: Core Service Layer
1. **Create IoT Integration Service**
```python
# app/services/iot_integration_service.py
class IoTIntegrationService:
async def configure_equipment_iot(equipment_id, config)
async def test_connection(equipment_id)
async def get_realtime_data(equipment_id)
async def get_sensor_history(equipment_id, start, end)
async def store_sensor_reading(equipment_id, reading)
```
2. **Create Repository Methods**
```python
# app/repositories/equipment_repository.py
async def update_iot_config(equipment_id, config)
async def get_iot_config(equipment_id)
async def update_connection_status(equipment_id, status)
# app/repositories/sensor_reading_repository.py
async def create_reading(reading)
async def get_readings(equipment_id, start_time, end_time)
async def get_latest_reading(equipment_id)
```
3. **Create API Endpoints**
```python
# app/api/equipment_iot.py
router = APIRouter(prefix="/equipment", tags=["equipment-iot"])
@router.post("/{equipment_id}/iot-config")
@router.post("/{equipment_id}/test-connection")
@router.get("/{equipment_id}/realtime-data")
@router.get("/{equipment_id}/sensor-history")
```
### Priority 2: Background Processing
1. **Data Collection Worker**
- Poll IoT-enabled equipment at configured intervals
- Store sensor readings in database
- Handle connection errors gracefully
2. **Alert Generation**
- Monitor temperature deviations
- Detect connection losses
- Generate alerts for critical conditions
### Priority 3: Frontend Integration
1. **Equipment Configuration UI**
- IoT setup wizard
- Protocol selection
- Connection testing
- Credential management
2. **Real-time Dashboard**
- Live equipment status cards
- Temperature/humidity gauges
- Energy consumption charts
- Alert notifications
## Usage Examples
### Example 1: Configure Equipment for IoT
```python
from app.services.iot_integration_service import IoTIntegrationService
service = IoTIntegrationService()
# Configure Rational iCombi oven
config = {
"protocol": "rational",
"endpoint": "https://www.connectedcooking.com/api/v1",
"port": 443,
"credentials": {
"username": "bakery@example.com",
"password": "secure-password"
},
"additional_config": {
"unit_id": "12345"
}
}
await service.configure_equipment_iot(equipment_id="uuid-here", config=config)
```
### Example 2: Test Connection
```python
# Test connection before saving configuration
result = await service.test_connection(equipment_id="uuid-here")
if result.success:
print(f"Connected in {result.response_time_ms}ms")
print(f"Supported features: {result.supported_features}")
else:
print(f"Connection failed: {result.error_details}")
```
### Example 3: Get Real-time Data
```python
# Get current equipment data
data = await service.get_realtime_data(equipment_id="uuid-here")
print(f"Temperature: {data.temperature}°C")
print(f"Status: {data.operational_status}")
print(f"Progress: {data.cycle_progress_percentage}%")
print(f"Time remaining: {data.time_remaining_minutes} min")
```
### Example 4: Retrieve Sensor History
```python
from datetime import datetime, timedelta
# Get last 24 hours of data
end_time = datetime.now()
start_time = end_time - timedelta(hours=24)
history = await service.get_sensor_history(
equipment_id="uuid-here",
start_time=start_time,
end_time=end_time
)
# Plot temperature over time
for reading in history.readings:
print(f"{reading.reading_time}: {reading.temperature}°C")
```
## API Endpoint Specifications
### POST /api/v1/equipment/{equipment_id}/iot-config
Configure IoT connectivity for equipment.
**Request Body:**
```json
{
"protocol": "rational",
"endpoint": "https://www.connectedcooking.com/api/v1",
"port": 443,
"credentials": {
"username": "user@example.com",
"password": "password"
},
"additional_config": {
"unit_id": "12345"
},
"supports_realtime": true,
"poll_interval_seconds": 30
}
```
**Response:**
```json
{
"success": true,
"message": "IoT configuration saved successfully",
"equipment_id": "uuid",
"connection_test_result": {
"success": true,
"status": "connected",
"response_time_ms": 145
}
}
```
### GET /api/v1/equipment/{equipment_id}/realtime-data
Get current real-time sensor data.
**Response:**
```json
{
"equipment_id": "uuid",
"equipment_name": "Horno Principal #1",
"timestamp": "2025-01-12T10:30:00Z",
"connection_status": "connected",
"temperature": 185.5,
"temperature_zones": {
"cabinet": 180,
"core": 72
},
"humidity": 65.0,
"operational_status": "running",
"cycle_stage": "baking",
"cycle_progress_percentage": 45.0,
"time_remaining_minutes": 12,
"energy_consumption_kwh": 12.5,
"active_batch_id": "batch-uuid",
"active_batch_name": "Baguettes - Batch #123"
}
```
## Security Considerations
1. **Credential Storage**
- Store API keys/passwords encrypted in database
- Use environment variables for sensitive configuration
- Rotate credentials periodically
2. **SSL/TLS**
- Always use HTTPS for REST API connections
- Verify SSL certificates in production
- Support self-signed certificates for local equipment
3. **Authentication**
- Require user authentication for IoT configuration
- Log all configuration changes
- Implement role-based access control
4. **Network Security**
- Support firewall-friendly protocols
- Document required network ports
- Consider VPN for equipment access
## Troubleshooting
### Connection Issues
1. **Timeout errors**
- Increase timeout in additional_config
- Check network connectivity
- Verify firewall rules
2. **Authentication failures**
- Verify credentials are correct
- Check API key expiration
- Confirm endpoint URL is correct
3. **SSL certificate errors**
- Set `verify_ssl: false` for testing (not recommended for production)
- Install proper SSL certificates
- Use certificate bundles for corporate networks
### Data Quality Issues
1. **Missing sensor readings**
- Check equipment supports requested sensors
- Verify polling interval is appropriate
- Review connection logs for errors
2. **Anomalous data**
- Implement data validation
- Set reasonable min/max thresholds
- Flag outliers for manual review
## Resources
### Manufacturer Contacts
- **Rational:** cc-support@rational-online.com
- **Wachtel:** support@wachtel.de / https://www.wachtel.de
- **SALVA:** https://www.salva.es/en
### Standards and Protocols
- **OPC Foundation:** https://opcfoundation.org/
- **Weihenstephan Standards:** https://www.weihenstephan-standards.com
- **MQTT:** https://mqtt.org/
### Libraries
- **httpx:** https://www.python-httpx.org/
- **asyncua:** https://github.com/FreeOpcUa/opcua-asyncio
- **paho-mqtt:** https://pypi.org/project/paho-mqtt/
---
**Last Updated:** 2025-01-12
**Status:** Phase 1 Complete - Foundation & Connectors
**Next Milestone:** Service Layer & API Endpoints

View File

@@ -528,50 +528,89 @@ class QualityCheck(Base):
}
class IoTProtocol(str, enum.Enum):
"""IoT protocol enumeration"""
REST_API = "rest_api"
OPC_UA = "opc_ua"
MQTT = "mqtt"
MODBUS = "modbus"
CUSTOM = "custom"
class IoTConnectionStatus(str, enum.Enum):
"""IoT connection status enumeration"""
CONNECTED = "connected"
DISCONNECTED = "disconnected"
ERROR = "error"
UNKNOWN = "unknown"
class Equipment(Base):
"""Equipment model for tracking production equipment"""
__tablename__ = "equipment"
# Primary identification
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Equipment identification
name = Column(String(255), nullable=False)
type = Column(SQLEnum(EquipmentType), nullable=False)
model = Column(String(100), nullable=True)
serial_number = Column(String(100), nullable=True)
location = Column(String(255), nullable=True)
manufacturer = Column(String(100), nullable=True)
firmware_version = Column(String(50), nullable=True)
# Status tracking
status = Column(SQLEnum(EquipmentStatus), nullable=False, default=EquipmentStatus.OPERATIONAL)
# Dates
install_date = Column(DateTime(timezone=True), nullable=True)
last_maintenance_date = Column(DateTime(timezone=True), nullable=True)
next_maintenance_date = Column(DateTime(timezone=True), nullable=True)
maintenance_interval_days = Column(Integer, nullable=True) # Maintenance interval in days
# Performance metrics
efficiency_percentage = Column(Float, nullable=True) # Current efficiency
uptime_percentage = Column(Float, nullable=True) # Overall equipment effectiveness
energy_usage_kwh = Column(Float, nullable=True) # Current energy usage
# Specifications
power_kw = Column(Float, nullable=True) # Power in kilowatts
capacity = Column(Float, nullable=True) # Capacity (units depend on equipment type)
weight_kg = Column(Float, nullable=True) # Weight in kilograms
# Temperature monitoring
current_temperature = Column(Float, nullable=True) # Current temperature reading
target_temperature = Column(Float, nullable=True) # Target temperature
# IoT Connectivity
iot_enabled = Column(Boolean, default=False, nullable=False)
iot_protocol = Column(String(50), nullable=True) # rest_api, opc_ua, mqtt, modbus, custom
iot_endpoint = Column(String(500), nullable=True) # URL or IP address
iot_port = Column(Integer, nullable=True) # Connection port
iot_credentials = Column(JSON, nullable=True) # Encrypted credentials (API keys, tokens, username/password)
iot_connection_status = Column(String(50), nullable=True) # connected, disconnected, error, unknown
iot_last_connected = Column(DateTime(timezone=True), nullable=True)
iot_config = Column(JSON, nullable=True) # Additional configuration (polling interval, specific endpoints, etc.)
# Real-time monitoring
supports_realtime = Column(Boolean, default=False, nullable=False)
poll_interval_seconds = Column(Integer, nullable=True) # How often to poll for data
# Sensor capabilities
temperature_zones = Column(Integer, nullable=True) # Number of temperature zones
supports_humidity = Column(Boolean, default=False, nullable=False)
supports_energy_monitoring = Column(Boolean, default=False, nullable=False)
supports_remote_control = Column(Boolean, default=False, nullable=False)
# Status
is_active = Column(Boolean, default=True)
# Notes
notes = Column(Text, nullable=True)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
@@ -586,6 +625,8 @@ class Equipment(Base):
"model": self.model,
"serial_number": self.serial_number,
"location": self.location,
"manufacturer": self.manufacturer,
"firmware_version": self.firmware_version,
"status": self.status.value if self.status else None,
"install_date": self.install_date.isoformat() if self.install_date else None,
"last_maintenance_date": self.last_maintenance_date.isoformat() if self.last_maintenance_date else None,
@@ -599,6 +640,19 @@ class Equipment(Base):
"weight_kg": self.weight_kg,
"current_temperature": self.current_temperature,
"target_temperature": self.target_temperature,
"iot_enabled": self.iot_enabled,
"iot_protocol": self.iot_protocol,
"iot_endpoint": self.iot_endpoint,
"iot_port": self.iot_port,
"iot_connection_status": self.iot_connection_status,
"iot_last_connected": self.iot_last_connected.isoformat() if self.iot_last_connected else None,
"iot_config": self.iot_config,
"supports_realtime": self.supports_realtime,
"poll_interval_seconds": self.poll_interval_seconds,
"temperature_zones": self.temperature_zones,
"supports_humidity": self.supports_humidity,
"supports_energy_monitoring": self.supports_energy_monitoring,
"supports_remote_control": self.supports_remote_control,
"is_active": self.is_active,
"notes": self.notes,
"created_at": self.created_at.isoformat() if self.created_at else None,
@@ -606,3 +660,216 @@ class Equipment(Base):
}
class EquipmentSensorReading(Base):
"""Equipment sensor reading model for time-series IoT data"""
__tablename__ = "equipment_sensor_readings"
# Primary identification
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
equipment_id = Column(UUID(as_uuid=True), nullable=False, index=True)
batch_id = Column(UUID(as_uuid=True), nullable=True, index=True)
# Timestamp
reading_time = Column(DateTime(timezone=True), nullable=False, index=True)
# Temperature readings (support multiple zones)
temperature = Column(Float, nullable=True)
temperature_zones = Column(JSON, nullable=True) # {"zone1": 180, "zone2": 200, "zone3": 185}
target_temperature = Column(Float, nullable=True)
# Humidity
humidity = Column(Float, nullable=True)
target_humidity = Column(Float, nullable=True)
# Energy monitoring
energy_consumption_kwh = Column(Float, nullable=True)
power_current_kw = Column(Float, nullable=True)
# Equipment status
operational_status = Column(String(50), nullable=True) # running, idle, warming_up, cooling_down
cycle_stage = Column(String(100), nullable=True) # preheating, baking, cooling
cycle_progress_percentage = Column(Float, nullable=True)
time_remaining_minutes = Column(Integer, nullable=True)
# Process parameters
motor_speed_rpm = Column(Float, nullable=True)
door_status = Column(String(20), nullable=True) # open, closed
steam_level = Column(Float, nullable=True)
# Quality indicators
product_weight_kg = Column(Float, nullable=True)
moisture_content = Column(Float, nullable=True)
# Additional sensor data (flexible JSON for manufacturer-specific metrics)
additional_sensors = Column(JSON, nullable=True)
# Data quality
data_quality_score = Column(Float, nullable=True)
is_anomaly = Column(Boolean, default=False, nullable=False)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary following shared pattern"""
return {
"id": str(self.id),
"tenant_id": str(self.tenant_id),
"equipment_id": str(self.equipment_id),
"batch_id": str(self.batch_id) if self.batch_id else None,
"reading_time": self.reading_time.isoformat() if self.reading_time else None,
"temperature": self.temperature,
"temperature_zones": self.temperature_zones,
"target_temperature": self.target_temperature,
"humidity": self.humidity,
"target_humidity": self.target_humidity,
"energy_consumption_kwh": self.energy_consumption_kwh,
"power_current_kw": self.power_current_kw,
"operational_status": self.operational_status,
"cycle_stage": self.cycle_stage,
"cycle_progress_percentage": self.cycle_progress_percentage,
"time_remaining_minutes": self.time_remaining_minutes,
"motor_speed_rpm": self.motor_speed_rpm,
"door_status": self.door_status,
"steam_level": self.steam_level,
"product_weight_kg": self.product_weight_kg,
"moisture_content": self.moisture_content,
"additional_sensors": self.additional_sensors,
"data_quality_score": self.data_quality_score,
"is_anomaly": self.is_anomaly,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
class EquipmentConnectionLog(Base):
"""Equipment connection log for tracking IoT connectivity"""
__tablename__ = "equipment_connection_logs"
# Primary identification
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
equipment_id = Column(UUID(as_uuid=True), nullable=False, index=True)
# Connection event
event_type = Column(String(50), nullable=False) # connected, disconnected, error, timeout
event_time = Column(DateTime(timezone=True), nullable=False, index=True)
# Connection details
connection_status = Column(String(50), nullable=False)
protocol_used = Column(String(50), nullable=True)
endpoint = Column(String(500), nullable=True)
# Error tracking
error_message = Column(Text, nullable=True)
error_code = Column(String(50), nullable=True)
# Performance metrics
response_time_ms = Column(Integer, nullable=True)
data_points_received = Column(Integer, nullable=True)
# Additional details
additional_data = Column(JSON, nullable=True)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary following shared pattern"""
return {
"id": str(self.id),
"tenant_id": str(self.tenant_id),
"equipment_id": str(self.equipment_id),
"event_type": self.event_type,
"event_time": self.event_time.isoformat() if self.event_time else None,
"connection_status": self.connection_status,
"protocol_used": self.protocol_used,
"endpoint": self.endpoint,
"error_message": self.error_message,
"error_code": self.error_code,
"response_time_ms": self.response_time_ms,
"data_points_received": self.data_points_received,
"additional_data": self.additional_data,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
class EquipmentIoTAlert(Base):
"""Equipment IoT alert model for real-time equipment alerts"""
__tablename__ = "equipment_iot_alerts"
# Primary identification
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
equipment_id = Column(UUID(as_uuid=True), nullable=False, index=True)
batch_id = Column(UUID(as_uuid=True), nullable=True, index=True)
# Alert information
alert_type = Column(String(50), nullable=False) # temperature_deviation, connection_lost, equipment_error
severity = Column(String(20), nullable=False) # info, warning, critical
alert_time = Column(DateTime(timezone=True), nullable=False, index=True)
# Alert details
title = Column(String(255), nullable=False)
message = Column(Text, nullable=False)
sensor_reading_id = Column(UUID(as_uuid=True), nullable=True)
# Threshold information
threshold_value = Column(Float, nullable=True)
actual_value = Column(Float, nullable=True)
deviation_percentage = Column(Float, nullable=True)
# Status tracking
is_active = Column(Boolean, default=True, nullable=False)
is_acknowledged = Column(Boolean, default=False, nullable=False)
acknowledged_by = Column(UUID(as_uuid=True), nullable=True)
acknowledged_at = Column(DateTime(timezone=True), nullable=True)
is_resolved = Column(Boolean, default=False, nullable=False)
resolved_by = Column(UUID(as_uuid=True), nullable=True)
resolved_at = Column(DateTime(timezone=True), nullable=True)
resolution_notes = Column(Text, nullable=True)
# Automated response
auto_resolved = Column(Boolean, default=False, nullable=False)
corrective_action_taken = Column(String(255), nullable=True)
# Additional data
additional_data = Column(JSON, nullable=True)
# Timestamps
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary following shared pattern"""
return {
"id": str(self.id),
"tenant_id": str(self.tenant_id),
"equipment_id": str(self.equipment_id),
"batch_id": str(self.batch_id) if self.batch_id else None,
"alert_type": self.alert_type,
"severity": self.severity,
"alert_time": self.alert_time.isoformat() if self.alert_time else None,
"title": self.title,
"message": self.message,
"sensor_reading_id": str(self.sensor_reading_id) if self.sensor_reading_id else None,
"threshold_value": self.threshold_value,
"actual_value": self.actual_value,
"deviation_percentage": self.deviation_percentage,
"is_active": self.is_active,
"is_acknowledged": self.is_acknowledged,
"acknowledged_by": str(self.acknowledged_by) if self.acknowledged_by else None,
"acknowledged_at": self.acknowledged_at.isoformat() if self.acknowledged_at else None,
"is_resolved": self.is_resolved,
"resolved_by": str(self.resolved_by) if self.resolved_by else None,
"resolved_at": self.resolved_at.isoformat() if self.resolved_at else None,
"resolution_notes": self.resolution_notes,
"auto_resolved": self.auto_resolved,
"corrective_action_taken": self.corrective_action_taken,
"additional_data": self.additional_data,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}

View File

@@ -8,7 +8,31 @@ from typing import Optional, List
from datetime import datetime
from uuid import UUID
from app.models.production import EquipmentType, EquipmentStatus
from app.models.production import EquipmentType, EquipmentStatus, IoTProtocol, IoTConnectionStatus
class IoTConnectionConfig(BaseModel):
"""Schema for IoT connection configuration"""
protocol: str = Field(..., description="IoT protocol (rest_api, opc_ua, mqtt, modbus, custom)")
endpoint: str = Field(..., description="Connection endpoint (URL or IP address)")
port: Optional[int] = Field(None, description="Connection port")
username: Optional[str] = Field(None, description="Username for authentication")
password: Optional[str] = Field(None, description="Password for authentication")
api_key: Optional[str] = Field(None, description="API key for authentication")
token: Optional[str] = Field(None, description="Authentication token")
additional_config: Optional[dict] = Field(None, description="Additional protocol-specific configuration")
model_config = ConfigDict(
json_schema_extra={
"example": {
"protocol": "rest_api",
"endpoint": "https://connectedcooking.com/api/v1",
"port": 443,
"api_key": "your-api-key-here",
"additional_config": {"poll_interval": 30}
}
}
)
class EquipmentCreate(BaseModel):
@@ -18,6 +42,8 @@ class EquipmentCreate(BaseModel):
model: Optional[str] = Field(None, max_length=100, description="Equipment model")
serial_number: Optional[str] = Field(None, max_length=100, description="Serial number")
location: Optional[str] = Field(None, max_length=255, description="Physical location")
manufacturer: Optional[str] = Field(None, max_length=100, description="Manufacturer")
firmware_version: Optional[str] = Field(None, max_length=50, description="Firmware version")
status: EquipmentStatus = Field(default=EquipmentStatus.OPERATIONAL, description="Equipment status")
# Installation and maintenance
@@ -40,6 +66,23 @@ class EquipmentCreate(BaseModel):
current_temperature: Optional[float] = Field(None, description="Current temperature")
target_temperature: Optional[float] = Field(None, description="Target temperature")
# IoT Connectivity
iot_enabled: bool = Field(default=False, description="Enable IoT connectivity")
iot_protocol: Optional[str] = Field(None, description="IoT protocol")
iot_endpoint: Optional[str] = Field(None, description="IoT endpoint URL or IP")
iot_port: Optional[int] = Field(None, description="IoT connection port")
iot_config: Optional[dict] = Field(None, description="IoT configuration")
# Real-time monitoring
supports_realtime: bool = Field(default=False, description="Supports real-time monitoring")
poll_interval_seconds: Optional[int] = Field(None, ge=1, description="Polling interval in seconds")
# Sensor capabilities
temperature_zones: Optional[int] = Field(None, ge=1, description="Number of temperature zones")
supports_humidity: bool = Field(default=False, description="Supports humidity monitoring")
supports_energy_monitoring: bool = Field(default=False, description="Supports energy monitoring")
supports_remote_control: bool = Field(default=False, description="Supports remote control")
# Notes
notes: Optional[str] = Field(None, description="Additional notes")
@@ -70,6 +113,8 @@ class EquipmentUpdate(BaseModel):
model: Optional[str] = Field(None, max_length=100)
serial_number: Optional[str] = Field(None, max_length=100)
location: Optional[str] = Field(None, max_length=255)
manufacturer: Optional[str] = Field(None, max_length=100)
firmware_version: Optional[str] = Field(None, max_length=50)
status: Optional[EquipmentStatus] = None
# Installation and maintenance
@@ -92,6 +137,23 @@ class EquipmentUpdate(BaseModel):
current_temperature: Optional[float] = None
target_temperature: Optional[float] = None
# IoT Connectivity
iot_enabled: Optional[bool] = None
iot_protocol: Optional[str] = None
iot_endpoint: Optional[str] = None
iot_port: Optional[int] = None
iot_config: Optional[dict] = None
# Real-time monitoring
supports_realtime: Optional[bool] = None
poll_interval_seconds: Optional[int] = Field(None, ge=1)
# Sensor capabilities
temperature_zones: Optional[int] = Field(None, ge=1)
supports_humidity: Optional[bool] = None
supports_energy_monitoring: Optional[bool] = None
supports_remote_control: Optional[bool] = None
# Notes
notes: Optional[str] = None
@@ -119,6 +181,8 @@ class EquipmentResponse(BaseModel):
model: Optional[str] = None
serial_number: Optional[str] = None
location: Optional[str] = None
manufacturer: Optional[str] = None
firmware_version: Optional[str] = None
status: EquipmentStatus
# Installation and maintenance
@@ -141,6 +205,25 @@ class EquipmentResponse(BaseModel):
current_temperature: Optional[float] = None
target_temperature: Optional[float] = None
# IoT Connectivity
iot_enabled: bool = False
iot_protocol: Optional[str] = None
iot_endpoint: Optional[str] = None
iot_port: Optional[int] = None
iot_connection_status: Optional[str] = None
iot_last_connected: Optional[datetime] = None
iot_config: Optional[dict] = None
# Real-time monitoring
supports_realtime: bool = False
poll_interval_seconds: Optional[int] = None
# Sensor capabilities
temperature_zones: Optional[int] = None
supports_humidity: bool = False
supports_energy_monitoring: bool = False
supports_remote_control: bool = False
# Status
is_active: bool
notes: Optional[str] = None
@@ -196,3 +279,189 @@ class EquipmentDeletionSummary(BaseModel):
}
}
)
# ================================================================
# IoT-SPECIFIC SCHEMAS
# ================================================================
class EquipmentSensorReadingResponse(BaseModel):
"""Schema for equipment sensor reading response"""
id: UUID
tenant_id: UUID
equipment_id: UUID
batch_id: Optional[UUID] = None
reading_time: datetime
# Temperature readings
temperature: Optional[float] = None
temperature_zones: Optional[dict] = None
target_temperature: Optional[float] = None
# Humidity
humidity: Optional[float] = None
target_humidity: Optional[float] = None
# Energy monitoring
energy_consumption_kwh: Optional[float] = None
power_current_kw: Optional[float] = None
# Equipment status
operational_status: Optional[str] = None
cycle_stage: Optional[str] = None
cycle_progress_percentage: Optional[float] = None
time_remaining_minutes: Optional[int] = None
# Process parameters
motor_speed_rpm: Optional[float] = None
door_status: Optional[str] = None
steam_level: Optional[float] = None
# Quality indicators
product_weight_kg: Optional[float] = None
moisture_content: Optional[float] = None
# Additional sensor data
additional_sensors: Optional[dict] = None
# Data quality
data_quality_score: Optional[float] = None
is_anomaly: bool = False
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class EquipmentConnectionTestResponse(BaseModel):
"""Schema for IoT connection test response"""
success: bool = Field(..., description="Whether connection test succeeded")
status: str = Field(..., description="Connection status")
message: str = Field(..., description="Detailed message")
response_time_ms: Optional[int] = Field(None, description="Response time in milliseconds")
protocol_tested: str = Field(..., description="Protocol that was tested")
endpoint_tested: str = Field(..., description="Endpoint that was tested")
error_details: Optional[str] = Field(None, description="Error details if connection failed")
supported_features: Optional[List[str]] = Field(None, description="List of supported IoT features")
model_config = ConfigDict(
json_schema_extra={
"example": {
"success": True,
"status": "connected",
"message": "Successfully connected to equipment",
"response_time_ms": 145,
"protocol_tested": "rest_api",
"endpoint_tested": "https://connectedcooking.com/api/v1",
"supported_features": ["temperature", "humidity", "energy_monitoring"]
}
}
)
class RealTimeDataResponse(BaseModel):
"""Schema for real-time equipment data response"""
equipment_id: UUID
equipment_name: str
timestamp: datetime
connection_status: str
# Current readings
temperature: Optional[float] = None
temperature_zones: Optional[dict] = None
humidity: Optional[float] = None
energy_consumption_kwh: Optional[float] = None
power_current_kw: Optional[float] = None
# Status
operational_status: Optional[str] = None
cycle_stage: Optional[str] = None
cycle_progress_percentage: Optional[float] = None
time_remaining_minutes: Optional[int] = None
# Active batch
active_batch_id: Optional[UUID] = None
active_batch_name: Optional[str] = None
model_config = ConfigDict(
json_schema_extra={
"example": {
"equipment_id": "123e4567-e89b-12d3-a456-426614174000",
"equipment_name": "Horno Principal #1",
"timestamp": "2025-01-12T10:30:00Z",
"connection_status": "connected",
"temperature": 185.5,
"temperature_zones": {"zone1": 180, "zone2": 190, "zone3": 185},
"humidity": 65.0,
"operational_status": "running",
"cycle_stage": "baking",
"cycle_progress_percentage": 45.0,
"time_remaining_minutes": 12
}
}
)
class EquipmentIoTAlertResponse(BaseModel):
"""Schema for IoT alert response"""
id: UUID
tenant_id: UUID
equipment_id: UUID
batch_id: Optional[UUID] = None
# Alert information
alert_type: str
severity: str
alert_time: datetime
# Alert details
title: str
message: str
# Threshold information
threshold_value: Optional[float] = None
actual_value: Optional[float] = None
deviation_percentage: Optional[float] = None
# Status
is_active: bool
is_acknowledged: bool
acknowledged_by: Optional[UUID] = None
acknowledged_at: Optional[datetime] = None
is_resolved: bool
resolved_by: Optional[UUID] = None
resolved_at: Optional[datetime] = None
resolution_notes: Optional[str] = None
# Automated response
auto_resolved: bool
corrective_action_taken: Optional[str] = None
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class EquipmentSensorHistoryResponse(BaseModel):
"""Schema for sensor reading history response"""
equipment_id: UUID
equipment_name: str
start_time: datetime
end_time: datetime
total_readings: int
readings: List[EquipmentSensorReadingResponse]
model_config = ConfigDict(
json_schema_extra={
"example": {
"equipment_id": "123e4567-e89b-12d3-a456-426614174000",
"equipment_name": "Horno Principal #1",
"start_time": "2025-01-12T08:00:00Z",
"end_time": "2025-01-12T12:00:00Z",
"total_readings": 48,
"readings": []
}
}
)

View File

@@ -0,0 +1,19 @@
"""
IoT integration services for equipment connectivity
"""
from .base_connector import (
BaseIoTConnector,
SensorReading,
ConnectionStatus,
EquipmentCapabilities,
ConnectorFactory
)
__all__ = [
'BaseIoTConnector',
'SensorReading',
'ConnectionStatus',
'EquipmentCapabilities',
'ConnectorFactory',
]

View File

@@ -0,0 +1,242 @@
"""
Base IoT connector interface for equipment integration
"""
from abc import ABC, abstractmethod
from typing import Dict, Any, List, Optional
from datetime import datetime
from dataclasses import dataclass
@dataclass
class SensorReading:
"""Standardized sensor reading data structure"""
timestamp: datetime
temperature: Optional[float] = None
temperature_zones: Optional[Dict[str, float]] = None
target_temperature: Optional[float] = None
humidity: Optional[float] = None
target_humidity: Optional[float] = None
energy_consumption_kwh: Optional[float] = None
power_current_kw: Optional[float] = None
operational_status: Optional[str] = None
cycle_stage: Optional[str] = None
cycle_progress_percentage: Optional[float] = None
time_remaining_minutes: Optional[int] = None
motor_speed_rpm: Optional[float] = None
door_status: Optional[str] = None
steam_level: Optional[float] = None
product_weight_kg: Optional[float] = None
moisture_content: Optional[float] = None
additional_sensors: Optional[Dict[str, Any]] = None
@dataclass
class ConnectionStatus:
"""Connection status information"""
is_connected: bool
status: str # connected, disconnected, error, unknown
message: str
response_time_ms: Optional[int] = None
error_details: Optional[str] = None
last_successful_connection: Optional[datetime] = None
@dataclass
class EquipmentCapabilities:
"""Equipment IoT capabilities"""
supports_temperature: bool = False
supports_humidity: bool = False
supports_energy_monitoring: bool = False
supports_remote_control: bool = False
supports_realtime: bool = False
temperature_zones: int = 1
supported_protocols: List[str] = None
manufacturer_specific_features: Optional[Dict[str, Any]] = None
def __post_init__(self):
if self.supported_protocols is None:
self.supported_protocols = []
class BaseIoTConnector(ABC):
"""
Base abstract class for IoT equipment connectors
All manufacturer-specific connectors must implement this interface
"""
def __init__(self, equipment_id: str, config: Dict[str, Any]):
"""
Initialize the IoT connector
Args:
equipment_id: Unique equipment identifier
config: Connection configuration including endpoint, credentials, etc.
"""
self.equipment_id = equipment_id
self.config = config
self.endpoint = config.get('endpoint')
self.port = config.get('port')
self.credentials = config.get('credentials', {})
self._is_connected = False
self._last_error: Optional[str] = None
@abstractmethod
async def connect(self) -> ConnectionStatus:
"""
Establish connection to the equipment
Returns:
ConnectionStatus with connection details
"""
pass
@abstractmethod
async def disconnect(self) -> bool:
"""
Close connection to the equipment
Returns:
True if disconnected successfully
"""
pass
@abstractmethod
async def test_connection(self) -> ConnectionStatus:
"""
Test connection without establishing persistent connection
Returns:
ConnectionStatus with test results
"""
pass
@abstractmethod
async def get_current_reading(self) -> Optional[SensorReading]:
"""
Get current sensor readings from the equipment
Returns:
SensorReading with current data or None if unavailable
"""
pass
@abstractmethod
async def get_capabilities(self) -> EquipmentCapabilities:
"""
Discover equipment capabilities
Returns:
EquipmentCapabilities describing what the equipment supports
"""
pass
@abstractmethod
async def get_status(self) -> Dict[str, Any]:
"""
Get equipment status information
Returns:
Dictionary with status details
"""
pass
async def set_target_temperature(self, temperature: float) -> bool:
"""
Set target temperature (if supported)
Args:
temperature: Target temperature in Celsius
Returns:
True if command sent successfully
"""
raise NotImplementedError("Remote control not supported by this equipment")
async def start_cycle(self, params: Dict[str, Any]) -> bool:
"""
Start production cycle (if supported)
Args:
params: Cycle parameters
Returns:
True if cycle started successfully
"""
raise NotImplementedError("Remote control not supported by this equipment")
async def stop_cycle(self) -> bool:
"""
Stop current production cycle (if supported)
Returns:
True if cycle stopped successfully
"""
raise NotImplementedError("Remote control not supported by this equipment")
def get_protocol_name(self) -> str:
"""Get the protocol name used by this connector"""
return self.__class__.__name__.replace('Connector', '').lower()
def is_connected(self) -> bool:
"""Check if currently connected"""
return self._is_connected
def get_last_error(self) -> Optional[str]:
"""Get last error message"""
return self._last_error
def _set_error(self, error: str):
"""Set error message"""
self._last_error = error
def _clear_error(self):
"""Clear error message"""
self._last_error = None
class ConnectorFactory:
"""
Factory for creating appropriate IoT connectors based on protocol
"""
_connectors: Dict[str, type] = {}
@classmethod
def register_connector(cls, protocol: str, connector_class: type):
"""
Register a connector implementation
Args:
protocol: Protocol name (e.g., 'rest_api', 'opc_ua')
connector_class: Connector class implementing BaseIoTConnector
"""
cls._connectors[protocol.lower()] = connector_class
@classmethod
def create_connector(cls, protocol: str, equipment_id: str, config: Dict[str, Any]) -> BaseIoTConnector:
"""
Create connector instance for specified protocol
Args:
protocol: Protocol name
equipment_id: Equipment identifier
config: Connection configuration
Returns:
Connector instance
Raises:
ValueError: If protocol not supported
"""
connector_class = cls._connectors.get(protocol.lower())
if not connector_class:
raise ValueError(f"Unsupported IoT protocol: {protocol}")
return connector_class(equipment_id, config)
@classmethod
def get_supported_protocols(cls) -> List[str]:
"""Get list of supported protocols"""
return list(cls._connectors.keys())

View File

@@ -0,0 +1,156 @@
"""
Rational ConnectedCooking API connector
For Rational iCombi ovens with ConnectedCooking cloud platform
"""
from typing import Dict, Any
from .rest_api_connector import GenericRESTAPIConnector
from .base_connector import SensorReading, EquipmentCapabilities
class RationalConnectedCookingConnector(GenericRESTAPIConnector):
"""
Connector for Rational iCombi ovens via ConnectedCooking platform
Expected configuration:
{
"endpoint": "https://www.connectedcooking.com/api/v1",
"port": 443,
"credentials": {
"username": "your-email@example.com",
"password": "your-password",
# Or use API token if available
"token": "your-bearer-token"
},
"additional_config": {
"unit_id": "12345", # Rational unit ID from ConnectedCooking
"data_endpoint": "/units/{unit_id}/status",
"status_endpoint": "/units/{unit_id}",
"timeout": 15
}
}
API Documentation: Contact Rational at cc-support@rational-online.com
"""
def __init__(self, equipment_id: str, config: Dict[str, Any]):
# Replace equipment_id with unit_id for Rational API
self.unit_id = config.get('additional_config', {}).get('unit_id', equipment_id)
# Update endpoints to use unit_id
if 'additional_config' not in config:
config['additional_config'] = {}
config['additional_config'].setdefault(
'data_endpoint', f'/units/{self.unit_id}/status'
)
config['additional_config'].setdefault(
'status_endpoint', f'/units/{self.unit_id}'
)
super().__init__(equipment_id, config)
def _parse_sensor_data(self, data: Dict[str, Any]) -> SensorReading:
"""
Parse Rational-specific API response
Expected Rational ConnectedCooking response format (example):
{
"timestamp": "2025-01-12T10:30:00Z",
"unit_status": "cooking",
"cooking_mode": "combi_steam",
"cabinet_temperature": 185.0,
"core_temperature": 72.0,
"humidity": 65,
"door_open": false,
"time_remaining_seconds": 720,
"energy_consumption": 12.5,
...
}
"""
from datetime import datetime, timezone
# Map Rational fields to standard SensorReading
cabinet_temp = data.get('cabinet_temperature')
core_temp = data.get('core_temperature')
# Multi-zone temperature support
temperature_zones = {}
if cabinet_temp is not None:
temperature_zones['cabinet'] = cabinet_temp
if core_temp is not None:
temperature_zones['core'] = core_temp
# Map Rational-specific statuses
unit_status = data.get('unit_status', '').lower()
operational_status = self._map_rational_status(unit_status)
# Convert time remaining from seconds to minutes
time_remaining_seconds = data.get('time_remaining_seconds')
time_remaining_minutes = int(time_remaining_seconds / 60) if time_remaining_seconds else None
return SensorReading(
timestamp=self._parse_timestamp(data.get('timestamp')),
temperature=cabinet_temp, # Primary temperature is cabinet
temperature_zones=temperature_zones if temperature_zones else None,
target_temperature=data.get('target_temperature') or data.get('cabinet_target_temperature'),
humidity=data.get('humidity'),
target_humidity=data.get('target_humidity'),
energy_consumption_kwh=data.get('energy_consumption'),
power_current_kw=data.get('current_power_kw'),
operational_status=operational_status,
cycle_stage=data.get('cooking_mode') or data.get('program_name'),
cycle_progress_percentage=data.get('progress_percentage'),
time_remaining_minutes=time_remaining_minutes,
door_status='open' if data.get('door_open') else 'closed',
steam_level=data.get('steam_level'),
additional_sensors={
'cooking_mode': data.get('cooking_mode'),
'program_name': data.get('program_name'),
'fan_speed': data.get('fan_speed'),
'core_temperature': core_temp,
}
)
def _map_rational_status(self, rational_status: str) -> str:
"""Map Rational-specific status to standard operational status"""
status_map = {
'idle': 'idle',
'preheating': 'warming_up',
'cooking': 'running',
'cooling': 'cooling_down',
'cleaning': 'maintenance',
'error': 'error',
'off': 'idle'
}
return status_map.get(rational_status, 'unknown')
async def get_capabilities(self) -> EquipmentCapabilities:
"""Get Rational iCombi capabilities"""
return EquipmentCapabilities(
supports_temperature=True,
supports_humidity=True,
supports_energy_monitoring=True,
supports_remote_control=True, # ConnectedCooking supports remote operation
supports_realtime=True,
temperature_zones=2, # Cabinet + Core
supported_protocols=['rest_api'],
manufacturer_specific_features={
'manufacturer': 'Rational',
'product_line': 'iCombi',
'platform': 'ConnectedCooking',
'features': [
'HACCP_documentation',
'recipe_management',
'remote_start',
'cooking_programs',
'automatic_cleaning'
]
}
)
# Register connector
from .base_connector import ConnectorFactory
ConnectorFactory.register_connector('rational_connected_cooking', RationalConnectedCookingConnector)
ConnectorFactory.register_connector('rational', RationalConnectedCookingConnector) # Alias

View File

@@ -0,0 +1,328 @@
"""
Generic REST API connector for IoT equipment
Supports standard REST endpoints with JSON responses
"""
import httpx
import time
from typing import Dict, Any, Optional
from datetime import datetime, timezone
from .base_connector import (
BaseIoTConnector,
SensorReading,
ConnectionStatus,
EquipmentCapabilities
)
class GenericRESTAPIConnector(BaseIoTConnector):
"""
Generic REST API connector for equipment with standard REST interfaces
Expected configuration:
{
"endpoint": "https://api.example.com",
"port": 443,
"credentials": {
"api_key": "your-api-key",
"token": "bearer-token", # Optional
"username": "user", # Optional
"password": "pass" # Optional
},
"additional_config": {
"data_endpoint": "/api/v1/equipment/{equipment_id}/data",
"status_endpoint": "/api/v1/equipment/{equipment_id}/status",
"capabilities_endpoint": "/api/v1/equipment/{equipment_id}/capabilities",
"timeout": 10,
"verify_ssl": true
}
}
"""
def __init__(self, equipment_id: str, config: Dict[str, Any]):
super().__init__(equipment_id, config)
self.timeout = config.get('additional_config', {}).get('timeout', 10)
self.verify_ssl = config.get('additional_config', {}).get('verify_ssl', True)
# API endpoints (support templating with {equipment_id})
self.data_endpoint = config.get('additional_config', {}).get(
'data_endpoint', '/data'
).replace('{equipment_id}', equipment_id)
self.status_endpoint = config.get('additional_config', {}).get(
'status_endpoint', '/status'
).replace('{equipment_id}', equipment_id)
self.capabilities_endpoint = config.get('additional_config', {}).get(
'capabilities_endpoint', '/capabilities'
).replace('{equipment_id}', equipment_id)
# Build full base URL
port_str = f":{self.port}" if self.port and self.port not in [80, 443] else ""
self.base_url = f"{self.endpoint}{port_str}"
# Authentication headers
self._headers = self._build_auth_headers()
# HTTP client (will be created on demand)
self._client: Optional[httpx.AsyncClient] = None
def _build_auth_headers(self) -> Dict[str, str]:
"""Build authentication headers from credentials"""
headers = {
"Content-Type": "application/json",
"Accept": "application/json"
}
# API Key authentication
if 'api_key' in self.credentials:
headers['X-API-Key'] = self.credentials['api_key']
# Bearer token authentication
if 'token' in self.credentials:
headers['Authorization'] = f"Bearer {self.credentials['token']}"
# Basic auth (will be handled by httpx.BasicAuth if needed)
return headers
async def _get_client(self) -> httpx.AsyncClient:
"""Get or create HTTP client"""
if self._client is None:
auth = None
if 'username' in self.credentials and 'password' in self.credentials:
auth = httpx.BasicAuth(
username=self.credentials['username'],
password=self.credentials['password']
)
self._client = httpx.AsyncClient(
base_url=self.base_url,
headers=self._headers,
auth=auth,
timeout=self.timeout,
verify=self.verify_ssl
)
return self._client
async def connect(self) -> ConnectionStatus:
"""Establish connection (test connectivity)"""
try:
client = await self._get_client()
start_time = time.time()
# Try to fetch status to verify connection
response = await client.get(self.status_endpoint)
response_time = int((time.time() - start_time) * 1000)
if response.status_code == 200:
self._is_connected = True
self._clear_error()
return ConnectionStatus(
is_connected=True,
status="connected",
message="Successfully connected to equipment API",
response_time_ms=response_time,
last_successful_connection=datetime.now(timezone.utc)
)
else:
self._is_connected = False
error_msg = f"HTTP {response.status_code}: {response.text}"
self._set_error(error_msg)
return ConnectionStatus(
is_connected=False,
status="error",
message="Failed to connect to equipment API",
response_time_ms=response_time,
error_details=error_msg
)
except httpx.TimeoutException as e:
self._is_connected = False
error_msg = f"Connection timeout: {str(e)}"
self._set_error(error_msg)
return ConnectionStatus(
is_connected=False,
status="error",
message="Connection timeout",
error_details=error_msg
)
except Exception as e:
self._is_connected = False
error_msg = f"Connection error: {str(e)}"
self._set_error(error_msg)
return ConnectionStatus(
is_connected=False,
status="error",
message="Failed to connect",
error_details=error_msg
)
async def disconnect(self) -> bool:
"""Close connection"""
if self._client:
await self._client.aclose()
self._client = None
self._is_connected = False
return True
async def test_connection(self) -> ConnectionStatus:
"""Test connection without persisting client"""
result = await self.connect()
await self.disconnect()
return result
async def get_current_reading(self) -> Optional[SensorReading]:
"""Get current sensor readings from equipment"""
try:
client = await self._get_client()
response = await client.get(self.data_endpoint)
if response.status_code != 200:
self._set_error(f"Failed to fetch data: HTTP {response.status_code}")
return None
data = response.json()
# Parse response into SensorReading
# This mapping can be customized per manufacturer
return self._parse_sensor_data(data)
except Exception as e:
self._set_error(f"Error fetching sensor data: {str(e)}")
return None
def _parse_sensor_data(self, data: Dict[str, Any]) -> SensorReading:
"""
Parse API response into standardized SensorReading
Override this method for manufacturer-specific parsing
"""
# Default parsing - assumes standard field names
return SensorReading(
timestamp=self._parse_timestamp(data.get('timestamp')),
temperature=data.get('temperature'),
temperature_zones=data.get('temperature_zones'),
target_temperature=data.get('target_temperature'),
humidity=data.get('humidity'),
target_humidity=data.get('target_humidity'),
energy_consumption_kwh=data.get('energy_consumption_kwh'),
power_current_kw=data.get('power_current_kw') or data.get('power_kw'),
operational_status=data.get('operational_status') or data.get('status'),
cycle_stage=data.get('cycle_stage') or data.get('stage'),
cycle_progress_percentage=data.get('cycle_progress_percentage') or data.get('progress'),
time_remaining_minutes=data.get('time_remaining_minutes') or data.get('time_remaining'),
motor_speed_rpm=data.get('motor_speed_rpm'),
door_status=data.get('door_status'),
steam_level=data.get('steam_level'),
product_weight_kg=data.get('product_weight_kg'),
moisture_content=data.get('moisture_content'),
additional_sensors=data.get('additional_sensors') or {}
)
def _parse_timestamp(self, timestamp_value: Any) -> datetime:
"""Parse timestamp from various formats"""
if timestamp_value is None:
return datetime.now(timezone.utc)
if isinstance(timestamp_value, datetime):
return timestamp_value
if isinstance(timestamp_value, str):
# Try ISO format
try:
return datetime.fromisoformat(timestamp_value.replace('Z', '+00:00'))
except:
pass
if isinstance(timestamp_value, (int, float)):
# Unix timestamp
return datetime.fromtimestamp(timestamp_value, tz=timezone.utc)
return datetime.now(timezone.utc)
async def get_capabilities(self) -> EquipmentCapabilities:
"""Discover equipment capabilities"""
try:
client = await self._get_client()
response = await client.get(self.capabilities_endpoint)
if response.status_code == 200:
data = response.json()
return EquipmentCapabilities(
supports_temperature=data.get('supports_temperature', True),
supports_humidity=data.get('supports_humidity', False),
supports_energy_monitoring=data.get('supports_energy_monitoring', False),
supports_remote_control=data.get('supports_remote_control', False),
supports_realtime=data.get('supports_realtime', True),
temperature_zones=data.get('temperature_zones', 1),
supported_protocols=['rest_api'],
manufacturer_specific_features=data.get('additional_features')
)
else:
# Return default capabilities if endpoint not available
return EquipmentCapabilities(
supports_temperature=True,
supports_realtime=True,
supported_protocols=['rest_api']
)
except Exception as e:
# Return minimal capabilities on error
self._set_error(f"Error fetching capabilities: {str(e)}")
return EquipmentCapabilities(
supports_temperature=True,
supports_realtime=True,
supported_protocols=['rest_api']
)
async def get_status(self) -> Dict[str, Any]:
"""Get equipment status"""
try:
client = await self._get_client()
response = await client.get(self.status_endpoint)
if response.status_code == 200:
return response.json()
else:
return {
"error": f"HTTP {response.status_code}",
"connected": False
}
except Exception as e:
return {
"error": str(e),
"connected": False
}
async def set_target_temperature(self, temperature: float) -> bool:
"""Set target temperature (if supported)"""
try:
client = await self._get_client()
# POST to control endpoint
control_endpoint = self.config.get('additional_config', {}).get(
'control_endpoint', '/control'
).replace('{equipment_id}', self.equipment_id)
response = await client.post(
control_endpoint,
json={"target_temperature": temperature}
)
return response.status_code in [200, 201, 202]
except Exception as e:
self._set_error(f"Error setting temperature: {str(e)}")
return False
# Register this connector with the factory
from .base_connector import ConnectorFactory
ConnectorFactory.register_connector('rest_api', GenericRESTAPIConnector)

View File

@@ -0,0 +1,149 @@
"""
Wachtel REMOTE connector
For Wachtel bakery ovens with REMOTE monitoring system
"""
from typing import Dict, Any
from .rest_api_connector import GenericRESTAPIConnector
from .base_connector import SensorReading, EquipmentCapabilities
class WachtelREMOTEConnector(GenericRESTAPIConnector):
"""
Connector for Wachtel ovens via REMOTE monitoring system
Expected configuration:
{
"endpoint": "https://remote.wachtel.de/api", # Example endpoint
"port": 443,
"credentials": {
"username": "bakery-username",
"password": "bakery-password"
},
"additional_config": {
"oven_id": "oven-serial-number",
"data_endpoint": "/ovens/{oven_id}/readings",
"status_endpoint": "/ovens/{oven_id}/status",
"timeout": 10
}
}
Note: Actual API endpoints need to be obtained from Wachtel
Contact: support@wachtel.de or visit https://www.wachtel.de
"""
def __init__(self, equipment_id: str, config: Dict[str, Any]):
self.oven_id = config.get('additional_config', {}).get('oven_id', equipment_id)
if 'additional_config' not in config:
config['additional_config'] = {}
config['additional_config'].setdefault(
'data_endpoint', f'/ovens/{self.oven_id}/readings'
)
config['additional_config'].setdefault(
'status_endpoint', f'/ovens/{self.oven_id}/status'
)
super().__init__(equipment_id, config)
def _parse_sensor_data(self, data: Dict[str, Any]) -> SensorReading:
"""
Parse Wachtel REMOTE API response
Expected format (to be confirmed with actual API):
{
"timestamp": "2025-01-12T10:30:00Z",
"oven_status": "baking",
"deck_temperatures": [180, 185, 190], # Multiple deck support
"target_temperatures": [180, 185, 190],
"energy_consumption_kwh": 15.2,
"current_power_kw": 18.5,
"operation_hours": 1245,
...
}
"""
# Parse deck temperatures (Wachtel ovens typically have multiple decks)
deck_temps = data.get('deck_temperatures', [])
temperature_zones = {}
if deck_temps:
for i, temp in enumerate(deck_temps, 1):
temperature_zones[f'deck_{i}'] = temp
# Primary temperature is average or first deck
primary_temp = deck_temps[0] if deck_temps else data.get('temperature')
# Map Wachtel status to standard status
oven_status = data.get('oven_status', '').lower()
operational_status = self._map_wachtel_status(oven_status)
return SensorReading(
timestamp=self._parse_timestamp(data.get('timestamp')),
temperature=primary_temp,
temperature_zones=temperature_zones if temperature_zones else None,
target_temperature=data.get('target_temperature'),
humidity=None, # Wachtel deck ovens typically don't have humidity sensors
target_humidity=None,
energy_consumption_kwh=data.get('energy_consumption_kwh'),
power_current_kw=data.get('current_power_kw'),
operational_status=operational_status,
cycle_stage=data.get('baking_program'),
cycle_progress_percentage=data.get('cycle_progress'),
time_remaining_minutes=data.get('time_remaining_minutes'),
door_status=None, # Deck ovens don't typically report door status
steam_level=data.get('steam_injection_active'),
additional_sensors={
'deck_count': len(deck_temps),
'operation_hours': data.get('operation_hours'),
'maintenance_due': data.get('maintenance_due'),
'deck_temperatures': deck_temps,
'target_temperatures': data.get('target_temperatures'),
}
)
def _map_wachtel_status(self, wachtel_status: str) -> str:
"""Map Wachtel-specific status to standard operational status"""
status_map = {
'off': 'idle',
'standby': 'idle',
'preheating': 'warming_up',
'baking': 'running',
'ready': 'idle',
'error': 'error',
'maintenance': 'maintenance'
}
return status_map.get(wachtel_status, 'unknown')
async def get_capabilities(self) -> EquipmentCapabilities:
"""Get Wachtel oven capabilities"""
# Try to determine number of decks from config or API
deck_count = self.config.get('additional_config', {}).get('deck_count', 3)
return EquipmentCapabilities(
supports_temperature=True,
supports_humidity=False, # Typically not available on deck ovens
supports_energy_monitoring=True,
supports_remote_control=False, # REMOTE is monitoring only
supports_realtime=True,
temperature_zones=deck_count,
supported_protocols=['rest_api'],
manufacturer_specific_features={
'manufacturer': 'Wachtel',
'product_line': 'Deck Ovens',
'platform': 'REMOTE',
'features': [
'multi_deck_monitoring',
'energy_consumption_tracking',
'maintenance_alerts',
'operation_hours_tracking',
'deck_specific_temperature_control'
]
}
)
# Register connector
from .base_connector import ConnectorFactory
ConnectorFactory.register_connector('wachtel_remote', WachtelREMOTEConnector)
ConnectorFactory.register_connector('wachtel', WachtelREMOTEConnector) # Alias

View File

@@ -0,0 +1,241 @@
"""Add IoT equipment support
Revision ID: 002_add_iot_equipment_support
Revises: 001_unified_initial_schema
Create Date: 2025-01-12 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '002_add_iot_equipment_support'
down_revision = '001_unified_initial_schema'
branch_labels = None
depends_on = None
def upgrade():
"""Add IoT connectivity fields to equipment and create sensor data tables"""
# Add IoT connectivity fields to equipment table
op.add_column('equipment', sa.Column('iot_enabled', sa.Boolean(), nullable=False, server_default='false'))
op.add_column('equipment', sa.Column('iot_protocol', sa.String(50), nullable=True))
op.add_column('equipment', sa.Column('iot_endpoint', sa.String(500), nullable=True))
op.add_column('equipment', sa.Column('iot_port', sa.Integer(), nullable=True))
op.add_column('equipment', sa.Column('iot_credentials', postgresql.JSON(astext_type=sa.Text()), nullable=True))
op.add_column('equipment', sa.Column('iot_connection_status', sa.String(50), nullable=True))
op.add_column('equipment', sa.Column('iot_last_connected', sa.DateTime(timezone=True), nullable=True))
op.add_column('equipment', sa.Column('iot_config', postgresql.JSON(astext_type=sa.Text()), nullable=True))
op.add_column('equipment', sa.Column('manufacturer', sa.String(100), nullable=True))
op.add_column('equipment', sa.Column('firmware_version', sa.String(50), nullable=True))
# Add real-time monitoring fields
op.add_column('equipment', sa.Column('supports_realtime', sa.Boolean(), nullable=False, server_default='false'))
op.add_column('equipment', sa.Column('poll_interval_seconds', sa.Integer(), nullable=True))
# Add sensor capability fields
op.add_column('equipment', sa.Column('temperature_zones', sa.Integer(), nullable=True))
op.add_column('equipment', sa.Column('supports_humidity', sa.Boolean(), nullable=False, server_default='false'))
op.add_column('equipment', sa.Column('supports_energy_monitoring', sa.Boolean(), nullable=False, server_default='false'))
op.add_column('equipment', sa.Column('supports_remote_control', sa.Boolean(), nullable=False, server_default='false'))
# Create equipment_sensor_readings table for time-series data
op.create_table(
'equipment_sensor_readings',
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column('tenant_id', postgresql.UUID(as_uuid=True), nullable=False, index=True),
sa.Column('equipment_id', postgresql.UUID(as_uuid=True), nullable=False, index=True),
sa.Column('batch_id', postgresql.UUID(as_uuid=True), nullable=True, index=True),
# Timestamp
sa.Column('reading_time', sa.DateTime(timezone=True), nullable=False, index=True),
# Temperature readings (support multiple zones)
sa.Column('temperature', sa.Float(), nullable=True),
sa.Column('temperature_zones', postgresql.JSON(astext_type=sa.Text()), nullable=True),
sa.Column('target_temperature', sa.Float(), nullable=True),
# Humidity
sa.Column('humidity', sa.Float(), nullable=True),
sa.Column('target_humidity', sa.Float(), nullable=True),
# Energy monitoring
sa.Column('energy_consumption_kwh', sa.Float(), nullable=True),
sa.Column('power_current_kw', sa.Float(), nullable=True),
# Equipment status
sa.Column('operational_status', sa.String(50), nullable=True),
sa.Column('cycle_stage', sa.String(100), nullable=True),
sa.Column('cycle_progress_percentage', sa.Float(), nullable=True),
sa.Column('time_remaining_minutes', sa.Integer(), nullable=True),
# Process parameters
sa.Column('motor_speed_rpm', sa.Float(), nullable=True),
sa.Column('door_status', sa.String(20), nullable=True),
sa.Column('steam_level', sa.Float(), nullable=True),
# Quality indicators
sa.Column('product_weight_kg', sa.Float(), nullable=True),
sa.Column('moisture_content', sa.Float(), nullable=True),
# Additional sensor data (flexible JSON for manufacturer-specific metrics)
sa.Column('additional_sensors', postgresql.JSON(astext_type=sa.Text()), nullable=True),
# Data quality
sa.Column('data_quality_score', sa.Float(), nullable=True),
sa.Column('is_anomaly', sa.Boolean(), nullable=False, server_default='false'),
# Timestamps
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
# Foreign key constraints
sa.ForeignKeyConstraint(['equipment_id'], ['equipment.id'], ondelete='CASCADE'),
)
# Create indexes for time-series queries
op.create_index(
'idx_sensor_readings_equipment_time',
'equipment_sensor_readings',
['equipment_id', 'reading_time'],
)
op.create_index(
'idx_sensor_readings_batch',
'equipment_sensor_readings',
['batch_id', 'reading_time'],
)
op.create_index(
'idx_sensor_readings_tenant_time',
'equipment_sensor_readings',
['tenant_id', 'reading_time'],
)
# Create equipment_connection_logs table for tracking connectivity
op.create_table(
'equipment_connection_logs',
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column('tenant_id', postgresql.UUID(as_uuid=True), nullable=False, index=True),
sa.Column('equipment_id', postgresql.UUID(as_uuid=True), nullable=False, index=True),
# Connection event
sa.Column('event_type', sa.String(50), nullable=False), # connected, disconnected, error, timeout
sa.Column('event_time', sa.DateTime(timezone=True), nullable=False, index=True),
# Connection details
sa.Column('connection_status', sa.String(50), nullable=False),
sa.Column('protocol_used', sa.String(50), nullable=True),
sa.Column('endpoint', sa.String(500), nullable=True),
# Error tracking
sa.Column('error_message', sa.Text(), nullable=True),
sa.Column('error_code', sa.String(50), nullable=True),
# Performance metrics
sa.Column('response_time_ms', sa.Integer(), nullable=True),
sa.Column('data_points_received', sa.Integer(), nullable=True),
# Additional details
sa.Column('additional_data', postgresql.JSON(astext_type=sa.Text()), nullable=True),
# Timestamps
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
# Foreign key constraints
sa.ForeignKeyConstraint(['equipment_id'], ['equipment.id'], ondelete='CASCADE'),
)
# Create index for connection logs
op.create_index(
'idx_connection_logs_equipment_time',
'equipment_connection_logs',
['equipment_id', 'event_time'],
)
# Create equipment_alerts table for IoT-based alerts
op.create_table(
'equipment_iot_alerts',
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column('tenant_id', postgresql.UUID(as_uuid=True), nullable=False, index=True),
sa.Column('equipment_id', postgresql.UUID(as_uuid=True), nullable=False, index=True),
sa.Column('batch_id', postgresql.UUID(as_uuid=True), nullable=True, index=True),
# Alert information
sa.Column('alert_type', sa.String(50), nullable=False), # temperature_deviation, connection_lost, equipment_error
sa.Column('severity', sa.String(20), nullable=False), # info, warning, critical
sa.Column('alert_time', sa.DateTime(timezone=True), nullable=False, index=True),
# Alert details
sa.Column('title', sa.String(255), nullable=False),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('sensor_reading_id', postgresql.UUID(as_uuid=True), nullable=True),
# Threshold information
sa.Column('threshold_value', sa.Float(), nullable=True),
sa.Column('actual_value', sa.Float(), nullable=True),
sa.Column('deviation_percentage', sa.Float(), nullable=True),
# Status tracking
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
sa.Column('is_acknowledged', sa.Boolean(), nullable=False, server_default='false'),
sa.Column('acknowledged_by', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('acknowledged_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('is_resolved', sa.Boolean(), nullable=False, server_default='false'),
sa.Column('resolved_by', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('resolution_notes', sa.Text(), nullable=True),
# Automated response
sa.Column('auto_resolved', sa.Boolean(), nullable=False, server_default='false'),
sa.Column('corrective_action_taken', sa.String(255), nullable=True),
# Additional data
sa.Column('additional_data', postgresql.JSON(astext_type=sa.Text()), nullable=True),
# Timestamps
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now()),
# Foreign key constraints
sa.ForeignKeyConstraint(['equipment_id'], ['equipment.id'], ondelete='CASCADE'),
)
# Create indexes for alerts
op.create_index(
'idx_iot_alerts_equipment_time',
'equipment_iot_alerts',
['equipment_id', 'alert_time'],
)
op.create_index(
'idx_iot_alerts_active',
'equipment_iot_alerts',
['is_active', 'is_resolved'],
)
def downgrade():
"""Remove IoT equipment support"""
# Drop tables
op.drop_table('equipment_iot_alerts')
op.drop_table('equipment_connection_logs')
op.drop_table('equipment_sensor_readings')
# Remove columns from equipment table
op.drop_column('equipment', 'supports_remote_control')
op.drop_column('equipment', 'supports_energy_monitoring')
op.drop_column('equipment', 'supports_humidity')
op.drop_column('equipment', 'temperature_zones')
op.drop_column('equipment', 'poll_interval_seconds')
op.drop_column('equipment', 'supports_realtime')
op.drop_column('equipment', 'firmware_version')
op.drop_column('equipment', 'manufacturer')
op.drop_column('equipment', 'iot_config')
op.drop_column('equipment', 'iot_last_connected')
op.drop_column('equipment', 'iot_connection_status')
op.drop_column('equipment', 'iot_credentials')
op.drop_column('equipment', 'iot_port')
op.drop_column('equipment', 'iot_endpoint')
op.drop_column('equipment', 'iot_protocol')
op.drop_column('equipment', 'iot_enabled')

View File

@@ -0,0 +1,35 @@
"""Rename metadata to additional_data
Revision ID: 003_rename_metadata
Revises: 002_add_iot_equipment_support
Create Date: 2025-01-12 21:05:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '003_rename_metadata'
down_revision = '002_add_iot_equipment_support'
branch_labels = None
depends_on = None
def upgrade():
"""Rename metadata columns to additional_data to avoid SQLAlchemy reserved attribute conflict"""
# Rename metadata column in equipment_connection_logs
op.execute('ALTER TABLE equipment_connection_logs RENAME COLUMN metadata TO additional_data')
# Rename metadata column in equipment_iot_alerts
op.execute('ALTER TABLE equipment_iot_alerts RENAME COLUMN metadata TO additional_data')
def downgrade():
"""Revert column names back to metadata"""
# Revert metadata column in equipment_iot_alerts
op.execute('ALTER TABLE equipment_iot_alerts RENAME COLUMN additional_data TO metadata')
# Revert metadata column in equipment_connection_logs
op.execute('ALTER TABLE equipment_connection_logs RENAME COLUMN additional_data TO metadata')

View File

@@ -14,6 +14,10 @@ psycopg2-binary==2.9.10
# HTTP clients
httpx==0.28.1
# IoT and Industrial Protocols
# asyncua==1.1.5 # OPC UA client (uncomment when implementing OPC UA connector)
# paho-mqtt==2.1.0 # MQTT client (uncomment when implementing MQTT connector)
# Logging and monitoring
structlog==25.4.0
prometheus-client==0.23.1