diff --git a/.gitignore b/.gitignore
index 231ff5e..fe9fb81 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
node_modules
out
npm-debug.log
+.DS_Store
diff --git a/README.md b/README.md
index 2ee2cff..f417b08 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,9 @@
-
+
+
# Homebridge ZP
[![Downloads](https://img.shields.io/npm/dt/homebridge-zp.svg)](https://www.npmjs.com/package/homebridge-zp)
[![Version](https://img.shields.io/npm/v/homebridge-zp.svg)](https://www.npmjs.com/package/homebridge-zp)
@@ -20,11 +21,12 @@ Copyright © 2016-2024 Erik Baauw. All rights reserved.
This [Homebridge](https://github.com/homebridge/homebridge) plugin exposes [Sonos](http://www.sonos.com) zone players 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;
- Support for Sonos groups, created through the Sonos app;
- Control from HomeKit of play/pause, sleep timer, next/previous track, volume, and mute per Sonos group;
- Control from HomeKit of input selection per group, from Sonos favourites and local sources, like LineIn, Airplay;
-- Optional control from HomeKit of volume, mute, balance, bass, treble, loudness, night sound, and speech enhancement per Sonos zone;
+- Optional control from HomeKit of volume, mute, balance, bass, treble, loudness, night sound, and speech enhancement per Sonos zone, as well as surround/height level etc for home theater configurations;
- Optional control from HomeKit for Sonos zones leaving Sonos groups, and for Sonos zones creating/joining one Sonos group;
- Optional control from HomeKit to enable/disable Sonos alarms;
- Real-time monitoring from HomeKit of state per Sonos group and, optionally, per Sonos zone.
@@ -33,14 +35,32 @@ Like the Sonos app, Homebridge ZP subscribes to zone player events to receive no
Note that Sonos doesn't support events for these, so Homebridge ZP cannot provide real-time monitoring for this;
- Includes command-line tools, for controlling Sonos zone players and for troubleshooting.
+## Contents
+
+* [Prerequisites](#prerequisites)
+* [zones](#zones)
+* [TV](#tv)
+* [TV-Enabled Zones](#tv-enabled-zones)
+* [Groups](#groups)
+* [Speakers](#speakers)
+* [Command-line Tool](#command-line-tool)
+* [Installation](#installation)
+* [Configuration](#configuration)
+* [Troubleshooting](#troubleshooting)
+* [Caveats](#caveats)
+
+
+
### Prerequisites
You need a server to run Homebridge.
+
This can be anything running [Node.js](https://nodejs.org): from a Raspberry Pi, a NAS system, or an always-on PC running Linux, macOS, or Windows.
See the [Homebridge Wiki](https://github.com/homebridge/homebridge/wiki) for details.
I run Homebridge ZP on a Raspberry Pi 3B+.
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 the [Eve](https://www.evehome.com/en/eve-app) app (free) or Matthias Hochgatterer's [Home+](https://hochgatterer.me/home/) app (paid).
@@ -48,6 +68,7 @@ As Sonos uses UPnP to discover the zone players, the server running Homebridge m
As HomeKit uses Bonjour to discover Homebridge, the server running Homebridge must be on the same subnet as your iDevices running HomeKit.
For remote access and for HomeKit automations, you need to setup an Apple TV (4th generation or later), HomePod, or iPad as [home hub](https://support.apple.com/en-us/HT207057).
+
### Zones
Homebridge ZP 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. The standard `On` characteristic is used for play/pause control.
@@ -60,28 +81,46 @@ Despite the "HomeKit" branding, technically, this has nothing to do with HomeKit
No Homebridge plugin can expose speakers that look like AirPlay2 speakers in the Home app.
Also note that these Airplay2 speakers cannot be accessed by other HomeKit apps.
+
+### TV
+
When `"tv": true` is set in `config.json`, Homebridge ZP creates an additional *Television* accessory per zone, allowing input selection from Apple's Home app and control from the *Remote* widget.
Note that Apple has imposed some technical restrictions on *Television* accessories:
-- They cannot be bridged; they need to be paired to HomeKit individually;
+
+- They cannot be bridged; they need to be paired to HomeKit individually.
- They cannot be accessed by HomeKit apps; only from Apple's Home app.
+
+### Tv-Enabled Zones
+Many Sonos products, such as Amp, Beam, Arc, etc have HDMI inputs on them, which causes Homebridge ZP to think that there is a TV connected to any product which supports TV input. Because there is no reliable way to know if this is the case, the plugin allows you to customize what zones actually have a TV connected. All zones are enabled by default, and disabling a zone will cause it's TV audio input to be hidden within Homekit. The zone names will auto-populate anytime HomeBridge is restarted, and will create a boolian for each zone in your config. The easiest way to define this is by using [Homebridge Config UI X](https://github.com/homebridge/homebridge-config-ui-x) or manually by adding "tvEnabledZones": {
+ }
+to your config.json, like this:
+
+```json
+ "tvEnabledZones": {
+ "Patio": false,
+ "Kitchen": false,
+ "Living Room": true,
+ "Master Bathroom": false,
+ "Master Bedroom": true
+ },
+ "platform": "ZP"
+```
+
+
+
### Groups
-When multiple Sonos zones, e.g. *Living Room* and *Kitchen*, are grouped into one Sonos group, the Sonos app shows them as a single room, e.g. *Living Room + 1*, with shared control for play/pause, music source, and (group) volume and mute.
-When this group is broken, each zone forms a separate standalone group, containing only that zone.
-The Sonos app shows each standalone group as a separate room, with separate control per room for play/pause, music source, and (zone) volume and mute.
+When you combine Sonos zones, such as *Living Room* and *Kitchen*, into one group, the Sonos app shows them as a single room, e.g. *Living Room + 1*. This allows you to control both rooms together for play/pause, music source, and volume/mute. When you ungroup them, each room goes back to being separate, with its own controls.
+
+If Homebridge ZP would mimic this behaviour, dynamically creating and deleting accessories for groups, HomeKit would lose the assignment to HomeKit rooms, groups, scenes, and automations, every time an accessory is deleted. Consequently, you would have to reconfigure HomeKit each time you group or ungroup Sonos zones.
-If Homebridge ZP would mimic this behaviour, dynamically creating and deleting accessories for groups, HomeKit would lose the assignment to HomeKit rooms, groups, scenes, and automations, every time an accessory is deleted.
-Consequently, you would have to reconfigure HomeKit each time you group or ungroup Sonos zones.
+To overcome this, Homebridge ZP creates an accessory for each Sonos zone, which manages the group the zone belongs to. When zones are separate, the controls for *Living Room* only effect the *Living Room* zone, and the controls for *Kitchen* only effect the *Kitchen* zone. When the zones are grouped, controls in any zone in that group will effect all speakers in the group e.g. *Living Room + 1*.
-To overcome this, Homebridge ZP creates an accessory and corresponding service for each Sonos zone. This service actually controls the Sonos *group* the zone is in rather than the zone.
-When separated, the *Living Room Sonos* service controls the standalone *Living Room* group, consisting of only the *Living Room* zone; and the *Kitchen Sonos* service controls the standalone *Kitchen* group, consisting of only the *Kitchen* zone.
-When grouped, both the *Living Room Sonos* service and the *Kitchen Sonos* service control the multi-zone *Living Room + 1* group, containing both the *Living Room* and *Kitchen* zones.
-The `Sonos Group` characteristic shows which group the zone belongs to, or rather: the name of the group coordinator zone, in this example: *Living Room*.
+The `Sonos Group` characteristic shows which group a speaker belongs to by displaying the name of the main speaker in the group, like *Living Room*.
-So when grouped, changing the *Living Room Sonos* `Volume` changes the volume of both the *Living Room* zone and the *Kitchen* zone.
-So does changing the *Kitchen Sonos* `Volume`.
-When ungrouped, changing the *Living Room Sonos* `Volume` only changes the volume of the *Living Room* zone; and changing the *Kitchen Sonos* `Volume` only changes the volume of the *Kitchen* zone.
+So, when grouped, adjusting the *Living Room* volume changes the volume for both *Living Room* and *Kitchen*. The same happens if you adjust the volume for *Kitchen*. When ungrouped, changing the *Living Room* volume only affects *Living Room*, and changing the *Kitchen* volume only affects *Kitchen*.
+
### Speakers
To change the volume of an individual zone in a multi-zone group, an additional `Volume` characteristic is needed for the zone, next to the `Volume` characteristic for the group.
As HomeKit doesn't support multiple characteristics of the same type per service, it actually requires an additional service.
@@ -99,19 +138,23 @@ Note that `Bass`, `Treble`, and `Loudness` are custom characteristics. They mig
Like the *Sonos* service, the type of the *Speakers* service can be changed in `config.json` from the default `Switch`.
+
### Command-Line Tool
Homebridge ZP includes a command-line tool, `zp`, to interact with your Sonos Zone Players from the command line.
It takes a `-h` or `--help` argument to provide a brief overview of its functionality and command-line arguments.
+
### Installation
To install Homebridge ZP:
-- Follow the instructions on the [Homebridge Wiki](https://github.com/homebridge/homebridge/wiki) to install Node.js and Homebridge;
-- Install the Homebridge ZP plugin through Homebridge Config UI X or manually by:
+
+- Follow the instructions on the [Homebridge Wiki](https://github.com/homebridge/homebridge/wiki) to install Node.js and Homebridge
+- Install the Homebridge ZP plugin through [Homebridge Config UI X](https://github.com/homebridge/homebridge-config-ui-x) or manually by:
```
$ sudo npm -g i homebridge-zp
```
- Edit `config.json` and add the `ZP` platform provided by Homebridge ZP, see [**Configuration**](#configuration).
+
### Configuration
In Homebridge's `config.json` you need to specify Homebridge ZP as a platform plugin:
```json
@@ -127,9 +170,11 @@ Key | Default | Description
--- | ------- | -----------
`address` | _(discovered)_ | The IP address for the web server Homebridge ZP creates to receive notifications from Sonos zone players. This must be an IP address of the server running Homebridge ZP, reachable by the zone players. You might need to set this on a multi-homed server, if Homebridge ZP binds to the wrong network interface.
`alarms` | `false` | Flag whether to expose an additional service per Sonos alarm.
-`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.
-`excludeAirPlay` | `false` | Flag whether not to expose zone players that support Airplay, since they natively show up in Apple's Home app.
+`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.
+`excludeAirPlay` | `false` | Flag whether not to expose zone players that support Airplay, since they natively show up in Apple's Home app. Note that if you only have an S2 system, enabling this option will essentially render the plugin unusable, as all zones will be hidden from Homekit.
`forceS2` | `false` | Flag whether to expose only S2 zone players. See [**Split Sonos System**](#split-sonos-system) below.
+`filterFavourites` | `false` | Flag whether or not to exclude audio inputs from the favourites list in Homekit. see [**TV**](#tv) for details.
+`tvEnabledZones` | `dynamically updated` | Object key to specify whether a zone will show a TV audio input in Homekit. see [**TV-Enabled Zones**](#tv-enabled-zones) for details.
`heartrate` | (disabled) | Interval (in seconds) to poll zone players when `leds` is set.
`leds` | `false` | Flag whether to expose an additional *Lightbulb* service per zone for the status LED. This also supports locking the physical controls.
`nameScheme` | `"% Sonos"` | The name scheme for the HomeKit accessories. `%` is replaced with the zone 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.
@@ -139,7 +184,7 @@ Key | Default | Description
`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 zone player.
`timeout` | `15` | The timeout (in seconds) to wait for a response from a Sonos zone player.
-`tv` | `false` | Create an additional, non-bridged TV accessory for each zone.
Note that each TV accessory needs to be paired with HomeKit separately, using the same pin as for Homebridge, as specified in `config.json`.
+`tv` | `false` | Create an additional, non-bridged TV accessory for each zone.
Note that each TV accessory needs to be paired with HomeKit separately, using the same pin as for Homebridge, as specified in `config.json`. see [**TV**](#tv) for more details.
`tvIdPrefix` | `TV` | Prefix for serial number of TV accessories, to enable multiple instances of Homebridge ZP on the same network.
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:
@@ -154,24 +199,29 @@ Below is an example `config.json` that exposes the *Sonos* and *Speakers* servic
]
```
+
#### Split Sonos System
If you have a split Sonos system, Homebridge ZP will expose both the S2 and the S1 zone players.
Of course you can only group S2 zone players with other S2 zone players; and S1 zone players with other S1 zone players.
The same restriction applies when you have multiple Sonos households on your network: you can only group zone players with other zone players in the same household.
+
### Troubleshooting
+
#### Check Dependencies
If you run into Homebridge startup issues, please double-check what versions of Node.js and of Homebridge have been installed.
Homebridge ZP has been developed and tested using the [latest LTS](https://nodejs.org/en/about/releases/) version of Node.js and the [latest](https://www.npmjs.com/package/homebridge) version of Homebridge.
Other versions might or might not work - I simply don't have the bandwidth to test these.
+
#### Run Homebridge ZP Solo
If you run into Homebridge startup issues, please run a separate instance of Homebridge with only Homebridge ZP (and Homebridge Config UI X) enabled in `config.json`.
This way, you can determine whether the issue is related to Homebridge ZP itself, or to the interaction of multiple Homebridge plugins in your setup.
You can start this separate instance of Homebridge on a different system, as a different user, or from a different user directory (specified by the `-U` flag).
Make sure to use a different Homebridge `name`, `username`, and (if running on the same system) `port` in the `config.json` for each instance.
+
#### Debug Log File
Homebridge ZP outputs an info message for each HomeKit characteristic value it sets and for each HomeKit characteristic value change notification it receives.
When Homebridge is started with `-D`, Homebridge ZP outputs a debug message for each request it makes to a Sonos zone player and for each zone player notification event it receives.
@@ -190,6 +240,7 @@ To capture these messages into a log file do the following:
$ gzip homebridge.log
```
+
#### Web Server
Like the Sonos app, Homebridge ZP subscribes to the zone player events to be notified in real-time of changes. It creates a web server to receive these notifications. The IP address and port number for this listener are logged in a debug message, e.g.
```
@@ -197,6 +248,7 @@ Like the Sonos app, Homebridge ZP subscribes to the zone player events to be not
```
To check whether the listener is reachable from the network, open this URL in your web browser. You should see an overview of the active subscriptions per zone player.
+
#### Getting Help
If you have a question, please post a message to the **#zp** channel of the Homebridge community on [Discord](https://discord.gg/3qFgFMk).
@@ -204,6 +256,7 @@ If you encounter a problem, please open an issue on [GitHub](https://github.com/
Please **attach** a copy of `homebridge.log.gz` to the issue, see [**Debug Log File**](#debug-log-file).
Please do **not** copy/paste large amounts of log output.
+
### Caveats
Homebridge ZP is a hobby project of mine, provided as-is, with no warranty whatsoever. I've been running it successfully at my home for years, but your mileage might vary.
diff --git a/config.schema.json b/config.schema.json
index 18e3a43..3d6848e 100644
--- a/config.schema.json
+++ b/config.schema.json
@@ -23,9 +23,20 @@
},
"excludeAirPlay": {
"title": "Exclude AirPlay 2",
- "description": "Exclude AirPlay 2 zone players that are already exposed to Apple's Home app.",
+ "description": "Exclude AirPlay 2 zone players that are already exposed to Apple's Home app. (Please Note: If your system is S2 only, enabling this option will remove all zones from Homebridge. Use with caution.)",
"type": "boolean"
},
+ "filterFavourites": {
+ "title": "Filter Favorites",
+ "description": "Hide non-favorite audio inputs, (AirPlay, Line In etc.) from the favorites/TV Input list in Homekit.",
+ "type": "boolean",
+ "default": false
+ },
+ "forceS2": {
+ "description": "Force S2 compatibility.",
+ "type": "boolean",
+ "default": false
+ },
"heartrate": {
"description": "Interval (in seconds) to poll zone player. Default: disabled.",
"type": "integer"
@@ -103,6 +114,12 @@
"title": "TV ID Prefix",
"description": "Prefix for serial number of TV accessories. Default: 'TV'",
"type": "string"
+ },
+ "tvEnabledZones": {
+ "title": "TV-Enabled Zones",
+ "description": "Specify which zones have a TV connected.",
+ "type": "object",
+ "properties": {}
}
}
},
@@ -124,7 +141,15 @@
"functionBody": "return model.leds"
}
},
- "excludeAirPlay"
+ "excludeAirPlay",
+ "filterFavourites",
+ {
+ "type": "fieldset",
+ "expandable": true,
+ "title": "TV-Enabled Zones",
+ "description": "Specify which zones have a TV connected.",
+ "items": []
+ }
]
},
{
@@ -143,6 +168,7 @@
"tv",
{
"key": "maxFavourites",
+ "type": "text",
"condition": {
"functionBody": "return model.tv"
}
diff --git a/lib/ZpPlatform.js b/lib/ZpPlatform.js
index c81ff4c..a482410 100644
--- a/lib/ZpPlatform.js
+++ b/lib/ZpPlatform.js
@@ -5,26 +5,29 @@
'use strict'
-const events = require('events')
-const homebridgeLib = require('homebridge-lib')
-const ZpHousehold = require('./ZpHousehold')
-const ZpAccessory = require('./ZpAccessory')
-const ZpClient = require('./ZpClient')
-const ZpListener = require('./ZpListener')
-
-// Constructor for ZpPlatform. Called by homebridge on load time.
+const fs = require('fs');
+const path = require('path');
+const events = require('events');
+const homebridgeLib = require('homebridge-lib');
+const ZpHousehold = require('./ZpHousehold');
+const ZpAccessory = require('./ZpAccessory');
+const ZpClient = require('./ZpClient');
+const ZpListener = require('./ZpListener');
+const schemaPath = path.join(__dirname, '../config.schema.json');
+
+// Constructor for ZpPlatform. Called by homebridge on load time.
class ZpPlatform extends homebridgeLib.Platform {
constructor (log, configJson, homebridge) {
- super(log, configJson, homebridge)
- this.parseConfigJson(configJson)
- this.unInitialisedZpClients = 0
- this.households = {} // Households by household id.
- this.zpClients = {} // ZpClient by zoneplayer id.
- this.zpMasters = {} // ZpAccessory.Master delegates by zoneplayer id.
- this.zpSlaves = {} // ZpAccessory.Slave delegates by zonePlayer id.
- this.zpTvs = {} // ZpAccessory.Tv delegates by zonePlayer id.
- this.coordinators = {} // ZpAccessory.Master coordinator per household id.
- this.staleAccessories = {}
+ super(log, configJson, homebridge);
+ this.parseConfigJson(configJson);
+ this.unInitialisedZpClients = 0;
+ this.households = {}; // Households by household id.
+ this.zpClients = {}; // ZpClient by zoneplayer id.
+ this.zpMasters = {}; // ZpAccessory.Master delegates by zoneplayer id.
+ this.zpSlaves = {}; // ZpAccessory.Slave delegates by zonePlayer id.
+ this.zpTvs = {}; // ZpAccessory.Tv delegates by zonePlayer id.
+ this.coordinators = {}; // ZpAccessory.Master coordinator per household id.
+ this.staleAccessories = {};
this
.on('accessoryRestored', this.accessoryRestored)
@@ -32,129 +35,209 @@ class ZpPlatform extends homebridgeLib.Platform {
.on('shutdown', async () => {
for (const id in this.zpClients) {
try {
- await this.zpClients[id].close()
- } catch (error) { this.error(error) }
+ await this.zpClients[id].close();
+ } catch (error) { this.error(error); }
}
- })
+ });
// Setup listener for mDNS announcements.
- this.bonjour = new homebridgeLib.Bonjour()
- this.browser = this.bonjour.find({ type: 'sonos' })
- this.browser.on('up', (message) => { this.handleMdnsMessage(message) })
+ this.bonjour = new homebridgeLib.Bonjour();
+ this.browser = this.bonjour.find({ type: 'sonos' });
+ this.browser.on('up', (message) => { this.handleMdnsMessage(message); });
// Setup listener for UPnP announcements.
- this.upnpConfig({ class: 'urn:schemas-upnp-org:device:ZonePlayer:1' })
+ this.upnpConfig({ class: 'urn:schemas-upnp-org:device:ZonePlayer:1' });
this
.on('upnpDeviceAlive', this.handleUpnpMessage)
- .on('upnpDeviceFound', this.handleUpnpMessage)
+ .on('upnpDeviceFound', this.handleUpnpMessage);
// Setup listener for zoneplayer events.
- this.listener = new ZpListener(this.config.port)
+ this.listener = new ZpListener(this.config.port);
this.listener
- .on('listening', (url) => { this.log('listening on %s', url) })
- .on('close', (url) => { this.log('closed %s', url) })
- .on('error', (error) => { this.warn(error) })
+ .on('listening', (url) => { this.log('listening on %s', url); })
+ .on('close', (url) => { this.log('closed %s', url); })
+ .on('error', (error) => { this.warn(error); });
+
+ this.debug('config: %j', this.config);
+ this.debug('SpeakerService: %j', this.config.SpeakerService.UUID);
+ this.debug('VolumeCharacteristic: %j', this.config.VolumeCharacteristic.UUID);
- this.debug('config: %j', this.config)
- this.debug('SpeakerService: %j', this.config.SpeakerService.UUID)
- this.debug('VolumeCharacteristic: %j', this.config.VolumeCharacteristic.UUID)
+ setTimeout(() => this.updateConfigSchema(), 4000); // Adjust the timeout as needed
}
+
// Parse config.json into this.config.
- parseConfigJson (configJson) {
+ parseConfigJson(configJson) {
this.config = {
- maxFavourites: 96,
- port: 0,
- resetTimeout: 500, // milliseconds
- subscriptionTimeout: 30, // minutes
- timeout: 15, // seconds
- tvIdPrefix: 'TV',
- SpeakerService: this.Services.hap.Switch,
- VolumeCharacteristic: this.Characteristics.hap.Volume
- }
- const optionParser = new homebridgeLib.OptionParser(this.config, true)
+ maxFavourites: 96,
+ port: 0,
+ resetTimeout: 500, // milliseconds
+ subscriptionTimeout: 30, // minutes
+ timeout: 15, // seconds
+ tvIdPrefix: 'TV',
+ SpeakerService: this.Services.hap.Switch,
+ VolumeCharacteristic: this.Characteristics.hap.Volume,
+ filterFavourites: false, // Default value
+ forceS2: false,
+ tvEnabledZones: {} // dynamically updated based on zoneName
+ };
+ const optionParser = new homebridgeLib.OptionParser(this.config, true);
optionParser
- .on('userInputError', (message) => {
- this.warn('config.json: %s', message)
- })
- .stringKey('platform')
- .stringKey('name')
- .boolKey('alarms')
- .boolKey('brightness')
- .boolKey('excludeAirPlay')
- .intKey('heartrate', 1, 60)
- .boolKey('leds')
- .intKey('maxFavourites', 16, 96)
- .intKey('port', 0, 65535)
- .intKey('resetTimeout', 1, 60)
- .enumKey('service')
- .enumKeyValue('service', 'fan', () => {
- this.config.SpeakerService = this.Services.hap.Fan
- this.config.VolumeCharacteristic = this.Characteristics.hap.RotationSpeed
- })
- .enumKeyValue('service', 'light', () => {
- this.config.SpeakerService = this.Services.hap.Lightbulb
- this.config.VolumeCharacteristic = this.Characteristics.hap.Brightness
- })
- .enumKeyValue('service', 'speaker', () => {
- this.config.SpeakerService = this.Services.hap.Speaker
- this.config.VolumeCharacteristic = this.Characteristics.hap.Volume
- })
- .enumKeyValue('service', 'switch', () => {
- this.config.SpeakerService = this.Services.hap.Switch
- this.config.VolumeCharacteristic = this.Characteristics.hap.Volume
- })
- .boolKey('speakers')
- .intKey('subscriptionTimeout', 1, 1440) // minutes
- .intKey('timeout', 1, 60) // seconds
- .boolKey('tv')
- .stringKey('tvIdPrefix', true)
+ .on('userInputError', (message) => {
+ this.warn('config.json: %s', message);
+ })
+ .stringKey('platform')
+ .stringKey('name')
+ .boolKey('alarms')
+ .boolKey('brightness')
+ .boolKey('excludeAirPlay')
+ .boolKey('filterFavourites')
+ .boolKey('forceS2') // Add forceS2 key
+ .intKey('heartrate', 1, 60)
+ .boolKey('leds')
+ .intKey('maxFavourites', 16, 96)
+ .intKey('port', 0, 65535)
+ .intKey('resetTimeout', 1, 60)
+ .enumKey('service')
+ .enumKeyValue('service', 'fan', () => {
+ this.config.SpeakerService = this.Services.hap.Fan;
+ this.config.VolumeCharacteristic = this.Characteristics.hap.RotationSpeed;
+ })
+ .enumKeyValue('service', 'light', () => {
+ this.config.SpeakerService = this.Services.hap.Lightbulb;
+ this.config.VolumeCharacteristic = this.Characteristics.hap.Brightness;
+ })
+ .enumKeyValue('service', 'speaker', () => {
+ this.config.SpeakerService = this.Services.hap.Speaker;
+ this.config.VolumeCharacteristic = this.Characteristics.hap.Volume;
+ })
+ .enumKeyValue('service', 'switch', () => {
+ this.config.SpeakerService = this.Services.hap.Switch;
+ this.config.VolumeCharacteristic = this.Characteristics.hap.Volume;
+ })
+ .boolKey('speakers')
+ .intKey('subscriptionTimeout', 1, 1440) // minutes
+ .intKey('timeout', 1, 60) // seconds
+ .boolKey('tv')
+ .stringKey('tvIdPrefix', true)
+ .objectKey('tvEnabledZones') // Add tvEnabledZones key
try {
- optionParser.parse(configJson)
- if (this.config.port <= 1024) {
- this.config.port = 0
- }
- if (this.config.brightness) {
- if (this.config.service === 'speaker' || this.config.service === 'switch') {
- this.config.VolumeCharacteristic = this.Characteristics.hap.Brightness
+ optionParser.parse(configJson);
+ if (this.config.port <= 1024) {
+ this.config.port = 0;
+ }
+ if (this.config.brightness) {
+ if (this.config.service === 'speaker' || this.config.service === 'switch') {
+ this.config.VolumeCharacteristic = this.Characteristics.hap.Brightness;
+ } else {
+ this.warn(
+ 'config.json: ignoring "brightness" for "service": "%s"',
+ this.config.service
+ );
+ }
+ }
+ this.config.subscriptionTimeout *= 60; // minutes -> seconds
+ } catch (error) { this.fatal(error); }
+}
+
+
+ async fetchZoneNames() {
+ this.log('Fetching zone names...');
+ const zoneNames = new Set(); // Use a set to avoid duplicate zone names if they occur
+ for (const householdId in this.households) {
+ const zpClient = this.households[householdId].zpClient;
+ if (zpClient) {
+ this.log(`Household ID: ${householdId}, ZP Client ID: ${zpClient.id}`);
+ for (const zoneId in zpClient.zones) { // Loop through unique zones
+ const zone = zpClient.zones[zoneId];
+ if (zone && zone.name) {
+ zoneNames.add(zone.name);
+
} else {
- this.warn(
- 'config.json: ignoring "brightness" for "service": "%s"',
- this.config.service
- )
+ this.log(`Zone ID: ${zoneId} is null or undefined`);
}
}
- this.config.subscriptionTimeout *= 60 // minutes -> seconds
- } catch (error) { this.fatal(error) }
+ } else {
+ this.log(`ZP Client for Household ID: ${householdId} is null or undefined`);
+ }
+ }
+ const uniqueZoneNames = Array.from(zoneNames); // Convert set to array
+
+ return uniqueZoneNames;
+}
+
+
+
+ // Update the config schema with dynamic zone checkboxes
+ async updateConfigSchema() {
+ try {
+ this.log('Updating config schema...');
+ const schemaPath = path.join(__dirname, '../config.schema.json');
+ const schema = require(schemaPath);
+
+ // Fetch actual zone names dynamically
+ const zones = await this.fetchZoneNames();
+
+
+ // Reset the zones object
+ schema.schema.properties.tvEnabledZones.properties = {};
+
+ // Populate zones dynamically
+ zones.forEach(zone => {
+ schema.schema.properties.tvEnabledZones.properties[zone] = {
+ type: 'boolean',
+ title: zone,
+ default: true
+ };
+ });
+
+ // Update the form section for checkboxes
+ const zonesFormItems = zones.map(zone => `tvEnabledZones.${zone}`);
+ schema.form.forEach(section => {
+ if (section.title === "What") {
+ section.items.forEach(item => {
+ if (item.title === "TV-Enabled Zones") {
+ item.items = zonesFormItems;
+ }
+ });
+ }
+ });
+
+ // Save the updated schema back to the file
+ fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 2), 'utf-8');
+ this.log('Config schema updated successfully with dynamic zones:', zones);
+ } catch (error) {
+ this.error('Failed to update config schema:', error);
+ }
}
heartbeat (beat) {
if (beat % 300 === 30) {
if (Object.keys(this.households).length === 0) {
- this.warn('no zone players found')
- return
+ this.warn('no zone players found');
+ return;
}
- const now = new Date()
+ const now = new Date();
for (const householdId in this.households) {
- const associatedZpClient = this.households[householdId].zpClient
+ const associatedZpClient = this.households[householdId].zpClient;
for (const id in associatedZpClient.zonePlayers) {
try {
- const zpClient = this.zpClients[id]
+ const zpClient = this.zpClients[id];
if (zpClient == null || zpClient.lastSeen === 'n/a') {
- continue
+ continue;
}
- const delta = Math.round((now - new Date(zpClient.lastSeen)) / 1000)
- const log = (delta >= 600 ? this.log : this.debug).bind(this)
+ const delta = Math.round((now - new Date(zpClient.lastSeen)) / 1000);
+ const log = (delta >= 600 ? this.log : this.debug).bind(this);
log(
'%s [%s]: lastSeen: %s, %js ago at %s, bootSeq: %j', zpClient.id,
zpClient.zonePlayerName, zpClient.lastSeen, delta,
zpClient.address, zpClient.bootSeq
- )
+ );
if (zpClient.delta >= 600) {
- this.lostZonePlayer(zpClient.id)
+ this.lostZonePlayer(zpClient.id);
}
} catch (error) {
- this.error('%s: [%s]: %s', id, this.zpClients[id].address, error)
+ this.error('%s: [%s]: %s', id, this.zpClients[id].address, error);
}
}
}
@@ -163,52 +246,52 @@ class ZpPlatform extends homebridgeLib.Platform {
async accessoryRestored (className, version, id, name, context) {
// this.log(
- // '%s [%s]: restoring %s v%s context: %j',
- // id, name, className, version, context
+ // '%s [%s]: restoring %s v%s context: %j',
+ // id, name, className, version, context
// )
try {
- this.staleAccessories[id] = {}
- await this.createZpClient(id, context.address, context.household)
+ this.staleAccessories[id] = {};
+ await this.createZpClient(id, context.address, context.household);
} catch (error) {
- this.error(error)
+ this.error(error);
}
// this.log(
- // '%s [%s]: %s v%s restore done', id, name, className, version
+ // '%s [%s]: %s v%s restore done', id, name, className, version
// )
}
async handleMdnsMessage (message) {
+ const id = message.txt.info.split('/')[4];
+ const address = message.referer.address;
+ this.debug('mdns: found %s at %s', id, address);
+ const household = message.hhid;
+ const bootseq = parseInt(message.txt.bootseq);
try {
- const id = message.txt.info.split('/')[4]
- const address = message.referer.address
- this.debug('mdns: found %s at %s', id, address)
- const household = message.hhid
- const bootseq = parseInt(message.txt.bootseq)
- const zpClient = await this.createZpClient(id, address, household)
- await zpClient.handleAliveMessage({ id, address, household, bootseq })
- } catch (error) { this.error(error) }
+ const zpClient = await this.createZpClient(id, address, household);
+ await zpClient.handleAliveMessage({ id, address, household, bootseq });
+ } catch (error) { this.error(error); }
}
async handleUpnpMessage (address, message) {
+ const id = message.usn.split(':')[1];
+ if (message.st != null) {
+ this.debug('upnp: found %s at %s', id, address);
+ } else {
+ this.debug('upnp: %s is alive at %s', id, address);
+ }
+ const household = message['x-rincon-household'];
+ const bootseq = parseInt(message['x-rincon-bootseq']);
try {
- const id = message.usn.split(':')[1]
- if (message.st != null) {
- this.debug('upnp: found %s at %s', id, address)
- } else {
- this.debug('upnp: %s is alive at %s', id, address)
- }
- const household = message['x-rincon-household']
- const bootseq = parseInt(message['x-rincon-bootseq'])
- const zpClient = await this.createZpClient(id, address, household)
- await zpClient.handleAliveMessage({ id, address, household, bootseq })
- } catch (error) { this.error(error) }
+ const zpClient = await this.createZpClient(id, address, household);
+ await zpClient.handleAliveMessage({ id, address, household, bootseq });
+ } catch (error) { this.error(error); }
}
// Create new zpClient.
async createZpClient (id, address, household) {
- let zpClient = this.zpClients[id]
+ let zpClient = this.zpClients[id];
if (zpClient != null && zpClient.address === address) {
- return zpClient
+ return zpClient;
}
this.zpClients[id] = new ZpClient({
host: address,
@@ -216,8 +299,8 @@ class ZpPlatform extends homebridgeLib.Platform {
household,
listener: this.listener,
timeout: this.config.timeout
- })
- zpClient = this.zpClients[id]
+ });
+ zpClient = this.zpClients[id];
zpClient
.on('request', (request) => {
this.debug(
@@ -225,14 +308,14 @@ class ZpPlatform extends homebridgeLib.Platform {
zpClient.zonePlayerName == null ? zpClient.address : zpClient.zonePlayerName,
request.id, request.method, request.resource,
request.action == null ? '' : ' ' + request.action
- )
+ );
})
.on('response', (response) => {
this.debug(
'%s [%s]: request %s: status %d %s', zpClient.id,
zpClient.zonePlayerName == null ? zpClient.address : zpClient.zonePlayerName,
response.request.id, response.statusCode, response.statusMessage
- )
+ );
})
.on('error', (error) => {
if (error.request == null) {
@@ -240,85 +323,85 @@ class ZpPlatform extends homebridgeLib.Platform {
'%s [%s]: %s', zpClient.id,
zpClient.zonePlayerName == null ? zpClient.address : zpClient.zonePlayerName,
error
- )
- return
+ );
+ return;
}
if (error.request.body == null) {
this.log(
'%s [%s]: request %d: %s %s', zpClient.id,
zpClient.zonePlayerName == null ? zpClient.address : zpClient.zonePlayerName,
error.request.id, error.request.method, error.request.resource
- )
+ );
} else {
this.log(
'%s [%s]: request %d: %s %s', zpClient.id,
zpClient.zonePlayerName == null ? zpClient.address : zpClient.zonePlayerName,
error.request.id, error.request.method, error.request.resource,
error.request.action
- )
+ );
}
this.warn(
'%s [%s]: request %s: %s', zpClient.id,
zpClient.zonePlayerName == null ? zpClient.address : zpClient.zonePlayerName,
error.request.id, error
- )
+ );
})
.on('message', (message) => {
const notify = message.device === 'ZonePlayer'
? message.service
- : message.device + '/' + message.service
+ : message.device + '/' + message.service;
this.debug(
'%s [%s]: notify %s/Event', zpClient.id,
zpClient.zonePlayerName == null ? zpClient.address : zpClient.zonePlayerName,
notify
- )
+ );
this.vdebug(
'%s [%s]: notify %s/Event: %j', zpClient.id,
zpClient.zonePlayerName == null ? zpClient.address : zpClient.zonePlayerName,
notify, message.parsedBody
- )
+ );
this.vvdebug(
'%s [%s]: notify %s/Event: ', zpClient.id,
zpClient.zonePlayerName == null ? zpClient.address : zpClient.zonePlayerName,
notify, message.body
- )
+ );
})
.on('rebooted', (oldBootSeq) => {
this.warn(
'%s [%s]: rebooted (%j -> %j)', zpClient.id,
zpClient.zonePlayerName == null ? zpClient.address : zpClient.zonePlayerName,
oldBootSeq, zpClient.bootSeq
- )
+ );
})
.on('addressChanged', (oldAddress) => {
this.warn(
'%s [%s]: now at %s', zpClient.id,
zpClient.zonePlayerName == null ? oldAddress : zpClient.zonePlayerName,
zpClient.address
- )
- })
+ );
+ });
try {
- this.unInitialisedZpClients++
+ this.unInitialisedZpClients++;
this.debug(
'%s [%s]: probing (%d jobs)...',
id, address, this.unInitialisedZpClients
- )
- await zpClient.init()
+ );
+ await zpClient.init();
this.debug(
'%s [%s]: %s: %s (%s) v%s, reached over local address %s',
id, address, zpClient.zoneName,
zpClient.modelName, zpClient.modelNumber, zpClient.version,
zpClient.localAddress
- )
- this.topologyChanged = true
- await zpClient.initTopology()
- await this.parseZones(zpClient)
- await zpClient.open()
+ );
+ this.topologyChanged = true;
+ await zpClient.initTopology();
+ await this.parseZones(zpClient);
+ await zpClient.open();
if (!zpClient.invisible) {
- let zpHousehold = this.households[zpClient.household]
+ let zpHousehold = this.households[zpClient.household];
if (zpHousehold == null) {
- zpHousehold = new ZpHousehold(this, zpClient)
- this.households[zpClient.household] = zpHousehold
+ zpHousehold = new ZpHousehold(this, zpClient);
+ this.households[zpClient.household] = zpHousehold;
}
if (
zpHousehold.zpClient == null || (
@@ -326,141 +409,141 @@ class ZpPlatform extends homebridgeLib.Platform {
zpHousehold.zpClient.battery != null
)
) {
- zpHousehold.zpClient = zpClient
+ zpHousehold.zpClient = zpClient;
}
}
- delete this.staleAccessories[id]
- } catch (error) { this.error(error) }
- this.unInitialisedZpClients--
+ delete this.staleAccessories[id];
+ } catch (error) { this.error(error); }
+ this.unInitialisedZpClients--;
this.debug(
'%s [%s]: probing done (%d jobs remaining)',
id, address, this.unInitialisedZpClients
- )
+ );
if (this.unInitialisedZpClients === 0 && this.topologyChanged) {
- this.topologyChanged = false
- this.logTopology()
+ this.topologyChanged = false;
+ this.logTopology();
}
- return zpClient
+ return zpClient;
}
async parseZones (zpClient) {
- const jobs = []
+ const jobs = [];
for (const id in zpClient.zonePlayers) {
if (this.zpClients[id] == null) {
- const zonePlayer = zpClient.zonePlayers[id]
+ const zonePlayer = zpClient.zonePlayers[id];
if (zonePlayer == null) {
- continue
+ continue;
}
jobs.push(
this.createZpClient(
zonePlayer.id, zonePlayer.address, zpClient.household
- ).catch((error) => { this.error(error) })
- )
+ ).catch((error) => { this.error(error); })
+ );
}
}
for (const job of jobs) {
- await job
+ await job;
}
}
lostZonePlayer (id, zoneName) {
- const master = this.zpMasters[id]
+ const master = this.zpMasters[id];
if (master != null) {
- master.sonosService.values.on = false
+ master.sonosService.values.on = false;
master.sonosService.values.statusFault =
- this.Characteristics.hap.StatusFault.GENERAL_FAULT
+ this.Characteristics.hap.StatusFault.GENERAL_FAULT;
if (this.config.speakers) {
- master.speakerService.values.on = false
+ master.speakerService.values.on = false;
}
}
- const slave = this.zpSlaves[id]
+ const slave = this.zpSlaves[id];
if (slave != null) {
slave.ledService.values.statusFault =
- this.Characteristics.hap.StatusFault.GENERAL_FAULT
+ this.Characteristics.hap.StatusFault.GENERAL_FAULT;
}
}
async logTopology () {
for (const id in this.staleAccessories) {
if (this.zpClients[id] != null) {
- this.zpClients[id].removeAllListeners()
- delete this.zpClients[id]
+ this.zpClients[id].removeAllListeners();
+ delete this.zpClients[id];
}
}
if (Object.keys(this.households).length === 0) {
- this.warn('no zone players found')
+ this.warn('no zone players found');
if (Object.keys(this.staleAccessories).length === 0) {
- this.debug('initialised')
- this.emit('initialised')
+ this.debug('initialised');
+ this.emit('initialised');
}
- return
+ return;
}
- const jobs = []
- this.log('found %d households', Object.keys(this.households).length)
+ const jobs = [];
+ this.log('found %d households', Object.keys(this.households).length);
for (const householdId in this.households) {
- const zpHousehold = this.households[householdId]
- const associatedZpClient = zpHousehold.zpClient
+ const zpHousehold = this.households[householdId];
+ const associatedZpClient = zpHousehold.zpClient;
try {
- await zpHousehold.setAssociated(associatedZpClient)
- } catch (error) { this.error(error) }
- const zonePlayers = associatedZpClient.zonePlayers
- const zones = associatedZpClient.zones
- const nZones = Object.keys(zones).length
+ await zpHousehold.setAssociated(associatedZpClient);
+ } catch (error) { this.error(error); }
+ const zonePlayers = associatedZpClient.zonePlayers;
+ const zones = associatedZpClient.zones;
+ const nZones = Object.keys(zones).length;
this.log(
'%s: found %d %s zone players in %d zones', householdId,
Object.keys(zonePlayers).length, associatedZpClient.sonosOs, nZones
- )
- let i = 0
- let j = 0
- let nZonePlayers
+ );
+ let i = 0;
+ let j = 0;
+ let nZonePlayers;
for (const id in zonePlayers) {
try {
- const zpClient = this.zpClients[id]
+ const zpClient = this.zpClients[id];
if (zpClient == null) {
- this.warn('%s: zone player not found', id)
- continue
+ this.warn('%s: zone player not found', id);
+ continue;
}
if (zpClient.role === 'master') {
- i++
- j = 0
- let caps = ''
+ i++;
+ j = 0;
+ let caps = '';
if (zpClient.invisible) {
// Sonos Boost or Sonos Bridge
- caps = ' (invisible)'
+ caps = ' (invisible)';
}
this.log(
'%s %s%s', i < nZones ? '├─' : '└─',
zpClient.zoneDisplayName, caps
- )
- nZonePlayers = 1
- nZonePlayers += zpClient.slaves != null ? zpClient.slaves.length : 0
+ );
+ nZonePlayers = 1;
+ nZonePlayers += zpClient.slaves != null ? zpClient.slaves.length : 0;
// Fixme: handle missing satellites
nZonePlayers += zpClient.satellites != null
? zpClient.satellites.length
- : 0
+ : 0;
}
- j++
- let caps = zpClient.role
- caps += zpClient.airPlay ? ', airPlay' : ''
- caps += zpClient.audioIn ? ', audioIn' : ''
- caps += zpClient.tvIn ? ', tvIn' : ''
+ j++;
+ let caps = zpClient.role;
+ caps += zpClient.airPlay ? ', airPlay' : '';
+ caps += zpClient.audioIn ? ', audioIn' : '';
+ caps += zpClient.tvIn ? ', tvIn' : '';
this.log(
'%s %s %s [%s]: %s (%s) (%s)', i < nZones ? '│ ' : ' ',
j < nZonePlayers ? '├─' : '└─', zpClient.id,
zpClient.zonePlayerName, zpClient.modelName, zpClient.modelNumber, caps
- )
+ );
} catch (error) {
- this.error('%s: [%s]: %s', id, this.zpClients[id].address, error)
+ this.error('%s: [%s]: %s', id, this.zpClients[id].address, error);
}
}
for (const id in zonePlayers) {
try {
- const zpClient = this.zpClients[id]
+ const zpClient = this.zpClients[id];
if (zpClient == null) {
- this.warn('%s: cannot expose - zone player not found', id)
- continue
+ this.warn('%s: cannot expose - zone player not found', id);
+ continue;
}
- const a = zpClient.modelName.split(' ')
+ const a = zpClient.modelName.split(' ');
const params = {
name: zpClient.zoneName,
id: zpClient.id,
@@ -472,78 +555,78 @@ class ZpPlatform extends homebridgeLib.Platform {
model: a[1] + ' (' + zpClient.modelNumber + ')',
firmware: zpClient.version,
battery: zpClient.battery
- }
+ };
if (zpClient.channel != null && zpClient.channel !== '') {
- params.name += ' ' + zpClient.channel
+ params.name += ' ' + zpClient.channel;
}
const expose = !(this.config.excludeAirPlay && zpClient.airPlay) &&
- !zpClient.invisible
+ !zpClient.invisible;
if (zpClient.role === 'master') {
if (expose && this.zpMasters[zpClient.id] == null) {
- this.zpMasters[zpClient.id] = new ZpAccessory.Master(this, params)
- jobs.push(events.once(this.zpMasters[zpClient.id], 'initialised'))
+ this.zpMasters[zpClient.id] = new ZpAccessory.Master(this, params);
+ jobs.push(events.once(this.zpMasters[zpClient.id], 'initialised'));
}
if (expose && this.config.tv && this.zpTvs[zpClient.id] == null) {
const tvParams = Object.assign({
master: this.zpMasters[zpClient.id]
- }, params)
- delete tvParams.battery
- this.zpTvs[zpClient.id] = new ZpAccessory.Tv(this, tvParams)
- jobs.push(events.once(this.zpTvs[zpClient.id], 'initialised'))
+ }, params);
+ delete tvParams.battery;
+ this.zpTvs[zpClient.id] = new ZpAccessory.Tv(this, tvParams);
+ jobs.push(events.once(this.zpTvs[zpClient.id], 'initialised'));
}
} else { // zonePlayer.role !== 'master'
if (this.config.leds && this.zpSlaves[zpClient.id] == null) {
const slaveParams = Object.assign({
master: this.zpMasters[zpClient.zone]
- }, params)
- this.zpSlaves[zpClient.id] = new ZpAccessory.Slave(this, slaveParams)
- jobs.push(events.once(this.zpSlaves[zpClient.id], 'initialised'))
+ }, params);
+ this.zpSlaves[zpClient.id] = new ZpAccessory.Slave(this, slaveParams);
+ jobs.push(events.once(this.zpSlaves[zpClient.id], 'initialised'));
}
}
} catch (error) {
- this.error('%s: [%s]: %s', id, this.zpClients[id].address, error)
+ this.error('%s: [%s]: %s', id, this.zpClients[id].address, error);
}
}
}
for (const job of jobs) {
- await job
+ await job;
}
if (Object.keys(this.staleAccessories).length === 0) {
- this.debug('initialised')
- this.emit('initialised')
+ this.debug('initialised');
+ this.emit('initialised');
}
}
// Return coordinator for group.
groupCoordinator (groupId) {
- return this.zpMasters[groupId]
+ return this.zpMasters[groupId];
}
// Return array of members for group.
groupMembers (groupId) {
- const members = []
+ const members = [];
for (const id in this.zpMasters) {
- const accessory = this.zpMasters[id]
+ const accessory = this.zpMasters[id];
if (!accessory.isCoordinator && accessory.zpClient.zoneGroup === groupId) {
- members.push(accessory)
+ members.push(accessory);
}
}
- return members
+ return members;
}
// Set coordinator zpAccessory as default coordinator
setPlatformCoordinator (coordinator) {
- const household = coordinator.zpClient.household
- this.coordinators[household] = coordinator
+ const household = coordinator.zpClient.household;
+ this.coordinators[household] = coordinator;
for (const id in this.zpMasters) {
- const accessory = this.zpMasters[id]
- const service = accessory.sonosService
+ const accessory = this.zpMasters[id];
+ const service = accessory.sonosService;
if (service != null && accessory.zpClient.household === household) {
- service.values.sonosCoordinator = accessory === coordinator
- service.values.platformCoordinatorId = coordinator.zpClient.id
+ service.values.sonosCoordinator = accessory === coordinator;
+ service.values.platformCoordinatorId = coordinator.zpClient.id;
}
}
}
}
-module.exports = ZpPlatform
+module.exports = ZpPlatform;
diff --git a/lib/ZpService.js b/lib/ZpService.js
index c7484b5..8b0333e 100644
--- a/lib/ZpService.js
+++ b/lib/ZpService.js
@@ -950,9 +950,8 @@ function initRemoteKeys (characteristicHap) {
volumeSelectors[characteristicHap.VolumeSelector.INCREMENT] = 'Up'
volumeSelectors[characteristicHap.VolumeSelector.DECREMENT] = 'Down'
}
-
class Tv extends ZpService {
- constructor (zpAccessory, params = {}) {
+ constructor(zpAccessory, params = {}) {
params.name = params.master.sonosService.values.configuredName
params.Service = zpAccessory.Services.hap.Television
params.subtype = 'tv'
@@ -977,9 +976,6 @@ class Tv extends ZpService {
master: params.master
})
- // HomeKit doesn't like changes to service or characteristic properties,
- // so we create a static set of (disabled, hidden) InputSource services
- // to be configured later.
this.sources = []
this.inputSources = []
this.displayOrder = []
@@ -1043,7 +1039,7 @@ class Tv extends ZpService {
source.uri, source.meta
)
}
- zp.zpClient.play().catch((error) => { this.error(error) })
+ zp.zpClient.play().catch((error) => { this.log(error) })
if (value === 1) {
// Joined a group
setTimeout(() => {
@@ -1053,7 +1049,7 @@ class Tv extends ZpService {
}
}
} catch (error) {
- this.error(error)
+ this.log(error)
}
this.ignoreDidSet = true
}
@@ -1140,7 +1136,7 @@ class Tv extends ZpService {
this.zpHousehold.on('favouritesUpdated', this.favouritesUpdated.bind(this))
}
- activeIdentifier (uri) {
+ activeIdentifier(uri) {
for (let i = 0; i < this.sources.length; i++) {
if (this.sources[i].uri === uri) {
return i + 1
@@ -1149,9 +1145,10 @@ class Tv extends ZpService {
return 0
}
- nextIdentifier (value) {
+ nextIdentifier(value) {
let identifier = this.values.activeIdentifier
const oldIdentifier = identifier
+
do {
identifier += value
if (identifier < 2) {
@@ -1160,89 +1157,83 @@ class Tv extends ZpService {
if (identifier > this.platform.config.maxFavourites - 1) {
identifier = 2
}
- } while (
- this.inputSources[identifier - 1].values.currentVisibilityState !==
- this.Characteristics.hap.CurrentVisibilityState.SHOWN &&
- identifier !== oldIdentifier
- )
+
+ // Ensure inputSource is defined before checking its values
+ const inputSource = this.inputSources[identifier - 1]
+ if (!inputSource) {
+ this.log(`Input source not found for identifier ${identifier}`)
+ continue // Skip to the next iteration
+ }
+
+ if (
+ inputSource.values.currentVisibilityState ===
+ this.Characteristics.hap.CurrentVisibilityState.SHOWN ||
+ identifier === oldIdentifier
+ ) {
+ break
+ }
+ } while (true)
+
return identifier
}
- favouritesUpdated () {
+ favouritesUpdated() {
const favs = this.zpHousehold.favourites
- if (favs == null) {
- return
- }
+ if (!favs) return
+
this.sources = []
this.configureInputSource('n/a', null, false)
this.updateGroupInputSource(true)
- this.configureInputSource(
- 'AirPlay', 'x-sonos-vli:' + this.zpClient.id + ':1', false
- )
- this.configureInputSource(
- 'Audio In', 'x-rincon-stream:' + this.zpClient.id, this.zpClient.audioIn
- )
- this.configureInputSource(
- 'TV', 'x-sonos-htastream:' + this.zpClient.id + ':spdif',
- this.zpClient.tvIn
- )
+ this.configureInputSource('AirPlay', `x-sonos-vli:${this.zpClient.id}:1`, false)
+ this.configureInputSource('Audio In', `x-rincon-stream:${this.zpClient.id}`, this.zpClient.audioIn)
+ this.configureInputSource('TV', `x-sonos-htastream:${this.zpClient.id}:spdif`, this.zpClient.tvIn)
+
for (const key in favs) {
const fav = favs[key]
- this.configureInputSource(
- key.slice(0, 64), fav.uri, true, fav.container, fav.meta
- )
+ this.configureInputSource(key.slice(0, 64), fav.uri, true, fav.container, fav.meta)
}
- for (
- let index = this.sources.length;
- index < this.platform.config.maxFavourites - 1;
- index++
- ) {
+
+ for (let index = this.sources.length; index < this.platform.config.maxFavourites - 1; index++) {
this.configureInputSource(`Input ${index + 1}`, null, false)
}
+
this.configureInputSource('Sonos Chime', 'x-rincon-buzzer:0', true)
this.log(
'input sources: %j',
- this.sources.filter((source) => {
- return source.visible
- }).map((source) => {
- return source.configuredName
- })
+ this.sources
+ .filter((source) => source.visible)
+ .map((source) => source.configuredName)
)
+
if (this.notYetInitialised) {
delete this.notYetInitialised
this.emit('initialised')
}
+
this.values.activeIdentifier = this.activeIdentifier(this.sonosValues.uri)
}
- updateGroupInputSource (silent = false) {
+ updateGroupInputSource(silent = false) {
const index = 0
const source = this.sources[index]
const inputSource = this.inputSources[index]
- if (source == null || inputSource == null) {
- return
- }
+ if (!source || !inputSource) return
const platformCoordinatorId = this.sonosValues.platformCoordinatorId
const zpClient = this.platform.zpClients[platformCoordinatorId]
let configuredName = 'n/a'
let uri
let visible = false
- if (
- this.sonosValues.sonosGroup != null &&
- this.sonosValues.sonosGroup !== this.zpClient.zoneName
- ) {
+
+ if (this.sonosValues.sonosGroup && this.sonosValues.sonosGroup !== this.zpClient.zoneName) {
configuredName = 'Leave ' + this.sonosValues.sonosGroup
visible = true
- } else if (
- platformCoordinatorId != null &&
- platformCoordinatorId !== this.zpClient.id &&
- zpClient != null
- ) {
+ } else if (platformCoordinatorId && platformCoordinatorId !== this.zpClient.id && zpClient) {
configuredName = 'Join ' + zpClient.zoneGroupShortName
- uri = 'x-rincon:' + platformCoordinatorId
+ uri = `x-rincon:${platformCoordinatorId}`
visible = true
}
+
source.configuredName = configuredName
source.uri = uri
source.visible = visible
@@ -1253,51 +1244,83 @@ class Tv extends ZpService {
inputSource.values.targetVisibilityState = visible
? this.Characteristics.hap.TargetVisibilityState.SHOWN
: this.Characteristics.hap.TargetVisibilityState.HIDDEN
+
if (!silent) {
this.log(
'Input Sources: %j',
- this.sources.filter((source) => {
- return source.visible
- }).map((source) => {
- return source.configuredName
- })
+ this.sources
+ .filter((source) => source.visible)
+ .map((source) => source.configuredName)
)
}
}
- configureInputSource (configuredName, uri, visible, container, meta) {
+ configureInputSource(configuredName, uri, visible, container, meta) {
+ if (this.platform.config.filterFavourites) {
+ if (configuredName === 'AirPlay' || configuredName === 'Audio In') {
+ visible = false
+ }
+ }
+
+if (configuredName === 'TV' && this.platform.config.tvEnabledZones) {
+ const zoneName = this.zpAccessory.zpClient.zoneName;
+
+ if (zoneName && !this.platform.config.tvEnabledZones[zoneName]) {
+ this.log(`TV input will be hidden for zone: ${zoneName}`);
+ visible = false;
+ }
+}
+
+
+ if (configuredName === 'Sonos Chime') {
+ visible = false
+ }
+
this.sources.push({ configuredName, uri, visible, container, meta })
const identifier = this.sources.length
+
if (identifier <= this.platform.config.maxFavourites) {
const inputSource = this.inputSources[identifier - 1]
+
+ // Check if inputSource is undefined
+ if (!inputSource) {
+ this.log(`Input source not found for identifier ${identifier}`)
+ return
+ }
+
+ if (!inputSource.values) {
+ this.log(`Input source values not found for identifier ${identifier}`)
+ return
+ }
+
inputSource.values.configuredName = configuredName
inputSource.values.isConfigured = visible
? this.Characteristics.hap.IsConfigured.CONFIGURED
: this.Characteristics.hap.IsConfigured.NOT_CONFIGURED
- if (configuredName === 'Sonos Chime') {
- visible = false
- }
inputSource.values.targetVisibilityState = visible
? this.Characteristics.hap.TargetVisibilityState.SHOWN
: this.Characteristics.hap.TargetVisibilityState.HIDDEN
+
if (configuredName === 'AirPlay') {
- inputSource.values.inputSourceType =
- this.Characteristics.hap.InputSourceType.OTHER
+ inputSource.values.inputSourceType = this.Characteristics.hap.InputSourceType.OTHER
} else if (configuredName === 'TV') {
- inputSource.values.inputSourceType =
- this.Characteristics.hap.InputSourceType.HDMI
- } else if (uri != null && uri.startsWith('x-sonosapi-stream:')) {
- inputSource.values.inputSourceType =
- this.Characteristics.hap.InputSourceType.TUNER
+ inputSource.values.inputSourceType = this.Characteristics.hap.InputSourceType.HDMI
+ } else if (uri && uri.startsWith('x-sonosapi-stream:')) {
+ inputSource.values.inputSourceType = this.Characteristics.hap.InputSourceType.TUNER
}
+
+ this.log(`Configured input source: ${configuredName} with identifier ${identifier}`)
+ } else {
+ this.log.warn(`Identifier ${identifier} exceeds maxFavourites limit`)
}
}
- static get Speaker () { return TvSpeaker }
+ static get Speaker() { return TvSpeaker }
- static get InputSource () { return TvInputSource }
+ static get InputSource() { return TvInputSource }
}
+
class TvSpeaker extends ZpService {
constructor (zpAccessory, params = {}) {
params.name = zpAccessory.zpClient.zoneName + ' TV Speaker'