Skip to content

Commit

Permalink
Merge pull request #519 from svrooij/feature/types
Browse files Browse the repository at this point in the history
Typescript types
  • Loading branch information
svrooij authored Oct 4, 2021
2 parents d4fdbe8 + 9a87007 commit bea996e
Show file tree
Hide file tree
Showing 71 changed files with 5,373 additions and 603 deletions.
9 changes: 7 additions & 2 deletions .generator/node/all-services.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,21 @@ const {{serviceName}} = require('./{{#if data.filename}}{{data.filename}}{{else}
* @class AllServices
*/
class AllServices {
/**
*
* @param {string} host Sonos host
* @param {number} port Sonos port, default `1400`
*/
constructor (host, port) {
this.host = host
this.port = port
this.port = port || 1400
}

{{#each services}}
/**
* Get instance of {{name}} service
*
* @returns {{serviceName}}
* @returns { {{~ serviceName ~}} }
*/
{{serviceName}} () {
if (!this.{{lower serviceName}}) {
Expand Down
9 changes: 8 additions & 1 deletion .generator/node/service.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ const Service = require('./Service')
* @extends {Service}
*/
class {{serviceName}} extends Service {
/**
*
* @param {string} host Sonos host
* @param {number} port Sonos port, default `1400`
*/
constructor (host, port) {
super()
this.name = '{{name}}'
Expand All @@ -38,7 +43,9 @@ class {{serviceName}} extends Service {
* @remarks {{{remarks}}}
{{/if}}
{{#if outputs}}
* @returns {Object} response object, with these properties {{#each outputs}}'{{name}}'{{#unless @last}}, {{/unless}}{{/each}}
* @returns {Promise<{ {{#each outputs}}{{name}}: {{relatedStateVariable.dataType}}{{#unless @last}}, {{/unless}}{{/each ~}} }>} response object.
{{else}}
* @returns {Promise<Boolean>} request succeeded
{{/if}}
*/
{{#if inputs}}
Expand Down
31 changes: 18 additions & 13 deletions examples/deviceDiscovery.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
const Sonos = require('../')

console.log('Searching for Sonos devices for 5 seconds...')
console.log('Searching for Sonos devices for 10 seconds...')

const discovery = new Sonos.AsyncDeviceDiscovery()

discovery.discover().then((device, model) => {
console.log('Found one sonos device %s getting all groups', device.host)
return device.getAllGroups().then((groups) => {
console.log('Groups %s', JSON.stringify(groups, null, 2))
return groups[0].CoordinatorDevice().togglePlayback()
// discovery.discover({ port: 50333, timeout: 10000 }).then((device, model) => {
// console.log('Found one sonos device %s getting all groups', device.host)
// return device.getAllGroups().then((groups) => {
// console.log('Groups %s', JSON.stringify(groups, null, 2))
// //return groups[0].CoordinatorDevice().togglePlayback()
// })
// }).catch(e => {
// console.warn(' Error in discovery %o', e)
// })

discovery.discoverMultiple({ port: 50333, timeout: 10000 }).then((devices) => {
console.log('Found %d sonos devices', devices.length)
devices.forEach(device => {
console.log('Device IP: %s', device.host)
})
}).catch(e => {
console.warn(' Error in discovery %j', e)
})

// discovery.discoverMultiple({ timeout: 5000 }).then((devices) => {
// console.log('Found %d sonos devices', devices.length)
// devices.forEach(device => {
// console.log('Device IP: %s', device.host)
// })
// }).catch(e => {
// console.warn(' Error in discovery %j', e)
// const discovery = new Sonos.DeviceDiscovery({}, (device, model) => {
// console.log('Found one sonos device %s', device.host)
// process.exit(0);
// })
14 changes: 13 additions & 1 deletion lib/asyncDeviceDiscovery.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
const DeviceDiscovery = require('./deviceDiscovery')
class AsyncDeviceDiscovery {
/**
* Discover one device, will return first device found
* @param {object} options
* @param {number} options.timeout Milliseconds to timeout
* @returns {Promise<{device: import("./sonos").Sonos, model: string}>}
*/
async discover (options = { timeout: 5000 }) {
return new Promise((resolve, reject) => {
const discovery = DeviceDiscovery(options)
discovery.once('DeviceAvailable', (device, model) => {
discovery.destroy()
resolve(device, model)
resolve({ device, model })
})

discovery.once('timeout', () => {
Expand All @@ -14,6 +20,12 @@ class AsyncDeviceDiscovery {
})
}

/**
* Discover multiple devices, will return after timeout
* @param {object} options
* @param {number} options.timeout Milliseconds to timeout
* @returns {Promise<import("./sonos").Sonos[]>}
*/
async discoverMultiple (options = { timeout: 5000 }) {
return new Promise((resolve, reject) => {
const discovery = DeviceDiscovery(options)
Expand Down
146 changes: 84 additions & 62 deletions lib/deviceDiscovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,79 +7,101 @@
*/

const dgram = require('dgram')
const util = require('util')
const EventEmitter = require('events').EventEmitter
const Sonos = require('./sonos').Sonos

const PLAYER_SEARCH = Buffer.from(['M-SEARCH * HTTP/1.1',
'HOST: 239.255.255.250:1900',
'MAN: ssdp:discover',
'MX: 1',
'ST: urn:schemas-upnp-org:device:ZonePlayer:1'].join('\r\n'))

/**
* Create a new instance of DeviceDiscovery
* @class DeviceDiscovery
* @emits 'DeviceAvailable' on a Sonos Component Discovery
*/
const DeviceDiscovery = function DeviceDiscovery (options) {
const self = this
self.foundSonosDevices = {}
self.onTimeout = function () {
clearTimeout(self.pollTimer)
}
const PLAYER_SEARCH = Buffer.from(['M-SEARCH * HTTP/1.1',
'HOST: 239.255.255.250:1900',
'MAN: ssdp:discover',
'MX: 1',
'ST: urn:schemas-upnp-org:device:ZonePlayer:1'].join('\r\n'))
const sendDiscover = function () {
['239.255.255.250', '255.255.255.255'].forEach(function (addr) {
self.socket.send(PLAYER_SEARCH, 0, PLAYER_SEARCH.length, 1900, addr)
class DeviceDiscovery extends EventEmitter {
constructor (options) {
super()
// this.options = options
this.foundSonosDevices = {}
this.socket = dgram.createSocket('udp4')

this.socket.on('message', (buffer, rinfo) => {
const data = buffer.toString()
if (data.match(/.+Sonos.+/)) {
const modelCheck = data.match(/SERVER.*\((.*)\)/)
const model = (modelCheck.length > 1 ? modelCheck[1] : null)
const addr = rinfo.address
if (!(addr in this.foundSonosDevices)) {
const sonos = this.foundSonosDevices[addr] = new Sonos(addr)
this.emit('DeviceAvailable', sonos, model)
}
}
})

this.socket.on('close', () => {
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
clearTimeout(this.pollTimer)
})

this.socket.on('error', (err) => {
this.emit('error', err)
})

this.socket.on('listening', () => {
// console.log('UDP port %d opened for discovery', this.socket.address().port)
this.socket.setBroadcast(true)
this.sendDiscover()
})

this.socket.bind(options.port)

if (options && options.timeout) {
this.searchTimer = setTimeout(() => {
this.socket.close()
this.emit('timeout')
}, options.timeout)
}
}

onTimeout () {
clearTimeout(this.pollTimer)
}

sendDiscover () {
this.sendDiscoveryOnAddress('239.255.255.250')
this.sendDiscoveryOnAddress('255.255.255.255')

// Periodically send discover packet to find newly added devices
self.pollTimer = setTimeout(sendDiscover, 10000)
this.pollTimer = setTimeout(() => {
this.sendDiscover()
}, 10000)
// Remove the on timeout listener and add back in every iteration
self.removeListener('timeout', self.onTimeout)
self.on('timeout', self.onTimeout)
this.removeListener('timeout', this.onTimeout)
this.on('timeout', this.onTimeout)
}
this.socket = dgram.createSocket('udp4', function (buffer, rinfo) {
buffer = buffer.toString()
if (buffer.match(/.+Sonos.+/)) {
const modelCheck = buffer.match(/SERVER.*\((.*)\)/)
const model = (modelCheck.length > 1 ? modelCheck[1] : null)
const addr = rinfo.address
if (!(addr in self.foundSonosDevices)) {
const sonos = self.foundSonosDevices[addr] = new Sonos(addr)
self.emit('DeviceAvailable', sonos, model)

sendDiscoveryOnAddress (address) {
this.socket.send(PLAYER_SEARCH, 0, PLAYER_SEARCH.length, 1900, address, (err, bytes) => {
if (err) {
console.error(err)
}
})
}

destroy () {
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
})
this.socket.on('close', function () {
if (self.searchTimer) {
clearTimeout(self.searchTimer)
if (this.pollTimer) {
clearTimeout(this.pollTimer)
}
clearTimeout(self.pollTimer)
})
this.socket.on('error', function (err) {
self.emit('error', err)
})
this.socket.bind(options, function () {
self.socket.setBroadcast(true)
sendDiscover()
})
if (options.timeout) {
self.searchTimer = setTimeout(function () {
self.socket.close()
self.emit('timeout')
}, options.timeout)
this.socket.close()
}
return this
}
util.inherits(DeviceDiscovery, EventEmitter)

/**
* Destroys DeviceDiscovery class, stop searching, clean up
* @param {Function} callback ()
*/
DeviceDiscovery.prototype.destroy = function (callback) {
clearTimeout(this.searchTimer)
clearTimeout(this.pollTimer)
this.socket.close(callback)
}

/**
Expand All @@ -89,18 +111,18 @@ DeviceDiscovery.prototype.destroy = function (callback) {
* (in milliseconds).
* Set 'port' to use a specific inbound UDP port,
* rather than a randomly assigned one
* @param {Function} listener Optional 'DeviceAvailable' listener (sonos)
* @param {(device: Sonos, model: string) => void | undefined} listener Optional 'DeviceAvailable' listener (sonos)
* @return {DeviceDiscovery}
*/
const deviceDiscovery = function (options, listener) {
const deviceDiscovery = function (options, listener = undefined) {
if (typeof options === 'function') {
listener = options
options = null
options = undefined
}
options = options || {}
listener = listener || null
listener = listener || undefined
const search = new DeviceDiscovery(options)
if (listener !== null) {
if (listener !== undefined) {
search.on('DeviceAvailable', listener)
}
return search
Expand Down
Loading

0 comments on commit bea996e

Please sign in to comment.