1.7.0 • Published 1 day ago

wappsto-wapp v1.7.0

Weekly downloads
-
License
MIT
Repository
github
Last release
1 day ago

Wappsto Wapp API

CI codecov DeepScan grade Depfu Seluxit A/S Wappsto

This is a node/js library for easily creating wapps for Wappsto.

In depth documentation can be found on Github Pages

Table of Contents

Install

Node

To install the node module in your project, run this command:

npm i --save wappsto-wapp

Or using yarn:

yarn add wappsto-wapp

And include it in your project like this:

let Wappsto = require('wappsto-wapp');

or

import Wappsto from 'wappsto-wapp';

Browser

To use it in a webpage, include this script tag:

<script src="https://cdn.jsdelivr.net/npm/wappsto-wapp@latest/dist/wappsto-wapp.min.js"></script>

Usage

Here are some examples on how to use the library.

Create a new IoT Device

First you need to create a IoT network. To create a new network you need to call 'createNetwork'. If there is an Network with the same name, the existing network will be returned.

let network = await Wappsto.createNetwork({
    name: 'new network name',
});

Then you need to create a device. To create a new device under an existing network, you should call createDevice. If a device exists with the given name, the existing device will be returned.

let device = await network.createDevice({
    name: 'Device Name',
    product: 'Great Product',
    serial: 'SG123456',
    description: 'My great new device',
    protocol: 'WAPP-JS',
    communication: 'wapp',
    version: '1.0.0',
    manufacturer: 'My Self',
});

Then you need to create new values. To create a new value under an existing device, you should call createValue. If a value exists with the given name, the existing value will be returned. There will also be created the states needed based on the permission. The only allowed values for permission is 'r', 'w' and 'rw'. The state will have 'NA' as the initial data. This can be changed by setting the initialState when creating the value. You can also define the initial timestamp by setting initialState as an object with data and timestamp. The list of available value templates can be seen in the value_template.ts file.

let value = await device.createValue({
    name: 'Temperature',
    permission: 'r',
    template: Wappsto.ValueTemplate.TEMPERATURE_CELSIUS
});

It is also possible to define a period for how often the library should poll your values for updates. In this way regularly reports will be send to the cloud. It is also possible to filter out small changes by setting the delta for number values. With the delta set, reported changes that are not bigger then the delta will be discarded. In the example below, period is set to 3600 sec., i.e. 1 hour interval and delta to 2 deg.. So changes in value data will only apply if after one hour new temperature value is bigger than 2 deg. of previous temperature value.

let value = await device.createValue({
    name: 'Temperature',
    permission: 'r',
    template: Wappsto.ValueTemplate.TEMPERATURE_CELSIUS,
    period: 3600,
    delta: '2'
});

It is also possible to define a value where the data is not put into the historical log. This is done by setting the disableLog to true. This can be set on the createValue and the special versions of the createValue function.

let value = await device.createValue({
    name: 'Temperature',
    permission: 'r',
    template: Wappsto.ValueTemplate.TEMPERATURE_CELSIUS,
    disableLog: true
});

There are some helper functions to create custom number, string, blob and xml values. To create a custom number value:

let value = await device.createNumberValue({
    name: 'Value Name',
    permission: 'rw',
    type: 'Counter',
    period: '1h',
    delta: '1',
    min: 0,
    max: 10,
    step: 1,
    unit: 'count',
    si_conversion: 'c',
    disableLog: false,
    initialState: {
        data: 0,
        timestamp: '2022-02-02T02:02:02Z',
    },
});

To create a custom string value:

let value = await device.createStringValue({
    name: 'String Value Name',
    permission: 'rw',
    type: 'debug',
    max: 10,
    encoding: 'debug string',
});

To create a custom blob value:

let value = await device.createBlobValue({
    name: 'Blob Value Name',
    permission: 'r',
    type: 'binary',
    max: 10,
    encoding: 'binary',
});

To create a custom xml value:

let value = await device.createBlobValue({
    name: 'XML Value Name',
    permission: 'rw',
    type: 'config',
    namespace: 'seluxit'
    xsd: 'https://xsd.com/test.xsd',
});

To report a change in the value

