Source: modules/ptz.js

const Soap = require('../utils/soap')
const Util = require('../utils/util')

/**
 * @class
 * <p>
 * {@link https://www.onvif.org/specs/srv/ptz/ONVIF-PTZ-Service-Spec-v1712.pdf}<br>
 * {@link https://www.onvif.org/ver20/ptz/wsdl/ptz.wsdl}<br>
 * </p>
 * <h3>Functions</h3>
 * {@link Ptz#getNodes},
 * {@link Ptz#getNode},
 * {@link Ptz#getConfigurations},
 * {@link Ptz#getConfiguration},
 * {@link Ptz#getConfigurationOptions},
 * {@link Ptz#setConfiguration},
 * {@link Ptz#getCompatibleConfigurations},
 * {@link Ptz#absoluteMove},
 * {@link Ptz#relativeMove},
 * {@link Ptz#continuousMove},
 * {@link Ptz#geoMove},
 * {@link Ptz#stop},
 * {@link Ptz#getStatus},
 * {@link Ptz#getStatus},
 * {@link Ptz#setPreset},
 * {@link Ptz#getPresets},
 * {@link Ptz#gotoPreset},
 * {@link Ptz#removePreset},
 * {@link Ptz#gotoHomePosition},
 * {@link Ptz#setHomePosition},
 * {@link Ptz#sendAuxiliaryCommand}
 * <br><br>
 * <h3>Overview</h3>
 * The PTZ model groups the possible movements of the PTZ unit into a Pan/Tilt component and
 * into a Zoom component. To steer the PTZ unit, the service provides absolute move, relative
 * move and continuous move operations. Different coordinate systems and units are used to feed
 * these operations.<br>
 * The PTZ service provides an AbsoluteMove operation to move the PTZ device to an absolute
 * position. The service expects the absolute position as an argument referencing an absolute
 * coordinate system. The speed of the Pan/Tilt movement and the Zoom movement can be
 * specified optionally. Speed values are positive scalars and do not contain any directional
 * information. It is not possible to specify speeds for Pan and Tilt separately without knowledge
 * about the current position. This approach to specifying a desired position generally produces a
 * non-smooth and non-intuitive action.<br>
 * A RelativeMove operation is introduced by the PTZ service in order to steer the dome relative to
 * the current position, but without the need to know the current position. The operation expects a
 * positional translation as an argument referencing a relative coordinate system. This
 * specification distinguishes between relative and absolute coordinate systems, since there are
 * cases where no absolute coordinate system exists for a well-defined relative coordinate system.
 * An optional speed argument can be added to the RelativeMove operation with the same
 * meaning as for the AbsoluteMove operation.<br>
 * Finally, the PTZ device can be moved continuously via the ContinuousMove command in a
 * certain direction with a certain speed. Thereby, a velocity vector represents both, the direction
 * and the speed information. The latter is expressed by the length of the vector.
 * The Pan/Tilt and Zoom coordinates can be uniquely specified by augmenting the coordinates
 * with appropriate space URIs. A space URI uniquely represents the underlying coordinate system.
 * Section 5.7 defines a standard set of coordinate systems. A PTZ Node shall implement these
 * coordinate systems if the corresponding type of movement is supported by the PTZ Node. In
 * many cases, the Pan/Tilt position is represented by pan and tilt angles in a spherical coordinate
 * system. A digital PTZ, operating on a fixed megapixel camera, may express the camera’s
 * viewing direction by a pixel position on a static projection plane. Therefore, different coordinate
 * systems are needed in this case in order to capture the physical or virtual movements of the
 * PTZ device. Optionally, the PTZ Node may define its own device specific coordinate systems to
 * enable clients to take advantage of the specific properties of this PTZ Node.
 * The PTZ Node description retrieved via the GetNode or GetNodes operation contains all
 * coordinate systems supported by a specific PTZ Node. Each coordinate system belongs to one
 * of the following groups:
 * <ul>
 * <li>AbsolutePanTiltPositionSpace</li>
 * <li>RelativePanTiltTranslationSpace</li>
 * <li>ContinuousPanTiltVelocitySpace</li>
 * <li>PanTiltSpeedSpace</li>
 * <li>AbsoluteZoomPositionSpace</li>
 * <li>RelativeZoomTranslationSpace</li>
 * <li>ContinuousZoomVelocitySpace</li>
 * <li>ZoomSpeedSpace</li>
 * </ul>
 * If the PTZ node does not support the coordinate systems of a certain group, the corresponding
 * move operation will not be available for this PTZ node. For instance, if the list does not contain
 * an AbsolutePanTiltPositionSpace, the AbsoluteMove operation shall fail when an absolute
 * Pan/Tilt position is specified. The corresponding command section describes those spaces that
 * are required for a specific move command.<br>
 * <br>
 */
class Ptz {
  constructor () {
    this.soap = new Soap()
    this.timeDiff = 0
    this.serviceAddress = null
    this.username = null
    this.password = null
    this.defaultProfileToken = null

    this.namespaceAttributes = [
      'xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl"'
    ]
  }

  /**
   * Call this function directly after instantiating a Ptz object.
   * @param {number} timeDiff The onvif device's time difference.
   * @param {object} serviceAddress An url object from url package - require('url').
   * @param {string=} username Optional only if the device does NOT have a user.
   * @param {string=} password Optional only if the device does NOT have a password.
   */
  init (timeDiff, serviceAddress, username, password) {
    this.timeDiff = timeDiff
    this.serviceAddress = serviceAddress
    this.username = username
    this.password = password
  }

