Skip to content

Commit

Permalink
Make the location config option… optional.
Browse files Browse the repository at this point in the history
  • Loading branch information
BewhiskeredBard committed Apr 17, 2021
1 parent 76c9217 commit 7ab7fc0
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 31 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ All of the following configuration options are required. If any are missing or i

* **`"password"`:** Your Scout password. Don't forget to backslash-escape any double-quotes (e.g. `pwd"23` should be `"pwd\"123"`).

* **`"location"`:** The name of your Scout location. It's probably `"Home"` if you only have one Scout system and haven't renamed it. You can find this in the left-hand menu of the Scout app or dashboard.

#### Optional

The following configuration options are optional and change the default behavior.

* **`"location"`:** The name of the Scout location you want to integrate. If you only have a single location (common), this option is unnecessary. You can find your location names in the sidebar of the Scout app or dashboard.

* **`"modes"`:** If modes are defined, your Scout system will be added as a HomeKit security system. These map the HomeKit modes to your Scout modes. Your Scout mode names can be found in the Scout app or dashboard. Each HomeKit can be mapped to zero or more† Scout mode(s) (e.g., `[], ["Mode A"]` or `["Mode A", "Mode B"]`). If a HomeKit mode is empty or not provided, it will not be shown in Apple’s Home app.

