Build Your Own Weather Station With PHP, Python and a Raspberry Pi - Part II

September 08, 2022
Written by
Reviewed by

Build Your Own Weather Station With PHP, Python and a Raspberry Pi - Part II

In the first part in this series, you learned how to build a weather station with PHP, Python, a Raspberry Pi, and a DHT11 sensor.

In case you haven't read the tutorial, to summarise, the weather station is composed of two parts:

  1. A Python script that reads humidity and temperature data from the DHT11 sensor and stores it in a SQLite database.
  2. A PHP application that renders the stored data as HTML.

While that's already pretty decent functionality, how often do you need to look at all of the data? I'm guessing that you're most likely interested in the weather data for just the current, or previous week. Given that, wouldn't it be more convenient to have a daily or weekly summary sent directly to you?

Here, in the second part in the series, you're going to add the ability to send a daily summary notification via both SMS and email, using the Twilio and SendGrid APIs.

In the mock-up below, you can see an example of the SMS and email that the user will receive.

Mock-up of daily status notification SMS and email with the weather station data attached as a CSV file.

Prerequisites

If you prefer not to wire up the sensor yourself, you could use a hat, such as the Sense HAT (B). That way, you only need to connect the hat to the GPIO pins on your Raspberry Pi. It's a bit more expensive, but easier to assemble.

If you haven't already done so, work through the first tutorial, before continuing with this one, so that you have everything in place.

How the summary notifications will work

A cron job will make a request to a new endpoint named daily-summary, passing a start and end date to the request in the route's path. Then the endpoint will:

  1. Retrieve weather data from the database, limiting the records by the supplied date range.
  2. Convert the retrieved results into an in-memory CSV file.
  3. Send an email to the user with the generated CSV data attached.
  4. Send an SMS to the user's phone number with a link to the weather station's new route; as a single SMS has a 160-character limit.

Let's begin!

Clone the core application

This step is only required if you didn't work through the first version of the tutorial. To do that, SSH into the Raspberry Pi and set up the original version of the application.

This requires:

  • SSH'ing into the Raspberry Pi
  • Cloning the application from the first version of the tutorial
  • Changing into the cloned directory
  • Checking out the 1.0.0 tag
  • Installing the application's core dependencies

To do all of that, run the four commands below, after replacing the placeholder, <Your Raspberry Pi's hostname>, with your Raspberry Pi's hostname or IP address.

ssh pi@<Your Raspberry Pi's hostname>
git clone git@github.com:settermjd/php-python-weather-station.git
cd php-python-weather-station
git checkout -b 1.1.0 1.1.0
composer install

Install the required dependencies

Next, install the additional dependencies, listed in the table below, which the new features depend upon.

DependencyDescription
laminas-diactorosThis provides the Stream and Response classes, simplifying the work involved in working with the CSV file and sending responses from the new endpoint.
PHP dotenvThis loads environment variables from dotfiles, such as .env, into PHP's $_ENV and $_SERVER Superglobals.
Twilio's PHP Helper LibraryThis simplifies interaction with Twilio's Messaging API for sending SMS.
The Twilio SendGrid PHP API LibraryThis simplifies interaction with SendGrid's API for sending emails.

To install them, run the command below.

composer require \
    laminas/laminas-diactoros \
    sendgrid/sendgrid \
    twilio/sdk \
    vlucas/phpdotenv

Retrieve and set the required environment variables

The application requires several environment variables to function properly. These are:

VariableDescription
SENDGRID_API_KEYThis allows for authenticated access to the SendGrid API.
SENDGRID_SEND_FROM_NAMEThis is the name of the email sender.
SENDGRID_SEND_FROM_ADDRESSThis is the address of the email sender.
SENDGRID_SEND_TO_NAMEThis is the name of the email recipient.
SENDGRID_SEND_TO_ADDRESSThis is the address of the email recipient.
SEND_TOThis is the phone number to send the SMS to.
TWILIO_ACCOUNT_SIDThis is, effectively, the username to your Twilio account.
TWILIO_AUTH_TOKENThis is, effectively, the password to your Twilio account.
TWILIO_MESSAGING_SERVICE_SIDThis is required, in addition to the Twilio Account SID and Auth Token, so that you can use Twilio's Messaging Service.
WEATHER_STATION_HOSTNAMEThis is the weather station UI's hostname.

Before you retrieve them, first create a new file to store them, in the root directory of the project, by copying .env.example and naming it .env. You can see the file's contents below.

