Upload data from weather station to WordPress Website

This is a pretty good application of WordPress’s REST API, which would enable you to update the data via a POST request containing the new data in JSON format and poll for changes with GET requests to the same endpoint.

While you could establish this functionality with traditional AJAX request handlers, I think it makes a lot more sense to leverage the robustness of WordPress’s API which was purpose-built for things like this in order to take advantage of the in-built authentication, validation, security, and general more.


REST API Authentication

No matter the approach you choose, you’ll need your client on the weather station to authenticate by one means or another in order to update the data over the REST API. The easiest means to doing so is to create an “Application Password” for you user via the form provided in the Edit User screen. After which, set the Authorization header of your POST requests to your username concatenated with the application password with a : delimiter, encoded in base64. Many HTTP request libraries provide a shortcut for this mechanism as shown below.

Note that some security plugins such as WordFence disable the Application Password functionality by default, and WordPress itself disables the mechanism in the case that the site is being served over HTTP instead of HTTPS in order to prevent exposing Application Passwords in transit.

Via cURL an authenticated request might look like:

curl -X POST --user "fuxT:abcd EFGH 1234 ijkl MNOP 6789" https://example.com/wp-json/wp/v2/pages/1234?context=edit

Or JS executed in a Node environment:

const wp_user="fuxT";
const wp_secret="abcd EFGH 1234 ijkl MNOP 6789";

request(
  'https://example.com/wp-json/wp/v2/pages/1234?context=edit',
  {
    method: 'POST',
    auth: `${wp_user}:${wp_secret}`,
  },
  ( res ) => {
    // ...
  }
);

Your environment of choice may also have a WordPress REST API client library available to streamline development – though they may not provide too much benefit given the simplicity of this application (note there are many more available than those few listed on that page).


As Post/Page Meta-Data

If the data will only be presented in the GUI on a specific post or page, then it might make sense to store the data as post meta alongside that specific post, in which case the default page or post endpoint will be sufficient for storing and retrieving the value.

I’ll assume the meta-data to be tied to a Page for the example below.

You’ll need to expose the metadata on the REST API using register_meta(). While you can simply specify 'show_in_rest' => true, specifying a schema for the data provides some convenient automatic data validation and self-documents the field for REST API discovery:

function wpse407735_register_weather_meta() {
  register_meta(
    'post',
    'wpse407735_weather',
    [
      'object_subtype' => 'page',
      'type'           => 'object',
      'description'    => 'Live weather data provided by my RaspPi',
      'single'         => true,
      'show_in_rest'   => [
        'schema' => [
          'type'       => 'object',
          'properties' => [
            'timestamp'     => [
              'type' => 'integer',
            ],
            'windspeed'     => [
              'type' => 'number',
            ],
            'winddirection' => [
              'type' => 'number',
            ],
            'temperature'   => [
              'type' => 'number',
            ],
            'pressure'      => [
              'type' => 'number',
            ],
          ],
        ],
      ],
    ]
  );
}

add_action( 'init', 'wpse407735_register_weather_meta' );

The largest drawback of a post-meta approach is that it adds the functionality for all posts of that type, potentially adding unnecessary overhead. This might make more sense if you intend to associate many posts with individual weather stations – but in that situation you should consider adding a dedicated post type for the purpose.

Retrieving

By using the _fields parameter, we can restrict the retrieved data to just the page’s meta-data. So, assuming the Page’s post ID to be 1234, the GET request to retrieve the page’s meta-data would be sent to

https://example.com/wp-json/wp/v2/pages/1234?_fields=meta

This will return an application/json-encoded response body along the lines of:

