-
Notifications
You must be signed in to change notification settings - Fork 2
/
telnet-app-protocol.coffee
164 lines (143 loc) · 4.98 KB
/
telnet-app-protocol.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# TelnetAppProtocol class
module.exports = (env) ->
Promise = env.require 'bluebird'
retry = require('promise-retryer')(Promise)
net = require 'net'
_ = env.require 'lodash'
commons = require('pimatic-plugin-commons')(env)
commands =
POWER: /^(PW)([A-Z]+)/
VOLUME: /^(MV)([0-9]+)/
MAXVOLUME: /^(MVMAX)[ ]{0,1}([0-9]+)/
MAINMUTE: /^(MU)([A-Z]+)/
Z2MUTE: /^(Z2MU)([A-Z]+)/
Z3MUTE: /^(Z3MU)([A-Z]+)/
INPUT: /^(SI)(.+)/
MAIN: /^(ZM)(.+)/
ZONE2: /^(Z2)(.+)/
ZONE3: /^(Z3)(.+)/
settled = (promise) -> promise.reflect()
series = (input, mapper) -> Promise.mapSeries(input, mapper)
# ###DenonAvrPlugin class
class TelnetAppProtocol extends require('events').EventEmitter
constructor: (@config) ->
@isConnected = false
@commandQueue = []
@host = @config.host
@port = @config.port || 23
@debug = @config.debug || false
@lastFlush = new Promise.resolve()
@connectRequest = new Promise.resolve()
@base = commons.base @, 'TelnetAppProtocol'
_onCloseHandler: () ->
return () =>
@base.debug "Connection closed"
_onTimeoutHandler: () ->
return () =>
if @isConnected
@base.debug "Connection idle, closing"
@isConnected = false
@socket.setTimeout 0
@socket.destroy()
_onErrorHandler: () ->
return (error) =>
if @isConnected
@base.error "Connection Error:", error
@isConnected = false
@socket.setTimeout 0
@socket.destroy()
_onDataHandler: () ->
return (data) =>
responses = data.toString().trim().split '\r'
#responses = @base.unique responses
@base.debug('Responses:', responses)
for response in responses
for command, regex of commands
matchedResults = response.match regex
if matchedResults?
@emit 'response',
matchedResults: matchedResults
command: matchedResults[1]
param: matchedResults[2]
message: matchedResults[0]
break
responses.length = 0
_onConnectedSuccessHandler: (resolve) ->
return () =>
@isConnected = true
@base.debug "Connected"
@socket.removeAllListeners 'error'
@socket.on 'error', @_onErrorHandler()
@_flush().then =>
resolve()
_onConnectedErrorHandler: (reject) ->
return (error) =>
@isConnected = false
env.logger.debug "Connection failed:", error if @debug
@socket.removeAllListeners 'connect'
@socket.destroy()
reject(error)
_connect: () ->
return new Promise (resolve, reject) =>
if not @isConnected
@socket = new net.Socket allowHalfOpen: false
@socket.setEncoding 'utf8'
@socket.setNoDelay true
@socket.setTimeout 10000
@socket.removeAllListeners()
@socket.on 'data', @_onDataHandler()
@socket.once 'timeout', @_onTimeoutHandler()
@socket.once 'close', @_onCloseHandler()
@socket.on 'error', @_onErrorHandler()
@socket.once 'connect', @_onConnectedSuccessHandler(resolve)
@socket.once 'error', @_onConnectedErrorHandler(reject)
@base.debug "Trying to connect to #{@host}:#{@port}"
@socket.connect @port, @host
else
resolve()
connect: () ->
return @connectRequest = settled(@connectRequest).then () =>
retry.run({
maxRetries: 20
delay: 1000
promise: @_connect.bind @
}).catch (error) =>
Promise.reject "Unable to connect to #{@host}:#{@port} (retries: 20): " + error
_write: (cmd) ->
new Promise (resolve) =>
@socket.write cmd, () =>
if cmd.match(/^PWON/)?
# enforce 2 seconds delay after power on for next command
@pause(2000).then () =>
resolve()
else
resolve()
_flush: () ->
return @lastFlush = settled(@lastFlush).then () =>
commandsToSend = _.cloneDeep @commandQueue
@commandQueue.length = 0
@base.debug "Flushing:", commandsToSend
return series(commandsToSend, (cmd) =>
@base.debug "Sending:", [cmd]
@_write cmd
).finally () =>
commandsToSend.length = 0
pause: (ms=50) ->
@base.debug "Pausing:", ms, "ms"
Promise.delay ms
sendRequest: (command, param="") ->
return new Promise (resolve, reject) =>
@connect().then( =>
commandString ="#{command}#{param}\r"
# look for duplicate in commandQueue, remove it if found
#index = @commandQueue.indexOf commandString
#@commandQueue.splice index, 1 if index >= 0
@commandQueue.push commandString
process.nextTick () =>
if @isConnected
@_flush().then =>
resolve()
else
resolve()
).catch (error) =>
reject error