Henrik Sommerfeld

Asuswrt-Merlin Firmware Update Checker

I have an Asus RT-AC68U router at home. I’ve previously used the build-in update checker together with a notification script that ran on the router itself. Recently I noticed that I hadn’t got any update notifications in a long time, one of the downsides of silent failures.

When I ran a manual check using the router’s web interface, it just said: “Temporarily unable to get the latest firmware information. Please try again later.” It doesn’t seem to be that temporary though.

TL;DR

The code is in this GitHub repo and the scheduling piece with cron is described a the end of this post.

Building my own update checker

Since the project website of Asuswrt-Merlin presents the latest version in an easily parsable way, I decided to write my own checker using screen scraping in NodeJS.

Version checker

To find the latest version, I just looked at the website, inspected the HTML, installed the packages request-promise and cheerio, and finally extracted the version number of interest.

import rp from 'request-promise';
import $ from 'cheerio';

export async function getLatestStableVersion() {
  try {
    const html = await rp('https://www.asuswrt-merlin.net/');
    const text = $('#block-currentrelease', html).text();
    const rows = text.split('\n');
    const stableOthers = rows.find(r => r.startsWith('Others:'));
    const stableVersion = stableOthers.split('Others:')[1].trim();
    return stableVersion;
  }
  catch (error) {
    Promise.reject(error);
  }
}

Saving last checked version

In order to know if there is a new version since my last check, I of course need to keep track of what the version was the last time I checked. I did this with a simple text file on disk.

import { existsSync, readFileSync, writeFileSync } from 'fs';
import * as path from 'path';

const savedVersionFilePath = path.resolve(path.resolve(''), './last-checked-version.txt');

export function getLastCheckedVersion() {
  if (!existsSync(savedVersionFilePath)) {
    return "0.0"
  }
  
  const lastCheckedVersion = readFileSync(savedVersionFilePath, 'utf8');
  return lastCheckedVersion;
}

export function saveLastCheckedVersion(version) {
  writeFileSync(savedVersionFilePath, version);
}

Notifier

I already had a working notification script using the service Pushover that I ported from Bash to NodeJS.

import dotenv from 'dotenv';
import rp from 'request-promise';

export function sendPushoverNotification(message) {
  dotenv.config();
  var options = {
    method: 'POST',
    uri: 'https://api.pushover.net/1/messages.json',
    body: {
      token: `${process.env.PUSHOVER_TOKEN}`,
      user: `${process.env.PUSHOVER_USER}`,
      message: message
    },
    json: true
  };

  rp(options)
    .then(function (parsedBody) {
      console.log(`Pushover notification sent: ${message}`);
    })
    .catch(function (err) {
      console.error(err);
    });
  }

Gluing it together

By sending a notification both when there is no update and when an error occurs, I won’t have any silent failures unless I made a mistake here somewhere.

import { getLatestStableVersion } from './latest-version-checker.js';
import { sendPushoverNotification } from './notify.js';
import { getLastCheckedVersion, saveLastCheckedVersion } from './localFile.js';

async function main() {
  try {
    const lastCheckedVersion = getLastCheckedVersion();
    console.log("main -> lastCheckedVersion", lastCheckedVersion)
    const latestVersion = await getLatestStableVersion();
    
    if (latestVersion !== lastCheckedVersion) {
      const message = `🔔 New firmware version ${latestVersion} is now available at 
      
      https://www.asuswrt-merlin.net/`;
      sendPushoverNotification(message);
      saveLastCheckedVersion(latestVersion);
    }
    else {
      const message = `🤷‍♂️ No firmware released. ${latestVersion} is the latest.`;
      sendPushoverNotification(message);
    }
  } catch (error) {
    console.log("main -> error", error)
    const message = `⚠️ Router firmware update check failed`;
    sendPushoverNotification(message);
  }
}

main();

Scheduling the update checker

I’m running this on a RaspberryPi and it’s scheduled to run once a week, 18:10 on Wednesdays. I found https://crontab.guru to be helpful for not mixing up the time settings.

crontab -e

10 18 * * 3 /home/pi/router-update-check.sh >> /home/pi/router-update-check.log

The trickiest thing for me as a terrible Linux admin, was to get the cron scheduling working. Adding the output of echo $PATH at the top of the script did the trick. Logging the output (to router-update-check.log in this case), also helped.

router-update-check.sh script contains the following:

#!/bin/bash
PATH=/home/pi/.nvm/versions/node/v13.12.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games:/snap/bin

cd /home/pi/Code/asuswrt-merlin-update-check/
node ./main.js

The result of running the script once every minute (while troubleshooting) showed up in my phone like this. I now have an update checker that I can only blame myself if it doesn’t work. Great success!

Pushover notifications on iOS
No matching posts found. You can use wildcards and search only in titles, e.g. title:iot
Loading search index, please wait...
Search index failed to download 😢