From e129bf9098db0c63491277d631ac2afba8f99785 Mon Sep 17 00:00:00 2001 From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com> Date: Thu, 6 Jul 2023 12:44:35 +0200 Subject: [PATCH] Use zone events as fallback (#183) * feat: Using zone events as fallback Fetching all zones failed on 20+ speakers, using events as fallback Related to https://github.com/svrooij/sonos2mqtt/issues/134 --- examples/use-manager.js | 2 +- src/services/base-service.ts | 14 +++++++++++--- src/sonos-manager.ts | 27 ++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/examples/use-manager.js b/examples/use-manager.js index 91c49ac..0e8d71b 100644 --- a/examples/use-manager.js +++ b/examples/use-manager.js @@ -60,5 +60,5 @@ process.on('SIGINT', () => { manager.CancelSubscription() setTimeout(() => { process.exit(0) - }, 200) + }, 300) }) diff --git a/src/services/base-service.ts b/src/services/base-service.ts index d8f10b7..59fc3f5 100644 --- a/src/services/base-service.ts +++ b/src/services/base-service.ts @@ -16,6 +16,7 @@ import { EventsError, EventsErrorCode } from '../models/event-errors'; import SonosError from '../models/sonos-error'; import HttpError from '../models/http-error'; import { SonosUpnpError } from '../models/sonos-upnp-error'; +import AsyncHelper from '../helpers/async-helper'; /** * Base Service class will handle all the requests to the sonos device. @@ -356,6 +357,12 @@ export default abstract class BaseService { this.events = new EventEmitter(); this.events.on('removeListener', async (eventName: string | symbol) => { this.debug('Listener removed for %s', eventName); + // The ZoneGroupTopology service might resubscribe really soon after unsubscribing. + // Because we don't want it to cancel the subscription we wait 100 ms just to make sure there aren't any new subscriptions before unsubscribing + if (this.serviceNane === 'ZoneGroupTopology') { + this.debug('Waiting 100ms before unsubscribing'); + await AsyncHelper.Delay(100); + } const events = this.events?.eventNames().filter((e) => e !== 'removeListener' && e !== 'newListener' && e !== ServiceEvents.SubscriptionError); if (this.sid !== undefined && events?.length === 0) { @@ -368,12 +375,13 @@ export default abstract class BaseService { }); this.events.on('newListener', async (eventName: string | symbol) => { if (eventName === ServiceEvents.SubscriptionError) return; - this.debug('Listener added for %s (sid: \'%s\', SONOS_DISABLE_EVENTS: %o)', eventName, this.sid, (typeof process.env.SONOS_DISABLE_EVENTS === 'undefined')); - if (this.sid === undefined && process.env.SONOS_DISABLE_EVENTS === undefined) { + const eventsEnabled = (process.env.SONOS_DISABLE_EVENTS === undefined || process.env.SONOS_DISABLE_EVENTS !== 'true'); + this.debug('Listener added for %s (sid: \'%s\', SONOS_DISABLE_EVENTS: %o)', eventName, this.sid, !eventsEnabled); + if (this.sid === undefined && eventsEnabled) { this.debug('Subscribing to events'); await this.subscribeForEvents() .catch((err: Error) => { - this.debug('Subscriping for events failed', err); + this.debug('Subscribing for events failed', err); this.emitEventsError(new EventsError(EventsErrorCode.SubscribeFailed, err)); }); } diff --git a/src/sonos-manager.ts b/src/sonos-manager.ts index 1abeb1c..20bc10a 100644 --- a/src/sonos-manager.ts +++ b/src/sonos-manager.ts @@ -7,6 +7,8 @@ import SonosDeviceDiscovery from './sonos-device-discovery'; import { ServiceEvents, PlayNotificationOptions, PlayTtsOptions } from './models'; import IpHelper from './helpers/ip-helper'; import TtsHelper from './helpers/tts-helper'; +import AsyncHelper from './helpers/async-helper'; +import SonosError from './models/sonos-error'; /** * The SonosManager will manage the logical devices for you. It will also manage group updates so be sure to call .Close on exit to remove open listeners. * @@ -59,16 +61,39 @@ export default class SonosManager { private async Initialize(): Promise { this.debug('Initialize()'); - const groups = await this.LoadAllGroups(); + const groups = await this + .LoadAllGroups() + .catch((err) => { + this.debug('Error loading groups with pull %o', err); + if (err instanceof SonosError && err.UpnpErrorCode === 501) { + // This happens with big systems, try loading with events + return this.LoadAllGroupsWithEvent(); + } + throw err; + }); const success = this.InitializeWithGroups(groups); return this.SubscribeForGroupEvents(success); } private async LoadAllGroups(): Promise { + this.debug('LoadAllGroups()'); if (this.zoneService === undefined) throw new Error('Manager is\'t initialized'); return await this.zoneService.GetParsedZoneGroupState(); } + private async LoadAllGroupsWithEvent(): Promise { + this.debug('LoadAllGroupsWithEvent()'); + if (this.zoneService === undefined) throw new Error('Manager is\'t initialized'); + return await AsyncHelper + .AsyncEvent(this.zoneService.Events, ServiceEvents.ServiceEvent, 5) + .then((data) => { + if (!Array.isArray(data.ZoneGroupState)) { + throw new Error('No groups in event'); + } + return data.ZoneGroupState; + }); + } + private InitializeWithGroups(groups: ZoneGroup[]): boolean { groups.forEach((g) => { const coordinator = new SonosDevice(g.coordinator.host, g.coordinator.port, g.coordinator.uuid, g.coordinator.name, { name: g.name, managerEvents: this.events });