SENDGRID_API_KEY=<SENDGRID_API_KEY>
SENDGRID_SEND_FROM_ADDRESS=<SENDGRID_SEND_FROM_ADDRESS>
SENDGRID_SEND_FROM_NAME=<SENDGRID_SEND_FROM_NAME>
SENDGRID_SEND_TO_ADDRESS=<SENDGRID_SEND_TO_ADDRESS>
SENDGRID_SEND_TO_NAME=<SENDGRID_SEND_TO_NAME>
SEND_TO=<SEND_TO>
TWILIO_ACCOUNT_SID=<TWILIO_ACCOUNT_SID>
TWILIO_AUTH_TOKEN=<TWILIO_AUTH_TOKEN>
TWILIO_MESSAGING_SERVICE_SID=<TWILIO_MESSAGING_SERVICE_SID>
WEATHER_STATION_HOSTNAME=<WEATHER_STATION_HOSTNAME>

Retrieve your Twilio credentials

Let's start off with retrieving the relevant Twilio details.

Project Info section in the Twilio Console dashboard with two read-only fields: ACCOUNT SID and AUTH TOKEN.nd account SID

Login to the Twilio Console dashboard, which contains your Twilio Auth Token and Account SID. Copy them and paste them in place of <TWILIO_ACCOUNT_SID> and <TWILIO_AUTH_TOKEN> respectively, in .env.

Configure a Twilio Messaging Service

Then, you need to create a Messaging Service to send an SMS. To do that, from the Console, click "Explore Products > Messaging > Services" in the left-hand sidebar. Then click the "Create Messaging Service" button, on the far right-hand side.

Twilio Messaging Services - Initial Setup

On the first page of the creation process, enter a friendly name for the service, such as "Scheduled Messages", and select "Notify my users" in the "Select what you want to use Messaging for" dropdown.

Create a Twilio Messaging Service

Then, click the "Create Messaging Service" button to move to step two. In this part of the configuration, you have to add the sender phone number(s) to the sender pool used by the service. Click the "Add Senders" button to add the Twilio phone number you acquired in the previous section, or your existing one, if you already had one.

Twilio Messaging Service - Add Sender

In the "Add Senders" pop-up, leave the "Sender Type" dropdown set to "Phone Number", then click "Continue".

Add a checkmark next to the phone number that you want to use as a sender, and click "Add Phone Numbers". After that, click the "Step 3: Set up integration" button to move on to the next step.

Add a phone number to a Twilio Messaging Service

You don’t have to change any of the settings on this page. Just click "Step 4: Add compliance info".

To complete the configuration of your Messaging Service, click on "Complete Messaging Service Setup", and you will be offered the option to send a test message.

Twilio Messaging Service has been setup successfully

It's a good idea to do this, just to confirm that everything is working with your Twilio phone number.

Try sending an SMS

Click the "Try sending a message" button, then:

  • In the "To phone number" drop-down, select your personal number, which should be registered and verified with your Twilio account.
  • In the "From Messaging Service SID" drop-down, select the Messaging Service you just created.
  • In the "Body Text" enter some text to send to yourself.

Finally, click the "Send test SMS" button, and make sure you receive the SMS on your phone.

Create a SendGrid API key

Finally, login to your SendGrid account. Then, under "Settings > API Keys", click "Create API Key", in the upper right-hand corner.

Create SendGrid API key. Step One.

From there, add a meaningful name for the API key, such as "Weather Station Daily Summary", and click "Create & View", in the lower right side of the page.

Copy SendGrid API key.

You'll then be shown your API key. In .env, replace <SENDGRID_API_KEY> with the new API key.

It's important that you do that now, as once you leave the page you won't see or be able to copy the key again.

Set sender and recipient email details

The last thing to do is to set the sender and recipient email details. To do this, replace the following four placeholders in .env with the relevant details.

  • <SENDGRID_SEND_FROM_ADDRESS>
  • <SENDGRID_SEND_FROM_NAME>
  • <SENDGRID_SEND_TO_ADDRESS>
  • <SENDGRID_SEND_TO_NAME>

The final three placeholders can be set to whatever you want. SENDGRID_SEND_FROM_ADDRESS, however, needs to be a verified sender identity.

If you already have one (or more) you can find them under "Settings > Sender Authentication > Single Sender Verification".

If you don't already have a single sender verified email address, under "Settings > Sender Authentication" click "Create New Sender" near the upper right-hand corner, then fill out and submit the Create a Sender form.

Create a verified SendGrid sender

After that, you'll receive an email with the subject line "Please Verify Your Single Sender". In that email, click "Verify Single Sender".

Verify a single SendGrid sender

Load the environment variables

The next thing to do is to ensure that the environment variables are loaded and available to the application. To do that, add the following code after the require statement in public/index.php.

$dotenv = Dotenv\Dotenv::createImmutable(
    __DIR__ . '/../',
    '.env',
    false
);
$dotenv->safeLoad();

It calls PHP dotenv to read and retrieve the variables set in .env and add them to PHP's $_ENV and $_SERVER superglobals.