  /**
   * Sets the default profile token. This comes from media#getProfiles method.<br>
   * By default, this module will get the first Profile and use it as the default profile.
   * You can change the default profile by setting this function.
   * Note: This functionality is only used where API calls require a <strong>ProfileToken</strong> and one is not provided.
   * @param {string} profileToken The profileToken to use when one is not passed to a method requiring one.
   */
  setDefaultProfileToken (profileToken) {
    this.defaultProfileToken = profileToken
  }

  /**
   * Private function for creating a SOAP request.
   * @param {string} body The body of the xml.
   */
  createRequest (body) {
    const soapEnvelope = this.soap.createRequest({
      body: body,
      xmlns: this.namespaceAttributes,
      diff: this.timeDiff,
      username: this.username,
      password: this.password
    })
    return soapEnvelope
  }

  buildRequest (methodName, xml, callback) {
    const promise = new Promise((resolve, reject) => {
      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error(`The "callback" argument for ${methodName} is invalid:` + errMsg))
          return
        }
      }
      if (typeof methodName === 'undefined' || methodName === null) {
        reject(new Error('The "methodName" argument for buildRequest is required.'))
        return
      }
      else {
        if ((errMsg = Util.isInvalidValue(methodName, 'string'))) {
          reject(new Error('The "methodName" argument for buildRequest is invalid:' + errMsg))
          return
        }
      }
      let soapBody = ''
      if (typeof xml === 'undefined' || xml === null || xml === '') {
        soapBody += `<tptz:${methodName}/>`
      }
      else {
        soapBody += `<tptz:${methodName}>`
        soapBody += xml
        soapBody += `</tptz:${methodName}>`
      }
      const soapEnvelope = this.createRequest(soapBody)
      this.soap.makeRequest('ptz', this.serviceAddress, methodName, soapEnvelope)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * Used internally. Creates xml where PanTilt (x|y) and Zoom (z) are needed.
   * @param {object} vectors One of PanTilt (x,y) or Zoom (z), or both, is required.
   * @param {object=} vectors.x The x component corresponds to pan.
   * @param {object=} vectors.y The y component corresponds to tilt.
   * @param {object=} vectors.z The z component corresponds to zoom.
   */
  panTiltZoomOptions (vectors) {
    let soapBody = ''
    if (typeof vectors !== 'undefined' && vectors !== null) {
      if ('x' in vectors && 'y' in vectors) {
        soapBody += '<tt:PanTilt x="' + vectors.x + '" y="' + vectors.y + '"/>'
      }
      if ('z' in vectors) {
        soapBody += '<tt:Zoom x="' + vectors.z + '"/>'
      }
    }
    return soapBody
  }

  // ---------------------------------------------
  // PTZ API
  // ---------------------------------------------