To send a new data point to wappsto, just call the report function on the value. If you omit the timestamp, it will get the curren time as timestamp. It is possible to send number, string, boolean and objects.

await value.report(1);
await value.report('1', '2022-02-02T02:02:02Z');
await value.report(true);
await value.report({"my_data":"is updated"});

And to get the last reported data and timestamp.

let data = value.getReportData();
let timestamp = value.getReportTimestamp();

If you need to report multiple historical data, you can specify an array of data, timestamp pairs.

await value.report([
	{ data: 1, timestamp: '2022-02-02T02:02:01Z' },
	{ data: 2, timestamp: '2022-02-02T02:02:02Z' },
	{ data: 3, timestamp: '2022-02-02T02:02:03Z' },
]);

If you do not want the current report state to change, you can use the sendLogReports instead.

await value.sendLogReports([
	{ data: 1, timestamp: '2022-02-02T02:02:01Z' },
	{ data: 2, timestamp: '2022-02-02T02:02:02Z' },
	{ data: 3, timestamp: '2022-02-02T02:02:03Z' },
]);

Listing for requests to refresh the value

To receive request to refresh the value, register a callback on onRefresh. This can be triggered by an user request or an automatic event from the library based on the period.

value.onRefresh((value) => {
    value.report(1);
});

It is possible to get the type of the source of the refresh event. But you should not use this information to not send an update on the value. It is important to always send value updates on all refresh requests.

value.onRefresh((value, type) => {
    console.log(`Got a refresh event from ${type}`);
});

And you can cancel this callback by calling cancelOnRefresh.

value.cancelOnRefresh();

Reporting events for value

It is possible to save events in an event log for each value. Call addEvent on a value to create an event entry.

The possible values for level is:

  • important
  • error
  • success
  • warning
  • info
  • debug
await value.addEvent('error', 'something went wrong');

Accessing Existing Objects

To request access to an existing object, you need to send a request. You can request a single object or multiple objects of the same type. To request access to multiple objects, specify the desired quantity after using the findByName('name', 3) syntax. Alternatively, you can request access to all possible objects that match the query by calling findAllByName. If you only require read access to the data, you can set the readOnly parameter to true.

To request access to a network with a specific name, utilize the findByName function.

let oneNetwork = await Wappsto.Network.findByName('Network name');
let oneReadOnlyNetwork = await Wappsto.Network.findByName('Network name', 1, true);
let multipleNetworks = await Wappsto.Network.findByName('Network name', 3);
let allNetworks = await Wappsto.Network.findAllByName('Network name');
let allReadOnlyNetworks = await Wappsto.Network.findAllByName('Network name', true);

To request access to a device with a specific name, use findByName.

let oneDevice = await Wappsto.Device.findByName('Device name');
let oneReadOnlyDevice = await Wappsto.Device.findByName('Device name', 1, true);
let multipleDevices = await Wappsto.Device.findByName('Device name', 3);
let allDevices = await Wappsto.Device.findAllByName('Device name');
let allReadOnlyDevices = await Wappsto.Device.findAllByName('Device name', true);

To request access to a device with a specific product, use findByProduct.

let oneDevice = await Wappsto.Device.findByProduct('Product name');
let oneReadOnlyDevice = await Wappsto.Device.findByProduct('Product name', 1, true);
let multipleDevices = await Wappsto.Device.findByProduct('Product name', 3);
let allDevices = await Wappsto.Device.findAllByProduct('Product name');
let allReadOnlyDevices = await Wappsto.Device.findAllByProduct('Product name', true);

To request access to a value with a specific name, use findByName.

let oneValue = await Wappsto.Value.findByName('Value name');
let oneReadOnlyValue = await Wappsto.Value.findByName('Value name', 1, true);
let multipleValues = await Wappsto.Value.findByName('Value name', 3);
let allValues = await Wappsto.Value.findAllByName('Value name');
let allReadOnlyValues = await Wappsto.Value.findAllByName('Value name', true);

To request access to a value with a specific type, use findByType.

