diff --git a/README.md b/README.md index 4f75ce7..ab2ba16 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) ## Homebridge plugin for Sonos ZonePlayer -Copyright © 2016-2018 Erik Baauw. All rights reserved. +Copyright © 2016-2019 Erik Baauw. All rights reserved. This [homebridge](https://github.com/nfarina/homebridge) plugin exposes [Sonos](http://www.sonos.com) ZonePlayers to Apple's [HomeKit](http://www.apple.com/ios/home/). It provides the following features: - Automatic discovery of Sonos zones, taking into account stereo pairs and home theatre setup; @@ -17,12 +17,12 @@ This [homebridge](https://github.com/nfarina/homebridge) plugin exposes [Sonos]( ### Prerequisites You need a server to run homebridge. This can be anything running [Node.js](https://nodejs.org): from a Raspberri Pi, a NAS system, or an always-on PC running Linux, macOS, or Windows. See the [homebridge Wiki](https://github.com/nfarina/homebridge/wiki) for details. I use a Mac mini server, and, occasionally, a Raspberri Pi 3 model B. -To interact with HomeKit, you need Siri or a HomeKit app on an iPhone, Apple Watch, iPad, iPod Touch, or Apple TV (4th generation or later). I recommend to use the latest released versions of iOS, watchOS, and tvOS. -Please note that Siri and even Apple's [Home](https://support.apple.com/en-us/HT204893) app still provide only limited HomeKit support. To use the full features of homebridge-zp, you might want to check out some other HomeKit apps, like Elgato's [Eve](https://www.elgato.com/en/eve/eve-app) app (free) or Matthias Hochgatterer's [Home](http://selfcoded.com/home/) app (paid). +To interact with HomeKit, you need Siri or a HomeKit app on an iPhone, Apple Watch, iPad, iPod Touch, or Apple TV (4th generation or later). I recommend to use the latest released versions of iOS, watchOS, and tvOS. +Please note that Siri and even Apple's [Home](https://support.apple.com/en-us/HT204893) app still provide only limited HomeKit support. To use the full features of homebridge-zp, you might want to check out some other HomeKit apps, like Elgato's [Eve](https://www.elgato.com/en/eve/eve-app) app (free) or Matthias Hochgatterer's [Home](http://selfcoded.com/home/) app (paid). For HomeKit automation, you need to setup an Apple TV (4th generation or later) or iPad as [Home Hub](https://support.apple.com/en-us/HT207057). ### Zones -The homebridge-zp plugin creates an accessory per Sonos zone, named after the zone, e.g. *Living Room Sonos* for the *Living Room* zone. By default, this accessory contains a single `Switch` service, with the same name as the accessory. In addition to the standard `Power State` characteristic for play/pause control, additional characteristics are provided for `Volume`, `Mute`, `Current Track` (read-only) and `Sonos Group` (read-only). Note that `Current Track` and `Sonos Group` are custom characteristics. They might not be supported by all HomeKit apps, see [**Caveats**](#caveats). +The homebridge-zp plugin creates an accessory per Sonos zone, named after the zone, e.g. *Living Room Sonos* for the *Living Room* zone. By default, this accessory contains a single `Switch` service, with the same name as the accessory. In addition to the standard `Power State` characteristic for play/pause control, additional characteristics are provided for `Volume`, `Mute`, `Current Track` (read-only) and `Sonos Group` (read-only). Note that `Current Track` and `Sonos Group` are custom characteristics. They might not be supported by all HomeKit apps, see [**Caveats**](#caveats). Note that neither Siri nor the Apple's Home app support `Volume` or `Mute`, even thought these are standard HomeKit characteristics. Because of this, the type of the service, as well as the type of characteristic used for volume can be changed from `config.json`, see [**Configuration**](#configuration) and [issue #10](https://github.com/ebaauw/homebridge-zp/issues/10). @@ -49,7 +49,7 @@ The homebridge-zp plugin obviously needs homebridge, which, in turn needs Node.j - Install the latest v10 LTS version of Node.js. On a Raspberry Pi, use the 10.x [Debian package](https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions). On other platforms, download the [10.x.x LTS](https://nodejs.org) installer. Both installations include the `npm` package manager; - On macOS, make sure `/usr/local/bin` is in your `$PATH`, as `node`, `npm`, and, later, `homebridge` install there. On a Raspberry Pi, these install to `/usr/bin`; - You might want to update `npm` through `sudo npm -g update npm@latest`; -- Install homebridge through `sudo npm -g install homebridge --unsafe-perm`. Follow the instructions on [GitHub](https://github.com/nfarina/homebridge#installation) to create a `config.json` in `~/.homebridge`, as described; +- Install homebridge through `sudo npm -g install homebridge`. Follow the instructions on [GitHub](https://github.com/nfarina/homebridge#installation) to create a `config.json` in `~/.homebridge`, as described; - Install the homebridge-zp plugin through `sudo npm -g install homebridge-zp`; - Edit `~/.homebridge/config.json` and add the `ZP` platform provided by homebridge-zp, see [**Configuration**](#configuration). @@ -69,7 +69,7 @@ The following optional parameters can be added to modify homebridge-zp's behavio Key | Default | Description --- | ------- | ----------- `alarms` | `false` | Flag whether to expose an additional service per Sonos alarm. -`brightness` | `false` | Flag whether to expose volume as `Brightness` in combination with `Switch` or `Speaker`. Setting this flag enables volume control from Siri. +`brightness` | `false` | Flag whether to expose volume as `Brightness` when `service` is `"switch"` or `"speaker"`. Setting this flag enables volume control from Siri, but not from Apple's Home app. `leds` | `false` | Flag whether to expose an additional *Lightbulb* service per zone for the status LED. `host` | _(discovered)_ | The hostname or IP address for the web server homebridge-zp creates to receive notifications from Sonos ZonePlayers. This must be the hostname or IP address of the server running homebridge-zp, reachable by the ZonePlayers. You might need to set this on a multi-homed server, if homebridge-zp binds to the wrong network interface. `port` | `0` _(random)_ | The port for the web server homebridge-zp creates to receive notifications from Sonos ZonePlayers. @@ -77,7 +77,7 @@ Key | Default | Description `service` | `"switch"` | Defines what type of service and volume characteristic homebridge-zp uses. Possible values are: `"switch"` for `Switch` and `Volume`; `"speaker"` for `Speaker` and `Volume`; `"light"` for `LightBulb` and `Brightness`; and `"fan"` for `Fan` and `Rotation Speed`. Selecting `"light"` or `"fan"` enables changing the Sonos volume from Siri and from Apple's Home app. Selecting `"speaker"` is not supported by Apple's Home app. `speakers` | `false` | Flag whether to expose a second *Speakers* service per zone, in addition to the standard *Sonos* service, see [**Speakers**](#speakers). You might want to set this if you're using Sonos groups in a configuration of multiple Sonos zones. `subscriptionTimeout` | `30` | The duration (in minutes) of the subscriptions homebridge-zp creates with each ZonePlayer. -`nameScheme` | `"% Sonos"` | The name scheme for ZonePlayers; `%` is replaced with the player name. Example: With the default name scheme, the player `Kitchen` is shown as `Kitchen Sonos` in Homekit. +`nameScheme` | `"% Sonos"` | The name scheme for the HomeKit accessories. `%` is replaced with the player name. E.g. with the default name scheme, the accessory for the `Kitchen` zone is set to `Kitchen Sonos`. Note that this does _not_ change the names of the HomeKit services, used by Siri. Below is an example `config.json` that exposes the *Sonos* and *Speakers* service as a HomeKit `Speaker` and volume as `Brightness`, so it can be controlled from Siri: ```json @@ -107,28 +107,10 @@ Like the Sonos app, homebridge-zp subscribes to the ZonePlayer events to be noti ``` To check whether the listener is reachable from the network, open this URL in your web browser. You should get a reply like: ``` -homebridge-zp v0.2.34, node v8.12.0, homebridge v0.4.45 +homebridge-zp v0.3.16, node v10.15.3, homebridge v0.4.49 ``` -To make sure homebridge-zp unsubscribes from the ZonePlayers when homebridge exits, it installs a handler for uncaught exceptions, that would otherwise cause homebridge (or rather NodeJS) to crash. The handler displays a message and sends the SIGTERM signal to homebridge, so it can exit cleanly. Note that the uncaught exception is _not_ caused by homebridge-zp, it only handles it. In the example below, the exception is caused by homebridge itself (it cannot start its server, because another instance of homebridge is already running): -``` -[2018-9-28 12:51:42] [ZP] uncaught exception -Error: listen EADDRINUSE :::51826 - at Server.setupListenHandle [as _listen2] (net.js:1360:14) - at listenInCluster (net.js:1401:12) - at Server.listen (net.js:1485:7) - at EventedHTTPServer.listen (/usr/local/lib/node_modules/homebridge/node_modules/hap-nodejs/lib/util/eventedhttp.js:60:19) - at HAPServer.listen (/usr/local/lib/node_modules/homebridge/node_modules/hap-nodejs/lib/HAPServer.js:158:20) - at Bridge.Accessory.publish (/usr/local/lib/node_modules/homebridge/node_modules/hap-nodejs/lib/Accessory.js:607:16) - at Server._publish (/usr/local/lib/node_modules/homebridge/lib/server.js:128:16) - at Server. (/usr/local/lib/node_modules/homebridge/lib/server.js:404:14) - at /usr/local/lib/node_modules/homebridge/node_modules/hap-nodejs/lib/util/once.js:16:19 - at listen (/Users/ebaauw/GitHub/homebridge-zp/lib/ZpPlatform.js:183:14) -[2018-9-28 12:51:42] Got SIGTERM, shutting down Homebridge... -[2018-9-28 12:51:42] [ZP] cleaning up... -``` - -If you need help, please open an issue on [GitHub](https://github.com/ebaauw/homebridge-zp/issues). Please attach a copy of your full `config.json` (masking any sensitive info) and the debug logfile. +If you need help, please open an issue on [GitHub](https://github.com/ebaauw/homebridge-zp/issues). Please attach a copy of your full `config.json` (masking any sensitive info) and the debug logfile. For questions, you can also post a message to the **#homebridge-zp** channel of the [homebridge workspace on Slack](https://github.com/nfarina/homebridge#community). ### Caveats diff --git a/index.js b/index.js index 212b62d..09de106 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ // homebridge-zp/index.js -// Copyright © 2016-2018 Erik Baauw. All rights reserved. +// Copyright © 2016-2019 Erik Baauw. All rights reserved. // // Homebridge plugin for Sonos ZonePlayer. diff --git a/lib/ZpAccessory.js b/lib/ZpAccessory.js index 38a7e81..eb5f43d 100644 --- a/lib/ZpAccessory.js +++ b/lib/ZpAccessory.js @@ -1,5 +1,5 @@ // homebridge-zp/lib/ZpAccessory.js -// Copyright © 2016-2018 Erik Baauw. All rights reserved. +// Copyright © 2016-2019 Erik Baauw. All rights reserved. // // Homebridge plugin for Sonos ZonePlayer. @@ -24,11 +24,39 @@ let Service let Characteristic let my +const remoteKeys = {} +const volumeSelectors = {} + function setHomebridge (Homebridge) { Service = Homebridge.hap.Service Characteristic = Homebridge.hap.Characteristic + remoteKeys[Characteristic.RemoteKey.REWIND] = 'Rewind' + remoteKeys[Characteristic.RemoteKey.FAST_FORWARD] = 'Fast Forward' + remoteKeys[Characteristic.RemoteKey.NEXT_TRACK] = 'Next Track' + remoteKeys[Characteristic.RemoteKey.PREVIOUS_TRACK] = 'Previous Track' + remoteKeys[Characteristic.RemoteKey.ARROW_UP] = 'Up' + remoteKeys[Characteristic.RemoteKey.ARROW_DOWN] = 'Down' + remoteKeys[Characteristic.RemoteKey.ARROW_LEFT] = 'Left' + remoteKeys[Characteristic.RemoteKey.ARROW_RIGHT] = 'Right' + remoteKeys[Characteristic.RemoteKey.SELECT] = 'Select' + remoteKeys[Characteristic.RemoteKey.BACK] = 'Back' + remoteKeys[Characteristic.RemoteKey.EXIT] = 'Exit' + remoteKeys[Characteristic.RemoteKey.PLAY_PAUSE] = 'Play/Pause' + remoteKeys[Characteristic.RemoteKey.INFORMATION] = 'Info' + volumeSelectors[Characteristic.VolumeSelector.INCREMENT] = 'Up' + volumeSelectors[Characteristic.VolumeSelector.DECREMENT] = 'Down' } +const tvModels = [ + 'ZPS9', // PlayBar. + 'ZPS11', // Playbase, see #58. + 'ZPS16' // Amp, see #8. +] + +const stereoModels = [ + 'ZP90' // Connect +] + // ===== SONOS ACCESSORY ======================================================= // Constructor for ZpAccessory. @@ -37,6 +65,9 @@ function ZpAccessory (platform, zp) { this.uuid_base = zp.id this.zp = zp this.platform = platform + this.tv = tvModels.includes(this.zp.model) + this.hasBalance = stereoModels.includes(this.zp.model) || + (this.zp.hasSlaves && !tvModels.includes(this.zp.model)) my = my || this.platform.my this.subscriptions = {} this.state = { @@ -46,6 +77,9 @@ function ZpAccessory (platform, zp) { } this.log = this.platform.log this.parser = new xml2js.Parser() + if (this.tv !== this.zp.tv) { + this.log.warn('%s: warning: TV detection fails for %s: tv: %j, zp.tv: %j', this.name, this.zp.model, this.tv, this.zp.tv) + } this.infoService = new Service.AccessoryInformation() this.infoService @@ -55,10 +89,113 @@ function ZpAccessory (platform, zp) { .updateCharacteristic(Characteristic.FirmwareRevision, this.zp.version) this.services = [this.infoService] - this.groupService = new this.platform.SpeakerService(this.name, 'group') - this.groupService.addOptionalCharacteristic(Characteristic.On) - this.groupService.getCharacteristic(Characteristic.On) - .on('set', this.setGroupOn.bind(this)) + if (this.platform.tv) { + this.groupService = new Service.Television(this.name, 'group') + this.groupService.getCharacteristic(Characteristic.ConfiguredName) + .updateValue(this.name) + .on('set', (value, callback) => { + this.log.info('%s: configured name changed to %j', this.name, value) + callback() + }) + this.groupService.getCharacteristic(Characteristic.SleepDiscoveryMode) + .updateValue(Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE) + this.groupService.getCharacteristic(Characteristic.Active) + .on('set', (value, callback) => { + this.log.info('%s: active changed to %s', this.name, value) + const on = value === Characteristic.Active.ACTIVE + return this.setGroupOn(on, callback) + }) + this.groupService.getCharacteristic(Characteristic.ActiveIdentifier) + .setProps({ maxValue: this.tv ? 3 : 2 }) + .setValue(1) + .on('set', (value, callback) => { + this.log.info('%s: active identifier changed to %j', this.name, value) + callback() + }) + this.groupService.getCharacteristic(Characteristic.RemoteKey) + .on('set', (value, callback) => { + this.log.debug('%s: %s (%j)', this.name, remoteKeys[value], value) + switch (value) { + case Characteristic.RemoteKey.PLAY_PAUSE: + return this.setGroupOn(!this.state.group.on, callback) + case Characteristic.RemoteKey.ARROW_LEFT: + return this.setGroupChangeTrack(-1, callback, false) + case Characteristic.RemoteKey.ARROW_RIGHT: + return this.setGroupChangeTrack(1, callback, false) + default: + return callback() + } + }) + this.groupService.getCharacteristic(Characteristic.PowerModeSelection) + .on('set', (value, callback) => { + this.log.info('%s: power mode selection changed to %j', this.name, value) + return callback() + }) + this.services.push(this.groupService) + + this.televisionSpeakerService = new Service.TelevisionSpeaker(this.zp.zone + ' Speakers', 'zone') + this.televisionSpeakerService + .updateCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE) + this.televisionSpeakerService.getCharacteristic(Characteristic.VolumeSelector) + .on('set', (value, callback) => { + this.log.debug('%s: %s (%j)', this.name, volumeSelectors[value], value) + const volume = value === Characteristic.VolumeSelector.INCREMENT ? 1 : -1 + this.setZoneChangeVolume(volume, callback, false) + }) + this.services.push(this.televisionSpeakerService) + // this.groupService.addLinkedService(this.televisionSpeakerService) + + const displayOrder = [] + + this.inputService1 = new Service.InputSource(this.name, 1) + this.inputService1 + .updateCharacteristic(Characteristic.ConfiguredName, 'Uno') + .updateCharacteristic(Characteristic.Identifier, 1) + .updateCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.TUNER) + .updateCharacteristic(Characteristic.InputDeviceType, Characteristic.InputDeviceType.AUDIO_SYSTEM) + .updateCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) + .updateCharacteristic(Characteristic.CurrentVisibilityState, Characteristic.CurrentVisibilityState.SHOWN) + this.services.push(this.inputService1) + this.groupService.addLinkedService(this.inputService1) + displayOrder.push(0x01, 0x04, 0x01, 0x00, 0x00, 0x00) + + this.inputService2 = new Service.InputSource(this.name, 2) + this.inputService2 + .updateCharacteristic(Characteristic.ConfiguredName, 'Due') + .updateCharacteristic(Characteristic.Identifier, 2) + .updateCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.TUNER) + .updateCharacteristic(Characteristic.InputDeviceType, Characteristic.InputDeviceType.AUDIO_SYSTEM) + .updateCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) + .updateCharacteristic(Characteristic.CurrentVisibilityState, Characteristic.CurrentVisibilityState.SHOWN) + this.services.push(this.inputService2) + this.groupService.addLinkedService(this.inputService2) + displayOrder.push(0x01, 0x04, 0x02, 0x00, 0x00, 0x00) + + if (this.tv) { + this.inputService3 = new Service.InputSource(this.name, 3) + this.inputService3 + .updateCharacteristic(Characteristic.ConfiguredName, 'TV') + .updateCharacteristic(Characteristic.Identifier, 3) + .updateCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.TUNER) + .updateCharacteristic(Characteristic.InputDeviceType, Characteristic.InputDeviceType.TV) + .updateCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) + .updateCharacteristic(Characteristic.CurrentVisibilityState, Characteristic.CurrentVisibilityState.SHOWN) + this.services.push(this.inputService3) + this.groupService.addLinkedService(this.inputService3) + displayOrder.push(0x01, 0x04, 0x03, 0x00, 0x00, 0x00) + } + + displayOrder.push(0x00, 0x00) + this.groupService.getCharacteristic(Characteristic.DisplayOrder) + .updateValue(Buffer.from(displayOrder).toString('base64')) + } else { + this.groupService = new this.platform.SpeakerService(this.name, 'group') + this.services.push(this.groupService) + this.groupService.addOptionalCharacteristic(Characteristic.On) + this.groupService.getCharacteristic(Characteristic.On) + .on('set', this.setGroupOn.bind(this)) + } + this.groupService.addOptionalCharacteristic(this.platform.VolumeCharacteristic) this.groupService.getCharacteristic(this.platform.VolumeCharacteristic) .on('set', this.setGroupVolume.bind(this)) @@ -78,8 +215,14 @@ function ZpAccessory (platform, zp) { .updateValue(0) .on('set', this.setGroupChangeTrack.bind(this)) this.groupService.addOptionalCharacteristic(my.Characteristic.CurrentTrack) + if (this.tv) { + this.groupService.addOptionalCharacteristic(my.Characteristic.TV) + } this.groupService.addOptionalCharacteristic(my.Characteristic.SonosGroup) - this.services.push(this.groupService) + this.groupService.addOptionalCharacteristic(my.Characteristic.SonosCoordinator) + this.groupService.getCharacteristic(my.Characteristic.SonosCoordinator) + .updateValue(false) + .on('set', this.setGroupSonosCoordinator.bind(this)) this.zoneService = new this.platform.SpeakerService( this.zp.zone + ' Speakers', 'zone' @@ -97,6 +240,11 @@ function ZpAccessory (platform, zp) { this.zoneService.addOptionalCharacteristic(Characteristic.Mute) this.zoneService.getCharacteristic(Characteristic.Mute) .on('set', this.setZoneMute.bind(this)) + if (this.hasBalance) { + this.zoneService.addOptionalCharacteristic(my.Characteristic.Balance) + this.zoneService.getCharacteristic(my.Characteristic.Balance) + .on('set', this.setZoneBalance.bind(this)) + } this.zoneService.addOptionalCharacteristic(my.Characteristic.Bass) this.zoneService.getCharacteristic(my.Characteristic.Bass) .on('set', this.setZoneBass.bind(this)) @@ -165,11 +313,17 @@ ZpAccessory.prototype.copyCoordinator = function () { this.log.debug('%s: copy group characteristics from %s', this.name, coordinator.name) if (this.state.group.on !== coordinator.state.group.on) { this.log.debug( - '%s: set member power (play/pause) from %s to %s', this.name, + '%s: set member %s (play/pause) from %s to %s', this.name, + this.platform.tv ? 'active' : 'power', this.state.group.on, coordinator.state.group.on ) this.state.group.on = coordinator.state.group.on - this.groupService.updateCharacteristic(Characteristic.On, this.state.group.on) + if (this.platform.tv) { + const active = this.state.group.on ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE + this.groupService.updateCharacteristic(Characteristic.Active, active) + } else { + this.groupService.updateCharacteristic(Characteristic.On, this.state.group.on) + } } if (this.state.group.volume !== coordinator.state.group.volume) { this.log.debug( @@ -215,21 +369,13 @@ ZpAccessory.prototype.copyCoordinator = function () { ZpAccessory.prototype.becomePlatformCoordinator = function () { if (!this.platform.coordinator) { - this.log('%s: platform coordinator', this.name) - this.platform.coordinator = this + this.log.info('%s: platform coordinator', this.name) + this.platform.setPlatformCoordinator(this) this.state.zone.on = true this.zoneService.updateCharacteristic(Characteristic.On, this.state.zone.on) } } -ZpAccessory.prototype.quitPlatformCoordinator = function () { - if (this.platform.coordinator === this) { - this.platform.coordinator = null - } - this.state.zone.on = false - this.zoneService.updateCharacteristic(Characteristic.On, this.state.zone.on) -} - // ===== SONOS EVENTS ========================================================== ZpAccessory.prototype.createSubscriptions = function () { @@ -313,9 +459,9 @@ ZpAccessory.prototype.handleAVTransportEvent = function (data) { if (err) { return } - let on = this.state.group.on - let track = this.state.group.track - let currentTransportActions = this.state.group.currentTransportActions + let on + let track + let currentTransportActions const event = json.Event.InstanceID[0] // this.log.debug('%s: AVTransport event: %j', this.name, event) if (event.TransportState) { @@ -348,7 +494,7 @@ ZpAccessory.prototype.handleAVTransportEvent = function (data) { case 'x-sonosapi-vli': // Airplay2. track = 'Airplay2' break - case 'aac': // Radio stream (e.g. DI.fm) + case 'aac': // Radio stream (e.g. DI.fm) case 'x-sonosapi-stream': // Radio stream. case 'x-rincon-mp3radio': // AirTunes (by homebridge-zp). track = he.decode(item['r:streamContent'][0]) // info @@ -371,6 +517,7 @@ ZpAccessory.prototype.handleAVTransportEvent = function (data) { case 'x-file-cifs': // Library song. case 'x-sonos-http': // See issue #44. case 'http': // Song on iDevice. + case 'https': // Apple Music, see issue #68 case 'x-sonos-spotify': // Spotify song. track = item['dc:title'][0] // song // track = item['dc:creator'][0] // artist @@ -401,12 +548,20 @@ ZpAccessory.prototype.handleAVTransportEvent = function (data) { track = '' } } - if (on !== this.state.group.on) { - this.log.info('%s: set power (play/pause) from %s to %s', this.name, this.state.group.on, on) + if (on != null && on !== this.state.group.on) { + this.log.info( + '%s: set %s (play/pause) from %s to %s', this.name, + this.platform.tv ? 'active' : 'power', this.state.group.on, on + ) this.state.group.on = on - this.groupService.updateCharacteristic(Characteristic.On, this.state.group.on) + if (this.platform.tv) { + const active = on ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE + this.groupService.updateCharacteristic(Characteristic.Active, active) + } else { + this.groupService.updateCharacteristic(Characteristic.On, this.state.group.on) + } } - if (track !== this.state.group.track && + if (track != null && track !== this.state.group.track && track !== 'ZPSTR_CONNECTING' && track !== 'ZPSTR_BUFFERING') { this.log.info( '%s: set current track from %j to %j', this.name, @@ -415,7 +570,28 @@ ZpAccessory.prototype.handleAVTransportEvent = function (data) { this.state.group.track = track this.groupService.updateCharacteristic(my.Characteristic.CurrentTrack, this.state.group.track) } - if (currentTransportActions !== this.state.group.currentTransportActions) { + if (this.tv && this.on != null) { + const tv = on && track === 'TV' + if (tv !== this.state.group.tv) { + this.tvTimer = setTimeout(() => { + this.tvTimer = null + this.log.info( + '%s: set tv from %s to %s', this.name, + this.state.group.tv, tv + ) + this.state.group.tv = tv + this.groupService + .updateCharacteristic(my.Characteristic.TV, this.state.group.tv) + }, tv || this.state.group.tv == null ? 0 : 10000) + } else if (this.tvTimer != null) { + clearTimeout(this.tvTimer) + this.tvTimer = null + } + } + if ( + currentTransportActions != null && + currentTransportActions !== this.state.group.currentTransportActions + ) { // this.log.debug( // '%s: transport actions changed from %j to %j', this.name, // this.state.group.currentTransportActions, currentTransportActions @@ -430,6 +606,7 @@ ZpAccessory.prototype.handleAVTransportEvent = function (data) { ZpAccessory.prototype.handleGroupRenderingControlEvent = function (json) { this.log.debug('%s: GroupRenderingControl event', this.name) + // this.log.debug('%s: GroupRenderingControl event: %j', this.name, json) this.coordinator = this this.leaving = false if (json.GroupVolume) { @@ -460,13 +637,36 @@ ZpAccessory.prototype.handleRenderingControlEvent = function (data) { return } const event = json.Event.InstanceID[0] + // this.log.debug('%s: RenderingControl event: %j', this.name, event) if (event.Volume) { - const volume = Number(event.Volume[0].$.val) + let volume = 0 + let balance = 0 + for (const record of event.Volume) { + switch (record.$.channel) { + case 'Master': + volume = Number(record.$.val) + break + case 'LF': + balance -= Number(record.$.val) + break + case 'RF': + balance += Number(record.$.val) + break + default: + this.log.warn('%s: warning: %s: ingoring unknown Volume channel', this.name, record.$.channel) + return + } + } if (volume !== this.state.zone.volume) { this.log.info('%s: set volume from %s to %s', this.name, this.state.zone.volume, volume) this.state.zone.volume = volume this.zoneService.updateCharacteristic(this.platform.VolumeCharacteristic, this.state.zone.volume) } + if (this.hasBalance && balance !== this.state.zone.balance) { + this.log.info('%s: set balance from %s to %s', this.name, this.state.zone.balance, balance) + this.state.zone.balance = balance + this.zoneService.updateCharacteristic(my.Characteristic.Balance, this.state.zone.balance) + } } if (event.Mute) { const mute = event.Mute[0].$.val === '1' @@ -500,6 +700,32 @@ ZpAccessory.prototype.handleRenderingControlEvent = function (data) { this.zoneService.updateCharacteristic(my.Characteristic.Loudness, this.state.zone.loudness) } } + if (event.NightMode) { + if (this.state.zone.nightSound == null) { + this.zoneService.addOptionalCharacteristic(my.Characteristic.NightSound) + this.zoneService.getCharacteristic(my.Characteristic.NightSound) + .on('set', this.setZoneNightSound.bind(this)) + } + const nightSound = event.NightMode[0].$.val === '1' + if (nightSound !== this.state.zone.nightSound) { + this.log.info('%s: set night sound from %s to %s', this.name, this.state.zone.nightSound, nightSound) + this.state.zone.nightSound = nightSound + this.zoneService.updateCharacteristic(my.Characteristic.NightSound, this.state.zone.nightSound) + } + } + if (event.DialogLevel) { + if (this.state.zone.speechEnhancement == null) { + this.zoneService.addOptionalCharacteristic(my.Characteristic.SpeechEnhancement) + this.zoneService.getCharacteristic(my.Characteristic.SpeechEnhancement) + .on('set', this.setZoneSpeechEnhancement.bind(this)) + } + const speechEnhancement = event.DialogLevel[0].$.val === '1' + if (speechEnhancement !== this.state.zone.speechEnhancement) { + this.log.info('%s: set speech enhancement from %s to %s', this.name, this.state.zone.speechEnhancement, speechEnhancement) + this.state.zone.speechEnhancement = speechEnhancement + this.zoneService.updateCharacteristic(my.Characteristic.SpeechEnhancement, this.state.zone.speechEnhancement) + } + } }) } @@ -624,17 +850,19 @@ ZpAccessory.prototype.setZoneVolume = function (volume, callback) { } // Called by homebridge when characteristic is changed from homekit. -ZpAccessory.prototype.setZoneChangeVolume = function (volume, callback) { +ZpAccessory.prototype.setZoneChangeVolume = function (volume, callback, reset = true) { if (volume === 0) { return callback() } - setTimeout(() => { - this.log.debug('%s: reset volume change to 0', this.name) - this.zoneService.updateCharacteristic(my.Characteristic.ChangeVolume, 0) - }, this.platform.resetTimeout) + if (reset) { + setTimeout(() => { + this.log.debug('%s: reset volume change to 0', this.name) + this.zoneService.updateCharacteristic(my.Characteristic.ChangeVolume, 0) + }, this.platform.resetTimeout) + } this.log.info('%s: volume change %s', this.name, volume) const newVolume = Math.min(Math.max(this.state.zone.volume + volume, 0), 100) - return this.setZoneVolume(newVolume, callback) + this.setZoneVolume(newVolume, callback) } // Called by homebridge when characteristic is changed from homekit. @@ -654,6 +882,66 @@ ZpAccessory.prototype.setZoneMute = function (mute, callback) { }) } +// Called by homebridge when characteristic is changed from homekit. +ZpAccessory.prototype.setZoneBalance = function (balance, callback) { + if (this.state.zone.balance === balance) { + return callback() + } + this.log.info('%s: balance changed from %s to %s', this.name, this.state.zone.balance, balance) + const oldLeft = this.state.zone.balance > 0 ? 100 - this.state.zone.balance : 100 + const oldRight = this.state.zone.balance < 0 ? 100 + this.state.zone.balance : 100 + const left = balance > 0 ? 100 - balance : 100 + const right = balance < 0 ? 100 + balance : 100 + if (oldLeft !== left) { + const args = { + InstanceID: 0, + Channel: 'LF', + DesiredVolume: left + '' + } + this.log.debug('%s: set volume LF from %s to %s', this.name, oldLeft, left) + this.renderingControl._request('SetVolume', args, (err, status) => { + if (err) { + this.log.error('%s: set volume LF: %s', this.name, err) + return callback(err) + } + if (oldRight !== right) { + const args = { + InstanceID: 0, + Channel: 'RF', + DesiredVolume: right + '' + } + this.log.debug('%s: set volume RF from %s to %s', this.name, oldRight, right) + this.renderingControl._request('SetVolume', args, (err, status) => { + if (err) { + this.log.error('%s: set volume RF: %s', this.name, err) + return callback(err) + } + this.state.zone.balance = balance + return callback() + }) + } else { + this.state.zone.balance = balance + return callback() + } + }) + } else if (oldRight !== right) { + const args = { + InstanceID: 0, + Channel: 'RF', + DesiredVolume: right + '' + } + this.log.debug('%s: set volume RF from %s to %s', this.name, oldRight, right) + this.renderingControl._request('SetVolume', args, (err, status) => { + if (err) { + this.log.error('%s: set volume RF: %s', this.name, err) + return callback(err) + } + this.state.zone.balance = balance + return callback() + }) + } +} + // Called by homebridge when characteristic is changed from homekit. ZpAccessory.prototype.setZoneBass = function (bass, callback) { if (this.state.zone.bass === bass) { @@ -716,13 +1004,60 @@ ZpAccessory.prototype.setZoneLoudness = function (loudness, callback) { }) } +// Called by homebridge when characteristic is changed from homekit. +ZpAccessory.prototype.setZoneNightSound = function (nightSound, callback) { + nightSound = !!nightSound + if (this.state.zone.nightSound === nightSound) { + return callback() + } + this.log.info('%s: night sound changed from %s to %s', this.name, this.state.zone.nightSound, nightSound) + const args = { + InstanceID: 0, + EQType: 'NightMode', + DesiredValue: nightSound ? '1' : '0' + } + this.renderingControl._request('SetEQ', args, (err, status) => { + if (err) { + this.log.error('%s: set night mode: %s', this.name, err) + return callback(err) + } + this.state.zone.nightSound = nightSound + return callback() + }) +} + +// Called by homebridge when characteristic is changed from homekit. +ZpAccessory.prototype.setZoneSpeechEnhancement = function (speechEnhancement, callback) { + speechEnhancement = !!speechEnhancement + if (this.state.zone.speechEnhancement === speechEnhancement) { + return callback() + } + this.log.info('%s: speech enhancement changed from %s to %s', this.name, this.state.zone.speechEnhancement, speechEnhancement) + const args = { + InstanceID: 0, + EQType: 'DialogLevel', + DesiredValue: speechEnhancement ? '1' : '0' + } + this.renderingControl._request('SetEQ', args, (err, status) => { + if (err) { + this.log.error('%s: set speech enhancement: %s', this.name, err) + return callback(err) + } + this.state.zone.speechEnhancement = speechEnhancement + return callback() + }) +} + // Called by homebridge when characteristic is changed from homekit. ZpAccessory.prototype.setGroupOn = function (on, callback) { on = !!on if (this.state.group.on === on) { return callback() } - this.log.info('%s: power (play/pause) changed from %s to %s', this.name, this.state.group.on, on) + this.log.info( + '%s: %s (play/pause) changed from %s to %s', this.name, + this.platform.tv ? 'active' : 'power', this.state.group.on, on + ) if (!this.isCoordinator) { return this.coordinator.setGroupOn(on, callback) } @@ -761,7 +1096,16 @@ ZpAccessory.prototype.setGroupOn = function (on, callback) { }) } else { this.log.debug('%s: play/pause not available', this.name) - return callback(new Error()) + setTimeout(() => { + this.log.debug('%s: reset play/pause to %j', this.name, this.state.group.on) + if (this.platform.tv) { + const active = this.state.group.on ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE + this.groupService.updateCharacteristic(Characteristic.Active, active) + } else { + this.groupService.updateCharacteristic(Characteristic.On, this.state.group.on) + } + }, this.platform.resetTimeout) + return callback() } } @@ -790,17 +1134,19 @@ ZpAccessory.prototype.setGroupVolume = function (volume, callback) { } // Called by homebridge when characteristic is changed from homekit. -ZpAccessory.prototype.setGroupChangeVolume = function (volume, callback) { +ZpAccessory.prototype.setGroupChangeVolume = function (volume, callback, reset = true) { if (volume === 0) { return callback() } - setTimeout(() => { - this.log.debug('%s: reset group volume change to 0', this.name) - this.groupService.updateCharacteristic(my.Characteristic.ChangeVolume, 0) - }, this.platform.resetTimeout) + if (reset) { + setTimeout(() => { + this.log.debug('%s: reset group volume change to 0', this.name) + this.groupService.updateCharacteristic(my.Characteristic.ChangeVolume, 0) + }, this.platform.resetTimeout) + } this.log.info('%s: group volume change %s', this.name, volume) const newVolume = Math.min(Math.max(this.state.group.volume + volume, 0), 100) - return this.setGroupVolume(newVolume, callback) + this.setGroupVolume(newVolume, callback) } // Called by homebridge when characteristic is changed from homekit. @@ -830,31 +1176,33 @@ ZpAccessory.prototype.setGroupMute = function (mute, callback) { } // Called by homebridge when characteristic is changed from homekit. -ZpAccessory.prototype.setGroupChangeInput = function (input, callback) { - if (input === 0) { - return callback() - } - setTimeout(() => { - this.log.debug('%s: reset group input change to 0', this.name) - this.groupService.updateCharacteristic(my.Characteristic.ChangeInput, 0) - }, this.platform.resetTimeout) - this.log.info('%s: group input change %s', this.name, input) - if (!this.isCoordinator) { - return this.coordinator.setGroupChangeInput(input, callback) - } - this.log.debug('%s: input change not yet implemented', this.name) - return callback(new Error()) -} +// ZpAccessory.prototype.setGroupChangeInput = function (input, callback) { +// if (input === 0) { +// return callback() +// } +// setTimeout(() => { +// this.log.debug('%s: reset group input change to 0', this.name) +// this.groupService.updateCharacteristic(my.Characteristic.ChangeInput, 0) +// }, this.platform.resetTimeout) +// this.log.info('%s: group input change %s', this.name, input) +// if (!this.isCoordinator) { +// return this.coordinator.setGroupChangeInput(input, callback) +// } +// this.log.debug('%s: input change not yet implemented', this.name) +// return callback() +// } // Called by homebridge when characteristic is changed from homekit. -ZpAccessory.prototype.setGroupChangeTrack = function (track, callback) { +ZpAccessory.prototype.setGroupChangeTrack = function (track, callback, reset = true) { if (track === 0) { return callback() } - setTimeout(() => { - this.log.debug('%s: reset group track change to 0', this.name) - this.groupService.updateCharacteristic(my.Characteristic.ChangeTrack, 0) - }, this.platform.resetTimeout) + if (reset) { + setTimeout(() => { + this.log.debug('%s: reset group track change to 0', this.name) + this.groupService.updateCharacteristic(my.Characteristic.ChangeTrack, 0) + }, this.platform.resetTimeout) + } this.log.info('%s: group track change %s', this.name, track) if (!this.isCoordinator) { return this.coordinator.setGroupChangeTrack(track, callback) @@ -879,8 +1227,24 @@ ZpAccessory.prototype.setGroupChangeTrack = function (track, callback) { }) } else { this.log.debug('%s: next/previous track not available', this.name) - return callback(new Error()) + return callback() + } +} + +// Called by homebridge when characteristic is changed from homekit. +ZpAccessory.prototype.setGroupSonosCoordinator = function (on, callback) { + on = !!on + if (on && this.platform.coordinator === this) { + return callback() } + this.zoneService.updateCharacteristic(Characteristic.On, false) + this.setZoneOn(false, () => { + this.platform.coordinator = null + if (on) { + this.becomePlatformCoordinator() + } + return callback() + }) } // Called by homebridge when characteristic is read from homekit. diff --git a/lib/ZpAlarm.js b/lib/ZpAlarm.js index 3b548e7..9e6c8e6 100644 --- a/lib/ZpAlarm.js +++ b/lib/ZpAlarm.js @@ -1,5 +1,5 @@ -// homebridge-zp/lib/ZpAccessory.js -// Copyright © 2016-2018 Erik Baauw. All rights reserved. +// homebridge-zp/lib/ZpAlarm.js +// Copyright © 2016-2019 Erik Baauw. All rights reserved. // // Homebridge plugin for Sonos ZonePlayer. diff --git a/lib/ZpPlatform.js b/lib/ZpPlatform.js index d178750..03f7e56 100644 --- a/lib/ZpPlatform.js +++ b/lib/ZpPlatform.js @@ -1,5 +1,5 @@ // homebridge-zp/lib/ZpPlatform.js -// Copyright © 2016-2018 Erik Baauw. All rights reserved. +// Copyright © 2016-2019 Erik Baauw. All rights reserved. // // Homebridge plugin for Sonos ZonePlayer. // @@ -123,16 +123,18 @@ function ZpPlatform (log, config) { this.VolumeCharacteristic = Characteristic.Volume break } + this.tv = config.tv || false this.speakers = config.speakers || false this.leds = config.leds || false this.alarms = config.alarms || false - this.resetTimeout = config.resetTimeout || 250 // milliseconds + this.resetTimeout = config.resetTimeout || 500 // milliseconds this.searchTimeout = config.searchTimeout || 15 // seconds this.searchTimeout *= 1000 // milliseconds this.subscriptionTimeout = config.subscriptionTimeout || 30 // minutes this.subscriptionTimeout *= 60 // seconds this.players = [] + this.slaves = [] this.zpAccessories = {} var msg = util.format( @@ -159,14 +161,6 @@ function ZpPlatform (log, config) { process.on('exit', () => { this.log.info('exit') }) _homebridge.on('shutdown', this.onExit.bind(this)) - if (process.listenerCount('uncaughtException') === 0) { - process.on('uncaughtException', (error) => { - this.log.error('uncaught exception\n%s', error.stack) - if (!this.shuttingDown) { - process.kill(process.pid, 'SIGTERM') - } - }) - } this.findPlayers() } @@ -177,6 +171,9 @@ ZpPlatform.prototype.accessories = function (callback) { setTimeout(() => { this.listen(() => { for (const zp of this.players) { + if (this.slaves.includes(zp.zone)) { + zp.hasSlaves = true + } const accessory = new ZpAccessory(this, zp) this.zpAccessories[zp.id] = accessory accessoryList.push(accessory) @@ -188,13 +185,13 @@ ZpPlatform.prototype.accessories = function (callback) { host: 'registry.npmjs.org', name: 'npm registry' }) - npmRegistry.get(packageJson.name).then((response) => { + npmRegistry.get('/' + packageJson.name).then((response) => { if ( response && response['dist-tags'] && response['dist-tags'].latest !== packageJson.version ) { this.log.warn( - 'warning: lastest version: %s v%s', packageJson.name, + 'warning: latest version: %s v%s', packageJson.name, response['dist-tags'].latest ) } @@ -277,6 +274,7 @@ ZpPlatform.prototype.findPlayers = function () { zp.id = 'RINCON_' + info.MACAddress.replace(/:/g, '') + ('00000' + zp.port).substr(-5, 5) zp.version = info.DisplaySoftwareVersion + zp.tv = Number(info.HTAudioIn) > 0 zoneGroupTopology.GetZoneGroupAttributes({}, (err, attrs) => { if (err) { this.log.error('%s: error %s', zp.zone, err) @@ -287,6 +285,7 @@ ZpPlatform.prototype.findPlayers = function () { '%s: ignore slave %s v%s player %s at %s:%s', zp.zone, zp.model, zp.version, zp.id, zp.host, zp.port ) + this.slaves.push(zp.zone) } else { this.log.debug( '%s: setup %s v%s player %s at %s:%s', @@ -340,6 +339,16 @@ ZpPlatform.prototype.groupMembers = function (group) { return members } +ZpPlatform.prototype.setPlatformCoordinator = function (group) { + this.coordinator = group + for (const id in this.zpAccessories) { + const accessory = this.zpAccessories[id] + accessory.groupService.updateCharacteristic( + my.Characteristic.SonosCoordinator, accessory === group + ) + } +} + ZpPlatform.prototype.onExit = function () { if (this.shuttingDown) { return diff --git a/package-lock.json b/package-lock.json index d446203..51627a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "homebridge-zp", - "version": "0.3.0", + "version": "0.3.16", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -15,6 +15,19 @@ "json-schema-traverse": "^0.3.0" } }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -52,16 +65,57 @@ "tweetnacl": "^0.14.3" } }, + "bonjour-hap": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.5.1.tgz", + "integrity": "sha512-JqJXX5+i1NRGt8GyIPb+nBNjwrHbWe5Pb+HSuRMG/B62tPRHQ4Jyv3yX7hy1pHfRrV2OhnWpd+ljBtMb24R5rA==", + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==" + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, "combined-stream": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", @@ -91,11 +145,38 @@ "ms": "2.0.0" } }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=" + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "requires": { + "buffer-indexof": "^1.0.0" + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -106,6 +187,11 @@ "safer-buffer": "^2.1.0" } }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -163,26 +249,33 @@ "har-schema": "^2.0.0" } }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, "homebridge-lib": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/homebridge-lib/-/homebridge-lib-2.0.0.tgz", - "integrity": "sha512-sEYn12ZFRWMmwzqmH5xGs3loAVU02eca+or2E71Kx+qxbqvl/a+o1YPlQQBvgv4TB0lgksnZfkKaxXaJy9DZkA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/homebridge-lib/-/homebridge-lib-4.1.1.tgz", + "integrity": "sha512-tj3jQBl6LX0n1Tu0MnHhZKmis+mYX+wEYcC0Qo893Z4DOV/PDjGbjOCO0xU39ST1t+x5h9s6C7SvugTLZqTI3A==", "requires": { - "debug": "^4.1.0", - "moment": "^2.22.2", - "request": "^2.88.0", - "semver": "^5.6.0" + "bonjour-hap": "~3.5.1", + "chalk": "~2.4.2", + "debug": "~4.1.1", + "moment": "~2.24.0", + "request": "~2.88.0", + "semver": "~6.0.0" }, "dependencies": { "debug": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", - "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { "ms": "^2.1.1" } @@ -204,6 +297,11 @@ "sshpk": "^1.7.0" } }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -260,15 +358,29 @@ } }, "moment": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", - "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=" + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -344,9 +456,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz", + "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==" }, "sonos": { "version": "0.22.2", @@ -390,6 +502,19 @@ "resolved": "https://registry.npmjs.org/step/-/step-1.0.0.tgz", "integrity": "sha1-swDp0q6QV9TXhjOq4jA4E6lL3/I=" }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "thunky": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.3.tgz", + "integrity": "sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow==" + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", diff --git a/package.json b/package.json index 8cc7d45..895bf04 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "homebridge-zp", "description": "Homebridge plugin for Sonos ZonePlayer", - "version": "0.3.0", + "version": "0.3.16", "author": "Erik Baauw", "license": "Apache-2.0", "keywords": [ @@ -14,16 +14,16 @@ ], "main": "index.js", "engines": { - "homebridge": "^0.4.45", - "node": "^10.13.0" + "homebridge": "^0.4.49", + "node": "^10.15.3" }, "dependencies": { - "he": "^1.2.0", - "homebridge-lib": "^2.0.0", - "request": "^2.88.0", - "semver": "^5.6.0", - "sonos": "^0.22.2", - "xml2js": "^0.4.19" + "he": "~1.2.0", + "homebridge-lib": "~4.1.1", + "request": "~2.88.0", + "semver": "~6.0.0", + "sonos": "~0.22.2", + "xml2js": "~0.4.19" }, "scripts": { "test": "standard && echo \"Error: no test specified\" && exit 1"