Skip to main content

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
object
Current weather conditions
temperature
number
Current temperature
condition
string
Weather condition (e.g., “Sunny”, “Cloudy”)
humidity
number
Humidity percentage (0-100)
windSpeed
number
Wind speed in configured units
pressure
number
Atmospheric pressure
forecast
array
7-day weather forecast
location
object
Location information

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:
id
string
Device ID
name
string
Device name
state
string
Current state (e.g., “on”, “off”)
attributes
object
Device-specific attributes
lastChanged
Date
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:
active
number
Number of active downloads
paused
number
Number of paused downloads
completed
number
Number of completed downloads
downloadSpeed
number
Current download speed (bytes/second)
uploadSpeed
number
Current upload speed (bytes/second)
items
array
Download queue items

Media Server Subscriptions

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:
sessionId
string
Playback session ID
user
string
Username
title
string
Media title
type
string
Media type (movie, episode, track)
progress
number
Playback progress (0-100)
state
string
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:
id
string
Check ID
name
string
Check name
status
'up' | 'down' | 'paused'
Current status
lastPing
Date
Last successful ping
nextPing
Date
Expected next ping

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();
}

Performance Optimization

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,
      });
    },
  },
);

Build docs developers (and LLMs) love