WebSocket API

Real-time bidirectional communication for instant sensor updates and relay control

Why WebSocket?

WebSocket provides persistent connection for real-time data streaming, eliminating the need for polling. Perfect for live sensor monitoring, instant relay control, and notifications.

Connection

WebSocket Endpoint

Productiontext
wss://api.gridova.com/ws
Developmenttext
ws://localhost:8080/ws

Authentication

Send authentication token immediately after connection:

websocket_client.jsjavascript
const ws = new WebSocket('wss://api.gridova.com/ws');

ws.onopen = () => {
  console.log('Connected to WebSocket');
  
  // Send authentication
  ws.send(JSON.stringify({
    type: 'auth',
    token: 'your_jwt_token_here'
  }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data);
  
  if (data.type === 'auth_success') {
    console.log('Authenticated successfully');
  }
};

ws.onerror = (error) => {
  console.error('WebSocket error:', error);
};

ws.onclose = () => {
  console.log('Disconnected from WebSocket');
};

Message Types

1. Authentication

Client → Server: Authenticate connection

auth_message.jsonjson
{
  "type": "auth",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Server → Client: Authentication result

auth_response.jsonjson
{
  "type": "auth_success",
  "user_id": "user_123",
  "device_count": 3
}
2. Subscribe to Device

Subscribe to real-time updates from specific device

subscribe_message.jsonjson
{
  "type": "subscribe",
  "device_id": "ESP32_001"
}
3. Sensor Data Stream

Server → Client: Real-time sensor readings

sensor_data_message.jsonjson
{
  "type": "sensor_data",
  "device_id": "ESP32_001",
  "timestamp": "2024-06-25T10:30:45Z",
  "data": {
    "voltage": 220.5,
    "current": 2.83,
    "power": 623.81,
    "energy": 12.456,
    "frequency": 50.0,
    "power_factor": 0.99
  }
}
4. Relay Control

Client → Server: Control relay state

relay_control_message.jsonjson
{
  "type": "relay_control",
  "device_id": "ESP32_001",
  "relay": 1,
  "state": true
}

Server → Client: Confirmation

relay_response.jsonjson
{
  "type": "relay_updated",
  "device_id": "ESP32_001",
  "relay": 1,
  "state": true,
  "timestamp": "2024-06-25T10:31:00Z"
}
5. Device Status

Server → Client: Device online/offline status

device_status_message.jsonjson
{
  "type": "device_status",
  "device_id": "ESP32_001",
  "status": "online",
  "last_seen": "2024-06-25T10:30:45Z"
}
6. Alert/Notification

Server → Client: System alerts and notifications

alert_message.jsonjson
{
  "type": "alert",
  "device_id": "ESP32_001",
  "severity": "warning",
  "message": "High power consumption detected",
  "data": {
    "power": 2500.0,
    "threshold": 2000.0
  },
  "timestamp": "2024-06-25T10:32:00Z"
}

ESP32 WebSocket Client

esp32_websocket_client.inocpp
#include <WiFi.h>
#include <WebSocketsClient.h>
#include <ArduinoJson.h>
#include <PZEM004Tv30.h>

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* wsHost = "api.gridova.com";
const uint16_t wsPort = 443;  // or 80 for ws://
const char* wsPath = "/ws";
const char* authToken = "YOUR_JWT_TOKEN";

WebSocketsClient webSocket;
PZEM004Tv30 pzem(Serial2, 16, 17);

#define RELAY1_PIN 25

void setup() {
  Serial.begin(115200);
  pinMode(RELAY1_PIN, OUTPUT);
  digitalWrite(RELAY1_PIN, HIGH);  // OFF
  
  // Connect to WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
  
  // Setup WebSocket
  webSocket.beginSSL(wsHost, wsPort, wsPath);  // Use .begin() for ws://
  webSocket.onEvent(webSocketEvent);
  webSocket.setReconnectInterval(5000);
  
  Serial.println("WebSocket initialized");
}

void loop() {
  webSocket.loop();
  
  // Send sensor data every 5 seconds
  static unsigned long lastSend = 0;
  if (millis() - lastSend > 5000) {
    sendSensorData();
    lastSend = millis();
  }
}

void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
  switch(type) {
    case WStype_DISCONNECTED:
      Serial.println("WebSocket Disconnected");
      break;
      
    case WStype_CONNECTED:
      Serial.println("WebSocket Connected");
      authenticate();
      break;
      
    case WStype_TEXT:
      handleMessage((char*)payload);
      break;
      
    case WStype_ERROR:
      Serial.println("WebSocket Error");
      break;
  }
}

void authenticate() {
  StaticJsonDocument<256> doc;
  doc["type"] = "auth";
  doc["token"] = authToken;
  
  String json;
  serializeJson(doc, json);
  webSocket.sendTXT(json);
  
  Serial.println("Sent authentication");
}

void handleMessage(char* payload) {
  StaticJsonDocument<512> doc;
  DeserializationError error = deserializeJson(doc, payload);
  
  if (error) {
    Serial.println("Failed to parse JSON");
    return;
  }
  
  const char* type = doc["type"];
  
  if (strcmp(type, "auth_success") == 0) {
    Serial.println("✓ Authenticated");
    
    // Subscribe to device updates
    StaticJsonDocument<128> subDoc;
    subDoc["type"] = "subscribe";
    subDoc["device_id"] = "ESP32_001";
    
    String json;
    serializeJson(subDoc, json);
    webSocket.sendTXT(json);
  }
  else if (strcmp(type, "relay_control") == 0) {
    int relay = doc["relay"];
    bool state = doc["state"];
    
    if (relay == 1) {
      digitalWrite(RELAY1_PIN, state ? LOW : HIGH);
      Serial.printf("Relay 1: %s\n", state ? "ON" : "OFF");
    }
  }
  else if (strcmp(type, "ping") == 0) {
    // Respond to ping
    webSocket.sendTXT("{\"type\":\"pong\"}");
  }
}

void sendSensorData() {
  float voltage = pzem.voltage();
  float current = pzem.current();
  float power = pzem.power();
  float energy = pzem.energy();
  
  if (isnan(voltage)) return;
  
  StaticJsonDocument<512> doc;
  doc["type"] = "sensor_data";
  doc["device_id"] = "ESP32_001";
  doc["timestamp"] = millis();
  
  JsonObject data = doc.createNestedObject("data");
  data["voltage"] = voltage;
  data["current"] = current;
  data["power"] = power;
  data["energy"] = energy;
  data["frequency"] = pzem.frequency();
  data["power_factor"] = pzem.pf();
  
  String json;
  serializeJson(doc, json);
  webSocket.sendTXT(json);
  
  Serial.println("Sent sensor data");
}

Library Required

Install WebSockets by Markus Sattler from Arduino Library Manager

Flutter Implementation

gridova_websocket_service.dartdart
import 'package:web_socket_channel/web_socket_channel.dart';
import 'dart:convert';
import 'dart:async';

class GridovaWebSocketService {
  WebSocketChannel? _channel;
  StreamController<Map<String, dynamic>> _controller = 
      StreamController.broadcast();
  
  String? _token;
  bool _isConnected = false;
  Timer? _reconnectTimer;
  
  Stream<Map<String, dynamic>> get stream => _controller.stream;
  bool get isConnected => _isConnected;
  
  void connect(String token) {
    _token = token;
    _reconnectTimer?.cancel();
    
    try {
      _channel = WebSocketChannel.connect(
        Uri.parse('wss://api.gridova.com/ws'),
      );
      
      _isConnected = true;
      print('✓ WebSocket connected');
      
      // Authenticate
      send({
        'type': 'auth',
        'token': token,
      });
      
      // Listen to messages
      _channel!.stream.listen(
        _onMessage,
        onError: _onError,
        onDone: _onDone,
      );
    } catch (e) {
      print('Connection error: $e');
      _scheduleReconnect();
    }
  }
  
  void _onMessage(dynamic message) {
    try {
      final data = json.decode(message);
      print('Received: ${data['type']}');
      
      if (data['type'] == 'auth_success') {
        print('✓ Authenticated');
      }
      
      _controller.add(data);
    } catch (e) {
      print('Parse error: $e');
    }
  }
  
  void _onError(error) {
    print('WebSocket error: $error');
    _isConnected = false;
    _scheduleReconnect();
  }
  
  void _onDone() {
    print('WebSocket closed');
    _isConnected = false;
    _scheduleReconnect();
  }
  
  void _scheduleReconnect() {
    _reconnectTimer = Timer(Duration(seconds: 5), () {
      if (_token != null) {
        print('Reconnecting...');
        connect(_token!);
      }
    });
  }
  
  void send(Map<String, dynamic> data) {
    if (_channel != null && _isConnected) {
      _channel!.sink.add(json.encode(data));
    }
  }
  
  void controlRelay(String deviceId, int relay, bool state) {
    send({
      'type': 'relay_control',
      'device_id': deviceId,
      'relay': relay,
      'state': state,
    });
  }
  
  void subscribe(String deviceId) {
    send({
      'type': 'subscribe',
      'device_id': deviceId,
    });
  }
  
  void dispose() {
    _reconnectTimer?.cancel();
    _channel?.sink.close();
    _controller.close();
    _isConnected = false;
  }
}

// Usage example
void main() {
  final ws = GridovaWebSocketService();
  
  ws.stream.listen((data) {
    switch (data['type']) {
      case 'sensor_data':
        print('Voltage: ${data['data']['voltage']} V');
        break;
      case 'relay_updated':
        print('Relay ${data['relay']}: ${data['state']}');
        break;
      case 'alert':
        print('Alert: ${data['message']}');
        break;
    }
  });
  
  ws.connect('your_jwt_token');
  
  // Subscribe to device
  Future.delayed(Duration(seconds: 2), () {
    ws.subscribe('ESP32_001');
  });
  
  // Control relay
  Future.delayed(Duration(seconds: 5), () {
    ws.controlRelay('ESP32_001', 1, true);
  });
}

Error Handling

Connection Errors
  • Implement automatic reconnection with exponential backoff
  • Store unsent messages and resend after reconnection
  • Show connection status to user
  • Fallback to REST API if WebSocket unavailable
Authentication Errors
  • Token expired: Refresh token and reconnect
  • Invalid token: Redirect to login
  • Server closes connection: Check error code
Message Validation
  • Always validate received message structure
  • Handle missing or malformed fields gracefully
  • Log unexpected message types for debugging
  • Implement message timeout for critical operations

Best Practices

  • Keep connection alive: Implement ping/pong heartbeat every 30-60 seconds to detect disconnections
  • Compress data: Use WebSocket compression extension for bandwidth efficiency
  • Limit subscriptions: Subscribe only to devices currently being viewed by user
  • Secure connection: Always use WSS (WebSocket Secure) in production
  • Rate limiting: Throttle message sending to avoid overwhelming server
  • Battery optimization: On mobile, reduce update frequency when app is in background

Next Steps

Data Models
Explore complete data structure and models used in Gridova API
REST API Documentation
See REST API endpoints for initial data loading and bulk operations
Authentication
Learn how to obtain JWT tokens for API authentication