Skip to main content

WebSocket Events

Complete reference for WebSocket tunnel events.

Connection

Endpoint

wss://cloud.asahome.io/tunnel

Authentication

const socket = io('wss://cloud.asahome.io/tunnel', {
transports: ['websocket'],
auth: {
token: 'your-jwt-access-token'
}
});

Events Overview

Client Events (Emit)

EventDirectionDescription
tunnel:messageApp → Cloud → DeviceSend command to device
tunnel:responseDevice → Cloud → AppSend response to user
tunnel:subscribeApp → CloudSubscribe to device updates
tunnel:unsubscribeApp → CloudUnsubscribe from device

Server Events (Listen)

EventDirectionDescription
tunnel:responseCloud → AppReceive response from device
tunnel:stateCloud → AppReceive state update
tunnel:errorCloud → App/DeviceReceive error message
tunnel:messageCloud → DeviceReceive command from user

Client Events

tunnel:message

Send a command from the Flutter app to an AsaHome OS device.

Payload

interface TunnelMessage {
deviceUuid: string; // Target device UUID
requestId?: string; // Optional request ID for correlation
message: {
type: string; // Command type
domain?: string; // Service domain (for call_service)
service?: string; // Service name (for call_service)
target?: { // Target entities
entity_id: string | string[];
};
data?: object; // Additional data
};
}

Examples

Turn on a light:

socket.emit('tunnel:message', {
deviceUuid: '550e8400-e29b-41d4-a716-446655440000',
requestId: 'req-12345',
message: {
type: 'call_service',
domain: 'light',
service: 'turn_on',
target: {
entity_id: 'light.living_room'
},
data: {
brightness_pct: 80,
color_temp: 350
}
}
});

Set thermostat temperature:

socket.emit('tunnel:message', {
deviceUuid: '550e8400-e29b-41d4-a716-446655440000',
message: {
type: 'call_service',
domain: 'climate',
service: 'set_temperature',
target: {
entity_id: 'climate.living_room'
},
data: {
temperature: 22,
hvac_mode: 'heat'
}
}
});

Get all states:

socket.emit('tunnel:message', {
deviceUuid: '550e8400-e29b-41d4-a716-446655440000',
message: {
type: 'get_states'
}
});

Get single entity state:

socket.emit('tunnel:message', {
deviceUuid: '550e8400-e29b-41d4-a716-446655440000',
message: {
type: 'get_state',
entity_id: 'light.living_room'
}
});

tunnel:response

Sent by the device to relay a response back to the user.

Payload

interface TunnelResponse {
userId: string; // Target user ID
requestId?: string; // Correlates to original request
message: {
success: boolean; // Whether command succeeded
result?: any; // Command result data
error?: string; // Error message if failed
};
}

Example (Device Side)

socket.emit('tunnel:response', {
userId: 'user-uuid-here',
requestId: 'req-12345',
message: {
success: true,
result: {
entity_id: 'light.living_room',
state: 'on',
attributes: {
brightness: 204,
color_temp: 350
}
}
}
});

tunnel:subscribe

Subscribe to real-time state updates from a device.

Payload

interface SubscribePayload {
deviceUuid: string;
entities?: string[]; // Optional: specific entities
}

Example

// Subscribe to all updates from a device
socket.emit('tunnel:subscribe', {
deviceUuid: '550e8400-e29b-41d4-a716-446655440000'
});

// Subscribe to specific entities only
socket.emit('tunnel:subscribe', {
deviceUuid: '550e8400-e29b-41d4-a716-446655440000',
entities: [
'light.living_room',
'climate.living_room',
'sensor.temperature'
]
});

tunnel:unsubscribe

Unsubscribe from device updates.

Payload

interface UnsubscribePayload {
deviceUuid: string;
}

Example

socket.emit('tunnel:unsubscribe', {
deviceUuid: '550e8400-e29b-41d4-a716-446655440000'
});

Server Events

tunnel:response

Received by the Flutter app with the device's response.

Payload

interface ResponseEvent {
deviceUuid: string;
requestId?: string;
message: {
success: boolean;
result?: any;
error?: string;
};
}

Example Handler

socket.on('tunnel:response', (data) => {
if (data.message.success) {
console.log('Command succeeded:', data.message.result);
} else {
console.error('Command failed:', data.message.error);
}
});

tunnel:state

Real-time state update from a subscribed device.

Payload

interface StateEvent {
deviceUuid: string;
entity_id: string;
old_state: EntityState;
new_state: EntityState;
}

interface EntityState {
entity_id: string;
state: string;
attributes: object;
last_changed: string;
last_updated: string;
}

Example Handler

