[DevNotes] What's my mobile data consumption ?

At the moment, I am spending some time in Hong-Kong. As I use a pre-paid SIM card, one thing I face once in a while is to know that my mobile data allowance is expiring / has expired.

Unfortunately, I realise it only when I can't connect anymore... it is infrequent and annoying. I need to find a wifi spot I can connect to. I don't remember the URL of the website to top up. I don't remember the password CSL set for me and need to reset it, ...

So, to illustrate what I advocate on this website, I build a small script to help me solve this problem. It is a faster way for me to check my consumption status.

It's not perfect, could use a lot of improvements, but, I know my audience with this tool: me, I know what's the intended goal is: be able to track what's left on my account. So, the following  output (which comes almost straight from the CSL website) is enough.

Now I know, 39.3 GB left - I just topped up!

If I needed more, I could parse the output to extract the values, maybe have a nicer display with gauges or ... Well, thinking about it, I would likely get rid of the UI altogether. I would set a scheduled task, daily, that would parse the output I currently have, retrieve the value and set some alert to be sent to my phone when I am running below a certain threshold. But, as I mentioned, it is infrequent enough to not over-do it...

Code

Here is the full script, split into the various sections. Copy / Paste them in order to get it working.

Initialisation / Configuration

<?php

$number = '<Replace with phone number>';
$password = '<Replace with CSL website password';

Pretty straightforward, that's what we need to access the data. Don't forget to keep those secure.

Authentication


$cookieFileName = __DIR__ . '/csl_cookie.txt';

if (isset($forceAuthentication) || !file_exists($cookieFileName)) {
    $curl = curl_init();

    curl_setopt_array($curl, [
            CURLOPT_URL => 'https://prepaid.hkcsl.com/login_add',
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 0,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_COOKIESESSION => true,
            CURLOPT_COOKIEJAR => $cookieFileName,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => "msisdn=$number&password=$password",
            CURLOPT_HTTPHEADER => [
                'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/114.0',
            ]
        ]
    );

    $result = @curl_exec($curl);

    curl_close($curl);
}

Here, we call the CSL website to authenticate and get the right cookies set.

I like to save them so that future use of the script would use the local version instead of calling the website again. That's what the COOKIEJAR option is for (along with the COOKIEFILE below)

This has two benefits : you are being respectful of the provider as well as "hiding" your activity.

The User-Agent configuration is required to trick CSL into giving us what we want. If we don't, it seems to be lost, not knowing what to do.

If for some reason the session / cookies are obsolete and we can't get the required data, we'll have to invalidate/delete the cookie file and re-authenticate. That's done a little bit further.

Retrieving the data of interest


$curl = curl_init();

curl_setopt_array($curl, [
    CURLOPT_URL => 'https://prepaid.hkcsl.com/usage',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_MAXREDIRS => 10,
    CURLOPT_TIMEOUT => 0,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_COOKIEFILE => $cookieFileName,
    CURLOPT_HTTPHEADER => [
        'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/114.0',
    ],
]);

$response = curl_exec($curl);

curl_close($curl);

Once we have the authentication cookies, we can call the page we need. The CSL website is fairly simple to deal with. A straight call to the usage page gives us the required data.

Authentication - Part 2


if (strpos(curl_getinfo($curl, CURLINFO_EFFECTIVE_URL), 'login')) {
    if (isset($forceAuthentication)) {
        echo "FAILED TO AUTHENTICATE";
    } else {
        $forceAuthentication = true;
        if (file_exists($cookieFileName)) {
            unlink($cookieFileName);
        }
        require 'csl.php';
    }

    exit;
}

Well, not really authentication here, but it is linked. It may happen that the session expires between times I look at the page.

In that case we get redirected to the login page.

The easiest way to fix it is to delete the cookie file and reload the same script to replay from the beginning, going through the authentication step above.

Rendering the result

It's, a 2-step process.

Because we get the full page html from CSL, the first step is to extract the data we need. There are multiple ways to do it, in this context, I liked the DomDocument / Xpath approach

$doc = new DOMDocument();
@$doc->loadHTML($response);

$xpath = new DOMXpath($doc);

$items = $xpath->query('//*[@class="mainContent"]');

$content = $items[0]->ownerDocument->saveHTML($items[0]);

?>

Once we have the table from CSL, I format it a bit more nicely to display. Nothing fancy here, because it is simple, inline styles and some styling in the header for CSL generated classes.


<html>
<head>
    <title>CSL Consumption</title>
    <style>
        body {
            font-family: 'Open Sans', sans-serif
        }
        
        #doticon {
            display: none;
        }

        .infoTitle {
            font-size: x-large;
        }
    </style>
</head>
<body>

<div style="padding: 1em; border: 1px solid #061a4c; border-radius: 10px; background-color: #F9A602; color: #061a4c; width: 420px;">
    <h1 style="margin-top: 0;">CSL Consumption</h1>
    <div>
        <?php echo $content; ?>
    </div>
    <div style="text-align: right;">
        <a href="https://prepaid.hkcsl.com" style="padding: 0.5em 1em; text-decoration: none; background-color: #061a4c; color: #FFFFFF; border-radius: 10px; border: 1px solid #FFFFFF;">CSL</a>
    </div>
</div>
</body>
</html>

Conclusion

All-in-all, it is rather simple once put together. A clear problem. A possible solution to it. It may or may not be the best one, but it will work for now.

If / when I need more, I can do more with it. But for now, it is not worth it yet.

Another aspect that can be improved, but, being the only user, it doesn't matter much to me either: error handling. I don't care much of how it can blow up, so, I have minimal handling here.

For external users, more care is needed.

Let me know if you want more of those technical breakdowns when I work on new tools, integrations and automations.