New and Updated Libraries

We now have a library for the Forecast.io Forecast API, which you can use to obtain all manner of meteorological minutiae. More details in the library documentation. You can use the library now; it’s at version 1.0.0.

Next, our driver for 8x8 matrix LEDs controlled by a Holtek HT16K33 chip (as used by Adafruit’s 1.2-inch monochrome LED matrix display), has been updated to version 1.1.0. The update adds some refinements, including a new proportionally spaced font, though you can add your own symbols with a new user-defined character facility. More details in the library documentation.

I have a project underway which retrieves data from Forecast.io and this class could be useful. However, it would be nice if the library accepted a units parameter because the Forecast.io API can feed data in the units of choice.

The API accepts additional options… I happen to use the “units=ca” option:
https://developer.forecast.io/docs/v2#options

I am REALLY new to GitHub, but I attempted to submit the change to the forecast.io library to add a units parameter which will fall back to “us” if the parameter given is not valid (which is the default for the forecast.io API if no parameter is given).

It should be backward compatible if people are using it currently EXCEPT in the case where someone uses the callback parameter (because I bumped it by one to add units).

Also, let me know if I’m horribly butchering my usage of GitHub. I have to admit as a fairly saavy computer user, the interface is VERY unclear.

Thanks for the suggested changes, @dheadrick. To prevent any issues with compatibiltiy, I’ve updated the Forecastio library as follows:

• Added setUnits() and setLanguage() methods so that these options can be changed without the risk of breaking existing code. Both return a reference to the class instance, so you can chain them at startup, eg

`local fc = Forecastio(API_KEY);
fc.setUnits("si").setLanguage("de");`

Once applied, the settings stick until they’re subsequently changed.

The interface to timeMachineRequest() and forecastRequest() are therefore unchanged, but they will make use of your units and/or language choices.

This bumps the library to version 1.1.0, so the require statement becomes

#require "Forecastio.class.nut:1.1.0"

Finally, and I’ve added ‘Develop’ branch to the library’s GitHub repo. Please post future pull requests to that branch, not ‘master’ (my fault: I should have set this up before)

Sounds good. I’d rather improve existing libraries instead of making a fork and diluting good programming effort over more projects. :slight_smile:

I was just looking over your code. There are a couple of weird bits with the units selection:

The ForecastIO website doesn’t list “uk” as a valid unit parameter, yet accepts it as input with the same data returned as uk2.

Your code allows “uk” but looks like it switches it to “uk2”.

Also, your code doesn’t allow “auto” as an option (which ForecastIO does).

The “uk” -> “uk2” thing is just to allow folks to enter “uk” (the logical name) and get away with it.

I completely overlooked ‘auto’, which should probably be the default setting. Something to add in v1.1.1.

Ah. Sounds good. I had to look a couple of times to notice that “uk” was missing and only “uk2” is an option. Enough people must make this mistake because ForecastIO actually accepts “uk” as an option when they say that “uk” should use “si”.

However, their default is “us” the same as your code.

Forecast IO library now at 1.1.1 as per above: https://electricimp.com/docs/libraries/webservices/forecastio/

@smittytone: perhaps a stupid question, but is there a possibility as user to define your own libraries e.g. I have a ShowHardwareStatus() function I want to use in all my models/IMPs. Not really a fan of copy/pasting that one all over the place… :slight_smile:

Not at this point, but aside from @smittytone’s Squinter (which does preprocessing) we have some tools coming soon that do this in the more general sense.

Another question… if a device library available for use when the IMP is temporarily disconnected from WIFI? For example, when the code is run, does the Imp download the #require code and save it locally in memory for use if it can’t access the agent?

All the device side code (including requires) is compiled and cached within the imp. It will run even without a network connection but you need to take care that the code doesn’t try and use the network in certain modes - see the imp doc section for more info.

@Hugo, is it somehow possible to obtain the modelname from within the code? I know that when manually entering the modelname it can be used to obtain the buildnr, but as both are also part of my generic ShowHardwareStatus function it would be great not to have to edit the modelname for each model (and making even more sense would the ShowHardwareStatus be a library call).

Not currently, no, but that’s been on the list for a while. It’d be a server-side change so doesn’t need an OS release to happen (ie, it’s not in release 34!)

Given that model name and model code are so tightly coupled, ie. given code has to have a name, surely it’s easy enough to hard-code that name?

The other option is to access the Build API from within your code using the following class:

