aca-device-modules 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,346 @@
1
+ module Panasonic; end
2
+ module Panasonic::Camera; end
3
+
4
+
5
+ class Panasonic::Camera::He50
6
+ include ::Orchestrator::Constants
7
+ include ::Orchestrator::Transcoder
8
+
9
+ def on_load
10
+ on_update
11
+ end
12
+
13
+ def on_update
14
+ defaults({
15
+ delay: 130, # As per the manual page 8
16
+ keepalive: false,
17
+ inactivity_timeout: 1.5, # seconds before closing the connection if no response
18
+ connect_timeout: 2 # max seconds for the initial connection to the device
19
+ })
20
+
21
+ self[:pan_max] = 0xD2F5
22
+ self[:pan_min] = 0x2D08
23
+ self[:pan_center] = 0x7FFF
24
+ self[:tilt_max] = 0x8E38
25
+ self[:tilt_min] = 0x5556
26
+ self[:tilt_center] = 0x7FFF
27
+
28
+ self[:joy_left] = 0x01
29
+ self[:joy_right] = 0x99
30
+ self[:joy_center] = 0x50
31
+
32
+ self[:zoom_max] = 0xFFF
33
+ self[:zoom_min] = 0x555
34
+
35
+ self[:focus_max] = 0xFFF
36
+ self[:focus_min] = 0x555
37
+
38
+ self[:iris_max] = 0xFFF
39
+ self[:iris_min] = 0x555
40
+
41
+ # {near: {zoom: val, pan: val, tilt: val}}
42
+ @presets = setting(:presets) || {}
43
+ self[:presets] = @presets.keys
44
+ end
45
+
46
+ def connected
47
+ schedule.every('60s', method(:do_poll))
48
+ do_poll
49
+ end
50
+
51
+
52
+ RESP = {
53
+ power: 'p',
54
+ installation: 'iNS',
55
+
56
+ pantilt: 'aPC',
57
+ joystick: 'pTS',
58
+ limit: 'lC',
59
+
60
+ zoom: /axz|gz/,
61
+ manual_zoom: 'zS',
62
+ link_zoom: 'sWZ',
63
+
64
+ focus: /axf|gf/,
65
+ manual_focus: 'fS',
66
+ auto_focus: 'd1',
67
+
68
+ iris: /axi|gi/,
69
+ auto_iris: 'd3'
70
+ }
71
+ LIMITS = {
72
+ up: 1,
73
+ down: 2,
74
+ left: 3,
75
+ right: 4
76
+ }
77
+ LIMITS.merge!(LIMITS.invert)
78
+
79
+
80
+ # Responds with:
81
+ # 0 == standby
82
+ # 1 == power on
83
+ # 3 == powering on
84
+ def power(state = nil, &blk)
85
+ state = (is_affirmative?(state) ? 1 : 0) unless state.nil?
86
+
87
+ options = {}
88
+ options[:emit] = blk if blk
89
+ options[:delay] = 6000 if state
90
+
91
+ logger.debug "Camera requested power #{state}"
92
+
93
+ req('O', state, :power, options) do |data, resolve|
94
+ val = extract(:power, data, resolve)
95
+ if val
96
+ self[:power] = val.to_i > 0
97
+ :success
98
+ end
99
+ end
100
+ end
101
+
102
+ def installation(pos = nil)
103
+ pos = (pos.to_sym == :desk ? 0 : 1) if pos
104
+
105
+ req('INS', pos, :installation) do |data, resolve|
106
+ val = extract(:installation, data, resolve)
107
+ if val
108
+ self[:installation] = val == '0' ? :desk : :ceiling
109
+ :success
110
+ end
111
+ end
112
+ end
113
+
114
+ def pantilt(pan = nil, tilt = nil)
115
+ unless pan.nil?
116
+ pan = in_range(pan.to_i, self[:pan_max], self[:pan_min]).to_s(16).upcase.rjust(4, '0')
117
+ tilt = in_range(tilt.to_i, self[:tilt_max], self[:tilt_min]).to_s(16).upcase.rjust(4, '0')
118
+ end
119
+
120
+ req('APC', "#{pan}#{tilt}", :pantilt) do |data, resolve|
121
+ val = extract(:pantilt, data, resolve)
122
+ if val
123
+ comp = []
124
+ val.scan(/.{4}/) { |com| comp << com.to_i(16) }
125
+ self[:pan] = comp[0]
126
+ self[:tilt] = comp[1]
127
+ :success
128
+ end
129
+ end
130
+ end
131
+
132
+ # Recall a preset from the database
133
+ def preset(name)
134
+ values = @presets[name.to_sym]
135
+ if values
136
+ pantilt(values[:pan], values[:tilt])
137
+ zoom(values[:zoom])
138
+ true
139
+ else
140
+ false
141
+ end
142
+ end
143
+
144
+ def joystick(pan_speed, tilt_speed)
145
+ left_max = self[:joy_left]
146
+ right_max = self[:joy_right]
147
+ pan_speed = in_range(pan_speed.to_i, right_max, left_max).to_s(16).upcase.rjust(2, '0')
148
+ tilt_speed = in_range(tilt_speed.to_i, right_max, left_max).to_s(16).upcase.rjust(2, '0')
149
+
150
+ is_centered = false
151
+ if pan_speed == '50' && tilt_speed == '50'
152
+ is_centered = true
153
+ end
154
+
155
+ options = {}
156
+ options[:retries] = is_centered ? 1 : 0
157
+
158
+ logger.debug("Sending camera: #{pan_speed}#{tilt_speed}");
159
+
160
+ req('PTS', "#{pan_speed}#{tilt_speed}", :joystick, options) do |data, resolve|
161
+ val = extract(:joystick, data, resolve)
162
+ if val
163
+ comp = []
164
+ val.scan(/.{2}/) { |com| comp << com.to_i(16) }
165
+ self[:joy_pan] = comp[0]
166
+ self[:joy_tilt] = comp[1]
167
+ :success
168
+ end
169
+ end
170
+ end
171
+
172
+ def limit(direction, state = nil)
173
+ dir = LIMITS[direction.to_sym]
174
+ state = (is_affirmative?(set) ? 1 : 0) unless state.nil?
175
+
176
+ req('LC', "#{dir}#{state}", :limit) do |data, resolve|
177
+ val = extract(:limit, data, resolve)
178
+ if val
179
+ self[:"limit_#{LIMITS[val[0].to_i]}"] = val[1] == '1'
180
+ :success
181
+ end
182
+ end
183
+ end
184
+
185
+
186
+ def zoom(pos = nil)
187
+ cmd = 'AXZ'
188
+ if pos
189
+ pos = in_range(pos.to_i, self[:zoom_max], self[:zoom_min]).to_s(16).upcase.rjust(3, '0')
190
+ else
191
+ cmd = 'GZ'
192
+ end
193
+
194
+ req(cmd, pos, :zoom) do |data, resolve|
195
+ val = extract(:zoom, data, resolve)
196
+ if val
197
+ self[:zoom] = val.to_i(16)
198
+ :success
199
+ end
200
+ end
201
+ end
202
+
203
+ def manual_zoom(speed)
204
+ speed = in_range(speed.to_i, self[:joy_right], self[:joy_left]).to_s(16).upcase.rjust(3, '0')
205
+
206
+ req('Z', speed, :manual_zoom) do |data, resolve|
207
+ val = extract(:manual_zoom, data, resolve)
208
+ if val
209
+ self[:manual_zoom] = val.to_i(16)
210
+ :success
211
+ end
212
+ end
213
+ end
214
+
215
+ def link_zoom(state = nil) # Link pantilt speed to zoom
216
+ state = (is_affirmative?(state) ? 1 : 0) unless state.nil?
217
+
218
+ req('SWZ', state, :link_zoom) do |data, resolve|
219
+ val = extract(:link_zoom, data, resolve)
220
+ if val
221
+ self[:link_zoom] = val == '1'
222
+ :success
223
+ end
224
+ end
225
+ end
226
+
227
+
228
+ def focus(pos = nil)
229
+ cmd = 'AXF'
230
+ if pos
231
+ pos = in_range(pos.to_i, self[:focus_max], self[:focus_min]).to_s(16).upcase.rjust(3, '0')
232
+ else
233
+ cmd = 'GF'
234
+ end
235
+
236
+ req(cmd, pos, :focus) do |data, resolve|
237
+ val = extract(:focus, data, resolve)
238
+ if val
239
+ self[:focus] = val.to_i(16)
240
+ :success
241
+ end
242
+ end
243
+ end
244
+
245
+ def manual_focus(speed)
246
+ speed = in_range(speed.to_i, self[:joy_right], self[:joy_left]).to_s(16).upcase.rjust(2, '0')
247
+
248
+ req('F', speed, :manual_focus) do |data, resolve|
249
+ val = extract(:manual_focus, data, resolve)
250
+ if val
251
+ self[:manual_focus] = val.to_i(16)
252
+ :success
253
+ end
254
+ end
255
+ end
256
+
257
+ def auto_focus(state = nil)
258
+ state = (is_affirmative?(state) ? 1 : 0) unless state.nil?
259
+
260
+ req('D1', state, :auto_focus) do |data, resolve|
261
+ val = extract(:auto_focus, data, resolve)
262
+ if val
263
+ self[:auto_focus] = val == '1'
264
+ :success
265
+ end
266
+ end
267
+ end
268
+
269
+
270
+ def iris(level = nil)
271
+ cmd = 'AXI'
272
+ if level
273
+ level = in_range(level.to_i, self[:iris_max], self[:iris_min]).to_s(16).upcase.rjust(3, '0')
274
+ else
275
+ cmd = 'GI'
276
+ end
277
+
278
+ req(cmd, level, :iris) do |data, resolve|
279
+ val = extract(:iris, data, resolve)
280
+ if val
281
+ self[:iris] = val[0..2].to_i(16)
282
+ if val.length == 4
283
+ self[:auto_iris] = val == '1'
284
+ end
285
+ :success
286
+ end
287
+ end
288
+ end
289
+
290
+ def auto_iris(state = nil)
291
+ state = (is_affirmative?(state) ? 1 : 0) unless state.nil?
292
+
293
+ req('D3', state, :auto_iris) do |data, resolve|
294
+ val = extract(:auto_iris, data, resolve)
295
+ if val
296
+ self[:auto_iris] = val == '1'
297
+ :success
298
+ end
299
+ end
300
+ end
301
+
302
+
303
+ protected
304
+
305
+
306
+ def req(cmd, data, name, options = {}, &blk)
307
+ if data.nil? || (data.respond_to?(:empty?) && data.empty?)
308
+ options[:delay] = 0
309
+ options[:priority] = 0 # Actual commands have a higher priority
310
+ else
311
+ options[:name] = name
312
+ end
313
+ request_string = "/cgi-bin/aw_ptz?cmd=%23#{cmd}#{data}&res=1"
314
+ get(request_string, options, &blk)
315
+
316
+ logger.debug "requesting #{name}: #{request_string}"
317
+ end
318
+
319
+ def extract(name, data, resp)
320
+ logger.debug "received #{data} for command #{name}"
321
+
322
+ body = data[:body]
323
+ if body[0] == 'e'
324
+ notify_error(body, 'invalid command sent', data)
325
+ resp.call(:failed)
326
+ nil
327
+ else
328
+ body.sub(RESP[name], '')
329
+ end
330
+ end
331
+
332
+ def do_poll(*args)
333
+ power do
334
+ if self[:power] # only request status if online
335
+ pantilt
336
+ zoom
337
+ end
338
+ end
339
+ end
340
+
341
+ def notify_error(err, msg, cmd)
342
+ cmd = cmd[:request]
343
+ logger.warn "Camera error response: #{err} - #{msg} for #{cmd[:path]} #{cmd[:query]}"
344
+ end
345
+ end
346
+
@@ -0,0 +1,266 @@
1
+ module Panasonic; end
2
+ module Panasonic::Projector; end
3
+
4
+
5
+ require 'digest/md5'
6
+
7
+ #
8
+ # Port: 1024
9
+ #
10
+ class Panasonic::Projector::PjLink
11
+ include ::Orchestrator::Constants
12
+ include ::Orchestrator::Transcoder
13
+
14
+ def on_load
15
+ # PJLink is slow
16
+ defaults({
17
+ timeout: 4000,
18
+ delay_on_receive: 400
19
+ })
20
+
21
+ config({
22
+ tokenize: true,
23
+ delimiter: "\r",
24
+ wait_ready: 'NTCONTROL'
25
+ })
26
+
27
+ @check_scheduled = false
28
+ self[:power] = false
29
+ self[:stable_state] = true # Stable by default (allows manual on and off)
30
+ self[:input_stable] = true
31
+
32
+ # Meta data for inquiring interfaces
33
+ self[:type] = :projector
34
+ end
35
+
36
+ def on_update
37
+ end
38
+
39
+ def connected
40
+ @polling_timer = schedule.every('60s', method(:do_poll))
41
+ end
42
+
43
+ def disconnected
44
+ self[:power] = false
45
+
46
+ @polling_timer.cancel unless @polling_timer.nil?
47
+ @polling_timer = nil
48
+ end
49
+
50
+
51
+
52
+ #
53
+ # Power commands
54
+ #
55
+ def power(state, opt = nil)
56
+ self[:stable_state] = false
57
+ if is_affirmative?(state)
58
+ self[:power_target] = On
59
+ do_send(:POWR, 1, {:retries => 10, :name => :power})
60
+ logger.debug "-- panasonic Proj, requested to power on"
61
+ do_send('POWR', '?', :name => :power_state)
62
+ else
63
+ self[:power_target] = Off
64
+ do_send(:POWR, 0, {:retries => 10, :name => :power})
65
+ logger.debug "-- panasonic Proj, requested to power off"
66
+ do_send('POWR', '?', :name => :power_state)
67
+ end
68
+ end
69
+
70
+ def power?(options = {}, &block)
71
+ options[:emit] = block if block_given?
72
+ options[:name] = :power_state
73
+ do_send(:POWR, options)
74
+ end
75
+
76
+
77
+
78
+ #
79
+ # Input selection
80
+ #
81
+ INPUTS = {
82
+ :hdmi => 31,
83
+ :hdmi2 => 32,
84
+ :digital => 33,
85
+ :miracast => 52
86
+ }
87
+ INPUTS.merge!(INPUTS.invert)
88
+
89
+
90
+ def switch_to(input)
91
+ input = input.to_sym
92
+ return unless INPUTS.has_key? input
93
+
94
+ do_send(:INPT, INPUTS[input], {:retries => 10, :name => :inpt_source})
95
+ do_send('INPT', '?', {:name => :inpt_query})
96
+ logger.debug "-- panasonic LCD, requested to switch to: #{input}"
97
+
98
+ self[:input] = input # for a responsive UI
99
+ self[:input_stable] = false
100
+ end
101
+
102
+
103
+ #
104
+ # Mute Audio and Video
105
+ #
106
+ def mute
107
+ logger.debug "-- panasonic Proj, requested to mute"
108
+ do_send(:AVMT, 31, {:name => :video_mute}) # Audio + Video
109
+ end
110
+
111
+ def unmute
112
+ logger.debug "-- panasonic Proj, requested to unmute"
113
+ do_send(:AVMT, 30, {:name => :video_mute})
114
+ end
115
+
116
+
117
+ ERRORS = {
118
+ :ERR1 => '1: Undefined control command'.freeze,
119
+ :ERR2 => '2: Out of parameter range'.freeze,
120
+ :ERR3 => '3: Busy state or no-acceptable period'.freeze,
121
+ :ERR4 => '4: Timeout or no-acceptable period'.freeze,
122
+ :ERR5 => '5: Wrong data length'.freeze,
123
+ :ERRA => 'A: Password mismatch'.freeze
124
+ }
125
+
126
+
127
+ def received(data, resolve, command) # Data is default received as a string
128
+ logger.debug "panasonic Proj sent: #{data}"
129
+
130
+ # This is the ready response
131
+ if data[0] == ' '
132
+ @mode = data[1]
133
+ if @mode == '1'
134
+ @pass = "#{setting(:username) || 'admin1'}:#{setting(:password) || 'panasonic'}:#{data.strip.split(/\s+/)[-1]}"
135
+ @pass = Digest::MD5.hexdigest(@hash)
136
+ end
137
+
138
+ :success
139
+
140
+ # Error Response
141
+ elsif data[0] == 'E'
142
+ error = data.to_sym
143
+ #self[:last_error] = ERRORS[error] (for error status query)
144
+
145
+ # Check for busy or timeout
146
+ if error == :ERR3 || error == :ERR4
147
+ logger.warn "Panasonic Proj busy: #{self[:last_error]}"
148
+ :retry
149
+ else
150
+ logger.error "Panasonic Proj error: #{self[:last_error]}"
151
+ :abort
152
+ end
153
+
154
+ # Success Response
155
+ else
156
+ data = data[2..-1].split('=')
157
+
158
+ if data[1] = 'OK'
159
+ return :success
160
+ else
161
+ type = data[0][2..-1].to_sym
162
+ response = data[1].to_i
163
+ resolve.call(:success)
164
+
165
+ case type
166
+ when :POWR
167
+ self[:power] = response >= 1 && response != 2
168
+ self[:warming] = response == 3
169
+ self[:cooling] = response == 2
170
+ if response >= 2 && !@check_scheduled && !self[:stable_state]
171
+ @check_scheduled = true
172
+ schedule.in('20s') do
173
+ @check_scheduled = false
174
+ logger.debug "-- checking panasonic state"
175
+ power?({:priority => 0}) do
176
+ state = self[:power]
177
+ if state != self[:power_target]
178
+ if self[:power_target] || !self[:cooling]
179
+ power(self[:power_target])
180
+ end
181
+ elsif self[:power_target] && self[:cooling]
182
+ power(self[:power_target])
183
+ else
184
+ self[:stable_state] = true
185
+ switch_to(self[:input]) if self[:power_target] == On && !self[:input].nil?
186
+ end
187
+ end
188
+ end
189
+ end
190
+ when :INPT
191
+ if INPUTS[response].present?
192
+ self[:input] = INPUTS[response] if self[:input].nil?
193
+ self[:actual_input] = INPUTS[response]
194
+ if self[:input] == self[:actual_input]
195
+ self[:input_stable] = true
196
+ elsif self[:input_stable] == false
197
+ schedule.in('5s') do
198
+ logger.debug "-- forcing panasonic input"
199
+ switch_to(self[:input]) if self[:input_stable] == false
200
+ end
201
+ end
202
+ end
203
+ when :AVMT
204
+ self[:mute] = response == 31 # 10 == video mute off, 11 == video mute, 20 == audio mute off, 21 == audio mute, 30 == AV mute off
205
+ when :LAMP
206
+ self[:lamp] = data[1][0..-2].to_i
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+
213
+
214
+ protected
215
+
216
+
217
+ def do_poll(*args)
218
+ power?({:priority => 0}) do
219
+ if self[:power]
220
+ if self[:stable_state] == false && self[:power_target] == Off
221
+ power(Off)
222
+ else
223
+ self[:stable_state] = true
224
+ do_send(:INPT, {
225
+ :name => :inpt_query,
226
+ :priority => 0
227
+ })
228
+ do_send(:AVMT, {
229
+ :name => :mute_query,
230
+ :priority => 0
231
+ })
232
+ do_send(:LAMP, {
233
+ :name => :lamp_query,
234
+ :priority => 0
235
+ })
236
+ end
237
+ elsif self[:stable_state] == false
238
+ if self[:power_target] == On
239
+ power(On)
240
+ else
241
+ self[:stable_state] = true
242
+ end
243
+ end
244
+ end
245
+ end
246
+
247
+ def do_send(command, param = nil, options = {})
248
+ if param.is_a? Hash
249
+ options = param
250
+ param = nil
251
+ end
252
+
253
+ if param.nil?
254
+ pj = "#{command} ?"
255
+ else
256
+ pj = "#{command} #{param}"
257
+ end
258
+
259
+ if @mode == '0'
260
+ send("00#{pj}\r", options)
261
+ else
262
+ send("#{@pass}00#{pj}\r", options)
263
+ end
264
+ end
265
+ end
266
+