"use strict";
/** @module */
module.exports = WirelessTag;
var util = require('util'),
EventEmitter = require('events'),
u = require('./util'),
OperationIncompleteError = require('./error/OperationIncompleteError'),
RetryUnsuccessfulError = require('./error/RetryUnsuccessfulError'),
WirelessTagSensor = require('./sensor'),
kumostat = require('./kumostat');
const roTagProps = ["uuid", "slaveId", "tagType", "alive", "rev"];
const rwTagProps = ["name",
["updateInterval", "postBackInterval"],
["lowPowerMode", "rssiMode"]
];
/**
* @const {number} - the minimum amount of time in milliseconds to wait
* between consecutively fetching a new update from the cloud
* @default
*/
const MIN_UPDATE_LOOP_WAIT = 3000; // minimum wait time between loops
/**
* @const {number} - the maximum amount of time in milliseconds to wait
* between consecutively fetching a new update from the cloud
* @default
*/
const MAX_UPDATE_LOOP_WAIT = 1800000; // maximum wait time between loops - 30min
/**
* @const {number} - the typical delay (in milliseconds) between the time
* a tag's data record is from (see [lastUpdated()]{@link WirelessTag#lastUpdated}),
* and the time it shows up in the cloud.
* @default
*/
const CLOUD_DATA_DELAY = 55000; // delay with which data shows up in cloud
/**
* @const {object} - Default options for [retrying updates]{@link WirelessTag#retryUpdateUntil}
* from the cloud.
* @default
*/
const DEFAULT_RETRY_OPTIONS = {
minTimeout: MIN_UPDATE_LOOP_WAIT,
maxTimeout: MAX_UPDATE_LOOP_WAIT,
retries: 4
};
/**
* The cloud instance of a Wireless Tag. One {@link WirelessTagManager}
* can manage multiple Wireless Tags. A user will not normally need to
* create instances directly; instead they are found, and created by
* {@link WirelessTagManager#discoverTags}.
*
* @param {WirelessTagManager} tagManager - the tag manager instance that
* discovered this tag
* @param {Object} tagData - the object comprising the tag's status
* properties, as returned by the API endpoint.
*
* @class
* @alias WirelessTag
*
* @property {string} uuid - unique identifier for the tag (r/o)
* @property {number} slaveId - number enumerating all tags associated with
* a tag manager, thus only unique for a tag manager (r/0)
* @property {string} name - the (user-assigned) name of the tag (r/w)
* @property {number} tagType - a numeric code identifying the type of the
* tag (r/o)
* @property {boolean} alive - whether the tag is "alive" (r/o)
* @property {number} rev - a numeric code identifying the hardware
* revision of a tag (r/o)
* @property {number} updateInterval - the interval in seconds at which
* the tag should update the cloud with the latest data (r/w)
* (when setting the value, the change must be put into effect
* by {@link WirelessTag#setUpdateInterval})
* @property {boolean} lowPowerMode - whether or not the tag is in low
* power receiving mode (low power mode can considerably prolong
* battery life, at the expense of needing longer to respond
* to the tag manager) (when setting the value, the change must
* be put into effect by {@link WirelessTag#setLowPowerMode})
*/
function WirelessTag(tagManager, tagData) {
EventEmitter.call(this);
/** @member {WirelessTagManager} */
this.wirelessTagManager = tagManager;
this.errorHandler =
tagManager ? tagManager.errorHandler : u.defaultHandler;
if (tagManager && tagManager.wirelessTagPlatform) {
let platform = tagManager.wirelessTagPlatform;
/** @member {function} - see {@link WirelessTagPlatform.callAPI} */
this.callAPI = platform.callAPI;
this.log = platform.log;
}
u.defineOnChangeProperty(this, 'data', 'data');
roTagProps.forEach((p) => u.defineLinkedProperty(this, p, 'data', true));
rwTagProps.forEach((p) => u.defineLinkedProperty(this, p, 'data', false));
/** @member {object} - the JSON object returned by the cloud API */
this.data = tagData || {};
if (this.isKumostat()) kumostat(this);
this.on('_data', this.bounceUpdateLoop.bind(this));
}
util.inherits(WirelessTag, EventEmitter);
/**
* Obtains the list of sensor types supported by this tag, such as `light`,
* `humidity`, `temp` (for temperature), etc.
*
* @returns {string[]}
*/
WirelessTag.prototype.sensorCapabilities = function() {
var capabilities = [];
for (let propName in this) {
if (propName.startsWith("has") && propName.endsWith("Sensor")) {
let propValue = this[propName];
if ((('function' === typeof propValue) && propValue.call(this))
|| (('function' !== typeof propValue) && propValue)) {
capabilities.push(propName.
replace(/has(\w+)Sensor/, '$1').
toLowerCase());
}
}
}
return capabilities;
};
/**
* Obtains list of positive capabilities (canXXX()) and facts (isYYY(),
* hasZZZ()). Does not include sensor capabilities, see
* {@link WirelessTag#sensorCapabilities} for that.
*
* @returns {string[]}
*/
WirelessTag.prototype.hardwareFacts = function() {
let facts = [];
for (let cap in this) {
if (cap.startsWith("can")
|| cap.startsWith("is")
|| (cap.startsWith("has") && !cap.endsWith("Sensor"))) {
if (cap === "isHTU") continue; // deprecated, hence skip
let propValue = this[cap];
if ((('function' === typeof propValue) && propValue.call(this))
|| (('function' !== typeof propValue) && propValue)) {
facts.push(cap);
}
}
}
return facts;
};
/**
* Invokes the given action on each previously created sensor object, and
* returns the results as an array. The default action if none is specified
* simply returns the sensor object, and hence results in the list of
* (previously created) sensor objects.
*
* Note that this method will not create or initialize sensor objects that
* would be supported by the tag but have not been
* [initialized]{@link WirelessTag#initializeSensor} or
* [created]{@link WirelessTag#createSensor} yet.
*
* @param {function} action - the function to invoke for each sensor object
* @returns {Array} the results of each invocation
*/
WirelessTag.prototype.eachSensor = function(action) {
if (action === undefined) action = (s) => s;
let capabilities = this.sensorCapabilities();
let retVals = [];
for (let cap of capabilities) {
let sensor = this[cap + "Sensor"];
if (sensor) retVals.push(action(sensor));
}
return retVals;
};
/** The version of the tag as a string. */
WirelessTag.prototype.version = function() {
switch (this.data.version1) {
case 2:
switch (this.rev) {
case 14: return "2.1";
case 15: return "2.2";
case 31: return "2.3";
case 32: return "2.4";
}
if (this.rev > 32) return "2.5";
return "2.0";
case 3:
return "3.0";
case 4:
return "4.0";
}
// future proof with a default
if (this.data.version1 >= 5) return this.data.version1.toFixed(1);
// at this point version1 == 1 or undefined (which we'll take as 1 too)
if (this.tagType === 12) {
switch (this.rev) {
case 0: return "1.1";
case 1: return "1.2";
case 11: return "1.3";
case 12: return "1.4";
case 13: return "1.5";
}
}
return this.data.version1 ? this.data.version1.toFixed(1) : "1.0";
};
/** Whether the tag has a motion sensor. */
WirelessTag.prototype.hasMotionSensor = function() {
if ((this.rev & 0x0F) === 0x0E && this.rev >= 0x4E) return false;
return (this.tagType === 12 || this.tagType === 13 || this.tagType === 21);
};
/** Whether the tag has a light sensor. */
WirelessTag.prototype.hasLightSensor = function() {
return (this.tagType === 26);
};
/** Whether the tag has a moisture sensor. */
WirelessTag.prototype.hasMoistureSensor = function() {
if (this.isOutdoorTag() && (this.rev & 0x0F) === 0x0E) return true;
return (this.tagType === 32 || this.tagType === 33);
};
/** Whether the tag has a water sensor. */
WirelessTag.prototype.hasWaterSensor = function() {
return (this.tagType === 32 || this.tagType === 33);
};
/** Whether the tag has a Reed sensor. */
WirelessTag.prototype.hasReedSensor = function() {
return (this.tagType === 52 || this.tagType === 53);
};
/** Whether the tag has a PIR (motion) sensor. */
WirelessTag.prototype.hasPIRSensor = function() {
return (this.tagType === 72);
};
/**
* Whether the tag has an 'event' sensor. This is a virtual rather than a
* physical sensor. Events include 'Moved', 'Opened', etc, and are reported
* by tags with motion, light, Reed, and PIR sensors.
*/
WirelessTag.prototype.hasEventSensor = function() {
return (this.hasMotionSensor()
|| this.hasLightSensor()
|| this.hasReedSensor()
|| this.hasPIRSensor());
};
/** Whether the tag has a humidity sensor. */
WirelessTag.prototype.hasHumiditySensor = function() {
if (this.isOutdoorTag()) return false;
if ((this.rev & 0x0F) === 0x0D && this.rev >= 0x4D) return false;
return this.canHighPrecTemp();
};
/** Whether the tag has a temperature sensor. */
WirelessTag.prototype.hasTempSensor = function() {
return !(this.tagType === 82 || this.tagType === 92);
};
/**
* Whether the tag has an secondary temperature sensor available concurrent
* with a primary one.
*
* For tags that can measure temperature in different ways but for which one
* temperature sensor stands in for the other (which currently includes the
* GE Protimeter model of the Outdoor tags), this will return `false`.
*/
WirelessTag.prototype.hasSecondaryTempSensor = function() {
return (this.isOutdoorTag() && (this.rev & 0x0F) === 0x0F);
};
/**
* Whether the tag has a current sensor.
*
* Note that the current sensor tag has been discontinued, so this will now
* always return false.
*/
WirelessTag.prototype.hasCurrentSensor = function() {
// This used to be tagType 42, but apparently current sensor has been
// discontinued. Unfortunately tagType 42 is now used for a different
// type of sensor, namely Outdoor Temperature & Humidity, so we have
// currently no way of detecting whether a current sensor is back or not.
return false;
};
/**
* Whether the tag tracks and reports out of range status. (All non-virtual
* tags do.)
*/
WirelessTag.prototype.hasOutOfRangeSensor = function() {
return this.isPhysicalTag();
};
/**
* Whether the tag reports battery charge status. (All non-virtual tags do.)
*/
WirelessTag.prototype.hasBatterySensor = function() {
return this.isPhysicalTag();
};
/**
* Whether the tag reports the signal strength from the tag manager. (All
* non-virtual tags do.)
*/
WirelessTag.prototype.hasSignalSensor = function() {
return this.isPhysicalTag();
};
/** Whether the tag's motion sensor is an accelerometer. */
WirelessTag.prototype.hasAccelerometer = function() {
return this.hasMotionSensor() && ((this.rev & 0x0F) === 0x0A);
};
/**
* Whether an external probe for measuring temperature can be connected to
* the tag.
*/
WirelessTag.prototype.canExternalTempProbe = function() {
return this.hasReedSensor() || this.isOutdoorTag();
};
/** Whether the tag's motion sensor can time out. */
WirelessTag.prototype.canMotionTimeout = function() {
return this.hasMotionSensor()
&& this.rev >= 14
&& (this.tagType !== 12 || this.rev !== 15);
};
/** Whether the tag can beep. */
WirelessTag.prototype.canBeep = function() {
return this.tagType === 13
|| this.tagType === 12
|| this.tagType === 21
|| this.tagType === 26;
};
/** Whether the tag can play back data recorded while offline. */
WirelessTag.prototype.canPlayback = function() {
return (this.tagType === 21);
};
/** Whether the tag's temperature sensor is high-precision (> 8-bit). */
WirelessTag.prototype.canHighPrecTemp = function() {
return this.tagType === 13 || this.tagType === 21 // motion && type != 12
|| this.tagType === 52 // reed && type != 53
|| this.tagType === 26 // ambient light
|| this.tagType === 72 // PIR
|| this.tagType === 42 // outdoor tag w/ probe
|| this.isKumostat()
;
};
/**
* Whether the tag's temperature sensor is high-precision (> 8-bit).
*
* @deprecated since v0.7.x, use [canHighPrecTemp()]{@link WirelessTag#canHighPrecTemp} instead
*/
WirelessTag.prototype.isHTU = function() {
if (this.log) {
let log = this.log.warn || this.log.info;
log(__filename.replace(__dirname, "").substring(1)
+ ": tag.isHTU() is deprecated, use tag.canHighPrecTemp() instead");
}
return this.canHighPrecTemp();
};
/** Whether the tag object represents a physical rather than a virtual tag. */
WirelessTag.prototype.isPhysicalTag = function() {
return ! (this.isKumostat()
|| this.isNest()
|| this.isWeMo()
|| this.isCamera());
};
/**
* Whether the tag is of the Outdoor series.
*
* Tag models of the Outdoor series use external probes for temperature
* and/or humidity, and feature a water and dustproof enclosure.
*/
WirelessTag.prototype.isOutdoorTag = function() {
return this.tagType === 42;
};
/**
* Whether the tag is (i.e., requires) an external temperature probe.
*
* In contrast to [canExternalTempProbe]{@link WirelessTag#canExternalTempProbe},
* if `true` there is no alternative for measuring temperature.
*/
WirelessTag.prototype.isExternalTempProbe = function() {
return this.isOutdoorTag() && ((this.rev & 0x0F) === 0x0D);
};
/** Whether the tag object represents a linked thermostat. */
WirelessTag.prototype.isKumostat = function() {
return (this.tagType === 62);
};
/** Whether the tag object represents a Nest thermostat. */
WirelessTag.prototype.isNest = function() {
return (this.data.thermostat !== null
&& this.data.thermostat.nest_id !== null);
};
/** Whether the tag object represents WeMo lights. */
WirelessTag.prototype.isWeMo = function() {
return (this.tagType === 82);
};
/** Whether the tag object represents a WeMo LED. */
WirelessTag.prototype.isWeMoLED = function() {
return (this.isWeMo() && (this.data.cap > 0));
};
/** Whether the tab object represents a Dropcam camera. */
WirelessTag.prototype.isCamera = function() {
return (this.tagType === 92);
};
/**
* When the tag last updated the cloud with its latest data.
*
* @returns {Date}
*/
WirelessTag.prototype.lastUpdated = function() {
return new Date(u.FILETIMEtoDate(this.data.lastComm));
};
/**
* Discovers the [sensor objects]{@ink WirelessTagSensor} supported by this
* tag. More specifically, for each [sensor capability]{@link WirelessTag#sensorCapabilities}
* of the tag, [initializes]{@link WirelessTag#initializeSensor} the sensor
* object.
*
* Emits a `discover` event for each newly created sensor object once it
* completes initialization. For previously created sensor objects no
* `discover` event will be emitted.
*
* @returns {Promise} Resolves to a list of initialized {@link WirelessTagSensor}
* objects, representing the sensors supported by this tag.
*/
WirelessTag.prototype.discoverSensors = function() {
let proms = [];
this.sensorCapabilities().forEach(
(sensorType) => proms.push(this.initializeSensor(sensorType))
);
return Promise.all(proms);
};
/**
* Obtains the [sensor object]{@link WirelessTagSensor} of the given type
* for this tag. If the sensor object hasn't been created yet for this tag,
* creates and initializes it.
*
* Note that before completion of initialization, the sensor's configuration
* will not be loaded, and hence actions depending on it will not work
* correctly. This includes, for example, reading the temperature in the
* configured unit (°C/°F), because the unit is part of the sensor
* configuration.
*
* Emits a `discover` event for a newly created sensor object once it
* completes initialization.
*
* @param {string} sensorType - the type of the sensor for which to initialize
* the sensor object
* @returns {Promise} Resolves to the sensor object. If it was newly created,
* resolves once initialization of the sensor object completes.
*/
WirelessTag.prototype.initializeSensor = function(sensorType) {
let sensorProp = sensorType + 'Sensor';
if (this[sensorProp]) return Promise.resolve(this[sensorProp]);
let sensor = this.createSensor(sensorType);
// asynchronously populate the sensor's monitoring config, and issue
// the 'discover' event only when that completes (successfully or not)
return sensor.monitoringConfig().update().then(
() => {
this.emit('discover', sensor);
return sensor;
},
(error) => {
this.emit('discover', sensor);
this.errorHandler()(error);
}
);
};
/**
* Creates (and subsequently caches) a [sensor object]{@link WirelessTagSensor}
* for the given type of sensor. A sensor object created in this way will
* subsequently be available as property `tag.zzzzSensor`, where `zzzz` is the
* type of the sensor.
*
* Note that no further initialization involving the cloud API is performed,
* and so this method behaves mostly as a factory. Specifically this means
* that the returned sensor object will not have its sensor configuration data
* loaded.
*
* @returns {WirelessTagSensor}
*/
WirelessTag.prototype.createSensor = function(sensorType) {
if (this.sensorCapabilities().indexOf(sensorType) < 0) {
throw Error(`tag ${this.name} does not support ${sensorType} sensor`);
}
let sensor = new WirelessTagSensor(this, sensorType);
Object.defineProperty(this, sensorType + 'Sensor', {
enumerable: true, value: sensor
});
return sensor;
};
/**
* Updates the tag object's data from the cloud. How current the data are
* will depend on the interval at which the actual tag posts its latest
* data to the cloud (property `updateInterval`).
*
* Emits a `data` event if the update results in new data being fetched.
* Note that "new data" does not have to mean that any of the sensor-specific
* data changed (such as temperature or humidity).
*
* @param {module:wirelesstags~apiCallback} [callback]
* @returns {Promise} Resolves to this tag object once the update completes.
*/
WirelessTag.prototype.update = function(callback) {
var req = this.callAPI(
'/ethClient.asmx/GetTagForSlaveId',
{ slaveid: this.slaveId },
callback);
return req.then(
(result) => {
this.data = result;
if (callback) callback(null, { object: this });
return this;
});
};
/**
* Retries calling [update()]{@link WirelessTag#update} until it is
* considered successful.
*
* A typical use-case for this method is when as a result of invoking a
* cloud API method actuating the tag or one of its sensors the tag's
* updated data do not reflect the change even though the API call returned
* success. For exampe, an API call to arm a sensor might have succeeded, yet
* the updated data continue to show the sensor as disarmed. Most of the time
* this discrepancy will resolve itself after some time by simply continuing to
* fetch updates from the cloud until the armed status is properly reflected.
*
* This method will wait an exponentially increasing amount of time between
* consecutive retries, and by default will give up after a certain number
* of unsuccessful attempts (see parameter `options` and their defaults).
*
* Note that if `update()` rejects with anything other than a
* [OperationIncompleteError]{@link WirelessTagPlatform.OperationIncompleteError}
* it will not be retried.
*
* @param {function} success - A function evaluating whether the update is
* to be considered successful or not. It is passed the tag object
* and a number giving the attempt, and is expected to return a value
* evaluating to `true` if retries should stop. Otherwise, the
* function should throw a [RetryUnsuccessfulError]{@link WirelessTagPlatform.RetryUnsuccessfulError}.
* @param {object} [options] - options for controlling the retries, see
* [DEFAULT_RETRY_OPTIONS]{@link module:lib/tag~DEFAULT_RETRY_OPTIONS}
* for defaults.
* @param {number} [options.minTimeout] - the minimum amount of time in
* milliseconds to wait between retries
* @param {number} [options.maxTimeout] - the maximum amount of time in
* milliseconds to wait between retries
* @param {number} [options.retries] - the number of times to retry before
* giving up
* @returns {Promise} Resolves to the tag if retrying is eventually
* considered successful, and rejects with an
* [OperationIncompleteError]{@link WirelessTagPlatform.OperationIncompleteError}
* otherwise.
*/
WirelessTag.prototype.retryUpdateUntil = function(success, options) {
let successFunc = (tag, attempt) => {
if (success(tag, attempt)) return true;
// the success() function did not return truthy, convert to throw
throw new RetryUnsuccessfulError(
"retrying tag update remains unsuccessful",
tag,
"update",
attempt);
};
options = Object.assign(DEFAULT_RETRY_OPTIONS, options);
return u.retryUntil(this.update.bind(this), successFunc, options);
};
/**
* Similar to {@link WirelessTag#retryUpdateUntil}, except that the first
* attempt to [update]{@link WirelessTag#update} is made immediately,
* and `success()` is called first without the retry attempt number.
*
* Hence, if the initial call to `success()` evaluates to true, no retry
* will be made. Otherwise, retries are passed to `retryUpdateUntil()`.
*/
WirelessTag.prototype.updateUntil = function(success, retryOptions) {
return this.update().then((tag) => {
if (success(tag)) return tag;
// success function didn't return truthy, convert to throw
throw new OperationIncompleteError("update of tag deemed unsuccessful",
tag,
"update");
}).catch((e) => {
if (e instanceof OperationIncompleteError) {
return this.retryUpdateUntil(success, retryOptions);
}
throw e;
});
};
/**
* Similar to {@link WirelessTag#update}, but requests that the physical
* tag posts its current "live" data to the cloud, which will then be fetched.
*
* Therefore, to succeed this method requires a response from the tag to a
* request issued by the tag manager to which it is associated. If the tag
* is in `lowPowerMode`, response can take 5-15 seconds (or even longer).
*
* Emits a `data` event on success. Note that this does not have to mean
* that any of the sensor-specific data changed (such as temperature or
* humidity); all that may have changed could be the [lastUpdated()]{@link WirelessTag#lastUpdated}
* value.
*
* @param {module:wirelesstags~apiCallback} [callback]
* @returns {Promise} Resolves to this tag object once the update completes.
*/
WirelessTag.prototype.liveUpdate = function(callback) {
var req = this.callAPI(
'/ethClient.asmx/RequestImmediatePostback',
{ id: this.slaveId },
callback);
return req.then(
(result) => {
this.data = result;
if (callback) callback(null, { object: this });
return this;
});
};
/**
* Start auto-updating (see [update()]{@link WirelessTag#update}) this tag
* object from the cloud according to the update interval configured for
* the tag (see property `updateInterval` and
* [setUpdateIJnterval()]{@link WirelessTag#setUpdateInterval}).
*
* @param {number} [minWait] - the minimum amount of time in milliseconds to
* wait before invoking the next update call to the cloud API
* (defaults to [MIN_UPDATE_LOOP_WAIT]{@link module:lib/tag~MIN_UPDATE_LOOP_WAIT})
* @returns {number} The ID of the timer triggering the next update call.
*/
WirelessTag.prototype.startUpdateLoop = function(minWait) {
if (minWait === undefined) {
minWait = MIN_UPDATE_LOOP_WAIT;
} else if (minWait > MAX_UPDATE_LOOP_WAIT) {
minWait = MAX_UPDATE_LOOP_WAIT;
}
if (this._updateTimer) return this._updateTimer;
this._updateTimer = true; // placeholder to avoid race conditions
let action = () => {
this._updateTimer = true; // timer is done but action not yet
this.update().then(() => {
// reset wait time upon success
minWait = undefined;
}).catch((err) => {
// report the error, but don't (re)throw it
this.log.error(err.stack ? err.stack : err);
// exponentially increase time until retry
minWait *= 2;
}).then(() => {
// with the preceding catch() this is in essence a finally()
if (this._updateTimer) {
this._updateTimer = null;
this.startUpdateLoop(minWait);
}
// otherwise we have been cancelled while running the update
});
};
let timeNextExpected =
this.lastUpdated().getTime()
+ (this.updateInterval * 1000)
+ CLOUD_DATA_DELAY;
let remainingTime = timeNextExpected - Date.now();
if (remainingTime < minWait) {
remainingTime = minWait;
}
// ensure that updates weren't cancelled since we entered here
if (this._updateTimer === true) {
this._updateTimer = setTimeout(action, remainingTime);
// stop if and when we are disconnected
if (! this._disconnectHandler) {
this._disconnectHandler = this.stopUpdateLoop.bind(this);
let platform = this.wirelessTagManager.wirelessTagPlatform;
platform.on('disconnect', this._disconnectHandler);
}
}
return this._updateTimer;
};
/**
* Stops the automatic update loop for this tag if one was running. Does
* nothing otherwise.
*
* This will be called automatically if the platform object through which
* this tag object was obtained (directly or indirectly) is
* [disconnected]{@link WirelessTagPlatform#signoff}.
*/
WirelessTag.prototype.stopUpdateLoop = function() {
let timer = this._updateTimer;
this._updateTimer = null; // avoid race conditions
if (timer && timer !== true) {
clearTimeout(timer);
}
if (this._disconnectHandler) {
let platform = this.wirelessTagManager.wirelessTagPlatform;
platform.removeListener('disconnect', this._disconnectHandler);
delete this._disconnectHandler;
}
};
/**
* [Stops]{@link WirelessTag#stopUpdateLoop} and then
* [starts]{@link WirelessTag#startUpdateLoop} again the automatic update
* loop for this tag, if one was currently active. Otherwise does nothing.
*/
WirelessTag.prototype.bounceUpdateLoop = function() {
let timer = this._updateTimer;
if (timer && timer !== true) {
this.log.warn("## bouncing update timer loop for tag", this.slaveId);
this.stopUpdateLoop();
this.startUpdateLoop();
}
};
/**
* String representation of the tag and its data. Includes a reference to
* the tag manager (as `name` and `mac`), properties, time data were last
* posted to cloud, the tag's hardware facts and sensor capabilities, and
* its version.
*
* @returns {string}
*/
WirelessTag.prototype.toString = function() {
let propsObj = {
manager: {
name: this.wirelessTagManager.name,
mac: this.wirelessTagManager.mac
}
};
// all data properties except private ones
for (let propName of Object.getOwnPropertyNames(this)) {
if (! (propName.endsWith('Sensor')
|| propName.startsWith('_')
|| (propName === 'wirelessTagManager')
|| ('function' === typeof this[propName]))) {
propsObj[propName] = this[propName];
}
}
// remove domain, data, and other undesired properties picked up above
delete propsObj.data;
delete propsObj.domain;
// when tag was last updated
propsObj.lastUpdated = this.lastUpdated().toString();
// version
propsObj.version = this.version();
// list of positive capabilities (canXXX()) and facts (isYYY(), hasZZZ())
propsObj.facts = this.hardwareFacts();
// list of sensor capabilities
propsObj.sensors = this.sensorCapabilities();
return JSON.stringify(propsObj);
};
/**
* Sets the interval at which the physical tag corresponding to this tag
* object should update the cloud with its current data. Does nothing if the
* value to be set is already equal to the currently active update interval.
*
* @param {number} [value] - the new update interval in seconds; if omitted,
* the update interval will be set to the value of the
* `updateInterval` property
* @param {module:wirelesstags~apiCallback} [callback]
* @returns {Promise} Resolves to this tag object when the operation
* completes successfully. Will [retry updating]{@link WirelessTag#retryUpdateUntil}
* until the tag's data reflect the new update interval. Rejects
* with an [OperationIncompleteError]{@link WirelessTagPlatform.OperationIncompleteError}
* if this is still not the case after the
* [default number of retries]{@link module:lib/tag~DEFAULT_RETRY_OPTIONS}.
*/
WirelessTag.prototype.setUpdateInterval = function(value, callback) {
if ('function' === typeof value) {
callback = value;
value = undefined;
}
if (value === undefined) {
value = this.updateInterval;
} else if (value === this.updateInterval) {
// don't call the API if there is no change
return Promise.resolve(this);
}
if (('number' !== typeof value) || (value <= 0)) {
throw new TypeError("invalid update interval for tag " + this.name);
}
var req = this.callAPI(
'/ethClient.asmx/SetPostbackIntervalFor',
{ id: this.slaveId, sec: value },
callback);
return req.then(
(result) => {
this.data = result;
if (this.updateInterval !== value) {
// the API call itself succeeded, so this should resolve
// itself if we retry updating after a short delay
return this.retryUpdateUntil(
(tag) => tag.updateInterval === value
);
}
if (callback) callback(null, { object: this });
return this;
});
};
/**
* Turns the low power mode of the tag on or off. Does nothing if the
* requested mode is already the one that is active.
*
* Note that tags of older hardware revisions don't necessarily support a
* low power mode. Their low power mode will seem off, but an attempt to turn
* it on will result in an error.
*
* @param {boolean} [value] - whether to enable (`true`) or disable (`false`)
* low power mode; if omitted, the value of the `lowPowerMode`
* property will be used
* @param {module:wirelesstags~apiCallback} [callback]
* @returns {Promise} Resolves to this tag object when the operation
* completes successfully. Will [retry updating]{@link WirelessTag#retryUpdateUntil}
* until the tag's data reflect the requested value. Rejects
* with an [OperationIncompleteError]{@link WirelessTagPlatform.OperationIncompleteError}
* if this is still not the case after the
* [default number of retries]{@link module:lib/tag~DEFAULT_RETRY_OPTIONS}.
*/
WirelessTag.prototype.setLowPowerMode = function(value, callback) {
if ('function' === typeof value) {
callback = value;
value = undefined;
}
if (value === undefined) {
value = this.lowPowerMode;
} else if ('boolean' === typeof value) {
if (this.lowPowerMode === value) {
if (callback) callback(null, { Object: this });
return Promise.resolve(this);
}
} else {
throw new TypeError("invalid power mode value for tag " + this.name);
}
var req = this.callAPI(
'/ethClient.asmx/SetLowPowerWOR',
{ id: this.slaveId, enable: value },
callback);
return req.then(
(result) => {
this.data = result;
if (this.lowPowerMode !== value) {
// the API call itself succeeded, so this should resolve
// itself if we retry updating after a short delay
return this.retryUpdateUntil(
(tag) => tag.lowPowerMode === value
);
}
if (callback) callback(null, { object: this });
return this;
});
};