{
  "meta": {
    "wpse407735_weather": {
      "timestamp": 1657989715,
      "windspeed": 10,
      "winddirection": 130,
      "temperature": 30,
      "pressure": 1012.4
  }
}

Updating

Simply send an authenticated POST request to the same endpoint with a Content-Type: application/json header and the JSON structured to update the meta-field. Generically,

POST /wp-json/wp/v2/pages/1234?context=edit HTTP/1.1
Content-Type: application/json
Authorization: Basic ZnV4VDphYmNkIEVGR0ggMTIzNCBpamtsIE1OT1AgNjc4OQ==
Host: example.com

{
    "meta": {
        "wpse407735_weather": {
            "timestamp": 1657989715,
            "windspeed": 10,
            "winddirection": 130,
            "temperature": 30,
            "pressure": 1012.4
        }
    }
}

As a Transient/Custom Storage

If the weather data will be displayed in something like a site header across the whole site, it may make more sense to store it in an option or transient, or in a custom database table as @birgie suggests in the comments.

To do this, it probably makes the most sense to create a simple dedicated endpoint with a few calls to register_route() – one to handle serving the data, and one to handle updating it.

Here, I’m creating the route /wp-json/wpse407735/v1/weather, and restricting updating to just your specific user. In a more robust solution, you might change that to a capabilities check for if the user can edit posts – or even better, a custom capability for managing weather data which is only provided to users specifically created for use by the weather station.

Again, though not strictly necessary, I’m also providing a schema for the weather data response to send to the client for REST API discovery, and another for the update request arguments which tells WordPress to use it to enforce basic validation and sanitization. In either case, the schemas could be expanded upon quite a bit – in their current state they are just bare-bones examples of how they might be leveraged.

function wpse407735_register_weather_endpoint() {
  $namespace="wpse407735/v1";
  $endpoint="/weather";

  register_rest_route(
    $namespace,
    $endpoint,
    [
      'methods'             => 'GET',
      'callback'            => 'wpse407735_get_weather',
      'permission_callback' => '__return_true',
      'schema'              => wpse407735_weather_data_schema(),
    ]
  );

  register_rest_route(
    $namespace,
    $endpoint,
    [
      'methods'             => 'POST',
      'callback'            => 'wpse407735_update_weather',
      'permission_callback' => 'wpse407735_update_weather_permission_check',
      'args'                => wpse407735_update_weather_args(),
    ]
  );
}

add_action( 'rest_api_init', 'wpse407735_register_weather_endpoint' );

/**
 * Provides a JSON Schema describing the structure of the data
 * returned by the GET route to aide in API discovery.
 **/
function wpse407735_weather_data_schema() {
  return [
    'title'      => 'weather-data',
    '$schema'    => 'http://json-schema.org/draft-04/schema#',
    'type'       => 'object',
    'properties' => [
      'timestamp'     => [
        'type' => 'integer',
      ],
      'windspeed'     => [
        'type' => 'number',
      ],
      'winddirection' => [
        'type' => 'number',
      ],
      'temperature'   => [
        'type' => 'number',
      ],
      'pressure'      => [
        'type' => 'number',
      ],
    ],
  ];
}

/**
 * Provides a structure describing the arguments which the POST route
 * accepts, and tells WordPress to perform basic validation and
 * sanitization of the incoming values against it's argument definition.
 **/
function wpse407735_update_weather_args() {
  return [
    'timestamp'     => [
      'type'              => 'integer',
      'validate_callback' => 'rest_validate_request_arg',
      'sanitize_callback' => 'rest_sanitize_request_arg',
    ],
    'windspeed'     => [
      'type'              => 'number',
      'validate_callback' => 'rest_validate_request_arg',
      'sanitize_callback' => 'rest_sanitize_request_arg',
    ],
    'winddirection' => [
      'type'              => 'number',
      'validate_callback' => 'rest_validate_request_arg',
      'sanitize_callback' => 'rest_sanitize_request_arg',
    ],
    'temperature'   => [
      'type'              => 'number',
      'validate_callback' => 'rest_validate_request_arg',
      'sanitize_callback' => 'rest_sanitize_request_arg',
    ],
    'pressure'      => [
      'type'              => 'number',
      'validate_callback' => 'rest_validate_request_arg',
      'sanitize_callback' => 'rest_sanitize_request_arg',
    ],
  ];
}

function wpse407735_get_weather() {
  $data     = get_transient( 'wpse407735_weather' );
  $response = $data;
  
  if( !data )
    $response = new WP_Error( 'No weather data available' );

  return rest_ensure_response( $response );
}

function wpse407735_update_weather( $request ) {
  set_transient(
    'wpse407735_weather',
    [
      'timestamp' => $request['timestamp'],
      'windspeed' => $request['windspeed'],
      'winddirection' => $request['winddirection'],
      'temperature' => $request['temperature'],
      'pressure' => $request['pressure'],
    ]
  );

  return new WP_REST_Response();
}

function wpse407735_update_weather_permission_check() {
  $user = wp_get_current_user();

  return $user->user_login === 'fuxT';
}

Retrieving

GET /wp-json/wpse407735/v1/weather HTTP/1.1
HOST example.com
{
  "timestamp": 1657989715,
  "windspeed": 10,
  "winddirection": 130,
  "temperature": 30,
  "pressure": 1012.4
}

Updating

POST /wp-json/wp/v2/pages/1234?context=edit HTTP/1.1
Content-Type: application/json
Authorization: Basic ZnV4VDphYmNkIEVGR0ggMTIzNCBpamtsIE1OT1AgNjc4OQ==
Host: example.com

{
  "timestamp": 1657989715,
  "windspeed": 10,
  "winddirection": 130,
  "temperature": 30,
  "pressure": 1012.4
}