Integrating SailServer with Home Assistant and WordPress

I’m a keen sailor and have been using a sailserver device on my boat to log my trips. As sailserver uploads its data to a cloud, I’ve always played with the idea to connect it to HomeAssistant. Today I’ve gone a step further, by also exporting some of the data to my boats website.

I hope there are some fellow sailserver owners here that can benefit from this work, so decided to share.

Have fun!

Integrating SailServer with Home Assistant and WordPress

A practical, secure pattern for publishing live boat telemetry


Publication-ready community guide
All examples use BoatName as a replaceable placeholder

Disclaimer.This community guide is provided as-is and is not official documentation from SailServer, Home Assistant, or WordPress. APIs and platform behavior may change. Test the configuration and never publish your SailServer API key or Home Assistant token.

Overview

This guide connects SailServer, Home Assistant and WordPress to retrieve vessel telemetry, transform it into readable Home Assistant entities, show a combined status-and-map card, and publish a deliberately limited status view on a website.

Goal and architecture

SailServer remains the telemetry source. Home Assistant polls SailServer and converts the response into sensors. A WordPress plugin reads one selected Home Assistant entity over HTTPS, caches the rendered result for five minutes, and exposes it through a shortcode.


Naming convention used in this guide

Every example uses BoatName as a placeholder. Replace BoatName with your vessel name. The examples assume that Home Assistant creates the lowercase entity prefix boatname. Verify the actual IDs in Developer Tools > States after reloading or restarting.

Before you start

  • A working SailServer account and a vessel visible in that account.
  • A Home Assistant installation that can reach the SailServer endpoint .
  • A WordPress site that can reach your external Home Assistant URL over HTTPS .
  • Access to configuration.yaml, secrets.yaml, wp-config.php, and WordPress plugin management.

Getting a SailServer API key

An API key is required for the getboat request. API access is controlled by SailServer. If your account does not expose an API or integration option, contact the SailServer owner or support channel, request API access for your vessel, and ask for confirmation that the getboat command is enabled.

3

Part 1: Configure Home Assistant

1. Add the SailServer REST sensor to configuration.yaml

If a top-level rest: section already exists, merge this item into the existing list rather than creating a second rest: key.

################################################
# SailServer data integration
################################################
rest:
  - resource: "https://app.sailserver.com/mod/api.php"
    method: POST
    scan_interval: 300
    payload: !secret sailserver_getboat_payload
    headers:
      Content-Type: application/json
    sensor:
      - name: BoatName Status
        value_template: >
          {{ value_json.data.inst.sog | float(0) }}
        json_attributes_path: "$.data"
        json_attributes:
          - boatname
          - inst
          - weather

2. Add the template entities to configuration.yaml

If a top-level template: section already exists, merge the list items beneath it.

################################################
# Boat telemetry template entities
################################################
template:
  - sensor:
      - name: BoatName Latitude
        state: >
          {% set inst = state_attr('sensor.boatname_status', 'inst') %}
          {{ inst.get('lat') if inst is mapping else none }}
      - name: BoatName Longitude
        state: >
          {% set inst = state_attr('sensor.boatname_status', 'inst') %}
          {{ inst.get('lon') if inst is mapping else none }}
      - name: BoatName Wind Speed
        unit_of_measurement: "kn"
        icon: mdi:weather-windy
        state: >
          {% set inst = state_attr('sensor.boatname_status', 'inst') %}
          {{ inst.get('tws') if inst is mapping else none }}
      - name: BoatName Water Temperature
        unit_of_measurement: "°C"
        device_class: temperature
        state: >
          {% set inst = state_attr('sensor.boatname_status', 'inst') %}
          {{ inst.get('watmp') if inst is mapping else none }}
      - name: BoatName Location
        icon: mdi:sail-boat
        state: >
          {% if is_state('binary_sensor.boatname_underway', 'on') %}
            Underway
          {% else %}
            Moored
          {% endif %}
        attributes:
          latitude: >
            {{ states('sensor.boatname_latitude') }}
          longitude: >
            {{ states('sensor.boatname_longitude') }}
          entity_picture: /local/boat_icon.png
  - binary_sensor:
      - name: BoatName Underway
        state: >
          {% set inst = state_attr('sensor.boatname_status', 'inst') %}
          {{ inst is mapping and inst.get('sog', 0) | float(0) > 0.5 }}
      - name: BoatName Moored
        state: >
          {% set inst = state_attr('sensor.boatname_status', 'inst') %}
          {{ inst is mapping and inst.get('sog', 0) | float(0) <= 0.5 }}



4

3. Store the SailServer key in secrets.yaml

# SailServer
sailserver_getboat_payload: >
  {
    "apikey": "YOUR_SAILSERVER_API_KEY",
    "cmd": "getboat"
  }


4. Validate and reload

Validate the Home Assistant configuration.

  • Restart Home Assistant or reload the relevant YAML integrations where supported .
  • Verify all eight entity IDs in Developer Tools > State s.
  • Inspect sensor.boatname_status and confirm that inst contains sog, lat, lon, tws, and wat mp.
  • If Home Assistant generated different IDs, update every reference be low.

