Skip to content

Commit

Permalink
feat: Handle pyatv exceptions for getState() and event listeners
Browse files Browse the repository at this point in the history
closes #105
  • Loading branch information
sebbo2002 committed Nov 29, 2021
1 parent 8a01c63 commit a695e11
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 12 deletions.
9 changes: 7 additions & 2 deletions src/lib/device-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,13 @@ export default class NodePyATVDeviceEvents extends EventEmitter {
}

private applyPushUpdate(update: NodePyATVInternalState, reqId: string): void {
const newState = parseState(update, reqId, this.options);
this.applyStateAndEmitEvents(newState);
try {
const newState = parseState(update, reqId, this.options);
this.applyStateAndEmitEvents(newState);
}
catch(error) {
this.emit('error', error);
}
}

private checkListener(): void {
Expand Down
17 changes: 11 additions & 6 deletions src/lib/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,19 @@ export default class NodePyATVDevice implements EventEmitter{
}

const id = addRequestId();
const parameters = getParamters(this.options);

const result = await request(id, NodePyATVExecutableType.atvscript, [...parameters, 'playing'], this.options);
const newState = parseState(result, id, this.options);
this.applyState(newState);
try {
const parameters = getParamters(this.options);

removeRequestId(id);
return newState;
const result = await request(id, NodePyATVExecutableType.atvscript, [...parameters, 'playing'], this.options);
const newState = parseState(result, id, this.options);

this.applyState(newState);
return newState;
}
finally {
removeRequestId(id);
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/lib/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,14 @@ export function parseState(input: NodePyATVInternalState, id: string, options: N
if (!input || typeof input !== 'object') {
return result;
}
if(input.exception) {
let errorStr = 'Got pyatv Error: ' + input.exception;
if(input.stacktrace) {
errorStr += '\n\npyatv Stacktrace:\n' + input.stacktrace;
}

throw new Error(errorStr);
}

// datetime
if (typeof input.datetime === 'string') {
Expand Down
1 change: 1 addition & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export interface NodePyATVInternalState {
power_state?: string | unknown,
push_updates?: string | unknown,
exception?: string | unknown,
stacktrace?: string | unknown,
connection?: string | unknown
}

Expand Down
23 changes: 23 additions & 0 deletions test/device-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,29 @@ describe('NodePyATVDeviceEvents', function () {

assert.deepStrictEqual(sort, ['update:title', 'update']);
});
it('should emit error events on failures', async function () {
const device = new NodePyATVDevice({
name: 'My Testdevice',
host: '192.168.178.2',
spawn: createFakeSpawn(cp => {
cp.onStdIn(() => cp.end());
cp.stdout({
result: 'failure',
datetime: '2021-11-24T21:13:36.424576+03:00',
exception: 'invalid credentials: 321',
stacktrace: 'Traceback (most recent call last):\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/scripts/atvscript.py\", line 302, in appstart\n print(args.output(await _handle_command(args, abort_sem, loop)), flush=True)\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/scripts/atvscript.py\", line 196, in _handle_command\n atv = await connect(config, loop, protocol=Protocol.MRP)\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/__init__.py\", line 96, in connect\n for setup_data in proto_methods.setup(\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/protocols/airplay/__init__.py\", line 192, in setup\n stream = AirPlayStream(config, service)\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/protocols/airplay/__init__.py\", line 79, in __init__\n self._credentials: HapCredentials = parse_credentials(self.service.credentials)\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/auth/hap_pairing.py\", line 139, in parse_credentials\n raise exceptions.InvalidCredentialsError(\"invalid credentials: \" + detail_string)\npyatv.exceptions.InvalidCredentialsError: invalid credentials: 321\n'
});
})
});

await new Promise(cb => {
device.once('error', error => {
assert.ok(error instanceof Error);
assert.ok(error.toString().includes('invalid credentials: 321'));
cb(undefined);
});
});
});
it('should not emit an update if new value is same as old one', async function () {
let spawnCounter = 0;
let eventCounter = 0;
Expand Down
18 changes: 18 additions & 0 deletions test/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,24 @@ describe('NodePyATVDevice', function () {
powerState: null
});
});
it('should reject with error if pyatv fails', async function () {
const device = new NodePyATVDevice({
name: 'My Testdevice',
host: '192.168.178.2',
spawn: createFakeSpawn(cp => {
cp.end({
result: 'failure',
datetime: '2021-11-24T21:13:36.424576+03:00',
exception: 'invalid credentials: 321',
stacktrace: 'Traceback (most recent call last):\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/scripts/atvscript.py\", line 302, in appstart\n print(args.output(await _handle_command(args, abort_sem, loop)), flush=True)\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/scripts/atvscript.py\", line 196, in _handle_command\n atv = await connect(config, loop, protocol=Protocol.MRP)\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/__init__.py\", line 96, in connect\n for setup_data in proto_methods.setup(\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/protocols/airplay/__init__.py\", line 192, in setup\n stream = AirPlayStream(config, service)\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/protocols/airplay/__init__.py\", line 79, in __init__\n self._credentials: HapCredentials = parse_credentials(self.service.credentials)\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/auth/hap_pairing.py\", line 139, in parse_credentials\n raise exceptions.InvalidCredentialsError(\"invalid credentials: \" + detail_string)\npyatv.exceptions.InvalidCredentialsError: invalid credentials: 321\n'
});
})
});

assert.rejects(async () => {
await device.getState();
}, /Got pyatv Error: invalid credentials: 321/);
});
it('should cache requests for a bit', async function () {
let executions = 0;
const device = new NodePyATVDevice({
Expand Down
19 changes: 15 additions & 4 deletions test/tools.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

import assert from 'assert';
import {addRequestId, debug, getExecutable, getParamters, parseState, removeRequestId} from '../src/lib/tools';
import { addRequestId, debug, getExecutable, getParamters, parseState, removeRequestId } from '../src/lib/tools';
import {
NodePyATVDeviceState,
NodePyATVExecutableType,
Expand All @@ -28,7 +28,7 @@ describe('Tools', function () {
debug('TEST', 'Hello World.', {});
});
it('should work with default logger', function () {
debug('TEST', 'Hello World.', {debug: true});
debug('TEST', 'Hello World.', { debug: true });
});
it('should work with custom logger', function () {
debug('TEST', 'Hello World.', {
Expand All @@ -39,7 +39,7 @@ describe('Tools', function () {
});
});
it('should work with colors disabled', function () {
debug('TEST', 'Hello World.', {noColors: true});
debug('TEST', 'Hello World.', { noColors: true });
});
it('should work with custom logger and colors disabled', function () {
debug('TEST', 'Hello World.', {
Expand Down Expand Up @@ -201,8 +201,19 @@ describe('Tools', function () {
powerState: null
});
});
it('should throw an error for pyatv exceptions', function () {
const input = {
result: 'failure',
datetime: '2021-11-24T21:13:36.424576+03:00',
exception: 'invalid credentials: 321',
stacktrace: 'Traceback (most recent call last):\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/scripts/atvscript.py\", line 302, in appstart\n print(args.output(await _handle_command(args, abort_sem, loop)), flush=True)\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/scripts/atvscript.py\", line 196, in _handle_command\n atv = await connect(config, loop, protocol=Protocol.MRP)\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/__init__.py\", line 96, in connect\n for setup_data in proto_methods.setup(\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/protocols/airplay/__init__.py\", line 192, in setup\n stream = AirPlayStream(config, service)\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/protocols/airplay/__init__.py\", line 79, in __init__\n self._credentials: HapCredentials = parse_credentials(self.service.credentials)\n File \"/Users/free/Library/Python/3.8/lib/python/site-packages/pyatv/auth/hap_pairing.py\", line 139, in parse_credentials\n raise exceptions.InvalidCredentialsError(\"invalid credentials: \" + detail_string)\npyatv.exceptions.InvalidCredentialsError: invalid credentials: 321\n'
};
assert.throws(() => {
parseState(input, '', {});
}, /Got pyatv Error: invalid credentials: 321/);
});
it('should ignore date if it\'s an invalid date', function () {
const input = {datetime: 'today'};
const input = { datetime: 'today' };
const result = parseState(input, '', {});
assert.deepStrictEqual(result, {
dateTime: null,
Expand Down

0 comments on commit a695e11

Please sign in to comment.