let oneValue = await Wappsto.Value.findByType('Type name');
let oneReadOnlyValue = await Wappsto.Value.findByType('Type name', 1, true);
let multipleValues = await Wappsto.Value.findByType('Type name', 3);
let allValues = await Wappsto.Value.findAllByType('Type name');
let allReadOnlyValues = await Wappsto.Value.findAllByType('Type name', true);

Advanced Filtering for Accessing Existing Objects

Enhance your control over the specific objects you request by utilizing advanced filters. Combine various keys in the filter to refine the returned objects. To find multiple objects of the same type, utilize an array with the values. The filter primarily consists of three main entry points: network, device, and value. A filter value can be a string, number, or an array of strings or numbers. Additionally, you can specify the operator used to compare attributes using an object with the 'operator' and 'value' keys.

{ operator: '==', value: 'test' }

The permitted operators are: '=', '!=', '==', '<', '<=', '>', '>=', '~', '!~'.

You can see the possible filters below.

const filter = {
    network: {
        name: '',
        description: ''
    },
    device: {
        name: '',
        product: ['',''],
        serial: '',
        description: '',
        protocol: '',
        communication: '',
        version: '',
        manufacturer: ''
    },
    value: {
        name: '',
        permission: '',
        type: '',
        description: '',
        period: '',
        delta: '',
        number: {
            min: '',
            max: '',
            step: '',
            unit: '',
            si_conversion: ''
        },
        string: {
            max: '',
            encoding: '',
        },
        blob: {
            max: '',
            encoding: '',
        },
        xml: {
            xsd: '',
            namespace: ''
        }
    }
}

Once you've defined the filter, utilize it with the functions findByFilter and findAllByFilter on Network, Device, and Value.

const filter = { value: { type: 'energy' }};
let oneValue = await Wappsto.Value.findByFilter(filter);
let allValues = await Wappsto.Value.findAllByFilter(filter);

You can also use the filter to exclude objects by providing a second filter to findByFilter and findAllByFilter. Specify the number of items to request with a third parameter. Additionally, indicate that the items are only needed for reading by setting the fourth parameter to true.

const filter = { value: { type: 'energy' }};
const omit_filter = { device: { name: 'Wrong' }};
let oneValue = await Wappsto.Device.findByFilter(filter, omit_filter);
let multipleValues = await Wappsto.Value.findByFilter(filter, omit_filter, 3);
let multipleReadOnlyValues = await Wappsto.Value.findByFilter(filter, omit_filter, 3, true);
let allValues = await Wappsto.Value.findAllByFilter(filter, omit_filter);
let allReadOnlyValues = await Wappsto.Value.findAllByFilter(filter, omit_filter, true);

To find a child from an existing object

To find devices under a network, you can call findDeviceByName to search for all devices with the given name.

let devices = network.findDeviceByName('Device name');

Or you can search for all devices with a given product.

let devices = network.findDeviceByProduct('Device product');

To find all values under a network or device, you can call findValueByName to search for all values with the given name.

let values = network.findValueByName('value name');
let values = device.findValueByName('value name');

Or you can find all values with a given type, by calling findValueByType.

let values = network.findValueByType('value type');
let values = device.findValueByType('value type');

List all objects by type

To list all objects of a spefic type that you have access to use fetch to get then all.

let networks = await Wappsto.Network.fetch();

Retrieve object by ID

If you already have access to some objects, you can retrieve them directly by their ID.

let network = Wappsto.Network.findById('655937ac-c054-4cc0-80d7-400486b4ceb3');
let device = Wappsto.Device.findById('066c65d7-6612-4826-9e23-e63a579fbe8b');
let value = Wappsto.Value.findById('1157b4fa-2745-4940-9201-99eee5929eff');

To change a value on a network created outside your wapp

To send a new data point to another value, just call the control function on the value.

const result = await value.control('1');
if (!result) {
    console.warn('Failed to send control to device');
}

If you need to verify that the device send back a report, you can use controlWithAck to wait for the incoming report. It will return the received value from the device.

const result = await value.controlWithAck('1');
switch (result) {
    case undefined:
        console.log('Failed to control device');
        break;
    case null:
        console.log('Timeout while waiting for response');
        break;
    default:
        console.log(`Received ${result} from the device`);
        break;
}

And to get the last controlled data and timestamp.

