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)
| Event | Direction | Description |
|---|---|---|
tunnel:message | App → Cloud → Device | Send command to device |
tunnel:response | Device → Cloud → App | Send response to user |
tunnel:subscribe | App → Cloud | Subscribe to device updates |
tunnel:unsubscribe | App → Cloud | Unsubscribe from device |
Server Events (Listen)
| Event | Direction | Description |
|---|---|---|
tunnel:response | Cloud → App | Receive response from device |
tunnel:state | Cloud → App | Receive state update |
tunnel:error | Cloud → App/Device | Receive error message |
tunnel:message | Cloud → Device | Receive 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
| Code | Description |
|---|---|
UNAUTHORIZED | Invalid or expired token |
DEVICE_OFFLINE | Device is not connected |
DEVICE_NOT_FOUND | Device doesn't exist or no access |
COMMAND_TIMEOUT | Device didn't respond in time |
INVALID_MESSAGE | Message format is invalid |
RATE_LIMITED | Too 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
| Type | Description | Required Fields |
|---|---|---|
call_service | Call a device service | domain, service |
get_states | Get all entity states | None |
get_state | Get single entity state | entity_id |
subscribe_events | Subscribe to device events | event_type |
call_service Domains
Common domains and services:
| Domain | Services |
|---|---|
light | turn_on, turn_off, toggle |
switch | turn_on, turn_off, toggle |
climate | set_temperature, set_hvac_mode, set_preset_mode |
cover | open_cover, close_cover, set_cover_position |
scene | turn_on |
script | turn_on, turn_off |
automation | trigger, 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();
}
}