Overview
Homarr’s WebSocket server provides real-time subscriptions for widget data and system events. Subscriptions use the same tRPC procedures as HTTP endpoints but deliver updates automatically.
Subscription Types
WebSocket subscriptions are available on the widget router for real-time data:
- Weather updates
- Smart home device states
- Download progress
- Media server streams
- System monitoring
Weather Subscriptions
subscribeAtLocation
Receive real-time weather updates for a location.
const subscription = api.widget.weather.subscribeAtLocation.subscribe(
{
latitude: 40.7128,
longitude: -74.0060,
},
{
onData: (weather) => {
console.log('Temperature:', weather.current.temperature);
console.log('Condition:', weather.current.condition);
updateUI(weather);
},
onError: (error) => {
console.error('Weather subscription failed:', error);
},
},
);
// Unsubscribe when done
subscription.unsubscribe();
Update Frequency: Every 10 minutes (cached)
Event Data:
Current weather conditionsWeather condition (e.g., “Sunny”, “Cloudy”)
Humidity percentage (0-100)
Wind speed in configured units
Smart Home Subscriptions
subscribeToDeviceStates
Monitor smart home device state changes.
const subscription = api.widget.smartHome.subscribeToDeviceStates.subscribe(
{
integrationId: "homeassistant-id",
deviceIds: ["light.living_room", "switch.bedroom"],
},
{
onData: (states) => {
states.forEach((device) => {
console.log(`${device.name}: ${device.state}`);
updateDeviceUI(device);
});
},
},
);
Update Frequency: Real-time (pushed from integration)
Event Data:
Current state (e.g., “on”, “off”)
Device-specific attributes
Last state change timestamp
Download Subscriptions
subscribeToQueue
Monitor download queue status.
const subscription = api.widget.downloads.subscribeToQueue.subscribe(
{
integrationId: "qbittorrent-id",
},
{
onData: (queue) => {
console.log(`Active: ${queue.active}`);
console.log(`Speed: ${formatBytes(queue.downloadSpeed)}/s`);
updateDownloadUI(queue);
},
},
);
Update Frequency: Every 5 seconds
Event Data:
Number of active downloads
Number of paused downloads
Number of completed downloads
Current download speed (bytes/second)
Current upload speed (bytes/second)
subscribeToStreams
Monitor active media streams.
const subscription = api.widget.mediaServer.subscribeToStreams.subscribe(
{
integrationId: "plex-id",
},
{
onData: (streams) => {
console.log(`Active streams: ${streams.length}`);
streams.forEach((stream) => {
console.log(`${stream.user} watching ${stream.title}`);
});
},
},
);
Update Frequency: Every 10 seconds
Event Data:
Media type (movie, episode, track)
Playback progress (0-100)
Playback state (playing, paused, buffering)
System Monitoring Subscriptions
subscribeToHealthChecks
Monitor service health status.
const subscription = api.widget.healthMonitoring.subscribeToHealthChecks.subscribe(
{
integrationId: "healthchecks-id",
},
{
onData: (checks) => {
checks.forEach((check) => {
if (check.status === 'down') {
showAlert(`${check.name} is down!`);
}
});
},
},
);
Update Frequency: Every 30 seconds
Event Data:
Subscription Patterns
React Hook Pattern
import { useEffect, useState } from 'react';
import { api } from '~/trpc/react';
function useWeather(latitude: number, longitude: number) {
const [weather, setWeather] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const subscription = api.widget.weather.subscribeAtLocation.subscribe(
{ latitude, longitude },
{
onData: setWeather,
onError: setError,
},
);
return () => subscription.unsubscribe();
}, [latitude, longitude]);
return { weather, error };
}
// Usage
function WeatherWidget() {
const { weather, error } = useWeather(40.7128, -74.0060);
if (error) return <div>Error: {error.message}</div>;
if (!weather) return <div>Loading...</div>;
return <div>{weather.current.temperature}°</div>;
}
Multiple Subscriptions
function DashboardWidget() {
const [weather, setWeather] = useState(null);
const [downloads, setDownloads] = useState(null);
useEffect(() => {
const weatherSub = api.widget.weather.subscribeAtLocation.subscribe(
{ latitude: 40.7128, longitude: -74.0060 },
{ onData: setWeather },
);
const downloadsSub = api.widget.downloads.subscribeToQueue.subscribe(
{ integrationId: "qbit-id" },
{ onData: setDownloads },
);
return () => {
weatherSub.unsubscribe();
downloadsSub.unsubscribe();
};
}, []);
return (
<div>
<WeatherDisplay data={weather} />
<DownloadsDisplay data={downloads} />
</div>
);
}
Conditional Subscriptions
function ConditionalWidget({ enabled }: { enabled: boolean }) {
const [data, setData] = useState(null);
useEffect(() => {
if (!enabled) return;
const subscription = api.widget.mediaServer.subscribeToStreams.subscribe(
{ integrationId: "plex-id" },
{ onData: setData },
);
return () => subscription.unsubscribe();
}, [enabled]);
return enabled ? <StreamsDisplay data={data} /> : null;
}
Error Handling
Subscription Error Handler
const subscription = api.widget.weather.subscribeAtLocation.subscribe(
{ latitude: 40.7128, longitude: -74.0060 },
{
onData: (weather) => {
console.log('Weather updated:', weather);
},
onError: (error) => {
if (error.code === 'UNAUTHORIZED') {
redirectToLogin();
} else if (error.code === 'NOT_FOUND') {
showError('Integration not found');
} else {
showError('Failed to load weather data');
}
},
},
);
Retry Logic
function subscribeWithRetry(integrationId: string, maxRetries = 3) {
let retries = 0;
const subscribe = () => {
return api.widget.downloads.subscribeToQueue.subscribe(
{ integrationId },
{
onData: (data) => {
retries = 0; // Reset on success
updateUI(data);
},
onError: (error) => {
if (retries < maxRetries) {
retries++;
setTimeout(() => subscribe(), 1000 * retries);
} else {
showError('Max retries exceeded');
}
},
},
);
};
return subscribe();
}
Debounced Updates
import { debounce } from 'lodash';
function WeatherWidget() {
const [weather, setWeather] = useState(null);
useEffect(() => {
const updateWeather = debounce(setWeather, 1000);
const subscription = api.widget.weather.subscribeAtLocation.subscribe(
{ latitude: 40.7128, longitude: -74.0060 },
{ onData: updateWeather },
);
return () => {
updateWeather.cancel();
subscription.unsubscribe();
};
}, []);
return <div>{weather?.current.temperature}°</div>;
}
Memoized Subscriptions
import { useMemo } from 'react';
function useSubscription(integrationId: string) {
const subscription = useMemo(() => {
return api.widget.downloads.subscribeToQueue.subscribe(
{ integrationId },
{
onData: (data) => console.log(data),
},
);
}, [integrationId]);
useEffect(() => {
return () => subscription.unsubscribe();
}, [subscription]);
}
Best Practices
Always Unsubscribe
Always clean up subscriptions to prevent memory leaks:
useEffect(() => {
const subscription = api.widget.weather.subscribeAtLocation.subscribe(
{ latitude, longitude },
{ onData: setWeather },
);
// Critical: unsubscribe on unmount
return () => subscription.unsubscribe();
}, [latitude, longitude]);
Handle Connection Loss
Provide feedback when the connection is lost:
function useConnectionStatus() {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const wsClient = getWSClient();
wsClient.onClose(() => setIsConnected(false));
wsClient.onOpen(() => setIsConnected(true));
}, []);
return isConnected;
}
function Widget() {
const isConnected = useConnectionStatus();
return (
<div>
{!isConnected && <Banner>Reconnecting...</Banner>}
{/* Widget content */}
</div>
);
}
Batch Updates
Group related state updates to minimize re-renders:
const subscription = api.widget.downloads.subscribeToQueue.subscribe(
{ integrationId },
{
onData: (queue) => {
// Batch updates
setBatch({
activeCount: queue.active,
downloadSpeed: queue.downloadSpeed,
items: queue.items,
});
},
},
);