let data = value.getControlData();
let timestamp = value.getControlTimestamp();

Listing for changes on values

You can register onControl and onReport to receive events when the value changes.

value.onReport((value, data, timestamp) => {
    console.log(`${value.name} changed it's report value to ${data}`);
});

value.onControl((value, data, timestamp) => {
    console.log(`Request to change ${value.name} to ${data}`);
});

If you want the onReport callback to be called with the current data, then you can set the ´callOnInit´ to true when registering your onReport callback.

value.onReport(reportCallback, true);

And you can cancel these callbacks by calling cancelOnReport and cancelOnControl.

value.cancelOnReport();

value.cancelOnControl();

To reload a model from the server

If you want to load the newsiest data from the server manually, you can call reload on the model, to get the latest data for that model.

await device.reload();

If you want to also load all the children data, you should call reload with true.

await device.reload(true);

Sending an update event to a device

To send an event to a device to get a new report data.

await value.refresh();

Loading historical data

It is possible to load historical data for a value. Both report and control data can be loaded using getReportLog and getControlLog. Each function takes a LogRequest object, where you can define the parameters for the log service.

const historicalReportData = await value.getReportLog({});
const historicalControlData = await value.getControlLog({});

The LogRequest object is defined like this:

KeyTypeMeaning
startdateThe start time for the log request
enddateThe end time for the log request
limitnumberThe maximum number of items in the result
offsetnumberHow many items that should be skipped before returning the result
operationstringWhat operation should be performed on the data before returning it
group_bystringWhat time interval should the log data be grouped by
timestamp_formatstringThe format the timestamp should be converted into
timezonestringIf the timestamp should be converted to a timezone instead of UTZ
orderstringShould the result be ordered ascending or descending
order_bystringThe field that should be used to order by
numberbooleanIf only numbers should be returned in the log response

Check if a device is online

It is possible to check if a network or device is online by calling isOnline.

if (network.isOnline()) {
    console.log('Network is online');
}
if (device.isOnline()) {
    console.log('Device is online');
}

You can also register a callback that are called every time the connection status changes.

network.onConnectionChange((network, status) => {
    console.log(
        `Network ${network.name} is now ${status ? 'online' : 'offline'}`
    );
});

To remove the registered callback, you must call the cancelOnConnectionChange method with the same callback function as a parameter.

If you have a created your own device, you can set it online/offline by calling setConnectionStatus on the device. This will automatic create a CONNECTION_STATUS value if needed.

await device.setConnectionStatus(true);

Changing the period and delta of a value

To change the delta and period of a value you can call setDelta or setPeriod.

value.setPeriod('300');

value.setDelta(2.2);

Deleting an item that was created

It is possible to remove an item that you have created by calling delete on the item. This will remove the item from the backend.

await value.delete();

Analyzing historical data

It is possible to use the Analytic Backend to analyze the historical data of a value.

Energy Data

To convert the total energy value into a load curve value, call analyzeEnergy with a start and end time on the value that you want to analyze.

const energy_data = await value.analyzeEnergy('2022-01-01T00:00:00Z', '2023-01-01T00:00:00Z');

Sending messages to and from the background

Messages can be exchanged between the foreground and the background of your Wapp, using event handlers. These event handlers are triggered for each message sent and the return value of your event handler is sent back to the sender.

Example code for the foreground part

Wappsto.fromBackground((msg) => {
    console.log('Message from the background: ' + msg);
    return 'Hello back';
});

let backgroundResult = await Wappsto.sendToBackground('hello');
console.log('Result from background: ' + backgroundResult);

Example code for the background part

Wappsto.fromForeground((msg) => {
    console.log('Message from the foreground: ' + msg);
    return 'Hello front';
});

let foregroundResult = await Wappsto.sendToForeground('hello');
console.log('Result from foreground: ' + foregroundResult);

Sending signal

If you want to send a message, but do not want a reply, you can use signalBackground.

await Wappsto.signalBackground('start');
await Wappsto.signalForeground('started');

Cancel event handlers

If you do not want to receive anymore messages, you can cancel the event handler.

Wappsto.cancelFromBackground();
Wappsto.cancelFromForeground();

