Skip to content

Commit da97f91

Browse files
committed
fix(#47): setting to configure retry attempts, setting to toggle debug logging, refactoring
1 parent 9e50bf3 commit da97f91

13 files changed

+260
-148
lines changed

CHANGELOG.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.3.2] - 2023-08-08
11+
12+
### Added
13+
14+
- New plugin setting for debug logging. This is helpful for troubleshooting issues and when requesting support.
15+
16+
### Fixed
17+
18+
- New plugin settings to enable and configure one automatic retry because some Wi-Fi enabled Spotify speakers cannot be found when attempting to start a playlist after a period of inactivity. (#47)
19+
1020
## [1.3.1] - 2023-04-29
1121

1222
### Changed
@@ -32,7 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3242

3343
- Change poblouin references to joeyhage since the old homebridge plugin is no longer maintained
3444

35-
[unreleased]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.3.1...HEAD
45+
[unreleased]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.3.2...HEAD
46+
[1.3.2]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.3.1...v1.3.2
3647
[1.3.1]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.3.0...v1.3.1
3748
[1.3.0]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.2.4...v1.3.0
3849
[1.2.4]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v1.2.3...v1.2.4

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Once the spotify authentication flow is done, the plugin will display the list o
9595

9696
You can then take the `name` from the Spotify device that you want to control and this is what you put in the plugin's configuration as the `spotifyDeviceName`.
9797

98-
This is the suggested option because the device id used by Spotify is prone to change.
98+
This is the suggested option because the device id used by Spotify has been known to change.
9999

100100
### Alternative option
101101

config.schema.json

+28
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,27 @@
4141
"placeholder": "Get speaker status every __ seconds",
4242
"description": "Speaker status does not update live and must be fetched periodically. This is done every 20 seconds by default"
4343
},
44+
"deviceNotFoundRetry": {
45+
"type": "object",
46+
"properties": {
47+
"enable": {
48+
"title": "Enable retry when 'device not found' errors occur",
49+
"type": "boolean",
50+
"required": false,
51+
"default": false,
52+
"description": "Wi-Fi enabled Spotify devices may disconnect from Spotify after a period of time causing device not found errors. This setting determines if the current operation (e.g. play/set volume) should be retried."
53+
},
54+
"retryDelay": {
55+
"title": "Device not found retry delay milliseconds",
56+
"type": "integer",
57+
"default": 2000,
58+
"minimum": 500,
59+
"maximum": 10000,
60+
"placeholder": "Retry operation after __ milliseconds if Spotify reports device was not found.",
61+
"description": "Wi-Fi enabled Spotify devices may disconnect from Spotify after a period of time causing device not found errors. This setting determines how long to wait to allow the device to wake (reconnect to Spotify) before retrying once."
62+
}
63+
}
64+
},
4465
"devices": {
4566
"type": "array",
4667
"items": {
@@ -95,6 +116,13 @@
95116
}
96117
}
97118
}
119+
},
120+
"debug": {
121+
"title": "Debug logging",
122+
"description": "Whether to enable debug logging for troubleshooting issues and support.",
123+
"type": "boolean",
124+
"required": false,
125+
"default": false
98126
}
99127
}
100128
}

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"displayName": "Homebridge Spotify Speaker",
33
"name": "homebridge-spotify-speaker",
4-
"version": "1.3.2-beta.1",
4+
"version": "1.3.1",
55
"description": "Homebridge plugin that creates a speaker that plays a specific Spotify playlist",
66
"license": "MIT",
77
"author": "Joey Hage <contact@jmhage.com>",

src/global.d.ts

Whitespace-only changes.

src/platform.ts