5. Add the complete dashboard card

The following example uses the custom:vertical-stack-in-card frontend component to combine the status Markdown card and a map into one visual card. Replace BoatName and boatname with your own display name and generated entity prefix.

type: custom:vertical-stack-in-card
cards:

  • type: markdown
    content: >-
    BoatName:
    {% if is_state(‘binary_sensor.boatname_underway’, ‘off’) %}
    :anchor: Moored | Water
    {{ states(‘sensor.boatname_water_temperature’) }} °C
    {% else %}
    :sailboat: {{ states(‘sensor.boatname_status’) }} kn |
    Wind {{ states(‘sensor.boatname_wind_speed’) }} kn
    {% endif %}
  • type: map
    entities:
    • entity: sensor.boatname_location
      aspect_ratio: ‘2’
      default_zoom: 12
      theme_mode: dark

Part 2: Publish a limited status in WordPress

The WordPress plugin below requests sensor.boatname_status from Home Assistant, renders selected attributes, and caches the HTML for five minutes.

6

  1. Create a long-lived Home Assistant access token

Open your Home Assistant user profile, create a Long-Lived Access Token, copy it once, and store it securely. WordPress sends it in the Authorization: Bearer header.

Official reference: Home Assistant REST API documentation

2. Store the endpoint and token in wp-config.php

define( 'BOAT_TRACKER_HA_URL', 'https://your-home-assistant-host' ); 

define( 'BOAT_TRACKER_HA_TOKEN', 'YOUR_LONG_LIVED_ACCESS_TOKEN' );  


7

3. Create the WordPress plugin

Create wp-content/plugins/boat-tracker/boat-tracker.php with the following code