TypeScript - Supplying Generic Types

When using TypeScript, it is possible to supply a generic type for the fromForeground and fromBackground methods to specify the expected return type. This can be useful when you want to ensure type safety.

Wappsto.fromBackground<string>((msg) => {
    console.log(`The type is: ${typeof msg}`); // The type is: string
});

Additionally, you can supply both a generic type and a return type for sendToBackground and sendToForeground methods. This can be useful when you want to ensure type safety and specify the expected return type.

const response = await Wappsto.sendToBackground<string, number>('return a number');
console.log(`The type is: ${typeof response}`); // The type is: number

Waiting for the background to be ready

If you need to communicate with the background, it is a good idea to wait for the background to be ready and have registered the waitForBackground handler. This can be do using the waitForBackground function.

const result = await Wappsto.waitForBackground();
if (result) {
    console.log('The background is now ready');
}

It waits for 10 seconds, but this can be changed by giving it a new timeout. And -1 to wait for ever.

const result = await Wappsto.waitForBackground(20);

Permission updated

It is possible to get notified when there are a change in the permissions. This is helpful when an user adds new items to the Wapp and the Wapp needs to show an updated list of items.

Wappsto.onPermissionUpdate(() => {
    console.log('Permissions changed');
});

You can also stop getting notified again, by calling cancelPermissionUpdate.

Web Hook

If you want to handle WebHooks, it is possible to register an event handler. This event handler will be called for each incoming WebHook and can return a value to the caller.

The format of the webhook url is https://wappsto.com/services/extsync/request/<token>. You can get the token from the value extSyncToken.

console.log(`WebHook URL: https://wappsto.com/services/extsync/request/${Wappsto.extSyncToken}`);

Register WebHook handler

To register the event handler, you can use the onWebHook function.

Wappsto.onWebHook((event, method, uri, headers) => {
    console.log('Web Hook event', event);
    return { status: "ok" };
});

It is also possible to change the return code byt returning a object with a body and code. This also supports adding headers to the response.

Wappsto.onWebHook((event, method, uri, headers) => {
    console.log('Web Hook event', event);
    return {
        code: 201,
        headers: {
            'Content-Type': 'application/json',
        },
        body: { status: "ok" };
    };
});

Cancel WebHook handler

And if you want to cancel the web hook event handler, you can call cancelWebHook.

Wappsto.cancelOnWebHook(handler);

Wapp Storage

The Wapp Storage feature allows for the storage of configuration parameters and other information within Wappsto. This data is persistent across both foreground and background wapps.

let storage = await Wappsto.wappStorage();

If you are using TypeScript, it is possible to define the type of the data in the storage.

let storage = await Wappsto.wappStorage<string>();

Data Manipulation:

  • Single or multiple data entries can be stored in the Wapp Storage.
// Set new data into the store
await storage.set('key', 'item');
await storage.set({key2: 'item 2', key3: 'item 3'});
  • Multiple keys can be retrieved or removed simultaneously, providing efficient data management capabilities.
// Get data from the store
let data = storage.get('key');
let data2 = storage.get(['key2', 'key3']);
// Remove data from the store
await store.remove('key1');
await store.remove(['key2', 'key3']);

Features:

  • Data can be reloaded from the server using the reload function.
// Reload the data from the server
await storage.reload();
  • Data can be deleted by using the reset function.
// Delete all the saved data
await storage.reset();
  • Callbacks can be registered to receive notifications when the storage is updated.
// Signal when storage is changed
storage.onChange(() => {
    console.log('Storage is updated');
});

Background Storage:

  • Secret information can specifically be stored in the background part of the wapp, ensuring its confidentiality.

  • This secret data is exclusively accessible within the background wapp and remains isolated from the frontend part of the application.

await storage.setSecret('key', 'item');
const secret_data = storage.getSecret('key', 'item');
await storage.removeSecret('key', 'item');

OAuth

To get an already created OAuth token, call the getToken with the name of the oauth. If there is no token generated yet, a Request Handler needs to be supplied, so that the library can call it with the url that the user needs to visit, in order to generate the OAuth Token.