* **`"stay"`:** Possibly `["Home"]`.
Expand All @@ -73,7 +73,6 @@ Update the `"platforms"` section of your `~/.homebridge/config.json`:
"email": "[email protected]",
"password": "password123"
},
"location": "Home",
"modes": {
"stay": ["Home"],
"away": ["Away", "Vacation"],
Expand Down
4 changes: 2 additions & 2 deletions scripts/generateConfigSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const schema = {
{
type: 'fieldset',
expandable: false,
title: 'Required Options',
title: 'Basic Options',
description: 'Your Scout login credentials. Use a member account, not an admin account.',
items: [
{
Expand All @@ -48,7 +48,7 @@ const schema = {
title: 'Location Name',
placeholder: 'e.g., Home',
description:
"The name of your Scout location. It's probably <em>Home</em> if you only have one Scout system and haven't renamed it. You can find this in the left-hand menu of the Scout app or dashboard.",
'The name of the Scout location you want to integrate. If you only have a single location (common), this option is unnecessary. You can find your location names in the sidebar of the Scout app or dashboard.',
},
],
},
Expand Down
2 changes: 1 addition & 1 deletion src/context/homebridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface HomebridgeConfig extends PlatformConfig {
email: string;
password: string;
};
location: string;
location?: string;
modes?: {
[key in HomebridgeConfigMode]: string[];
};
Expand Down
28 changes: 23 additions & 5 deletions src/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,33 @@ export class Orchestrator {
this.logger.warn(`The authenticated member [${memberId}] is an admin. It is highly recommended to use a non-admin member.`);
}

const matchingLocation = locations.find(location => location.name === homebridge.config.location);
const location = this.chooseLocation(homebridge, locations);

if (!matchingLocation) {
throw new Error(`No location found for "${homebridge.config.location}".`);
this.logger.info(`Using "${location.name}" location [${location.id}].`);

return location;
}

private chooseLocation(homebridge: HomebridgeContext, locations: Location[]): Location {
const defaultLocation = locations[0];

if (!defaultLocation) {
throw new Error('No locations found.');
}

this.logger.info(`Using "${homebridge.config.location}" location [${matchingLocation.id}].`);
if (homebridge.config.location) {
const matchingLocation = locations.find(location => location.name === homebridge.config.location);

return matchingLocation;
if (!matchingLocation) {
throw new Error(`No location found for "${homebridge.config.location}".`);
}

return matchingLocation;
} else if (1 < locations.length) {
throw new Error(`You must configure one of the following locations: ${locations.map(location => location.name).join(', ')}.`);
} else {
return defaultLocation;
}
}

private async registerAccessories(homebridge: HomebridgeContext, scout: ScoutContext, locationId: string): Promise<void> {
Expand Down
6 changes: 0 additions & 6 deletions tst/context/homebridge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,6 @@ describe(`${HomebridgeContextFactory.name}`, () => {
expect(() => factory.create(api, logger, config)).toThrowError("Configuration error: config/auth must have required property 'password'");
});

test('missing config.location', () => {
delete config.location;

expect(() => factory.create(api, logger, config)).toThrowError("Configuration error: config must have required property 'location'");
});

test('missing config.modes', () => {
delete config.modes;

Expand Down
77 changes: 63 additions & 14 deletions tst/orchestrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ describe(`${Orchestrator.name}`, () => {
let scout: ScoutContext;
let scoutContextFactory: ScoutContextFactory;
let authMember: AuthenticatedMember;
let location: Location;
let location1: Location;
let location2: Location;
let orchestrator: Orchestrator;
let accessoryFactories: (homebridgeContext: HomebridgeContext, scoutContext: ScoutContext) => AccessoryFactory<unknown>[];
let accessoryFactory: AccessoryFactory<unknown>;
Expand Down Expand Up @@ -68,9 +69,14 @@ describe(`${Orchestrator.name}`, () => {
};
});

location = {
location1 = {
id: 'locationId1',
name: homebridge.config.location,
name: 'Location 1',
} as Location;

location2 = {
id: 'locationId2',
name: 'Location 2',
} as Location;

accessoryFactories = jest.fn();
Expand Down Expand Up @@ -101,6 +107,8 @@ describe(`${Orchestrator.name}`, () => {
});

test('no Scout locations', async () => {
delete homebridge.config.location;

(scout.api.getLocations as jest.Mock).mockImplementation(() => {
return {
data: [],
Expand All @@ -109,11 +117,11 @@ describe(`${Orchestrator.name}`, () => {

await listen();

expect(homebridge.logger.error).toBeCalledWith(new Error(`No location found for "${homebridge.config.location}".`));
expect(homebridge.logger.error).toBeCalledWith(new Error(`No locations found.`));
});

test('no matching Scout locations', async () => {
location.name = 'foo';
homebridge.config.location = 'foo';

(scout.api.getLocations as jest.Mock).mockImplementation(() => {
return {
Expand All @@ -126,12 +134,27 @@ describe(`${Orchestrator.name}`, () => {
expect(homebridge.logger.error).toBeCalledWith(new Error(`No location found for "${homebridge.config.location}".`));
});

test('multiple Scout locations without config', async () => {
delete homebridge.config.location;

(scout.api.getLocations as jest.Mock).mockImplementation(() => {
return {
data: [location1, location2],
};
});

await listen();

expect(homebridge.logger.error).toBeCalledWith(new Error(`You must configure one of the following locations: ${location1.name}, ${location2.name}.`));
});

test('using admin account', async () => {
location.admin_ids = [authMember.id];
homebridge.config.location = location1.name;
location1.admin_ids = [authMember.id];

(scout.api.getLocations as jest.Mock).mockImplementation(() => {
return {
data: [location],
data: [location1],
};
});

Expand All @@ -142,15 +165,17 @@ describe(`${Orchestrator.name}`, () => {
await listen();

expect(scout.listener.connect).toBeCalled();
expect(scout.listener.addLocation).toBeCalledWith(location.id);
expect(scout.listener.addLocation).toBeCalledWith(location1.id);
expect(homebridge.logger.warn).toBeCalledWith('The authenticated member [memberId1] is an admin. It is highly recommended to use a non-admin member.');
expect(homebridge.logger.error).not.toBeCalled();
});

test('register new accessory', async () => {
homebridge.config.location = location1.name;

(scout.api.getLocations as jest.Mock).mockImplementation(() => {
return {
data: [location],
data: [location1],
};
});

Expand All @@ -165,17 +190,39 @@ describe(`${Orchestrator.name}`, () => {
await listen();

expect(scout.listener.connect).toBeCalled();
expect(scout.listener.addLocation).toBeCalledWith(location.id);
expect(scout.listener.addLocation).toBeCalledWith(location1.id);
expect(accessoryFactory.configureAccessory).toBeCalledWith(accessory);
expect(homebridge.api.registerPlatformAccessories).toBeCalledWith(ScoutPlatformPlugin.PLUGIN_NAME, ScoutPlatformPlugin.PLATFORM_NAME, [accessory]);
expect(homebridge.api.unregisterPlatformAccessories).toBeCalledWith(ScoutPlatformPlugin.PLUGIN_NAME, ScoutPlatformPlugin.PLATFORM_NAME, []);
expect(homebridge.logger.error).not.toBeCalled();
});

test('single uncofigured location', async () => {
delete homebridge.config.location;

(scout.api.getLocations as jest.Mock).mockImplementation(() => {
return {
data: [location1],
};
});

(accessoryFactories as jest.Mock).mockImplementation(() => {
return [];
});

await listen();

expect(scout.listener.connect).toBeCalled();
expect(scout.listener.addLocation).toBeCalledWith(location1.id);
expect(homebridge.logger.error).not.toBeCalled();
});

test('configured accessory not found', async () => {
homebridge.config.location = location1.name;

(scout.api.getLocations as jest.Mock).mockImplementation(() => {
return {
data: [location],
data: [location1],
};
});

Expand All @@ -192,7 +239,7 @@ describe(`${Orchestrator.name}`, () => {
await listen();

expect(scout.listener.connect).toBeCalled();
expect(scout.listener.addLocation).toBeCalledWith(location.id);
expect(scout.listener.addLocation).toBeCalledWith(location1.id);
expect(accessoryFactory.configureAccessory).not.toBeCalled();
expect(homebridge.api.registerPlatformAccessories).toBeCalledWith(ScoutPlatformPlugin.PLUGIN_NAME, ScoutPlatformPlugin.PLATFORM_NAME, []);
expect(homebridge.api.unregisterPlatformAccessories).toBeCalledWith(ScoutPlatformPlugin.PLUGIN_NAME, ScoutPlatformPlugin.PLATFORM_NAME, [
Expand All @@ -202,9 +249,11 @@ describe(`${Orchestrator.name}`, () => {
});

test('configured accessory found', async () => {
homebridge.config.location = location1.name;

(scout.api.getLocations as jest.Mock).mockImplementation(() => {
return {
data: [location],
data: [location1],
};
});

Expand All @@ -221,7 +270,7 @@ describe(`${Orchestrator.name}`, () => {
await listen();

expect(scout.listener.connect).toBeCalled();
expect(scout.listener.addLocation).toBeCalledWith(location.id);
expect(scout.listener.addLocation).toBeCalledWith(location1.id);
expect(cachedAccessory.context).toStrictEqual(accessory.context);
expect(accessoryFactory.configureAccessory).toBeCalledWith(cachedAccessory);
expect(homebridge.api.registerPlatformAccessories).toBeCalledWith(ScoutPlatformPlugin.PLUGIN_NAME, ScoutPlatformPlugin.PLATFORM_NAME, []);
Expand Down

0 comments on commit 7ab7fc0

Please sign in to comment.