Add a function to create a CSV file from the retrieved weather data

The next thing to do is to add a new function to convert weather data into an in-memory CSV file. It's not, strictly, necessary, but helps make the new route smaller and easier to both reason about and maintain.

To create it, paste the function below into public/index.php, before the existing route definition.

function createCsvFileFromWeatherData(ResultSet $weatherData): Stream
{
    $fp = fopen('php://memory', 'rw');
    foreach ($weatherData->toArray() as $weatherDatum) {
        fputcsv($fp, $weatherDatum);
    }
    $stream = new Stream($fp);
    $stream->rewind();

    return $stream;
}

Then, add the following two use statements to the top of the file.

use Laminas\Db\ResultSet\ResultSet;
use Laminas\Diactoros\Stream;

The function takes a ResultSet object containing weather data and iterates over the available records using a php://memory stream and PHP's native fputcsv, creating an in-memory CSV file.

For what it's worth, you could write to a normal file or a temporary one. However, keeping the data in-memory saves you from having to deal with creating the file,  path traversal, and dealing with file permissions.

A laminas-diactoros Stream object is then initialised with the file's pointer and returned, to simplify working with the created file, later.

If the application was working with a lot of data, then in-memory might not be the best option. But in my local tests, the CSV data never came to more than 34 bytes, which is acceptable.

Add a function to send the daily summary email

Next, add the function below, in index.php, after the one that you just added.

function sendDailySummaryEmail(
    string $startDate, 
    string $endDate, 
    ResultSet $weatherData
): void {
    $email = new Mail();
    $email->setFrom(
        $_SERVER['SENDGRID_SEND_FROM_ADDRESS'], 
        $_SERVER['SENDGRID_SEND_FROM_NAME']
    );
    $email->setSubject("Your daily weather station data summary");
    $email->addTo(
        $_SERVER['SENDGRID_SEND_TO_ADDRESS'], 
        $_SERVER['SENDGRID_SEND_TO_NAME']
    );
    $email->addContent(
        "text/plain",
        sprintf(
            "Your daily weather station data summary is available at http://localhost/daily-summary/%s/%s",
            $startDate,
            $endDate
        )
    );
    $email->addContent(
        "text/html",
        sprintf(
            "Your daily weather station data summary for %s - %s",
            $startDate,
            $endDate
        )
    );
    $stream = createCsvFileFromWeatherData($weatherData);
    $file_encoded = base64_encode($stream->getContents());
    $email->addAttachment(
        $file_encoded,
        "text/csv",
        "daily-summary.csv",
        "attachment"
    );
    $sendgrid = new \SendGrid($_SERVER['SENDGRID_API_KEY']);
    try {
        $response = $sendgrid->send($email);
        print $response->statusCode() . "\n";
        print_r($response->headers());
        print $response->body() . "\n";
    } catch (Exception $e) {
        echo 'Caught exception: ' . $e->getMessage() . "\n";
    }
}

After that, add the following use statement to the top of the file.

use SendGrid\Mail\Mail;

This function initialises a SendGrid Mail object and sets the email's from address, subject, and recipient. After that, it sets the email's plain text and HTML body to "Your daily weather station data summary for %s - %s, replacing the two %s" placeholders with the start and end date respectively.

Then, using the createCsvFileFromWeatherData function, it retrieves the Stream object, containing the CSV representation of the filtered weather data, and Base64-encodes it, before attaching it to the email, naming it daily-summary.csv.

After that, it sends the email.

Add a function to send the daily summary SMS

Add the function below, in index.php, after the one that you just added.

function sendDailySummarySMS($startDate, $endDate): void
{
    $twilio = new Client(
        $_SERVER["TWILIO_ACCOUNT_SID"],
        $_SERVER["TWILIO_AUTH_TOKEN"]
    );
    $twilio
        ->messages
        ->create(
            $_SERVER["SEND_TO"],
            [
                "messagingServiceSid" => $_SERVER["TWILIO_MESSAGING_SERVICE_SID"],
                "body" => sprintf(
                    "Your daily weather station data summary is available at http://localhost/daily-summary/%s/%s",
                    $startDate,
                    $endDate
                ),
            ]
        );
}

Then, add the following use statement to the top of the file.

use Twilio\Rest\Client;

This function initialises a new Twilio Client object, passing in the Twilio Account SID and authentication tokens, so that it can make authenticated requests against Twilio's SMS API.

It then creates an SMS to send to the provided phone number. The body will be set to Your daily weather station data summary is available at http://localhost/daily-summary/%s/%s, with the two placeholders (%s) replaced with the start and end dates, respectively.

Add the daily summary route

Next, you need to add a new route for the daily summary data. To do that, in index.php, after the existing route, add the code below.

