Skip to content

Commit

Permalink
Use zone events as fallback (#183)
Browse files Browse the repository at this point in the history
* feat: Using zone events as fallback

Fetching all zones failed on 20+ speakers, using events as fallback

Related to svrooij/sonos2mqtt#134
  • Loading branch information
svrooij authored Jul 6, 2023
1 parent 0eb55f4 commit e129bf9
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 5 deletions.
2 changes: 1 addition & 1 deletion examples/use-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,5 @@ process.on('SIGINT', () => {
manager.CancelSubscription()
setTimeout(() => {
process.exit(0)
}, 200)
}, 300)
})
14 changes: 11 additions & 3 deletions src/services/base-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -356,6 +357,12 @@ export default abstract class BaseService <TServiceEvent> {
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) {
Expand All @@ -368,12 +375,13 @@ export default abstract class BaseService <TServiceEvent> {
});
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));
});
}
Expand Down
27 changes: 26 additions & 1 deletion src/sonos-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -59,16 +61,39 @@ export default class SonosManager {

private async Initialize(): Promise<boolean> {
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<ZoneGroup[]> {
this.debug('LoadAllGroups()');
if (this.zoneService === undefined) throw new Error('Manager is\'t initialized');
return await this.zoneService.GetParsedZoneGroupState();
}

private async LoadAllGroupsWithEvent(): Promise<ZoneGroup[]> {
this.debug('LoadAllGroupsWithEvent()');
if (this.zoneService === undefined) throw new Error('Manager is\'t initialized');
return await AsyncHelper
.AsyncEvent<ZoneGroupTopologyServiceEvent>(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 });
Expand Down

0 comments on commit e129bf9

Please sign in to comment.