`class BuildAPIAgent {
    // A very simple integration of the Electric Imp Build API
    // to provide agent code with extra model and device information
    //
    // Written by Tony Smith
    // Copyright Electric Imp, Inc. 2016
    // Released under the MIT License

    // Constants
    static BASE_URL = "https://build.electricimp.com/v4/";
    static version = [1,0,2];

    // Private properties
    _header = null;

    constructor(apiKey = null) {
        // Constructor requires a Build API key
        if (apiKey == null) {
            // No API key? Report error and bail
            server.error("BuildAPI class cannot be instantiated without an API key");
            return null;
        } else {
            // Build the header for all future Build API requests
            _header = { "Authorization" : "Basic " + http.base64encode(apiKey) };
        }
    }

    // *** PUBLIC FUNCTIONS ***

    function getDeviceName(deviceID = null, updateFlag = false) {
        if (deviceID == null || deviceID == "" || typeof deviceID != "string") {
            server.error("BuildAPIAgent.getDeviceName() requires a device ID passed as a string");
            return null;
        }

        local device = _getDeviceInfo(deviceID);
        if (device) return device.name;
        return null;
    }

    function getModelName(deviceID = null) {
        if (deviceID == null || deviceID == "" || typeof deviceID != "string") {
            server.error("BuildAPIAgent.getModelName() requires a device ID passed as a string");
            return null;
        }

        local models = _getModelsList();
        local myModel = null;
        foreach (model in models) {
            if ("devices" in model) {
                foreach (device in model.devices) {
                    if (device == deviceID) {
                        myModel = model;
                        break;
                    }
                }
            }

            if (myModel) break;
        }

        if (myModel) myModel = myModel.name;
        return myModel;
    }

    function getLatestBuild(modelName = null) {
        if (modelName == null || typeof modelName != "string") {
            server.error("BuildAPIAgent.getLatestBuild() requires a model name passed as a string");
            return null;
        }

        local maxBuild = null;
        local models = _getModelsList();

        foreach (model in models) {
            if (model.name == modelName) {
                local data = _getRevisions(model.id);
                if (!data) return null;
                maxBuild = data.revisions.len();
                foreach (rev in data.revisions) {
                    local v = rev.version.tointeger();
                    if (v > maxBuild) maxBuild = v;
                }

                break;
            }
        }

        return maxBuild;
    }

    // **** PRIVATE FUNCTIONS - DO NOT CALL ****

    function _getDeviceInfo(devID) {
        local data = _sendGetRequest("devices/" + devID);
        if (data) return data.device;
        return null;
    }

    function _getDevicesList() {
        local data = _sendGetRequest("devices");
        if (data) return data.devices;
        return null;
    }

    function _getModelInfo(modID) {
        local data = _sendGetRequest("models/" + modID);
        if (data) return data.model;
        return null;
    }

    function _getModelsList() {
        local data = _sendGetRequest("models");
        if (data) return data.models;
        return null;
    }

    function _getRevisions(modelID) {
        return _sendGetRequest("models/" + modelID + "/revisions");
    }

    function _sendGetRequest(url) {
        // Issues a GET request based on the passed URL using stock header
        local result = http.get(BASE_URL + url, _header).sendsync();
        if (result.statuscode == 200) {
            return http.jsondecode(result.body);
        } else {
            if (result.statuscode == 401) {
                server.error("Build API Error: " + result.statuscode + " - Unrecognised API key");
            } else {
                // TODO Handlers for common errors
                server.error("Build API Error: " + result.statuscode + " - " + result.body);
            }

            return null;
        }
    }
}`

Instantiate this as, say, build, and then you can run:

server.log("My model name is: " + build.getModelName(imp.configparams.deviceid));

@smittytone: basically you are fully right, but I am a bit allergic to defining constants in code that still somewhere have a relationship to. Changing a model name requires changing also this constant in the code, no issue at all but why not have this in some way linked if possible?
Anyway the code you provided works like a charm, thanks ^:)^

@smittytone, @moose: I’m a bit allergic to sync code, so I’ve updated the code above to be async by using Promises.

It still needs a BuildAPI key (might be nice if the Agent had programatic access to one of those) but as long as its used on a single imp account, no constants required!

