thsq v2.7.0
Description
thsq is a node.js interface for the Thingsquare REST API. Thingsquare is a software platform for IoT systems that allow connecting low-power devices over wireless networks to read or write information. The Thingsquare REST API allow developing server-side applications that operate on the data provided by the wireless devices.
The thsq module serves a dual purpose: to help develop server-side applications that work with Thingsquare device, and to help develop frontend applications that work with Thingsquare devices.
The Thingsquare native and web apps are based on the thsq module.
Installation
Server-side installation
npm install thsqExamples
Print out the platform names of all claimed devices for a user.
var thsq = require('thsq');
var token; // User API token, acquired via web app
thsq.init({ token: token }, function (devices) {
var id;
for (id in devices) {
console.log('Device with id ' + id + ' has platform ' + thsq.devicePlatform(devices[id]));
}
});Print out a message when a button is pushed on a device.
var thsq = require('thsq');
var token; // User API token, acquired via web app
thsq.on('device-updated', function (device, unique, update) {
if (update.s && update.s.button) {
console.log('Button changed, value is ' + update.s.button.value);
}
});
thsq.init({ token: token });Detect nearby devices and print out their name, if set, and platform.
var thsq = require('thsq');
thsq.on('device-nearby-seen', function (device) {
var name, platform;
name = 'unknown';
if (device.s.name) {
name = device.s.name.value;
}
platform = thsq.devicePlatform(device);
console.log('Nearby device with name ' + name + ' and platform ' + platform + ' seen');
});
thsq.init();Concepts
device: the representation of a wireless device and its variables.variables: devices have a set of variables that are either of three types,d,s, andmeta.dvariables can be set either by the device or via the API, and are pushed to the device.svariables can be set either by the device or via the API, but are not pushed to the device.metavariables can not be set by neither the device nor the via the API, and contain meta information about the device. Each variable has a value and a timestamp. The values and timestamps of variables are directly accessible via thedeviceobject. For example, the value of thesvariable callednameis accessed viadevice.s.name.valueand its timestamp asdevice.s.name.time. Variables are set with thethsq.setVariable()method.unique: each device is identified by a unique identity, contained in themeta.uniquedevice variable. Thethsqmodule uses this unique number when referring to a device.update: when thethsqmodule sees that a device has been updated, it will post the new variables in anupdateobject, which contains all the new values of the variables.user: a user account and its associated data. A user account has a login name, a set of devices, and a data structure that can be used to store user-specific application data.token: an API token. API tokens give access for a specific user and can be used as an alternative to login/password pairs for running server-side applications.Nearby device: Thingsquare devices with BLE transmitters send out a short-ranged BLE beacon that is picked up by nearby smartphones and laptops. This beacon is encrypted and changes over time. When a beacon is picked up by a smartphone or laptop, the Thingsquare system knows that the device and the smartphone or laptop are in close proximity of each other. This is typically used during installation phases, when devices are deployed.
Network device: when a Thingsquare device and a smartphone or laptop are on the same physical network (WiFi, Ethernet), the smartphone or laptop sends out an encrypted message over the network that the devices pick up. The Thingsquare system can then determine that the device and the smartphone or laptop are on the same network. This is used to prove proximity in a similar way as with BLE beacons, but can also be used for devices that does not have BLE beacon capabilities.
Theory of operation
The thsq module sets up and maintains a connection with its backend server. This lets applications receive notifications in the form of events when devices are updated, which relieves them from the need to poll the backend for updates.
The thsq module requires a user session. The application receives updates for devices that are claimed by the user account. A user account can be either identified with a login/password pair, which is typical for frontend applications, or an API token, which is typical for server-side applications. Starting the thsq module without a user account results in an anonymous user session to be created.
When running inside native app frontend, the thsq module is able to detect and interact with nearby devices. Nearby devices are detected via Bluetooth Low Energy (BLE) beacons.
API
Events
Server events
server-connected- Emitted whenthsqis connected with its backend server.server-disconnected- Emitted isthsqbecomes disconnected from its backend server. This can be used to indicate to a user that the system is currently offline.server-loading(url, method)- Emitted whenthsqis currently loading data via the REST API. Can be used to indicate to a user that the system is currently doing work. Theurlargument contains the URL that is currently being processed andmethodcontains the REST verb that is being used.server-loading-done(url, method)- Emitted whenthsqhas completed a REST API transaction. Theurlandmethodarguments are the same as for theserver-loadingevent.server-time-received(time, diff)- Emitted when we receive time information from the server. Thetimeargument contains the server's time in milliseconds since January 1, 1970 anddiffis the difference in milliseconds from the local time.
User events
logged-in- Emitted when a user account has logged in. This can be used to indicate to a user that the user is now logged in.logged-out- Emitted when a user account has logged out. This can be used to indicate to a user that the user is now logged out.
Bluetooth events
ble-enabled- Emitted when Bluetooth Low Energy (BLE) ability has been enabled. This can be used to indicate to a user that BLE is turned on and that nearby devices now can be detected.ble-disabled(ble, location)- Emitted when BLE ability has been disabled. This can be used to indicate to a user that BLE is turned off or disabled, which will make it impossible to detect nearby devices. Thebleargument istrueif BLE is turned on and thelocationargument istrueif location access is available. On most platforms, both BLE and location access is needed to be able to detect nearby devices. This can be used to display a user-friendly message telling the user to enable either BLE or location services, or both.
Device events
device-claimed(device, unique)- Emitted when a device has been claimed by the logged in user. Thedeviceargument contains the device data and theuniqueargument is the device's unique identifier.device-removed(unique)- Emitted when a device has been removed from the logged in user. The user no longer has access to the device and the device's data. Theuniqueargument is the unique identifier for the device.device-updated(device, unique, update)- Emitted when a device has been updated. Thedeviceobject contains all the current device variables, theuniqueargument is the unique identifier for the device, andupdateholds the variable values that were updated. An update may contain multiple variables being updated.
Nearby devices events
device-nearby-first-seen(device, unique, state)- Emitted when a new nearby device has been detected. Thedeviceargument contains the device information that the user has access to, theuniqueargument is the unique identifier for the device, and thestateargument is the device's current state. Thestatecan be fed into thethsq.deviceStateName()method to translate the number into a human-readable string.device-nearby-seen(device, unique, state)- Emitted when a nearby device is seen again. This is emitted periodically when a device is nearby, after thedevice-nearby-first-seenevent has been posted. The arguments are the same as for thedevice-nearby-first-seenevent.device-nearby-gone(device, unique)- Emitted when a nearby device has not been seen for a while and therefore is determined to be gone. Thedeviceargument contains the last known information about the device and theuniqueargument is the device's unique identifier.device-unknown-nearby-seen(key, platform, state)- Emitted when a nearby device that has not yet registered with the backend system is seen. Thekeyis an opaque string that the device has generated that later can be used to identify this device with adeviceobject, once the device has registered itself with the backend system. Theplatformargument is the name of the device's platform and thestateargument is the device's current state. Thestatecan be fed into thethsq.deviceStateName()method to translate the number into a human-readable string.
Methods
Initialization and exit
thsq.init([ options, ] callback)- initialize thethsqmodule. This must be done after registering event handlers. Theoptionsargument is optional and can be a combination of the options below. Thecallbackwill be called once everything is initialized and will receive the list of claimed devices by the user.The available options are:
token: the user API token (default: none)server: the address of the backend server to use for REST API calls (default:developer.thingsquare.com)frontend: the frontend ID to use for REST API calls (default:0ac48bf3-9fab-4bad-8455-e394808eda6b)exit: batch mode operation: set totrueto cause thethsqmodule exit after performing thethsq.init()methoddevicedatafilter: a regular expression that will cause only the filtered data to be transmitted across the connection, thus reducing the overall data load. This can be used in scripts that will operate only on specific variables. Example:thsq.init({ devicedatafilter: 'devicedatafilter: 's\.(lat|lng)$' })cachefile: a filename that the system will use as a cache for device data, to avoid having to load all devices anew on each invocation (only available when running on node.js)noupdates: set totrueto indicate that the script is not interested in ongoing data updates via a connected websocket to the backend
thsq.exit(callback)- wait for any ongoing transfers to complete, then exit at the first possible time.callbackis called when everything is exited.thsq.pause()- disconnect from the backend untilthsq.unpause()is called. No new device data will be received while the connection is paused.thsq.unpause()- reconnect with the backend to start receiving device data updates again.
User methods
thsq.login(username, password, callback)- login with the username provided by theusernameargument and the password provided by thepasswordargument. Thecallbackwill be called with a string that indicates the result:login-ok: the user was successfully logged in.login-fail: the user could not be logged in.
thsq.logout(callback)- log out the currently logged in user. The callback functioncallbackwill be called when the user has been logged out.thsq.userSignup(username, password, callback)- sign up a new user with the usernameusernameand passwordpassword. The callback functioncallbackis called with a string that indicates the result of the operation:signup-ok: a new user account was successfully created.signup-fail-already-exists: a user account with the same name already exists.signup-fail-no-email: the username was not an email address.
thsq.userResendConfirmationEmail(callback)- request a new user account confirmation email to be sent. The calback functioncallbackwill be called with a string that indicates the result of the operation:resend-ok: a user confirmation email was successfully sent.resend-fail: the user confirmation email could not be sent.
thsq.userSendPasswordRecoveryEmail(username, callback)- request a password recover email to be send to the user with the user nameusername. The password recover email will contain a password token that later can be used as a parameter to therecoverNewPassword()function. The callback functioncallbackwill be called with a string that indicates the result of the operation:recover-ok: a password reset email was successfully sentrecover-fail: a password reset email could not be sent.
thsq.recoverNewPassword(username, passwordtoken, newpassword, callback)- set a new password for the user account. Theusernameis the username of the user, and must match the username that previously requested the password reset, thepasswordtokenis a password token that was previously generated as a result of a call touserSendPasswordRecoveryEmail(),newpasswordis the new password, andcallbackis a callback function that gets called with a string that indicates the result of the operation:recover-ok: the new password was successfully set.recover-fail: the new password was not set.
thsq.getUser(callback)- get the user data and application data associated with the user account. The callback functioncallbackwill be called with an object that represents the user information. The user object has the following fields:login: the user login name.data: the application user data that was previously stored withstoreUserData().
thsq.storeUserData(data, callback)- store new user data for the user account. The argumentdatais an object that holds the data to be stored and the callback functioncallbackwill be called once the data has been stored, with a string that indicates the result of the operation:user-data-ok: user data was successfully stored.user-data-fail: user data could not be stored.user-fail: no user account was logged in.
thsq.createAccessToken(callback)- create a new user API access token for the logged in user. The functioncallbackwill be called with a string that contains the new access token, or an error message that indicates the result of the operation:user-token-fail: user token could not be created.
thsq.deleteAccessToken(token, callback)- delete a user API access token. Thetokenargument should be a token that was previously created withcreateAccessToken(). The callback functioncallbackwill be called with a string that indicates the result of the operation:user-token-deleted: the user API access token was successfully deleted.user-token-fail: the user API access token could not be deleted.
Device methods
thsq.claimDevice(unique, callback)- claim a device for the logged in user account. This makes the device available for this user only. Any updated for the device will be received asdevice-updatedevents. Theuniqueargument is the unique device identifier andcallbackis a callback function that is called to indicate the result of the operation:- device ID: the device was successfully claimed
claim-fail: the device could not be claimedclaim-fail-no-auth: bad user account
thsq.removeDevice(unique, callback)- remove the device from the user account. The user will no longer have access to its data. Theuniqueargument is the device's unique identifier andcallbackis a callback function that gets called with a string that indicates the result of the operation:delete-ok: the device was successfully removeddelete-fail: the device could not be removed
thsq.shareDevice(unique, username, callback)- share the device with another user. This makes the device also be available for the other user. Theuniqueargument is the device's unique identifier, theusernameargument is the username of the user with which the device should be shared, andcallbackis a callback function that is called with a string that indicates the result of the operation:add-user-ok: the new user was successfully addedclaim-fail: the new user could not be added
thsq.getVariable(unique, type, variable, callback)- get the current value of a specified variable. Theuniqueargument is the unique identifier for the device, thetypecan be eitherd,s, ormeta, andvariableis the name of the variable. Thecallbackfunction will be called with an object that contains thevalueandtimefor the variable, orundefinedif the variable does not exist.thsq.getVariableValue(unique, type, variable, fallback, callback)- get the current value of a specified variable, and with a fallback if the value is not defined. Theuniqueargument is the unique identifier for the device, thetypecan be eitherd,s, ormeta, andvariableis the name of the variable. Thecallbackfunction will be called with two arguments: the first is the value of the variable, orfallbackif the value is not defined, and the second is an object that contains thevalueandtimefor the variable, orundefinedif the variable does not exist.thsq.getVariableStringValue(unique, type, variable, fallback, callback)- likethsq.getVariableValue(), except the value will be provided as a string.thsq.getVariableNumberValue(unique, type, variable, fallback, callback)- likethsq.getVariableValue(), except the value will be provided as a number. If the value wasNaN, thefallbackvalue is provided instead.thsq.getVariableBufferValue(unique, type, variable, fallback, callback)- likethsq.getVariableValue(), except the value will be provided as aBufferobject. (Node.js version only.)thsq.setVariable(unique, type, variable, value, [options,] callback)- set a device variable. Theuniqueargument is the unique identifier for the device, thetypecan be eitherdors,variableis the name of the variable, andvalueis the value of the variable. The optionaloptionsargument can take one{ timestamp: timestamp }value, which sets a specific timestamp, in seconds since the Unix epoch, for the variable. Thecallbackfunction will be called with an object that indicates the result of the operation:device-ok: the variable could be setdevice-fail: the variable could not be set
thsq.deleteVariable(unique, type, variable, callback)- delete a device variable. Theuniqueargument is the unique identifier for the device, thetypecan be eitherdors, andvariableis the name of the variable. Thecallbackfunction will be called with an object that indicates the result of the operation:delete-ok: the variable could be deleteddelete-fail: the variable could not be deleted
thsq.getVariableHistory(unique, type, variable, [options,] callback)- get the variable history for a specific variable. Theuniqueargument is the unique identifier for the device, thetypecan be eitherd,s, ormeta, andvariableis the name of the variable. The optionaloptionsargument is used to delimit the number of history items to be returned, per below. Thecallbackfunction will be called with an array of items from the data history, with each item as per below.The
optionsargument determines what history elements are returned. They are either returned starting from a given timestamp, or starting from a given logical history element id. If no timestamp or element id is provided, the most recent history is returned.startid: request history elements from given idstarttime: request history elements from given timestampnum: the number of elements to request in total (default:1000)chunksize: the maximum number of elements to retrieve per request (default:1000)progress(num): a callback function that gets called on each request, with thenumargument being the number of history elements read so far (default: none).
The callback will receive an array with objects with the following properties:
value: the value of the data object. For binary data, this is a Javascript object with the fieldtypeset to the stringBuffer. In this case, thedatafield will be an array of integers that represent the binary data value.time: the timestamp of the value
thsq.addHistoryListener(unique, type, name, options, listener)- adds a history listener for the given device and variable. This function returns a unique history listener id that can later be used to remove the history listener viathsq.clearHistoryListener().The
optionsparameter is the same as forthsq.getVariableHistory().The listener will receive the following parameters:
device: the device objectunique: the device uniquetype: the variable type e.g.svariable: the variable name e.g.buttondata: the variable value object containing time and variable value
thsq.clearHistoryListener(id)- clears a previously registered history listener.thsq.clearAllHistoryListeners()- clears all previously registered history listeners.thsq.sendCommand(unique, command, [options,] callback)- send a command to the device identified byunique. Thecommandis a string that will be sent to the device. Commands are sent in a best-effort fashion and there is no guarantee that it will be received by the device. The callback functioncallbackwill be called with a string that indicates the result of the API call, but does not indicate anything regarding the command propagation itself.Possible results for the
callbackfunction are:device-ok: command was sent towards the devicedevice-fail: the command could not be sent
The possible
optionsare:sendcallback: a function that gets called when the command has been sent towards the devicesendtimeout: the number of milliseconds to wait before giving up on the send callback (default 60000 ms)sendtimeoutcallback: a function that gets called when thesendtimeoutoccursackedcallback: a function that gets called when the command has been acked by the deviceackedtimeout: the number of milliseconds to wait after the command has been sent before giving up on the acked callback (default 10000 ms)ackedtimeoutcallback: a function that gets called when theackedtimeoutoccurspriority: the priority of the command (0: default urgency,1: urgent,2: most urgent)lifetime: the time, in milliseconds, that the command should be on the command queue before being removed
thsq.getDevice(unique, callback)- get the device object for the unique identifierunique. The callback functioncallbackwill be called with the device object, orundefinedif the device does not exist.thsq.getDevicelist(callback)- get all devices claimed by the user. The callback functioncallbackwill receive a Javascript object that is indexed by defice ID and where each item represents a device.thsq.deviceStateName(state)- returns a human-readable string representation of a nearby device's state, as given by thestateargument that has previously been received received via thedevice-nearby-seen,device-nearby-first-seen, anddevice-unknown-nearby-seenevents.thsq.deviceEUI(device)- returns the EUI for the device in thedeviceargument.thsq.deviceId(unique, callback)- receives the device ID for the device with unique provided in theuniqueargument as a callback to thecallback(deviceiq)function.thsq.devicePlatform(device)- returns the platform for the device in thedeviceargument.
Firmware update methods
thsq.getFirmwarelist(callback)- retreive the list of firmware updates that the user has access to. The callback functioncallbackwill be called with an array that contains items with the following structure:name: the filename of the firmware file, which later can be used with theupdateFirmware()method.version: the version of the firmware file.platform: the platform that the firmware file was compiled for.info: a free-form information object contained in the firmware update's corresponding.jsonfile. This information object typically includes:fromversion: a string with information about what firmware versions that can load this firmware updateplatforms: an object that contains:from: an array of strings of platform names that can load this firmware updateto: an object that describes what this firmware update contains:platform: the platform name that the device will have after the update.name: a human-readable representation of the platform namepower: the power configuration (highorlow) of this firmware update
warning: a string with a warning message to be displayed to a user, if anyexperimental: a boolean value that indicates if this is an experimental version or not
thsq.startFirmwareUpdate(unique, firmwarename, callback)- start a firmware update on the device identified by theuniqueargument and with the firmware update file namefirmwarename, which was previously retrieved from the list of available firmware updates ingetFirmwarelist(). The callback functioncallbackwill receive a string that indicates the status of the operation:device-ok: the firmware update was successfully startedfirmware-fail: the firmware update could not be started
thsq.stopFirmwareUpdate(unique, callback)- stop an ongoing firmware update for the device identified byunique. The callback functioncallbackwill receive a string that indicates the status of the operation:delete-ok: the firmware update was successfully stoppeddelete-fail: the firmware update could not be stopped
Nearby and network device methods
thsq.getNetworkDevices(callback)- get a list of devices that are in the same network as the user. The callback functioncallbackwill receive a Javascript object which is indexed by device IDs and where each item is a device object.thsq.scanBLE(milliseconds)- tell the underlying OS to listen for BLE devices formillisecondsmilliseconds. This must be called repeatedly to receive notifications of nearby devices (device-nearby-first-seen,device-nearby-seen,device-unknown-nearby-seenevents).
Status methods
thsq.sendPing()- send a ping to the backend to check that the backend connection is alive.
Convenience functions
thsq.deviceunique(device)- get the device unique of a device.thsq.isacked(variable)- returns true if a variable is acked by the device.thsq.valuestring(value)- convert a variable value to string value.thsq.valuebuffer(val)- convert a variable value to a Buffer value.thsq.valuecbor(val)- parse a CBOR Buffer value to a Javascript object.thsq.devicename(device)- get the device name of a device.thsq.deviceidstring(devicedata, str)- get the substringstrfrom thes.idvariable of a device.thsq.devicefwver(devicedata)- get the device firmware timestamp version of a device.thsq.devicefwid(devicedata)- get the device SDK version ID of a device.thsq.devicefreq(devicedata)- get the device frequency mode (fccoretsi) of a device.thsq.devicepower(devicedata)- get the device power mode (loworhigh) of a device.thsq.variablevalue(device, type, name, fallback)- get the variable value for a device variable with typetypeand namename. If the variable does not exist, returnfallback.thsq.variablevaluestring(device, type, name, fallback): - get the variable value as a string for a device variable with typetypeand namename. If the variable does not exist, returnfallback.thsq.variablevaluebuffer(device, type, name, fallback): - get the variable value as a Buffer for a device variable with typetypeand namename. If the variable does not exist, returnfallback.thsq.variablevaluecbor(device, type, name, fallback): - get the variable value as a Javascript object decoded from the CBOR (https://datatracker.ietf.org/doc/html/rfc8949) value in the device variable with typetypeand namename. If the variable is undefined, returnfallbackinstead.thsq.variablevaluejson(device, type, name, fallback): - get the variable value as a Javascript object decoded from the JSON value in the device variable with typetypeand namename. If the variable is undefined, or if the variable contains unparseable JSON, returnfallbackinstead.thsq.variableage(device, type, name, fallback)- get the variable age for a device variable with typetypeand namename. If the variable does not exist, returnfallback.thsq.variabletime(device, type, name, fallback)- get the variable time for a device variable with typetypeand namename. If the variable does not exist, returnfallback.thsq.ackedvariablevalue(device, type, name, fallback)- get the acknowledged variable (most recently known value) value for a device variable with typetypeand namename. If the variable does not exist, returnfallback.thsq.ackedvariablevaluebuffer(device, type, name, fallback): - get the acknowledged variable (most recently known value) value as a Buffer for a device variable with typetypeand namename. If the variable does not exist, returnfallback.thsq.ackedvariablevaluestring(device, type, name, fallback): - get the acknowledged variable (most recently known value) value as a string for a device variable with typetypeand namename. If the variable does not exist, returnfallback.thsq.variablevaluestringnum(device, type, name, num, fallback): - get thenumth value of a comma separated variable, as a string.thsq.variablevaluenumbernum(device, type, name, num, fallback)- get thenumth value of a comma separated variable, as a Number.thsq.currentmode(device): - get the current device mode (master,feather, ordeadleaf) for a device. Returns an array with 0 being the mode and 1 being the wake-up rate, if the mode isdeadleaf.thsq.reachable(device)- get the number of milliseconds until the device will be reachable, in case of adeadleafdevice.
Position functions
Each device may have a position. A position is a { lat: lat, lng: lng } object that is set with the thsq.setdeviceposition() method. The position is retreieved with the thsq.deviceposition() method. If the device has no position, the method returns undefined.
Positions may be locked in place with the thsq.lockdeviceposition() method and unlocked with the thsq.unlockdeviceposition() method. When a device's position is locked, thsq.setdeviceposition() will refuse to update its position. The thsq.devicepositionlocked() method checks if a device's position is locked.
The position is stored as a JSON string in the device's s.position variable.
Example code:
thsq.getDevice(unique, function (device) {
if (thsq.deviceposition(device)) {
let position = thsq.deviceposition(device);
console.log('The device ' + thsq.devicename(device) + ' has position latitude ' + position.lat + ' and longitude ' + position.lng);
} else {
console.log('The device ' + thsq.devicename(device) + ' has no position);
}
if (thsq.devicepositionlocked(device)) {
console.log('The position of the device ' + thsq.devicename(device) + ' is locked in place');
// This will not have any effect on the device's position here, because it is locked:
thsq.setdeviceposition(device, { lat: 59.3293, lng: 18.0686 });
}
});thsq.deviceposition(device)- get the position of the devicedevice, if it has one. The position is returned as a{ lat: lat, lng: lng }object.thsq.setdeviceposition(device, position, callback)- set the position of the devicedevice, wherepositionis a{ lat: lat, lng: lng }object. Thecallbackis called when the position is set.thsq.lockdeviceposition(device, callback)- lock the position of the devicedevice. Thecallbackis called when the position is locked.thsq.unlockdeviceposition(device, callback)- unlock the position of the devicedevice. Thecallbackis called when the position is unlocked.thsq.devicepositionlocked(device)- returnstrueif the position of the devicedeviceis locked. Returnsfalseif the position is unlocked, or if the device has no position.
Time methods
thsq.getServerTimeOffset()- returns the current estimate of the time difference between us and the server, in milliseconds. Useful for notifying the user if there is a significant difference, which can indicate an erroneous time setting either on the local machine or on the server.thsq.getServerTime()- gets the current time that the server has. This is computed by adding the most recent time offset from the server to our local time. This is useful for computing time deltas from data that is timestamped by the server.
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
8 years ago