socket.on('tunnel:state', (data) => {
console.log(`${data.entity_id} changed from ${data.old_state.state} to ${data.new_state.state}`);

// Update UI
updateEntityState(data.entity_id, data.new_state);
});

tunnel:error

Error notification from the gateway.

Payload

interface ErrorEvent {
code: string;
message: string;
deviceUuid?: string;
requestId?: string;
}

Error Codes

CodeDescription
UNAUTHORIZEDInvalid or expired token
DEVICE_OFFLINEDevice is not connected
DEVICE_NOT_FOUNDDevice doesn't exist or no access
COMMAND_TIMEOUTDevice didn't respond in time
INVALID_MESSAGEMessage format is invalid
RATE_LIMITEDToo many requests

Example Handler

socket.on('tunnel:error', (error) => {
switch (error.code) {
case 'UNAUTHORIZED':
// Refresh token and reconnect
handleTokenRefresh();
break;
case 'DEVICE_OFFLINE':
showNotification(`Device is offline`);
break;
case 'COMMAND_TIMEOUT':
showNotification('Command timed out, please try again');
break;
default:
console.error('Tunnel error:', error.message);
}
});

Command Types

Command Types Reference

TypeDescriptionRequired Fields
call_serviceCall a device servicedomain, service
get_statesGet all entity statesNone
get_stateGet single entity stateentity_id
subscribe_eventsSubscribe to device eventsevent_type

call_service Domains

Common domains and services:

DomainServices
lightturn_on, turn_off, toggle
switchturn_on, turn_off, toggle
climateset_temperature, set_hvac_mode, set_preset_mode
coveropen_cover, close_cover, set_cover_position
sceneturn_on
scriptturn_on, turn_off
automationtrigger, turn_on, turn_off

Connection Lifecycle

Events

// Connected successfully
socket.on('connect', () => {
console.log('Connected to tunnel');
// Resubscribe to devices after reconnection
resubscribeToDevices();
});

// Disconnected
socket.on('disconnect', (reason) => {
console.log('Disconnected:', reason);
});

// Connection error
socket.on('connect_error', (error) => {
if (error.message === 'unauthorized') {
handleTokenRefresh();
} else {
console.error('Connection error:', error);
}
});

// Reconnection attempt
socket.io.on('reconnect_attempt', (attempt) => {
console.log(`Reconnection attempt ${attempt}`);
});

// Reconnected
socket.io.on('reconnect', () => {
console.log('Reconnected to tunnel');
});

Flutter Socket.IO

Package

dependencies:
socket_io_client: ^2.0.0

Complete Implementation

import 'package:socket_io_client/socket_io_client.dart' as IO;

class TunnelClient {
late IO.Socket socket;
final Function(Map<String, dynamic>) onResponse;
final Function(Map<String, dynamic>) onStateUpdate;
final Function(Map<String, dynamic>) onError;
final Function() onConnect;
final Function(String) onDisconnect;

TunnelClient({
required this.onResponse,
required this.onStateUpdate,
required this.onError,
required this.onConnect,
required this.onDisconnect,
});

void connect(String accessToken) {
socket = IO.io(
'wss://cloud.asahome.io/tunnel',
IO.OptionBuilder()
.setTransports(['websocket'])
.setAuth({'token': accessToken})
.enableAutoConnect()
.enableReconnection()
.setReconnectionAttempts(5)
.setReconnectionDelay(1000)
.build(),
);

socket.onConnect((_) => onConnect());
socket.onDisconnect((reason) => onDisconnect(reason.toString()));

socket.on('tunnel:response', (data) =>
onResponse(Map<String, dynamic>.from(data)));

socket.on('tunnel:state', (data) =>
onStateUpdate(Map<String, dynamic>.from(data)));

socket.on('tunnel:error', (data) =>
onError(Map<String, dynamic>.from(data)));
}

void sendCommand(String deviceUuid, Map<String, dynamic> command, {String? requestId}) {
socket.emit('tunnel:message', {
'deviceUuid': deviceUuid,
'requestId': requestId ?? DateTime.now().millisecondsSinceEpoch.toString(),
'message': command,
});
}

void subscribe(String deviceUuid, {List<String>? entities}) {
socket.emit('tunnel:subscribe', {
'deviceUuid': deviceUuid,
if (entities != null) 'entities': entities,
});
}

void unsubscribe(String deviceUuid) {
socket.emit('tunnel:unsubscribe', {
'deviceUuid': deviceUuid,
});
}

void updateToken(String newToken) {
socket.auth = {'token': newToken};
}

void disconnect() {
socket.disconnect();
socket.dispose();
}
}