`
#require “Promise.class.nut:3.0.0”

class BuildAPI {
// A very simple integration of the Electric Imp Build API
// to provide agent code with extra model and device information
//
// Written by Tony Smith
// Copyright Electric Imp, Inc. 2016
// Released under the MIT License
//
// Written by Austin Eldridge
// Released under the MIT License

// 'Constants'
static BASE_URL = "https://build.electricimp.com/v4/";
static version = [2,0,0];

// Private properties
_header = null;

// Constructor requires a Build API key
constructor(apiKey = null) {
    if (apiKey == null) {
        // No API key? Bail
        throw "BuildAPI class will not instantiate without an API key";
    } else {
        // Build the header for all future Build API requests
        _header = { "Authorization" : "Basic " + http.base64encode(apiKey) };
    }
}

// *** PUBLIC FUNCTIONS ***

function getDeviceName(deviceID = imp.configparams.deviceid) {
   if (deviceID == null || deviceID == "" || typeof deviceID != "string") {
       local error = "BuildAPIAgent.getDeviceName() requires a device ID passed as a string"
       server.error(error);
       return Promise.reject(error);
   }

   return _getDeviceInfo(deviceID)
                .then(function(device){
                    return Promise.resolve(device.name)
                }.bindenv(this))

}

function getModelName(deviceID = imp.configparams.deviceid) {
if (deviceID == null || deviceID == “” || typeof deviceID != “string”) {
local error = "BuildAPIAgent.getModelName() requires a device ID passed as a string"
server.error(error);
return Promise.reject(error);
}

   return _getModelsList()
                .then(function(data){
                    foreach (model in data.models) {
                        if ("devices" in model) {
                            foreach (device in model.devices) {
                                if (device == deviceID) {
                                    return Promise.resolve(model.name)
                                }
                            }
                        }
                    }
                    return Promise.reject("deviceID " + deviceID + " not found in any models from Build API")
                }.bindenv(this))

}

// Gets and returns the latest build number for a given model
function getCurrentVersion(modelName = null) {
    if (modelName == null || (typeof modelName != "string")) {
        local error = "BuildAPI.getCurrentVersion() requires a model name passed as a string"
        server.error(error);
        return Promise.reject(error);
    }

    return _getModelsList()
                .then(function(data){
                    foreach (model in data.models) {
                        if (model.name == modelName) {
                            return _getRevisionList(model.id);
                        }
                    }
                }.bindenv(this))
                .then(function(result){
                    local maxBuild = -1;
                    foreach (rev in result.revisions) {
                        local v = rev.version.tointeger();
                        if (v > maxBuild) maxBuild = v;
                    }
                    return Promise.resolve(maxBuild)
                }.bindenv(this))
                .fail(function(error){
                    server.error("BuildAPI.getCurrentVersion failed - " + http.jsonenecode(error))
                    return Promise.reject(-3)
                }.bindenv(this))
}

// **** PRIVATE FUNCTIONS - DO NOT CALL ****

function _getDeviceInfo(deviceID) {
    return _sendGetRequest("devices/" + deviceID)
}

function _getModelsList() {
    return _sendGetRequest("models");
}

function _getModelInfo(modelID) {
    return _sendGetRequest("models/" + modelID);
}

function _getModelsList() {
    return _sendGetRequest("models");
}

function _getRevisionList(modelID) {
    return _sendGetRequest("models/" + modelID + "/revisions");
}

function _sendGetRequest(url) {
    return Promise(function(fulfill, reject){
        // Issues a GET request based on the passed URL using stock header
        http.get(BASE_URL + url, _header).sendasync(function(result){
            if (result.statuscode == 200) {
                fulfill(http.jsondecode(result.body));
            } else {
                if (result.statuscode == 401) {
                    server.error("Build API Error: " + result.statuscode + " - Unrecognised API key");
                } else {
                    // TODO Handlers for common errors
                    server.error("Build API Error: " + result.statuscode + " - " + result.body);
                }
                reject(result);
            }
        }.bindenv(this));
    }.bindenv(this))
}

}

g_Build <- BuildAPI("");
g_Build.getModelName()
//.then(g_Build.getCurrentVersion.bindenv(g_Build)) //Can use this version if you don’t want the model name logged
.then(function(modelName){
server.log("Model: " + modelName)
return g_Build.getCurrentVersion(modelName)
})
.then(function(buildVersion){
server.log("Build: " + buildVersion);
})
.fail(function(error){
server.error(“UNABLE TO GET BUILDVERSION VIA BUILDAPI”)
server.error(error)
})

`

I just received my Imp and started using the Forecast.io library. It works great… but the first mistake I made was passing it the long/lat coordinates as strings. While that was my mistake, the library doesn’t catch this situation when it does the check to see if if the coordinates are valid and within the expected range.

Just an FYI.

Good point, @dheadrick, I’ll add that in.