+61-23
Original file line numberDiff line numberDiff line change
@@ -10,60 +10,80 @@ import {
1010
import { URL } from 'url';
1111
import { PLATFORM_NAME, PLUGIN_NAME } from './settings';
1212
import { SpotifyApiWrapper } from './spotify-api-wrapper';
13-
import { HomebridgeSpotifySpeakerDevice, SpotifySpeakerAccessory } from './spotify-speaker-accessory';
13+
import { SpotifySpeakerAccessory } from './spotify-speaker-accessory';
14+
import type { HomebridgeSpotifySpeakerDevice } from './types';
15+
import { PluginLogger } from './plugin-logger';
1416

1517
const DEVICE_CLASS_CONFIG_MAP = {
1618
speaker: SpotifySpeakerAccessory,
1719
};
1820

19-
const DAY_INTERVAL = 60 * 60 * 24 * 1000;
21+
const DAY_INTERVAL_MS = 60 * 60 * 24 * 1000;
22+
const MINUTE_INTERVAL_MS = 60 * 1000;
23+
const DEFAULT_POLL_INTERVAL_S = 20;
2024

2125
export class HomebridgeSpotifySpeakerPlatform implements DynamicPlatformPlugin {
2226
public readonly Service: typeof Service = this.api.hap.Service;
2327
public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
28+
public readonly logger: PluginLogger;
2429
public readonly spotifyApiWrapper: SpotifyApiWrapper;
30+
public readonly pollIntervalSec: number;
2531
public readonly accessories: {
2632
[uuid: string]: PlatformAccessory;
2733
} = {};
2834

29-
constructor(public readonly log: Logger, public readonly config: PlatformConfig, public readonly api: API) {
30-
this.log.debug('Finished initializing platform:', this.config.name);
35+
constructor(readonly log: Logger, public readonly config: PlatformConfig, public readonly api: API) {
36+
this.logger = new PluginLogger(log, config);
37+
this.logger.debug('Finished initializing platform:', this.config.name);
3138

3239
if (!config.spotifyClientId || !config.spotifyClientSecret || !config.spotifyAuthCode) {
33-
this.log.error('Missing configuration for this plugin to work, see the documentation for initial setup');
40+
this.logger.error('Missing configuration for this plugin to work, see the documentation for initial setup');
3441
return;
3542
}
43+
this.pollIntervalSec = config.spotifyPollInterval || DEFAULT_POLL_INTERVAL_S;
3644

37-
this.spotifyApiWrapper = new SpotifyApiWrapper(log, config, api);
45+
this.spotifyApiWrapper = new SpotifyApiWrapper(this.logger, config, api);
3846

3947
this.api.on('didFinishLaunching', async () => {
40-
log.debug('Executed didFinishLaunching callback');
48+
this.logger.debug('Executed didFinishLaunching callback');
4149

4250
const isAuthenticated = await this.spotifyApiWrapper.authenticate();
4351
if (!isAuthenticated) {
4452
return;
4553
}
4654

47-
this.logAvailableSpotifyDevices();
55+
await this.logAvailableSpotifyDevices();
56+
await this.setSpotifyPlaybackState();
4857
this.discoverDevices();
58+
if (Object.keys(this.accessories).length) {
59+
setInterval(() => {
60+
this.setSpotifyDevices();
61+
}, MINUTE_INTERVAL_MS);
62+
63+
setInterval(() => {
64+
this.setSpotifyPlaybackState();
65+
}, this.pollIntervalSec * 1000);
66+
}
4967
});
5068

5169
// Make sure we have the latest tokens saved
5270
this.api.on('shutdown', () => {
5371
this.spotifyApiWrapper.persistTokens();
5472
});
5573

56-
setInterval(async () => await this.spotifyApiWrapper.refreshTokens(), DAY_INTERVAL);
74+
setInterval(async () => await this.spotifyApiWrapper.refreshTokens(), DAY_INTERVAL_MS);
5775
}
5876

5977
configureAccessory(accessory: PlatformAccessory) {
60-
this.log.info('Loading accessory from cache:', accessory.displayName);
78+
this.logger.info('Loading accessory from cache:', accessory.displayName);
6179
this.accessories[accessory.UUID] = accessory;
6280
}
6381

6482
discoverDevices() {
6583
if (!this.config.devices) {
66-
this.log.error('The "devices" section is missing in your plugin configuration, please add at least one device.');
84+
this.logger.error(
85+
'The "devices" section is missing in your plugin configuration, please add at least one device.',
86+
);
6787
return;
6888
}
6989

@@ -77,7 +97,7 @@ export class HomebridgeSpotifySpeakerPlatform implements DynamicPlatformPlugin {
7797
continue;
7898
}
7999
if (!this.deviceConfigurationIsValid(device)) {
80-
this.log.error(
100+
this.logger.error(
81101
`${
82102
device.deviceName ?? 'unknown device'
83103
} is not configured correctly. See the documentation for initial setup`,
@@ -95,13 +115,13 @@ export class HomebridgeSpotifySpeakerPlatform implements DynamicPlatformPlugin {
95115
existingAccessory ?? new this.api.platformAccessory(device.deviceName, uuid, deviceClass.CATEGORY);
96116
accessory.context.device = device;
97117
accessory.context.playlistId = playlistId;
98-
new deviceClass(this, accessory, device, this.log);
118+
new deviceClass(this, accessory, device, this.logger);
99119
activeAccessoryIds.push(uuid);
100120

101121
if (existingAccessory) {
102-
this.log.info('Restoring existing accessory from cache:', accessory.displayName);
122+
this.logger.info('Restoring existing accessory from cache:', accessory.displayName);
103123
} else {
104-
this.log.info('Adding new accessory:', device.deviceName);
124+
this.logger.info('Adding new accessory:', device.deviceName);
105125
platformAccessories.push(accessory);
106126
}
107127
}
@@ -115,14 +135,32 @@ export class HomebridgeSpotifySpeakerPlatform implements DynamicPlatformPlugin {
115135
.map((id) => this.accessories[id]);
116136

117137
staleAccessories.forEach((staleAccessory) => {
118-
this.log.info(`Removing stale cached accessory ${staleAccessory.UUID} ${staleAccessory.displayName}`);
138+
this.logger.info(`Removing stale cached accessory ${staleAccessory.UUID} ${staleAccessory.displayName}`);
119139
});
120140

121141
if (staleAccessories.length) {
122142
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, staleAccessories);
123143
}
124144
}
125145

146+
async setSpotifyDevices(): Promise<void> {
147+
try {
148+
const devices = await this.spotifyApiWrapper.getMyDevices();
149+
SpotifySpeakerAccessory.DEVICES = devices;
150+
} catch {
151+
this.logger.error('Error setting spotify devices');
152+
}
153+
}
154+
155+
async setSpotifyPlaybackState(): Promise<void> {
156+
try {
157+
const state = await this.spotifyApiWrapper.getPlaybackState();
158+
SpotifySpeakerAccessory.CURRENT_STATE = state?.body;
159+
} catch {
160+
this.logger.error('Error setting spotify playback state');
161+
}
162+
}
163+
126164
private extractPlaylistId(playlistUrl: string): string | null {
127165
try {
128166
// Empty playlist ID is allowed for cases where one wants to only
@@ -133,11 +171,11 @@ export class HomebridgeSpotifySpeakerPlatform implements DynamicPlatformPlugin {
133171

134172
const url = new URL(playlistUrl);
135173
const playlistId = url.pathname.split('/')[2];
136-
this.log.debug(`Found playlistId: ${playlistId}`);
174+
this.logger.debug(`Found playlistId: ${playlistId}`);
137175

138176
return playlistId;
139177
} catch (error) {
140-
this.log.error(
178+
this.logger.error(
141179
`Failed to extract playlist ID, the plugin might behave in an unexpected way.
142180
Please check the configuration and provide a valid playlist URL`,
143181
);
@@ -148,22 +186,22 @@ export class HomebridgeSpotifySpeakerPlatform implements DynamicPlatformPlugin {
148186

149187
private getDeviceConstructor(deviceType): typeof SpotifySpeakerAccessory | null {
150188
if (!deviceType) {
151-
this.log.error('It is missing the `deviceType` in the configuration.');
189+
this.logger.error('It is missing the `deviceType` in the configuration.');
152190
return null;
153191
}
154192

155193
return DEVICE_CLASS_CONFIG_MAP[deviceType];
156194
}
157195

158196
private async logAvailableSpotifyDevices(): Promise<void> {
159-
const spotifyDevices = await this.spotifyApiWrapper.getMyDevices();
197+
await this.setSpotifyDevices();
160198

161-
if (!spotifyDevices || spotifyDevices.length === 0) {
162-
this.log.warn(
199+
if (!SpotifySpeakerAccessory.DEVICES?.length) {
200+
this.logger.warn(
163201
'No available spotify devices found, make sure that the speaker you configured is On and visible by Spotify Connect',
164202
);
165203
} else {
166-
this.log.info('Available Spotify devices', spotifyDevices);
204+
this.logger.info('Available Spotify devices', SpotifySpeakerAccessory.DEVICES);
167205
}
168206
}
169207

src/plugin-logger.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import type { Logger, PlatformConfig } from 'homebridge';
3+
4+
export class PluginLogger {
5+
constructor(private readonly log: Logger, private readonly config: PlatformConfig) {}
6+
7+
debug(message: string, ...parameters: any[]): void {
8+
if (this.config.debug) {
9+
this.log.info(message, ...parameters);
10+
}
11+
}
12+
13+
info(message: string, ...parameters: any[]): void {
14+
this.log.info(message, ...parameters);
15+
}
16+
17+
warn(message: string, ...parameters: any[]): void {
18+
this.log.warn(message, ...parameters);
19+
}
20+
21+
error(message: string, ...parameters: any[]): void {
22+
this.log.error(message, ...parameters);
23+
}
24+
}

src/spotify-api-wrapper.it.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { API, Logger, PlatformConfig } from 'homebridge';
1+
import { API, PlatformConfig } from 'homebridge';
2+
import type { PluginLogger } from './plugin-logger';
23
import { SpotifyApiWrapper } from './spotify-api-wrapper';
34

45
it('should authenticate and persist tokens', async () => {
@@ -39,11 +40,12 @@ it('should retrieve playback state', async () => {
3940

4041
function getSpotifyApiWrapper(): SpotifyApiWrapper {
4142
return new SpotifyApiWrapper(
42-
console as Logger,
43+
console as unknown as PluginLogger,
4344
{
4445
spotifyAuthCode: process.env.SPOTIFY_AUTH_CODE!,
4546
spotifyClientId: process.env.SPOTIFY_CLIENT_ID!,
4647
spotifyClientSecret: process.env.SPOTIFY_CLIENT_SECRET!,
48+
deviceNotFoundRetry: { enable: false },
4749
} as unknown as PlatformConfig,
4850
{ user: { persistPath: () => '.' } } as API,
4951
);

0 commit comments

Comments
 (0)