$app->get(
    '/daily-summary/{startDate}/{endDate}', 
    function (Request $request, Response $response, array $args) 
{
    /** @var WeatherStation\Service\WeatherService $weatherService */
    $weatherService = $this->get(WeatherService::class);
    $startDate = $request->getAttribute('startDate');
    $endDate = $request->getAttribute('endDate');
    $weatherData = $weatherService->getWeatherData($startDate, $endDate);

    if ($weatherData->count() === 0) {
        return new JsonResponse('No weather data available for that date range.');
    }

    sendDailySummarySMS($startDate, $endDate);
    sendDailySummaryEmail($startDate, $endDate, $weatherData);

    return new JsonResponse(
        sprintf(
            "Weather data retrieved for %s - %s",
            $startDate,
            $endDate
        )
    );
})->setName('daily-summary');

Then, add the following use statements to the top of the file.

use Laminas\Diactoros\Response\JsonResponse;
use WeatherStation\Service\WeatherService;

This defines a new route, named daily-summary. The route's path starts with /daily-summary and has placeholders for the start and end dates. Note, they're in YYYY-MM-DD format.

You can see several examples of acceptable routes, based on that definition, in the list below.

  • /daily-summary/2022-03-22/2022-03-23
  • /daily-summary/2022-05-01/2022-05-02
  • /daily-summary/2022-10-02/2022-10-03

The route's handler function, like the existing ones, takes three parameters:

  • A Request object
  • A Response object
  • An array of request arguments

It retrieves the weather service from the DI container, and the start and end dates from the request, before retrieving the weather, limited by those dates. Then, if no weather data was retrieved for the dates supplied, a JSON response stating that is returned.

If data was retrieved, it, along with the start and end date, are passed to the sendDailySummarySMS and sendDailySummaryEmail functions, to prepare and send the SMS and email summary notifications, and a JSON response, stating that data has been retrieved and sent is returned.

It's worth pointing out that a lot of things could go wrong, such as network failures when connecting with the Twilio and SendGrid APIs. They've intentionally not been handled, to allow for shorter example code.

Set up the Cron job

Now, you need to set up the cron job which sends the daily email and SMS summaries. To do that, create a new file in the bin directory named send-summaries.php and in it, add the following code.

<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

$dotenv = Dotenv\Dotenv::createImmutable(
    __DIR__ . '/../',
    '.env',
    false
);
$dotenv->safeLoad();

$today = new \DateTime();

$requestUrl = sprintf(
    "%s/daily-summary/%s/%s",
    $_ENV['WEATHER_STATION_HOSTNAME'],
    $today->format("Y-m-d"),
    $today->add(new DateInterval("P1D"))->format("Y-m-d")
);

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $requestUrl);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);

The script loads the environment variables, builds the URL to request ($requestUrl), then requests it with cURL. As a little aside, I thought of creating this script in bash.

However, I decided against it, after appreciating the amount of work required to determine the start and end dates; work that PHP's DateTime class drastically simplifies.

With the script created, run crontab -e. In the editor window that appears, add the following, save the file, and exit.

55 23 * * * php /var/www/html/bin/send-summaries.php

This will call the new script every day at 23:55 (or 5 to midnight). However, since that won't make it that easy to test. Feel free to change it to the following, for a while, which calls the script every five minutes.

*/5 * * * * php /var/www/html/bin/send-summaries.php

Make the Raspberry Pi available to the internet

This is the last step. I promise!

Without it, the link in the SMS' and emails wouldn't be available. So, you're going to make it available to the wider internet using ngrok. If you've not heard of it, it's:

> a globally distributed reverse proxy fronting your web services running in any cloud or private network, or your machine.

To get started, run the command below, which exposes the Pi's port 80 to the public internet through ngrok.

ngrok http 80

Then, in the command's output copy the Forwarding URL and use it to replace <WEATHER_STATION_HOSTNAME> in .env.

You're all done

At this point, there is nothing more to do other than to keep an eye on your phone and email inbox for the SMS and email you'll receive with the daily weather data summary information. They should look similar to email and SMS in the screenshots below.

Mock-up of daily status notification SMS and email with the weather station data attached as a CSV file.

That's how to send a daily summary with Twilio SMS and SendGrid Email

I hope you've enjoyed this two-part series and that it's inspired you to both play with Twilio and SendGrid APIs as well as with a Raspberry Pi.

Would you have implemented the functionality differently? If so, share your feedback in the comments.

Matthew Setter is the PHP Editor in the Twilio Voices team and a PHP and Go developer. He’s also the author of Deploy With Docker Compose. When he’s not writing PHP code, he’s editing great PHP articles here at Twilio. You can find him at msetter@twilio.com, Twitter, GitHub, and LinkedIn.