-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
1,014 additions
and
199 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
'use strict'; | ||
|
||
import NodePyATVDevice from './device'; | ||
import {NodePyATVStateIndex} from './types'; | ||
|
||
export default class NodePyATVDeviceEvent { | ||
protected readonly values: {key: NodePyATVStateIndex, old: string, new: string, device: NodePyATVDevice}; | ||
|
||
constructor(values: {key: NodePyATVStateIndex, old: string, new: string, device: NodePyATVDevice}) { | ||
this.values = Object.assign({}, values, { | ||
key: values.key as NodePyATVStateIndex | ||
}); | ||
} | ||
|
||
get key(): NodePyATVStateIndex { | ||
return this.values.key; | ||
} | ||
|
||
get oldValue(): string { | ||
return this.values.old; | ||
} | ||
|
||
get newValue(): string { | ||
return this.values.new; | ||
} | ||
|
||
get value(): string { | ||
return this.values.new; | ||
} | ||
|
||
get device(): NodePyATVDevice { | ||
return this.values.device; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,262 @@ | ||
'use strict'; | ||
|
||
import { | ||
NodePyATVDeviceOptions, | ||
NodePyATVExecutableType, | ||
NodePyATVInternalState, | ||
NodePyATVListenerState, | ||
NodePyATVState, | ||
NodePyATVStateIndex | ||
} from './types'; | ||
import {ChildProcess} from 'child_process'; | ||
|
||
import {EventEmitter} from 'events'; | ||
import NodePyATVDevice from './device'; | ||
import NodePyATVDeviceEvent from './device-event'; | ||
import {addRequestId, debug, execute, getParamters, parseState, removeRequestId} from './tools'; | ||
import {FakeChildProcess} from './fake-spawn'; | ||
|
||
export default class NodePyATVDeviceEvents extends EventEmitter { | ||
private readonly options: NodePyATVDeviceOptions; | ||
private readonly state: NodePyATVState; | ||
private readonly device: NodePyATVDevice; | ||
private pyatv: ChildProcess | FakeChildProcess | undefined; | ||
private timeout: NodeJS.Timeout | undefined; | ||
private listenerState: NodePyATVListenerState; | ||
|
||
constructor(state: NodePyATVState, device: NodePyATVDevice, options: NodePyATVDeviceOptions) { | ||
super(); | ||
|
||
this.state = state; | ||
this.device = device; | ||
this.options = Object.assign({}, options); | ||
this.listenerState = NodePyATVListenerState.stopped; | ||
} | ||
|
||
applyStateAndEmitEvents(newState: NodePyATVState): void { | ||
Object.keys(this.state).forEach((key: string) => { | ||
// @ts-ignore | ||
const oldValue = this.state[key]; | ||
|
||
// @ts-ignore | ||
const newValue = newState[key]; | ||
|
||
if(oldValue === undefined || newValue === undefined || oldValue === newValue) { | ||
return; | ||
} | ||
|
||
const event = new NodePyATVDeviceEvent({ | ||
key: key as NodePyATVStateIndex, | ||
old: oldValue, | ||
new: newValue, | ||
device: this.device | ||
}); | ||
|
||
// @ts-ignore | ||
this.state[key] = newState[key]; | ||
|
||
try { | ||
this.emit('update:' + key, event); | ||
this.emit('update', event); | ||
} | ||
catch(error) { | ||
this.emit('error', error); | ||
} | ||
}); | ||
} | ||
|
||
private applyPushUpdate(update: NodePyATVInternalState, reqId: string): void { | ||
const newState = parseState(update, reqId, this.options); | ||
this.applyStateAndEmitEvents(newState); | ||
} | ||
|
||
private checkListener(): void { | ||
if(this.listenerState === NodePyATVListenerState.stopped && this.listenerCount() === 0 && this.timeout) { | ||
clearTimeout(this.timeout); | ||
this.timeout = undefined; | ||
} | ||
if(this.listenerState === NodePyATVListenerState.stopped && this.listenerCount() > 0) { | ||
const id = addRequestId(); | ||
debug(id, `Start listeing to events from device ${this.options.name}`, this.options); | ||
|
||
this.startListening(id); | ||
removeRequestId(id); | ||
} | ||
else if(this.listenerState === NodePyATVListenerState.started && this.listenerCount() === 0) { | ||
const id = addRequestId(); | ||
debug(id, `Stop listening to events from device ${this.options.name}`, this.options); | ||
|
||
this.stopListening(id) | ||
.catch(error => debug(id, `Unable to stop listeing: ${error}`, this.options)) | ||
.finally(() => removeRequestId(id)); | ||
} | ||
} | ||
|
||
private startListening(reqId: string): void { | ||
if(this.listenerState !== NodePyATVListenerState.stopped) { | ||
return; | ||
} | ||
|
||
this.listenerState = NodePyATVListenerState.starting; | ||
|
||
const listenStart = new Date().getTime(); | ||
const parameters = getParamters(this.options); | ||
this.pyatv = execute(reqId, NodePyATVExecutableType.atvscript, [...parameters, 'push_updates'], this.options); | ||
|
||
const onError = (error: Error) => { | ||
debug(reqId, `Got error from child process: ${error}`, this.options); | ||
this.emit('error', error); | ||
}; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const onStdErr = (data: any) => { | ||
const error = new Error(`Got stderr output from pyatv: ${data}`); | ||
debug(reqId, data.toString(), this.options); | ||
this.emit('error', error); | ||
}; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const onStdOut = (data: any) => { | ||
let json: NodePyATVInternalState; | ||
|
||
try { | ||
json = JSON.parse(String(data).trim()); | ||
} | ||
catch(error) { | ||
const msg = `Unable to parse stdout json: ${error}`; | ||
debug(reqId, msg, this.options); | ||
this.emit('error', new Error(msg)); | ||
return; | ||
} | ||
|
||
this.applyPushUpdate(json, reqId); | ||
|
||
if(this.listenerState === NodePyATVListenerState.starting) { | ||
this.listenerState = NodePyATVListenerState.started; | ||
this.checkListener(); | ||
} | ||
}; | ||
const onClose = (code: number) => { | ||
if(this.pyatv === undefined) { | ||
// this should never happen… :/ | ||
return; | ||
} | ||
|
||
this.listenerState = NodePyATVListenerState.stopped; | ||
debug(reqId, `Listening with atvscript exited with code ${code}`, this.options); | ||
if(this.timeout !== undefined) { | ||
clearTimeout(this.timeout); | ||
this.timeout = undefined; | ||
} | ||
|
||
if (this.pyatv.stdout) { | ||
this.pyatv.stdout.off('data', onStdOut); | ||
} | ||
if (this.pyatv.stderr) { | ||
this.pyatv.stderr.off('data', onStdErr); | ||
} | ||
this.pyatv.off('error', onError); | ||
this.pyatv.off('close', onClose); | ||
|
||
|
||
if(this.listenerCount() > 0 && new Date().getTime() - listenStart < 30000) { | ||
debug(reqId, `Wait 15s and restart listeing to events from device ${this.options.name}`, this.options); | ||
|
||
this.timeout = setTimeout(() => { | ||
this.checkListener(); | ||
}, 15000); | ||
} | ||
else if(this.listenerCount() > 0) { | ||
debug(reqId, `Restart listeing to events from device ${this.options.name}`, this.options); | ||
this.checkListener(); | ||
} | ||
|
||
removeRequestId(reqId); | ||
}; | ||
|
||
this.pyatv.on('error', onError); | ||
this.pyatv.on('close', onClose); | ||
|
||
if (this.pyatv.stdout) { | ||
this.pyatv.stdout.on('data', onStdOut); | ||
} | ||
if (this.pyatv.stderr) { | ||
this.pyatv.stderr.on('data', onStdErr); | ||
} | ||
} | ||
|
||
protected async stopListening(reqId: string): Promise<void> { | ||
if(this.listenerState !== NodePyATVListenerState.started) { | ||
return; | ||
} | ||
|
||
this.listenerState = NodePyATVListenerState.stopping; | ||
if(this.pyatv === undefined) { | ||
throw new Error( | ||
'Unable to stop listening due to internal error: state is stopping, but there\'s no child process. ' + | ||
'This should never happen, please report this.' | ||
); | ||
} | ||
|
||
if(this.pyatv.stdin) { | ||
this.pyatv.stdin.write('\n'); | ||
} | ||
|
||
await new Promise(cb => this.timeout = setTimeout(cb, 50)); // @todo kill process | ||
|
||
this.listenerState = NodePyATVListenerState.stopped; | ||
return; | ||
} | ||
|
||
addListener(event: string | symbol, listener: (event: NodePyATVDeviceEvent) => void): this { | ||
super.addListener(event, listener); | ||
this.checkListener(); | ||
return this; | ||
} | ||
|
||
on(event: string | symbol, listener: (event: NodePyATVDeviceEvent) => void): this { | ||
super.on(event, listener); | ||
this.checkListener(); | ||
return this; | ||
} | ||
|
||
once(event: string | symbol, listener: (event: NodePyATVDeviceEvent) => void): this { | ||
super.once(event, listener); | ||
this.checkListener(); | ||
|
||
super.once(event, () => this.checkListener()); | ||
return this; | ||
} | ||
|
||
// @todo prependListener | ||
// @todo prependOnceListener | ||
// @todo rawListeners | ||
|
||
off(event: string | symbol, listener: (event: NodePyATVDeviceEvent) => void): this { | ||
super.off(event, listener); | ||
this.checkListener(); | ||
return this; | ||
} | ||
|
||
removeAllListeners(event?: string | symbol): this { | ||
super.removeAllListeners(event); | ||
this.checkListener(); | ||
return this; | ||
} | ||
|
||
removeListener(event: string | symbol, listener: (event: NodePyATVDeviceEvent) => void): this { | ||
super.removeListener(event, listener); | ||
this.checkListener(); | ||
return this; | ||
} | ||
|
||
listenerCount(event?: string | symbol): number { | ||
if(event !== undefined) { | ||
return super.listenerCount(event); | ||
} | ||
|
||
return this.eventNames() | ||
.map(event => this.listenerCount(event)) | ||
.reduce((a, b) => a + b, 0); | ||
} | ||
} |
Oops, something went wrong.