<?php /\*\* \* Plugin Name: Boat Tracker \* Description: Displays selected boat telemetry from Home Assistant. \* Version: 1.0.0 \*/ if ( ! defined( 'ABSPATH' ) ) { exit; } function boat_tracker_shortcode() { $cache_key = 'boat_tracker_markup_v1'; $cached = get_transient( $cache_key ); if ( false !== $cached ) { return $cached; } if ( ! defined( 'BOAT_TRACKER_HA_URL' ) || ! defined( 'BOAT_TRACKER_HA_TOKEN' ) ) { return '

Boat data is not configured.

'; } $entity_id = 'sensor.boatname_status'; $url = trailingslashit( BOAT_TRACKER_HA_URL ) . 'api/states/' . rawurlencode( $entity_id ); $response = wp_remote_get( $url, array( 'headers' => array( 'Authorization' => 'Bearer ' . BOAT_TRACKER_HA_TOKEN, 'Content-Type' => 'application/json', ), 'timeout' => 10, ) ); if ( is_wp_error( $response ) ) { return '

Boat data is temporarily unavailable.

'; } $status_code = wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( 200 !== $status_code || ! is_array( $data ) ) { return '

Boat data is temporarily unavailable.

'; } $speed = isset( $data\['state'\] ) ? (float) $data\['state'\] : 0; $inst = isset( $data\['attributes'\]\['inst'\] ) && is_array( $data\['attributes'\]\['inst'\] ) ? $data\['attributes'\]\['inst'\] : array(); $wind = isset( $inst\['tws'\] ) ? $inst\['tws'\] : 'unknown'; $water = isset( $inst\['watmp'\] ) ? $inst\['watmp'\] : 'unknown'; $movement = $speed > 0.5 ? 'Underway' : 'Moored'; $icon = $speed > 0.5 ? '⛵' : '⚓'; $ts = isset( $data\['last_updated'\] ) ? strtotime( $data\['last_updated'\] ) : false; $updated = $ts ? wp_date( 'd-m-Y H:i', $ts ) : 'unknown'; $markup = '
'; $markup .= 'BoatName
'; $markup .= esc_html( $icon . ' ' . $movement ) . '
'; $markup .= 'Speed: ' . esc_html( number_format_i18n( $speed, 1 ) ) . ' kn
'; $markup .= 'Wind: ' . esc_html( $wind ) . ' kn
'; $markup .= 'Water: ' . esc_html( $water ) . ' °C
'; $markup .= 'Last update: ' . esc_html( $updated ) . ''; $markup .= '
'; set_transient( $cache_key, $markup, 5 \* MINUTE_IN_SECONDS ); return $markup; } add_shortcode( 'boat_tracker', 'boat_tracker_shortcode' ); ![8|690x48](upload://qEESSW7QRpmq96NheNu6HviGMkB.jpeg) ## 4\. Install and activate the plugin · Create the folder wp-content/plugins/boat-tracker using the hosting file manager, SFTP, or your deployment process. · Upload boat-tracker.php . · Open WordPress administration > Plugins > Installed Plugin s. · Activate Boat Track er. · Alternatively, ZIP the boat-tracker folder and use Plugins > Add Plugin > Upload Plugin if your hosting policy allows it. ## 5\. Place the shortcode on a page ``` [boat_tracker] ``` Add the shortcode to a Shortcode block or compatible editor block, then publish or update the page. ### **Operational and security checklist** * The SailServer API key appears only in secrets.yaml. * The Home Assistant token appears only in wp-config.php. * WordPress connects to Home Assistant over HTTPS. * Every entity ID matches the generated Home Assistant IDs. * Only data suitable for public viewing is rendered. * Errors reveal no token, key, or raw API response . * The WordPress output is cached for five minutes. * Exposed access tokens are rotated immediately. * Precise vessel location is reviewed before any public map is enabled. # Troubleshooting ## Duplicate YAML keys Merge new entries into the existing top-level rest: or template: section. ## Unavailable templates Inspect sensor.boatname_status and verify the inst attribute and the fields sog, lat , lon, tws, and watmp. ## Different entity IDs Use Developer Tools > States to find the generated IDs and update all YAML, dashbo ard, and PHP references. ## WordPress reports temporary unavailability Check the external Home Assistant URL, token, entity ID, HTTPS connec tivity, and HTTP response. ## Displayed time is wrong wp_date uses the WordPress site timezone. Check Settings > General > Timezone. ## Changes are delayed The plugin caches output for five minutes. Remove the boat_tracker_markup_v1 transient during testing if immediate refresh is required. # Closing note This architecture keeps SailServer responsible for vessel telemetry, Home Assistant responsible for polling and transformation, and WordPress responsible for controlled publication. It can later be extended with battery and tank data, anchor status, route history, or separate public and private views. Consider the privacy and security impact before publishing a vesse l’s location or operat ing state. # Technical reference Source: [Home Assistant REST API documentation](https://developers.home-assistant.io/docs/api/rest/)
<?php 

/** 

 * Plugin Name: Boat Tracke r

 * Description: Displays selected boat telemetry from Home Assistan t.

 * Version: 1. 0.0

  */

if ( ! defined( 'ABSPATH' ) ) { ex it;  }

 

function boat_tracker_shortc ode() {

    $cache_key = 'boat_tracker_mar kup_v1';

    $cached = get_transient( $cac he_key );

    if ( false !== $cached ) { return  $cac hed; }

 

    if ( ! defined( 'BOAT_TRACKER_ HA_URL' ) ||

         ! defined( 'BOAT_TRACKER_HA _TOKEN' ) ) {

        return '<p class="boat-status boat-status--error">Boat data is not con figured. </p> ';

    }

 

    $entity_id = 'sensor. boatname_status';

    $url = trailingslashit( BOAT_ TRACKER_HA_URL ) .

           'api/states/' . rawurlen code( $entity_id );

    $response = wp_remo te_get( $url, array(

         'headers' => array(

            'Authorization' => 'Bearer ' .  BOAT_TRACKER_HA_TOKEN,

            'Content-Type'   => 'applicat ion/json',

        ),

         't imeo ut' => 10,

    ) );

 

    if (  is_wp_error( $response ) ) {

        return '<p class="boat-status boat-status--error">Boat data is t emporari ly unavailable.</p>';

    }

    $status_code = wp_remote_retri eve_response_code( $response );

    $data = json_decode( wp_remote_ret rieve_body( $response ), true );

    if ( 200 !== $statu s_code || ! is_array( $data ) ) {

        return '<p class="boat-status boat-status--error">Boat data  is temp orar ily unavailable.</p>';

    }

 

    $speed = isset( $data['st ate'] ) ? (float) $data['state'] : 0;

    $inst = i sset( $data['attributes']['inst'] ) &&

             is_array( $data['attributes']['inst'] )

             ? $data['attributes']['inst'] : array();

    $wind = isset( $ inst['tws'] ) ? $inst['tws'] : 'unknown';

    $water = isset( $ins t['watmp'] ) ? $inst['watmp'] : 'unknown';

    $movem ent = $speed > 0.5 ? 'Underway' : 'Moore d';

    $icon = $speed > 0.5 ? '⛵' : '⚓';

    $ts = isset( $data['last_updated'] )  ? strtotime( $data['last_updated'] ) : false;

    $updated =  $ts  ? wp_date( 'd-m-Y H:i', $ts ) : 'unknown';

  

    $markup  = '<div class="boat-status">';

 

    $markup .= '<strong>BoatName</strong><br>';

    $mar kup .= esc_html( $icon . ' ' . $movement ) . '<br>';

    $markup .= 'Speed: ' . esc _html( number_format_i18n( $speed, 1 ) ) . ' kn<br>';

     $markup .= 'Wind: ' . esc_html( $wind ) . ' kn<br>';

     $markup .= 'Water: ' . esc_html( $water ) . ' °C<br>';

    $markup .= '<s mall>Last update: ' . esc_h tml( $updated ) . '</small>';

    $markup .= '</div>';

    se t_transient( $cache_ke y, $ markup, 5 * MINUTE_IN_SECONDS );

    return $markup;

} 

add_shortcode( 'boat_tracker', 'boat_tracker_shortcode' );