try {
    let token = await Wappsto.OAuth.getToken('oauth name', (url) => {
        console.log(`Please visit ${url} to generate the OAuth token`);
    });
    console.log('OAuth Token', token);
} catch (e) {
    console.log('Failed to get OAuth token', e);
}

Notification

It is possible to send custom notification to the main Wappsto.com dashboard.

await Wappsto.notify('This is a custom notification from my Wapp');

You can also send an email to your self by calling sendMail.

await Wappsto.sendMail({
    subject: 'Mail with information',
    body: '<h1>Hello from the Wapp</h1><p>Here is some more information</p>',
    from: 'My New Wapp',
});

It is also possible to send a SMS to your self, if your phone number is verified.

await Wappsto.sendSMS('Simple SMS from your Wapp');

Ontology

To build a relationship graph of your data, you need to define the ontology.

To add a relationship between two objects, you call createEdge with the relationship that you want to define.

const edge = await startNode.createEdge({ relationship: 'child', to: endNode });

You can also supply more information about the edge.

const fullEdge = await startNode.createEdge({
    relationship: 'child',
    to: endNode,
    name: 'My Edge',
    description: 'This is my first edge',
    data: { msg: 'My own data for this edge' },
});

If you need a virtual object to create an edge to or from, you can create a new node. To create a node in the graph, you call the createNode function.

const startNode = await Wappsto.createNode('start node');

When you have created your ontology, you can transverse the graph and find 'leafs' in the graph. You need to define a path to follow. There are 3 special characters in the path, that have special meaning. . is used to separate each element in the path. * is used as matching any relationship. | is used as an 'or' between relationships.

const leafs = node.transverse('*.contains|child.*');

When calling transverse the result is the leaf at the end of the path. But if you want all the nodes that are found along the path, you can set 'getAll' to true when calling transverse.

const allNodes = node.transverse('*.contains|child.*', true);

To load all the ontology edges from an object, you can call getAllEdges.

const edges = await node.getAllEdges();

To the models that the edge points to is available in the models attribute.

edges.forEach((edge) => {
	edge.models.forEach((model) => {
		console.log(`The edge points to ${model.name}`);
	});
});

The edges are only loaded once, so if you need to refresh the edges from the backend, you need to force an update by calling getAllEdges with true.

const edges = await node.getAllEdges(true);

If not all models can be loaded that the edge points to, the failed models are stored in failedModels on the edge.

const edges = await node.getAllEdges();
console.log(edges[0].failedModels);

To remove an edge, you can just call delete on the edge, but if you want to remove the whole branch, you can call deleteBranch. This will remove all edges and nodes under this node or edge.

await startNode.deleteBranch();

Background logging

The debug log from the background wapp is enabled by default, but can be turned off by calling stopLogging.

Wappsto.stopLogging();

Raw requests

It is possible to send your own requests to wappsto by using the request object in wappsto.

let networks = await Wappsto.request.get('/network');
await Wappsto.request.post('/network', { name: 'Network Name' });

Wapp Version

To get the current version of the installed application, call getWappVersion.

const version = await Wappsto.getWappVersion();
console.log(`Current Installed Wapp Version: ${version}`);

Config

It is possible to change some of the behavior of the library using config.

Validation

It is possible to disable the validation of the input parameters, by changing it in the config. It can be 'none' or 'normal'. The default validation is 'normal'.

Wappsto.config({
    validation: 'none',
});

Stream Reconnect Count

It is possible to change from the default 10 times the stream will try to reconnect in case of connection errors.

Wappsto.config({
    reconnectCount: 3,
});

Debug information

It is possible to get a lot of extra debug information about when the library is doing by enabling debug options.

Wappsto.config({
    debug: true,
    requests: true,
    stream: true,
});
1.7.0

1 day ago

1.6.4

8 days ago

1.6.3

12 days ago

1.6.2

20 days ago

1.6.1

20 days ago

1.6.0

21 days ago

1.5.5

27 days ago

1.5.4

1 month ago

1.5.3

1 month ago

1.5.2

1 month ago

1.5.1

1 month ago

1.5.0

1 month ago

1.4.4

2 months ago

1.4.3

2 months ago

1.4.2

2 months ago

1.4.1

2 months ago