  /**
   * A PTZ-capable device shall implement this operation and return all PTZ nodes available on the device.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  getNodes (callback) {
    return this.buildRequest('GetNodes', null, callback)
  }

  /**
   * A PTZ-capable device shall implement the GetNode operation and return the properties of the
   * requested PTZ node, if it exists. Otherwise, the device shall respond with an appropriate fault
   * message.
   * @param {*} nodeToken Reference to the requested PTZNode.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  getNode (nodeToken, callback) {
    const promise = new Promise((resolve, reject) => {
      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for getNode is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(nodeToken, 'string'))) {
        reject(new Error('The "nodeToken" argument for getNode is invalid: ' + errMsg))
        return
      }

      let soapBody = ''
      soapBody += '<tptz:NodeToken>' + nodeToken + '</tptz:NodeToken>'

      this.buildRequest('GetNode', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * The PTZConfiguration contains a reference to the PTZ node in which it belongs. This reference
   * cannot be changed by a client.<br>
   * The following elements are part of the PTZ Configuration:
   * <ul>
   * <li>PTZNodeToken – A mandatory reference to the PTZ node that the PTZ Configuration
   * belongs to.</li>
   * <li>DefaultAbsolutePanTiltPositionSpace – If the PTZ node supports absolute Pan/Tilt
   * movements, it shall specify one Absolute Pan/Tilt Position Space as default.</li>
   * <li>DefaultRelativePanTiltTranslationSpace – If the PTZ node supports relative Pan/Tilt
   * movements, it shall specify one RelativePan/Tilt Translation Space as default.</li>
   * <li>DefaultContinuousPanTiltVelocitySpace – If the PTZ node supports continuous
   * Pan/Tilt movements, it shall specify one continuous Pan/Tilt velocity space as default.</li>
   * <li>DefaultPanTiltSpeedSpace – If the PTZ node supports absolute or relative
   * movements, it shall specify one Pan/Tilt speed space as default.</li>
   * <li>DefaultAbsoluteZoomPositionSpace – If the PTZ node supports absolute zoom
   * movements, it shall specify one absolute zoom position space as default.</li>
   * <li>DefaultRelativeZoomTranslationSpace – If the PTZ node supports relative zoom
   * movements, it shall specify one relative zoom translation space as default.</li>
   * <li>DefaultContinuousZoomVelocitySpace – If the PTZ node supports continuous zoom
   * movements, it shall specify one continuous zoom velocity space as default.</li>
   * <li>DefaultPTZSpeed – If the PTZ node supports absolute or relative PTZ movements, it
   * shall specify corresponding default Pan/Tilt and Zoom speeds.</li>
   * <li>DefaultPTZTimeout – If the PTZ node supports continuous movements, it shall
   * specify a default timeout, after which the movement stops.</li>
   * <li>PanTiltLimits – The Pan/Tilt limits element should be present for a PTZ node that
   * supports an absolute Pan/Tilt. If the element is present it signals the support for
   * configurable Pan/Tilt limits. If limits are enabled, the Pan/Tilt movements shall
   * always stay within the specified range. The Pan/Tilt limits are disabled by setting the
   * limits to –INF or +INF.</li>
   * <li>ZoomLimits – The zoom limits element should be present for a PTZ node that
   * supports absolute zoom. If the element is present it signals the supports for
   * configurable zoom limits. If limits are enabled the zoom movements shall always stay
   * within the specified range. The Zoom limits are disabled by settings the limits to –INF
   * and +INF.</li>
   * <li>MoveRamp – The optional acceleration ramp used by the device when moving.</li>
   * <li>PresetRamp – The optional acceleration ramp used by the device when recalling
   * presets.</li>
   * <li>PresetTourRamp – The optional acceleration ramp used by the device when
   * executing PresetTours.</li>
   * </ul>
   * The default position/translation/velocity spaces are introduced to allow clients sending move
   * requests without the need to specify a certain coordinate system. The default speeds are
   * introduced to control the speed of move requests (absolute, relative, preset), where no explicit
   * speed has been set.<br>
   * The allowed pan and tilt range for Pan/Tilt limits is defined by a two-dimensional space range
   * that is mapped to a specific absolute Pan/Tilt position space. At least one Pan/Tilt position
   * space is required by the PTZNode to support Pan/Tilt limits. The limits apply to all supported
   * absolute, relative and continuous Pan/Tilt movements. The limits shall be checked within the
   * coordinate system for which the limits have been specified. That means that even if movements
   * are specified in a different coordinate system, the requested movements shall be transformed to
   * the coordinate system of the limits where the limits can be checked. When a relative or
   * continuous movements is specified, which would leave the specified limits, the PTZ unit has to
   * move along the specified limits. The Zoom Limits have to be interpreted accordingly.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  getConfigurations (callback) {
    return this.buildRequest('GetConfigurations', null, callback)
  }

  /**
   * A PTZ-capable device shall implement the GetConfigurationOptions operation. It returns the list
   * of supported coordinate systems including their range limitations. Therefore, the options MAY
   * differ depending on whether the PTZ configuration is assigned to a profile(see ONVIF Media
   * Service Specification) containing a VideoSourceConfiguration. In that case, the options may
   * additionally contain coordinate systems referring to the image coordinate system described by
   * the VideoSourceConfiguration. Each listed coordinate system belongs to one of the groups
   * listed in Section 4. If the PTZ node supports continuous movements, it shall return a timeout
   * range within which timeouts are accepted by the PTZ node.
   * @param {string} configurationToken Reference to the requested PTZ configuration.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  getConfiguration (configurationToken, callback) {
    const promise = new Promise((resolve, reject) => {
      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for getConfiguration is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(configurationToken, 'string'))) {
        reject(new Error('The "configurationToken" argument for getConfiguration is invalid: ' + errMsg))
        return
      }

      let soapBody = ''
      soapBody += '<tptz:PTZConfigurationToken>' + configurationToken + '</tptz:PTZConfigurationToken>'

      this.buildRequest('GetConfiguration', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * A PTZ-capable device shall implement the GetConfigurationOptions operation. It returns the list
   * of supported coordinate systems including their range limitations. Therefore, the options MAY
   * differ depending on whether the PTZ configuration is assigned to a profile(see ONVIF Media
   * Service Specification) containing a VideoSourceConfiguration. In that case, the options may
   * additionally contain coordinate systems referring to the image coordinate system described by
   * the VideoSourceConfiguration. Each listed coordinate system belongs to one of the groups
   * listed in Section 4. If the PTZ node supports continuous movements, it shall return a timeout
   * range within which timeouts are accepted by the PTZ node.
   * @param {string} configurationToken Reference to the requested PTZ configuration.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  getConfigurationOptions (configurationToken, callback) {
    const promise = new Promise((resolve, reject) => {
      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for getConfigurationOptions is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(configurationToken, 'string'))) {
        reject(new Error('The "configurationToken" argument for getConfigurationOptions is invalid: ' + errMsg))
        return
      }

      let soapBody = ''
      soapBody += '<tptz:ConfigurationToken>' + configurationToken + '</tptz:ConfigurationToken>'

      this.buildRequest('GetConfigurationOptions', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * <strong>+++ TODO: This function is incomplete and requires a LOT of work for little return.</strong><br>
   * <strong>+++ Alternatively, you can pass XML in for ptzConfigurationOptions in the desired</strong><br>
   * <strong>+++ way required by the spec, in which case the function will work.</strong><br>
   * A PTZ-capable device shall implement the SetConfiguration operation. The ForcePersistence
   * flag indicates if the changes remain after reboot of the device.
   * @param {string} configurationToken Reference to the PTZ configuration to be modified.
   * @param {xml} ptzConfigurationOptions The requested PTZ node configuration options.
   * @param {boolean} forcePersistence Deprecated modifier for temporary settings if supported by the device.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  setConfiguration (configurationToken, ptzConfigurationOptions, forcePersistence, callback) {
    const promise = new Promise((resolve, reject) => {
      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for setConfiguration is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(configurationToken, 'string'))) {
        reject(new Error('The "configurationToken" argument for setConfiguration is invalid: ' + errMsg))
        return
      }

      if ((errMsg = Util.isInvalidValue(ptzConfigurationOptions, 'object'))) {
        reject(new Error('The "ptzConfigurationOptions" argument for setConfiguration is invalid: ' + errMsg))
        return
      }

      let soapBody = ''
      soapBody += '<tptz:PTZConfigurationToken>' + configurationToken + '</tptz:PTZConfigurationToken>'
      soapBody += '<tptz:PTZConfigurationOptions>' + ptzConfigurationOptions + '</tptz:PTZConfigurationOptions>'

      this.buildRequest('SetConfiguration', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * A device signalling support for GetCompatibleConfigurations via the capability
   * GetCompatibleConfigurations shall return all available PTZConfigurations that can be added to
   * the referenced media profile through the GetComatibleConfigurations operation.<br>
   * A device providing more than one PTZConfiguration or more than one
   * VideoSourceConfiguration or which has any other resource interdependency between
   * PTZConfiguration entities and other resources listable in a media profile should implement this
   * operation. PTZConfiguration entities returned by this operation shall not fail on adding them to
   * the referenced media profile.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  getCompatibleConfigurations (profileToken, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for getCompatibleConfigurations is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for getCompatibleConfigurations is invalid: ' + errMsg))
        return
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'

      this.buildRequest('GetCompatibleConfigurations', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * If a PTZ node supports absolute Pan/Tilt or absolute Zoom movements, it shall support the
   * AbsoluteMove operation. The position argument of this command specifies the absolute position
   * to which the PTZ unit moves. It splits into an optional Pan/Tilt element and an optional Zoom
   * element. If the Pan/Tilt position is omitted, the current Pan/Tilt movement shall NOT be affected
   * by this command. The same holds for the zoom position.<br>
   * The spaces referenced within the position shall be absolute position spaces supported by the
   * PTZ node. If the space information is omitted, the corresponding default spaces of the PTZ
   * configuration, a part of the specified Media Profile, is used. A device may support absolute
   * Pan/Tilt movements, absolute Zoom movements or no absolute movements by providing only
   * absolute position spaces for the supported cases.<br>
   * An existing Speed argument overrides the DefaultSpeed of the corresponding PTZ configuration
   * during movement to the requested position. If spaces are referenced within the Speed argument,
   * they shall be Speed Spaces supported by the PTZ Node.<br>
   * The operation shall fail if the requested absolute position is not reachable.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {object} position Vector specifying the absolute target position.
   * @param {float=} position.x The x component corresponds to pan.
   * @param {float=} position.y The y component corresponds to tilt.
   * @param {float=} position.z A zoom position.
   * @param {object=} speed Speed vector specifying the velocity of pan, tilt and zoom.
   * @param {float=} speed.x The x component corresponds to pan.  If omitted in a request, the current (if any) PanTilt movement should not be affected.
   * @param {float=} speed.y The y component corresponds to tilt.  If omitted in a request, the current (if any) PanTilt movement should not be affected
   * @param {float=} speed.z A zoom speed. If omitted in a request, the current (if any) Zoom movement should not be affected.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  absoluteMove (profileToken, position, speed, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for absoluteMove is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for absoluteMove is invalid: ' + errMsg))
        return
      }
      if ((errMsg = Util.isInvalidValue(position, 'object'))) {
        reject(new Error('The "position" argument for absoluteMove is invalid: ' + errMsg))
        return
      }
      if (typeof speed !== 'undefined' && speed !== null) {
        if ((errMsg = Util.isInvalidValue(speed, 'object'))) {
          reject(new Error('The "speed" argument for absoluteMove is invalid: ' + errMsg))
          return
        }
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'
      soapBody += '<tptz:Position>' + this.panTiltZoomOptions(position) + '</tptz:Position>'
      if (typeof speed !== 'undefined' && speed !== null) {
        soapBody += '<tptz:Speed>' + this.panTiltZoomOptions(speed) + '</tptz:Speed>'
      }

      this.buildRequest('AbsoluteMove', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * If a PTZ node supports relative Pan/Tilt or relative Zoom movements, then it shall support the
   * RelativeMove operation. The translation argument of this operation specifies the difference from
   * the current position to the position to which the PTZ device is instructed to move. The operation
   * is split into an optional Pan/Tilt element and an optional Zoom element. If the Pan/Tilt element is
   * omitted, the current Pan/Tilt movement shall NOT be affected by this command. The same holds
   * for the zoom element.<br>
   * The spaces referenced within the translation element shall be translation spaces supported by
   * the PTZ node. If the space information is omitted for the translation argument, the
   * corresponding default spaces of the PTZ configuration, which is part of the specified Media
   * Profile, is used. A device may support relative Pan/Tilt movements, relative Zoom movements or
   * no relative movements by providing only translation spaces for the supported cases.
   * An existing speed argument overrides the DefaultSpeed of the corresponding PTZ configuration
   * during movement by the requested translation. If spaces are referenced within the speed
   * argument, they shall be speed spaces supported by the PTZ node.<br>
   * The command can be used to stop the PTZ unit at its current position by sending zero values for
   * Pan/Tilt and Zoom. Stopping shall have the very same effect independent of the relative space
   * referenced.<br>
   * If the requested translation leads to an absolute position which cannot be reached, the PTZ
   * Node shall move to a reachable position along the border of valid positions.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {object} translation Vector specifying the positional Translation relative to the current position.
   * @param {float=} translation.x The x component corresponds to pan.
   * @param {float=} translation.y The y component corresponds to tilt.
   * @param {float=} translation.z A zoom position.
   * @param {object=} speed Speed vector specifying the velocity of pan, tilt and zoom.
   * @param {float=} speed.x The x component corresponds to pan.  If omitted in a request, the current (if any) PanTilt movement should not be affected.
   * @param {float=} speed.y The y component corresponds to tilt.  If omitted in a request, the current (if any) PanTilt movement should not be affected
   * @param {float=} speed.z A zoom speed. If omitted in a request, the current (if any) Zoom movement should not be affected.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  relativeMove (profileToken, translation, speed, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for relativeMove is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for relativeMove is invalid: ' + errMsg))
        return
      }
      if (typeof translation !== 'undefined' && translation !== null) {
        if ((errMsg = Util.isInvalidValue(translation, 'object'))) {
          reject(new Error('The "translation" argument for relativeMove is invalid: ' + errMsg))
          return
        }
      }
      if (typeof speed !== 'undefined' && speed !== null) {
        if ((errMsg = Util.isInvalidValue(speed, 'object'))) {
          reject(new Error('The "speed" argument for relativeMove is invalid: ' + errMsg))
          return
        }
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'
      if (typeof translation !== 'undefined' && translation !== null) {
        soapBody += '<tptz:Translation>' + this.panTiltZoomOptions(translation) + '</tptz:Translation>'
      }
      if (typeof speed !== 'undefined' && speed !== null) {
        soapBody += '<tptz:Speed>' + this.panTiltZoomOptions(speed) + '</tptz:Speed>'
      }

      this.buildRequest('RelativeMove', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * A PTZ-capable device shall support continuous movements. The velocity argument of this
   * command specifies a signed speed value for the Pan, Tilt and Zoom. The combined Pan/Tilt
   * element is optional and the Zoom element itself is optional. If the Pan/Tilt element is omitted,
   * the current Pan/Tilt movement shall NOT be affected by this command. The same holds for the
   * Zoom element. The spaces referenced within the velocity element shall be velocity spaces
   * supported by the PTZ Node. If the space information is omitted for the velocity argument, the
   * corresponding default spaces of the PTZ configuration belonging to the specified Media Profile
   * is used. A device MAY support continuous Pan/Tilt movements and/or continuous Zoom
   * movements by providing only velocity spaces for the supported cases.<br>
   * An existing timeout argument overrides the DefaultPTZTimeout parameter of the corresponding
   * PTZ configuration for this Move operation. The timeout parameter specifies how long the PTZ
   * node continues to move.<br>
   * A device shall stop movement in a particular axis (Pan, Tilt, or Zoom) when zero is sent as the
   * ContinuousMove parameter for that axis. Stopping shall have the same effect independent of
   * the velocity space referenced. This command has the same effect on a continuous move as the
   * stop command specified in section 5.3.5.<br>
   * If the requested velocity leads to absolute positions which cannot be reached, the PTZ node
   * shall move to a reachable position along the border of its range. A typical application of the
   * continuous move operation is controlling PTZ via joystick.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {object} velocity One of PanTilt (x,y) or Zoom (z), or both, is required.
   * @param {float=} velocity.x Pan speed.
   * @param {float=} velocity.y Tilt spped.
   * @param {float=} velocity.z Zoom speed.
   * @param {integer=} timeout Duration: An optional Timeout parameter.
   * @param {callback=} callback Optional callback, instead of a Promise.
   * @example
   * const OnvifManager = require('onvif-nvt')
   * OnvifManager.connect('10.10.1.60', 80, 'username', 'password')
   *   .then(results => {
   *     let camera = results
   *     if (camera.ptz) { // PTZ is supported on this device
   *       let velocity = { x: 1, y: 0 }
   *       camera.ptz.continuousMove(null, velocity)
   *         .then(() => {
   *           setTimeout(() => {
   *             camera.ptz.stop()
   *           }, 5000) // stop the camera after 5 seconds
   *         })
   *     }
   *   })
   */
  continuousMove (profileToken, velocity, timeout, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for continuousMove is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for continuousMove is invalid: ' + errMsg))
        return
      }
      if (velocity) {
        if ((errMsg = Util.isInvalidValue(velocity, 'object'))) {
          reject(new Error('The "velocity" argument for continuousMove is invalid: ' + errMsg))
          return
        }
      }
      if (typeof timeout !== 'undefined' && timeout !== null) {
        if ((errMsg = Util.isInvalidValue(timeout, 'integer'))) {
          reject(new Error('The "timeout" property for continuousMove is invalid: ' + errMsg))
          return
        }
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'
      if (velocity) {
        soapBody += '<tptz:Velocity>' + this.panTiltZoomOptions(velocity) + '</tptz:Velocity>'
      }
      if (timeout) {
        soapBody += '<tptz:Timeout>PT' + timeout + 'S</tptz:Timeout>'
      }

      this.buildRequest('ContinuousMove', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * <strong>+++ This function is untested as I do not have any cameras that support this feature.</strong><br>
   * A device signaling GeoMove in one of its PTZ nodes shall support this command.
   * The optional AreaHeight and AreaWidth parameters can be added to the request, so that the
   * PTZ-capable device can internally determine the zoom factor. In case both AreaHeight and
   * AreaWidth are not provided, the unit will not change the zoom. AreaHeight and AreaWidth are
   * expressed in meters.<br>
   * An existing speed argument overrides the DefaultSpeed of the corresponding PTZ configuration
   * during movement by the requested translation. If spaces are referenced within the speed
   * argument, they shall be speed spaces supported by the PTZ node.<br>
   * If the PTZ-capable device does not support automatic retrieval of the geolocation, it shall be
   * configured by using SetGeoLocation before it can perform geo-referenced commands. In case
   * the client requests a GeoMove command before the geolocation of the device is configured,
   * the device shall return an error.<br>
   * Depending on the kinematics of the PTZ-capable device, the requested position may not be
   * reachable. In this situation the device shall return an error, signalling that it cannot perform the
   * requested action due to physical limitations.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {object} geoLocation Target coordinates.
   * @param {float} geoLocation.lon East west location as angle.
   * @param {float} geoLocation.lat North south location as angle.
   * @param {float} geoLocation.elevation Height in meters above sea level.
   * @param {object=} speed Speed vector specifying the velocity of pan, tilt and zoom.
   * @param {float=} speed.x The x component corresponds to pan.  If omitted in a request, the current (if any) PanTilt movement should not be affected.
   * @param {float=} speed.y The y component corresponds to tilt.  If omitted in a request, the current (if any) PanTilt movement should not be affected
   * @param {float=} speed.z A zoom speed. If omitted in a request, the current (if any) Zoom movement should not be affected.
   * @param {float=} areaWidth Optional area to be shown.
   * @param {float=} areaHeight Optional area to be shown.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  geoMove (profileToken, geoLocation, speed, areaWidth, areaHeight, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for geoMove is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for geoMove is invalid: ' + errMsg))
        return
      }
      if ((errMsg = Util.isInvalidValue(geoLocation, 'object'))) {
        reject(new Error('The "position" argument for geoMove is invalid: ' + errMsg))
        return
      }
      if (!('lat' in geoLocation)) {
        reject(new Error('The "geoLocation.lat" is a required argument for geoMove: ' + errMsg))
        return
      }
      if ((errMsg = Util.isInvalidValue(geoLocation.lat, 'float'))) {
        reject(new Error('The "geoLocation.lat" argument for geoMove is invalid: ' + errMsg))
        return
      }
      if (!('lon' in geoLocation)) {
        reject(new Error('The "geoLocation.lon" is a required argument for geoMove: ' + errMsg))
        return
      }
      if ((errMsg = Util.isInvalidValue(geoLocation.lon, 'float'))) {
        reject(new Error('The "geoLocation.lon" argument for geoMove is invalid: ' + errMsg))
        return
      }
      if (!('elevation' in geoLocation)) {
        reject(new Error('The "geoLocation.elevation" is a required argument for geoMove: ' + errMsg))
        return
      }
      if ((errMsg = Util.isInvalidValue(geoLocation.elevation, 'float'))) {
        reject(new Error('The "geoLocation.elevation" argument for geoMove is invalid: ' + errMsg))
        return
      }
      if (typeof speed !== 'undefined' && speed !== null) {
        if ((errMsg = Util.isInvalidValue(speed, 'object'))) {
          reject(new Error('The "speed" argument for geoMove is invalid: ' + errMsg))
          return
        }
      }
      if (typeof areaWidth !== 'undefined' && areaWidth !== null) {
        if ((errMsg = Util.isInvalidValue(areaWidth, 'float'))) {
          reject(new Error('The "areaWidth" argument for geoMove is invalid: ' + errMsg))
          return
        }
      }
      if (typeof areaHeight !== 'undefined' && areaHeight !== null) {
        if ((errMsg = Util.isInvalidValue(areaHeight, 'float'))) {
          reject(new Error('The "areaHeight" argument for geoMove is invalid: ' + errMsg))
          return
        }
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'
      soapBody += '<tptz:Target lat="' + geoLocation.lat + '" lon="' + geoLocation.lon + '" elevation="' + geoLocation.elevation + '" />'
      if (typeof speed !== 'undefined' && speed !== null) {
        soapBody += '<tptz:Speed>' + this.panTiltZoomOptions(speed) + '</tptz:Speed>'
      }
      if (typeof areaWidth !== 'undefined' && areaWidth !== null) {
        soapBody += '<tptz:AreaWidth>' + areaWidth + '</tptz:AreaWidth>'
      }
      if (typeof areaHeight !== 'undefined' && areaHeight !== null) {
        soapBody += '<tptz:AreaHeight>' + areaWidth + '</tptz:AreaHeight>'
      }

      this.buildRequest('GeoMove', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * A PTZ-capable device shall support the stop operation. If no stop filter arguments are present,
   * this command stops all ongoing pan, tilt and zoom movements. The stop operation can be
   * filtered to stop a specific movement by setting the corresponding stop argument.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {boolean=} panTilt Defaults to true..........
   * @param {boolean=} zoom Defaults to true
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  stop (profileToken, panTilt, zoom, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for stop is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for stop is invalid: ' + errMsg))
        return
      }
      if (typeof panTilt !== 'undefined' && panTilt !== null) {
        if ((errMsg = Util.isInvalidValue(panTilt, 'boolean'))) {
          reject(new Error('The "panTilt" property for stop is invalid: ' + errMsg))
          return
        }
      }
      if (typeof zoom !== 'undefined' && zoom !== null) {
        if ((errMsg = Util.isInvalidValue(zoom, 'boolean'))) {
          reject(new Error('The "zoom" property for stop is invalid: ' + errMsg))
          return
        }
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'
      if (typeof panTilt !== 'undefined' && panTilt !== null) {
        // soapBody += '<tptz:PanTilt>' + (panTilt ? 1 : 0) + '</tptz:PanTilt>'
        soapBody += '<tptz:PanTilt>' + panTilt + '</tptz:PanTilt>'
      }
      if (typeof zoom !== 'undefined' && zoom !== null) {
        // soapBody += '<tptz:Zoom>' + (zoom ? 1 : 0) + '</tptz:Zoom>'
        soapBody += '<tptz:Zoom>' + zoom + '</tptz:Zoom>'
      }

      this.buildRequest('Stop', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * A PTZ-capable device shall be able to report its PTZ status through the GetStatus command.
   * The PTZ status contains the following information:
   * <ul>
   * <li>Position (optional) – Specifies the absolute position of the PTZ unit together with the
   * space references. The default absolute spaces of the corresponding PTZ configuration
   * shall be referenced within the position element. This information shall be present if the
   * device signals support via the capability StatusPosition.</li>
   * <li>MoveStatus (optional) – Indicates if the Pan/Tilt/Zoom device unit is currently moving, idle
   * or in an unknown state. This information shall be present if the device signals support
   * via the capability MoveStatus. The state Unknown shall not be used during normal
   * operation, but is reserved to initialization or error conditions.</li>
   * <li>Error (optional) – States a current PTZ error condition. This field shall be present if the
   * MoveStatus signals Unknown.</li>
   * <li>UTC Time – Specifies the UTC time when this status was generated.</li>
   * </ul>.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  getStatus (profileToken, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for getStatus is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for relativeMove is invalid: ' + errMsg))
        return
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'

      this.buildRequest('GetStatus', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * The SetPreset command saves the current device position parameters so that the device can
   * move to the saved preset position through the GotoPreset operation.<br>
   * If the PresetToken parameter is absent, the device shall create a new preset. Otherwise it shall
   * update the stored position and optionally the name of the given preset. If creation is successful,
   * the response contains the PresetToken which uniquely identifies the preset. An existing preset
   * can be overwritten by specifying the PresetToken of the corresponding preset. In both cases
   * (overwriting or creation) an optional PresetName can be specified. The operation fails if the PTZ
   * device is moving during the SetPreset operation.<br>
   * The device MAY internally save additional states such as imaging properties in the PTZ preset
   * which then should be recalled in the GotoPreset operation. A device shall accept a valid
   * SetPresetRequest that does not include the optional element PresetName.
   * Devices may require unique preset names and reject a request that contains an already existing
   * PresetName by responding with the error message ter:PresetExist.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {string=} presetToken Optional existing preset token to update a preset position.
   * @param {string=} presetName Optional name to be assigned to the preset position.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  setPreset (profileToken, presetToken, presetName, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for setPreset is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for setPreset is invalid: ' + errMsg))
        return
      }
      if (typeof presetToken !== 'undefined' && presetToken !== null) {
        if ((errMsg = Util.isInvalidValue(presetToken, 'string'))) {
          reject(new Error('The "presetToken" argument for setPreset is invalid: ' + errMsg))
          return
        }
      }
      if (typeof presetName !== 'undefined' && presetName !== null) {
        if ((errMsg = Util.isInvalidValue(presetName, 'string'))) {
          reject(new Error('The "presetName" argument for setPreset is invalid: ' + errMsg))
          return
        }
      }
      if (!presetToken && !presetName) {
        reject(new Error('Either the "profileToken" or the "presetName" argument must be specified in method "setPreset".'))
        return
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'
      if (presetToken) {
        soapBody += '<tptz:PresetToken>' + presetToken + '</tptz:PresetToken>'
      }
      if (presetName) {
        soapBody += '<tptz:PresetName>' + presetName + '</tptz:PresetName>'
      }

      this.buildRequest('SetPreset', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * The GetPresets operation returns the saved presets consisting of the following elements:
   * <ul>
   * <li>Token – A unique identifier to reference the preset.</li>
   * <li>Name – An optional mnemonic name.</li>
   * <li>PTZ Position – An optional absolute position. If the PTZ node supports absolute
   * Pan/Tilt position spaces, the Pan/Tilt position shall be specified. If the PTZ node
   * supports absolute zoom position spaces, the zoom position shall be specified.</li>
   * </ul>.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  getPresets (profileToken, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for getPresets is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for getPresets is invalid: ' + errMsg))
        return
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'

      this.buildRequest('GetPresets', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * The GotoPreset operation recalls a previously set preset. If the speed parameter is omitted, the
   * default speed of the corresponding PTZ configuration shall be used. The speed parameter can
   * only be specified when speed spaces are available for the PTZ node. The GotoPreset command
   * is a non-blocking operation and can be interrupted by other move commands.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {string} presetToken Reference to an existing preset token.
   * @param {object=} speed Speed vector specifying the velocity of pan, tilt and zoom.
   * @param {float=} speed.x The x component corresponds to pan.  If omitted in a request, the current (if any) PanTilt movement should not be affected.
   * @param {float=} speed.y The y component corresponds to tilt.  If omitted in a request, the current (if any) PanTilt movement should not be affected
   * @param {float=} speed.z A zoom speed. If omitted in a request, the current (if any) Zoom movement should not be affected.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  gotoPreset (profileToken, presetToken, speed, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for gotoPreset is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for gotoPreset is invalid: ' + errMsg))
        return
      }
      if ((errMsg = Util.isInvalidValue(presetToken, 'string'))) {
        reject(new Error('The "presetToken" argument for gotoPreset is invalid: ' + errMsg))
        return
      }
      if (typeof speed !== 'undefined' && speed !== null) {
        if ((errMsg = Util.isInvalidValue(speed, 'object'))) {
          reject(new Error('The "speed" argument for gotoPreset is invalid: ' + errMsg))
          return
        }
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'
      soapBody += '<tptz:PresetToken>' + presetToken + '</tptz:PresetToken>'
      if (typeof speed !== 'undefined' && speed !== null) {
        soapBody += '<tptz:Speed>' + this.panTiltZoomOptions(speed) + '</tptz:Speed>'
      }

      this.buildRequest('GotoPreset', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * The RemovePreset operation removes a previously set preset.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {string} presetToken Existing preset token to be removed.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  removePreset (profileToken, presetToken, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for removePreset is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for removePreset is invalid: ' + errMsg))
        return
      }
      if ((errMsg = Util.isInvalidValue(presetToken, 'string'))) {
        reject(new Error('The "presetToken" argument for removePreset is invalid: ' + errMsg))
        return
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'
      soapBody += '<tptz:PresetToken>' + presetToken + '</tptz:PresetToken>'

      this.buildRequest('RemovePreset', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * This operation moves the PTZ unit to its home position. If the speed parameter is omitted, the
   * default speed of the corresponding PTZ configuration shall be used. The speed parameter can
   * only be specified when speed spaces are available for the PTZ node.The command is nonblocking
   * and can be interrupted by other move commands.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {object=} speed Speed vector specifying the velocity of pan, tilt and zoom.
   * @param {float=} speed.x The x component corresponds to pan.  If omitted in a request, the current (if any) PanTilt movement should not be affected.
   * @param {float=} speed.y The y component corresponds to tilt.  If omitted in a request, the current (if any) PanTilt movement should not be affected
   * @param {float=} speed.z A zoom speed. If omitted in a request, the current (if any) Zoom movement should not be affected.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  gotoHomePosition (profileToken, speed, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for gotoHomePosition is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for gotoHomePosition is invalid: ' + errMsg))
        return
      }
      if (typeof speed !== 'undefined' && speed !== null) {
        if ((errMsg = Util.isInvalidValue(speed, 'object'))) {
          reject(new Error('The "speed" argument for gotoHomePosition is invalid: ' + errMsg))
          return
        }
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'
      if (typeof speed !== 'undefined' && speed !== null) {
        soapBody += '<tptz:Speed>' + this.panTiltZoomOptions(speed) + '</tptz:Speed>'
      }

      this.buildRequest('GotoHomePosition', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * The SetHome operation saves the current position parameters as the home position, so that the
   * GotoHome operation can request that the device move to the home position.<br>
   * The SetHomePosition command shall return with a failure if the “home” position is fixed and
   * cannot be overwritten. If the SetHomePosition is successful, it shall be possible to recall the
   * home position with the GotoHomePosition command.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  setHomePosition (profileToken, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for setHomePosition is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for setHomePosition is invalid: ' + errMsg))
        return
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'

      this.buildRequest('SetHomePosition', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }

  /**
   * This operation is used to call an auxiliary operation on the device. The supported commands
   * can be retrieved via the PTZ node properties. The AuxiliaryCommand should match the
   * supported command listed in the PTZ node; no other syntax is supported. If the PTZ node lists
   * the tt:IRLamp command, then the parameter of AuxiliaryCommand command shall conform to
   * the syntax specified in Section 8.6 Auxiliary operation of ONVIF Core Specification. The
   * SendAuxiliaryCommand shall be implemented when the PTZ node supports auxiliary commands.
   * @param {string=} profileToken If no profileToken is provided, then the defaultProfileToken will be used.
   * @param {string} auxiliaryData Auxiliary command to be applied.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  sendAuxiliaryCommand (profileToken, auxiliaryData, callback) {
    const promise = new Promise((resolve, reject) => {
      profileToken = profileToken || this.defaultProfileToken

      let errMsg = ''
      if (typeof callback !== 'undefined' && callback !== null) {
        if ((errMsg = Util.isInvalidValue(callback, 'function'))) {
          reject(new Error('The "callback" argument for sendAuxiliaryCommand is invalid:' + errMsg))
          return
        }
      }
      if ((errMsg = Util.isInvalidValue(profileToken, 'string'))) {
        reject(new Error('The "profileToken" argument for sendAuxiliaryCommand is invalid: ' + errMsg))
        return
      }
      if ((errMsg = Util.isInvalidValue(auxiliaryData, 'string'))) {
        reject(new Error('The "auxiliaryData" argument for sendAuxiliaryCommand is invalid: ' + errMsg))
        return
      }

      let soapBody = ''
      soapBody += '<tptz:ProfileToken>' + profileToken + '</tptz:ProfileToken>'
      soapBody += '<tptz:AuxiliaryData>' + auxiliaryData + '<tptz:AuxiliaryData>'

      this.buildRequest('SendAuxiliaryCommand', soapBody)
        .then(results => {
          resolve(results)
        }).catch(error => {
          reject(error)
        })
    })
    if (Util.isValidCallback(callback)) {
      promise.then(results => {
        callback(null, results)
      }).catch(error => {
        callback(error)
      })
    }
    else {
      return promise
    }
  }
}

module.exports = Ptz