1.4.0

2 months ago

1.3.1

3 months ago

1.3.0-rc1

3 months ago

1.3.0

3 months ago

1.2.3

3 months ago

1.2.2

3 months ago

1.2.1

3 months ago

1.2.0

3 months ago

1.1.1

4 months ago

1.1.0

4 months ago

1.0.3

4 months ago

1.0.2

4 months ago

1.0.1

6 months ago

1.0.0

6 months ago

0.24.5

7 months ago

0.24.9

6 months ago

0.24.8

6 months ago

0.24.7

7 months ago

0.24.6

7 months ago

0.21.0

11 months ago

0.22.4

9 months ago

0.22.3

9 months ago

0.22.2

9 months ago

0.22.1

9 months ago

0.22.0

9 months ago

0.23.0

9 months ago

0.24.4

7 months ago

0.24.3

7 months ago

0.24.2

7 months ago

0.24.1

7 months ago

0.20.5

11 months ago

0.24.0

8 months ago

0.20.4

11 months ago

0.20.3

11 months ago

0.20.1

1 year ago

0.20.0

1 year ago

0.17.2

1 year ago

0.17.3

1 year ago

0.17.4

1 year ago

0.17.5

1 year ago

0.17.6

1 year ago

0.18.1

1 year ago

0.18.2

1 year ago

0.18.0

1 year ago

0.19.0

1 year ago

0.20.2

12 months ago

0.14.10

1 year ago

0.15.4

1 year ago

0.15.5

1 year ago

0.13.0

2 years ago

0.13.1

2 years ago

0.15.0

1 year ago

0.15.1

1 year ago

0.17.0

1 year ago

0.15.2

1 year ago

0.17.1

1 year ago

0.15.3

1 year ago

0.16.3

1 year ago

0.14.5

2 years ago

0.16.4

1 year ago

0.14.6

1 year ago

0.16.5

1 year ago

0.14.7

1 year ago

0.16.6

1 year ago

0.14.8

1 year ago

0.14.9

1 year ago

0.12.0

2 years ago

0.12.1

2 years ago

0.14.0

2 years ago

0.12.2

2 years ago

0.14.1

2 years ago

0.12.3

2 years ago

0.16.0

1 year ago

0.14.2

2 years ago

0.12.4

2 years ago

0.16.1

1 year ago

0.14.3

2 years ago

0.12.5

2 years ago

0.16.2

1 year ago

0.14.4

2 years ago

0.11.0

2 years ago

0.11.1

2 years ago

0.11.2

2 years ago

0.11.3

2 years ago

0.11.4

2 years ago

0.11.5

2 years ago

0.11.6

2 years ago

0.9.15

2 years ago

0.10.1

2 years ago

0.10.0

2 years ago

0.9.12

2 years ago

0.9.13

2 years ago

0.9.14

2 years ago

0.9.10

2 years ago

0.9.11

2 years ago

0.9.8

2 years ago

0.9.7

2 years ago

0.9.9

2 years ago

0.9.4

2 years ago

0.9.3

2 years ago

0.9.6

2 years ago

0.9.5

2 years ago

0.9.0

2 years ago

0.9.2

2 years ago

0.9.1

2 years ago

0.7.9

2 years ago

0.7.6

2 years ago

0.7.5

2 years ago

0.7.8

2 years ago

0.7.7

2 years ago

0.3.0

2 years ago

0.2.1

2 years ago

0.2.0

2 years ago

0.8.1

2 years ago

0.7.2

2 years ago

0.6.3

2 years ago

0.5.4

2 years ago

0.8.0

2 years ago

0.7.1

2 years ago

0.6.2

2 years ago

0.5.3

2 years ago

0.7.4

2 years ago

0.6.5

2 years ago

0.8.2

2 years ago

0.7.3

2 years ago

0.6.4

2 years ago

0.5.5

2 years ago

0.5.0

2 years ago

0.3.2

2 years ago

0.4.0

2 years ago

0.3.1

2 years ago

0.7.0

2 years ago

0.6.1

2 years ago

0.5.2

2 years ago

0.6.0

2 years ago

0.5.1

2 years ago

0.1.0

3 years ago