aca-device-modules 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,250 @@
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::Tcp
11
+ include ::Orchestrator::Constants
12
+ include ::Orchestrator::Transcoder
13
+
14
+ def on_load
15
+ # Response time is slow
16
+ defaults({
17
+ timeout: 2000,
18
+ delay_on_receive: 1000
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
+
31
+ # Meta data for inquiring interfaces
32
+ self[:type] = :projector
33
+
34
+ # The projector drops the connection when there is no activity
35
+ schedule.every('60s') do
36
+ power?({:priority => 0})
37
+ end
38
+ end
39
+
40
+ def on_update
41
+ end
42
+
43
+ def connected
44
+ end
45
+
46
+ def disconnected
47
+ end
48
+
49
+
50
+ COMMANDS = {
51
+ power_on: :PON,
52
+ power_off: :POF,
53
+ power_query: :QPW,
54
+ freeze: :OFZ,
55
+ input: :IIS,
56
+ mute: :OSH,
57
+ lamp: :"Q$S"
58
+ }
59
+ COMMANDS.merge!(COMMANDS.invert)
60
+
61
+
62
+
63
+ #
64
+ # Power commands
65
+ #
66
+ def power(state, opt = nil)
67
+ self[:stable_state] = false
68
+ if is_affirmative?(state)
69
+ self[:power_target] = On
70
+ do_send(:power_on, {:retries => 10, :name => :power, delay_on_receive: 8000})
71
+ logger.debug "-- panasonic Proj, requested to power on"
72
+ do_send(:lamp)
73
+ else
74
+ self[:power_target] = Off
75
+ do_send(:power_off, {:retries => 10, :name => :power, delay_on_receive: 8000})
76
+ logger.debug "-- panasonic Proj, requested to power off"
77
+ do_send(:lamp)
78
+ end
79
+ end
80
+
81
+ def power?(options = {}, &block)
82
+ options[:emit] = block if block_given?
83
+ do_send(:lamp, options)
84
+ end
85
+
86
+
87
+
88
+ #
89
+ # Input selection
90
+ #
91
+ INPUTS = {
92
+ :hdmi => :HD1,
93
+ :hdmi2 => :HD2,
94
+ :vga => :RG1,
95
+ :vga2 => :RG2,
96
+ :miracast => :MC1
97
+ }
98
+ INPUTS.merge!(INPUTS.invert)
99
+
100
+
101
+ def switch_to(input)
102
+ input = input.to_sym
103
+ return unless INPUTS.has_key? input
104
+
105
+ # Projector doesn't automatically unmute
106
+ unmute if self[:mute]
107
+
108
+ do_send(:input, INPUTS[input], {:retries => 10, delay_on_receive: 2000})
109
+ logger.debug "-- panasonic LCD, requested to switch to: #{input}"
110
+
111
+ self[:input] = input # for a responsive UI
112
+ end
113
+
114
+
115
+ #
116
+ # Mute Audio and Video
117
+ #
118
+ def mute(val = true)
119
+ actual = val ? 1 : 0
120
+ logger.debug "-- panasonic Proj, requested to mute"
121
+ do_send(:mute, actual) # Audio + Video
122
+ end
123
+
124
+ def unmute
125
+ logger.debug "-- panasonic Proj, requested to unmute"
126
+ do_send(:mute, 0)
127
+ end
128
+
129
+
130
+ ERRORS = {
131
+ :ERR1 => '1: Undefined control command'.freeze,
132
+ :ERR2 => '2: Out of parameter range'.freeze,
133
+ :ERR3 => '3: Busy state or no-acceptable period'.freeze,
134
+ :ERR4 => '4: Timeout or no-acceptable period'.freeze,
135
+ :ERR5 => '5: Wrong data length'.freeze,
136
+ :ERRA => 'A: Password mismatch'.freeze,
137
+ :ER401 => '401: Command cannot be executed'.freeze,
138
+ :ER402 => '402: Invalid parameter is sent'.freeze
139
+ }
140
+
141
+
142
+ def received(data, resolve, command) # Data is default received as a string
143
+ logger.debug "panasonic Proj sent: #{data}"
144
+
145
+ # This is the ready response
146
+ if data[0] == ' '
147
+ @mode = data[1]
148
+ if @mode == '1'
149
+ @pass = "#{setting(:username) || 'admin1'}:#{setting(:password) || 'panasonic'}:#{data.strip.split(/\s+/)[-1]}"
150
+ @pass = Digest::MD5.hexdigest(@pass)
151
+ end
152
+
153
+ else
154
+ data = data[2..-1]
155
+
156
+ # Error Response
157
+ if data[0] == 'E'
158
+ error = data.to_sym
159
+ self[:last_error] = ERRORS[error]
160
+
161
+ # Check for busy or timeout
162
+ if error == :ERR3 || error == :ERR4
163
+ logger.warn "Panasonic Proj busy: #{self[:last_error]}"
164
+ return :retry
165
+ else
166
+ logger.error "Panasonic Proj error: #{self[:last_error]}"
167
+ return :abort
168
+ end
169
+ end
170
+
171
+ resp = data.split(':')
172
+ cmd = COMMANDS[resp[0].to_sym]
173
+ val = resp[1]
174
+
175
+ case cmd
176
+ when :power_on
177
+ self[:power] = true
178
+ when :power_off
179
+ self[:power] = false
180
+ when :power_query
181
+ self[:power] = val.to_i == 1
182
+ when :freeze
183
+ self[:frozen] = val.to_i == 1
184
+ when :input
185
+ self[:input] = INPUTS[val.to_sym]
186
+ when :mute
187
+ self[:mute] = val.to_i == 1
188
+ else
189
+ if command && command[:name] == :lamp
190
+ ival = resp[0].to_i
191
+ self[:power] = ival == 1 || ival == 2
192
+ self[:warming] = ival == 1
193
+ self[:cooling] = ival == 3
194
+
195
+ if (self[:warming] || self[:cooling]) && !@check_scheduled && !self[:stable_state]
196
+ @check_scheduled = true
197
+ schedule.in('13s') do
198
+ @check_scheduled = false
199
+ logger.debug "-- checking panasonic state"
200
+ power?({:priority => 0}) do
201
+ state = self[:power]
202
+ if state != self[:power_target]
203
+ if self[:power_target] || !self[:cooling]
204
+ power(self[:power_target])
205
+ end
206
+ elsif self[:power_target] && self[:cooling]
207
+ power(self[:power_target])
208
+ else
209
+ self[:stable_state] = true
210
+ switch_to(self[:input]) if self[:power_target] == On && !self[:input].nil?
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ :success
220
+ end
221
+
222
+
223
+ protected
224
+
225
+
226
+ def do_send(command, param = nil, options = {})
227
+ if param.is_a? Hash
228
+ options = param
229
+ param = nil
230
+ end
231
+
232
+ # Default to the command name if name isn't set
233
+ options[:name] = command unless options[:name]
234
+
235
+ if param.nil?
236
+ pj = "#{COMMANDS[command]}"
237
+ else
238
+ pj = "#{COMMANDS[command]}:#{param}"
239
+ end
240
+
241
+ if @mode == '0'
242
+ send("00#{pj}\r", options)
243
+ else
244
+ send("#{@pass}00#{pj}\r", options)
245
+ end
246
+
247
+ nil
248
+ end
249
+ end
250
+
@@ -0,0 +1,73 @@
1
+ module ScreenTechnics; end
2
+
3
+
4
+ class ScreenTechnics::Connect
5
+ include ::Orchestrator::Constants
6
+
7
+
8
+ def on_load
9
+ defaults({
10
+ delay: 2000,
11
+ keepalive: false,
12
+ inactivity_timeout: 1.5, # seconds before closing the connection if no response
13
+ connect_timeout: 2 # max seconds for the initial connection to the device
14
+ })
15
+
16
+ self[:state] = :up
17
+ end
18
+
19
+ def on_update
20
+ end
21
+
22
+ def connected
23
+ end
24
+
25
+ def state(new_state, index = 1)
26
+ if is_affirmative?(new_state)
27
+ down(index)
28
+ else
29
+ up(index)
30
+ end
31
+ end
32
+
33
+ def down(index = 1)
34
+ stop(index)
35
+ do_send({
36
+ state: :down,
37
+ body: "Down#{index}=Down",
38
+ name: :"position#{index}",
39
+ index: index
40
+ })
41
+ end
42
+
43
+ def up(index = 1)
44
+ stop(index)
45
+ do_send({
46
+ state: :up,
47
+ body: "Up#{index}=Up",
48
+ name: :"position#{index}",
49
+ index: index
50
+ })
51
+ end
52
+
53
+ def stop(index = 1)
54
+ do_send({
55
+ body: "Stop#{index}=Stop",
56
+ name: :"stop#{index}",
57
+ priority: 99
58
+ })
59
+ end
60
+
61
+
62
+ protected
63
+
64
+
65
+ def do_send(options)
66
+ state = options.delete(:state)
67
+ index = options.delete(:index)
68
+ post('/ADirectControl.html', options) do
69
+ self[:"screen#{index}"] = state if state
70
+ :success
71
+ end
72
+ end
73
+ end
@@ -5,7 +5,7 @@ module Sony::Display; end
5
5
  #
6
6
  # Port: 53484
7
7
  #
8
- class Sony::Display::GdxAndFwd
8
+ class Sony::Display::IdTalk
9
9
  include ::Orchestrator::Constants
10
10
  include ::Orchestrator::Transcoder
11
11
 
@@ -16,9 +16,19 @@ class Sony::Display::GdxAndFwd
16
16
  self[:contrast_max] = 0x64
17
17
  self[:volume_min] = 0x00
18
18
  self[:volume_max] = 0x64
19
+
19
20
  self[:power] = false
20
21
  self[:type] = :lcd
21
22
 
23
+ config({
24
+ tokenize: proc {
25
+ ::UV::AbstractTokenizer.new({
26
+ indicator: "\x02\x10", # Header
27
+ callback: method(:check_complete)
28
+ })
29
+ }
30
+ })
31
+
22
32
  on_update
23
33
  end
24
34
 
@@ -164,15 +174,14 @@ class Sony::Display::GdxAndFwd
164
174
  }
165
175
 
166
176
 
167
- def received(data, resolve, command) # Data is default received as a string
177
+ def received(byte_str, resolve, command) # Data is default received as a string
168
178
  logger.debug "sony display sent: 0x#{byte_to_hex(data)}"
169
179
 
170
- idt_header = data[0..1]
171
- idt_community = data[2..5]
172
- idt_command = str_to_array(data[6..9])
173
- resp = str_to_array(data[10..-1])
180
+ data = str_to_array(byte_str)
181
+ idt_command = data[5..6]
182
+ resp = data[8..-1]
174
183
 
175
- if idt_command[0] == 0x01
184
+ if data[4] == 0x01
176
185
  # resp is now equal to the unit control codes
177
186
 
178
187
  type = RESP[resp[1]]
@@ -216,6 +225,28 @@ class Sony::Display::GdxAndFwd
216
225
 
217
226
 
218
227
  protected
228
+
229
+
230
+ # Called by the Abstract Tokenizer to confirm we have the
231
+ # whole message.
232
+ def check_complete(byte_str)
233
+ bytes = str_to_array(byte_str)
234
+
235
+ # Min message length is 8 bytes
236
+ return false if bytes.length < 8
237
+
238
+ # Check we have the data
239
+ data = bytes[8..-1]
240
+ if data.length == bytes[7]
241
+ return true
242
+ elsif data.length > bytes[7]
243
+ # Let the tokeniser know we only want the following number of bytes
244
+ return 7 + bytes[7]
245
+ end
246
+
247
+ # Still waiting on data
248
+ return false
249
+ end
219
250
 
220
251
 
221
252
  def do_poll(*args)
@@ -0,0 +1,300 @@
1
+ module Sony; end
2
+ module Sony::Projector; end
3
+
4
+
5
+ #
6
+ # Port: 53484
7
+ #
8
+ class Sony::Projector::PjTalk
9
+ include ::Orchestrator::Constants
10
+ include ::Orchestrator::Transcoder
11
+
12
+ def on_load
13
+ self[:brightness_min] = 0x00
14
+ self[:brightness_max] = 0x64
15
+ self[:contrast_min] = 0x00
16
+ self[:contrast_max] = 0x64
17
+
18
+ self[:power] = false
19
+ self[:type] = :projector
20
+
21
+ on_update
22
+ config({
23
+ tokenize: proc {
24
+ ::UV::AbstractTokenizer.new({
25
+ indicator: "\x02\x0a", # Header
26
+ callback: method(:check_complete)
27
+ })
28
+ }
29
+ })
30
+
31
+ defaults({
32
+ delay_on_receive: 200
33
+ })
34
+ end
35
+
36
+ def on_update
37
+ # Default community value is SONY - can be changed in displays settings
38
+ @community = str_to_array(setting(:community) || 'SONY')
39
+ end
40
+
41
+
42
+ def connected
43
+ @polling_timer = schedule.every('60s', method(:do_poll))
44
+ end
45
+
46
+ def disconnected
47
+ @polling_timer.cancel unless @polling_timer.nil?
48
+ @polling_timer = nil
49
+ end
50
+
51
+
52
+ #
53
+ # Power commands
54
+ #
55
+ def power(state)
56
+ if is_affirmative?(state)
57
+ do_send(:set, :power_on, name: :power, delay_on_receive: 3000)
58
+ logger.debug "-- sony display requested to power on"
59
+ else
60
+ do_send(:set, :power_off, name: :power, delay_on_receive: 3000)
61
+ logger.debug "-- sony display requested to power off"
62
+ end
63
+
64
+ # Request status update
65
+ power?
66
+ end
67
+
68
+ def power?(options = {}, &block)
69
+ options[:emit] = block if block_given?
70
+ options[:priority] = 0
71
+ do_send(:get, :power_status, options)
72
+ end
73
+
74
+
75
+
76
+ #
77
+ # Input selection
78
+ #
79
+ INPUTS = {
80
+ :vga => [0x00, 0x03],
81
+ :dvi => [0x00, 0x04],
82
+ :hdmi => [0x00, 0x05]
83
+ }
84
+ INPUTS.merge!(INPUTS.invert)
85
+
86
+
87
+ def switch_to(input)
88
+ input = input.to_sym
89
+ return unless INPUTS.has_key? input
90
+
91
+ do_send(:set, :input, INPUTS[input], delay_on_receive: 500)
92
+ logger.debug "-- sony projector, requested to switch to: #{input}"
93
+
94
+ input?
95
+ end
96
+
97
+ def input?
98
+ do_send(:get, :input, {:priority => 0})
99
+ end
100
+
101
+
102
+ #
103
+ # Mute Audio and Video
104
+ #
105
+ def mute(val = true)
106
+ logger.debug "-- sony projector, requested to mute"
107
+
108
+ actual = is_affirmative?(val) ? [0x00, 0x01] : [0x00, 0x00]
109
+ do_send(:set, :mute, actual, delay_on_receive: 500)
110
+ end
111
+
112
+ def unmute
113
+ logger.debug "-- sony projector, requested to unmute"
114
+ mute(false)
115
+ end
116
+
117
+ def mute?
118
+ do_send(:get, :mute, {:priority => 0})
119
+ end
120
+
121
+
122
+ #
123
+ # Automatically creates a callable function for each command
124
+ # http://blog.jayfields.com/2007/10/ruby-defining-class-methods.html
125
+ # http://blog.jayfields.com/2008/02/ruby-dynamically-define-method.html
126
+ #
127
+ [:contrast, :brightness, :color, :hue, :sharpness].each do |command|
128
+ # Query command
129
+ define_method :"#{command}?" do
130
+ do_send(:get, command, {:priority => 0})
131
+ end
132
+
133
+ # Set value command
134
+ define_method command do |level|
135
+ level = in_range(level, 0x64)
136
+ do_send(:set, command, [0x00, level])
137
+ __send__(:"#{command}?")
138
+ end
139
+ end
140
+
141
+
142
+ ERRORS = {
143
+ 0x00 => 'No Error'.freeze,
144
+ 0x01 => 'Lamp Error'.freeze,
145
+ 0x02 => 'Fan Error'.freeze,
146
+ 0x04 => 'Cover Error'.freeze,
147
+ 0x08 => 'Temperature Error'.freeze,
148
+ 0x10 => 'D5V Error'.freeze,
149
+ 0x20 => 'Power Error'.freeze,
150
+ 0x40 => 'Warning Error'.freeze
151
+ }
152
+
153
+
154
+ def received(byte_str, resolve, command) # Data is default received as a string
155
+ # Remove community string (useless)
156
+ logger.debug "sony proj sent: 0x#{byte_to_hex(byte_str[4..-1])}"
157
+
158
+ data = str_to_array(byte_str)
159
+ pjt_command = data[5..6]
160
+ pjt_length = data[7]
161
+ pjt_data = data[8..-1]
162
+
163
+ if data[4] == 0x01
164
+ case COMMANDS[pjt_command]
165
+ when :power_on
166
+ self[:power] = On
167
+ when :power_off
168
+ self[:power] = Off
169
+ else
170
+ # Same switch however now we know there is data
171
+ if pjt_length > 0
172
+ case COMMANDS[pjt_command]
173
+ when :power_status
174
+ case pjt_data[-1]
175
+ when 0, 8
176
+ self[:warming] = self[:cooling] = self[:power] = false
177
+ when 1, 2
178
+ self[:cooling] = false
179
+ self[:warming] = self[:power] = true
180
+ when 3
181
+ self[:power] = true
182
+ self[:warming] = self[:cooling] = false
183
+ when 4, 5, 6, 7
184
+ self[:cooling] = true
185
+ self[:warming] = self[:power] = false
186
+ end
187
+
188
+ if self[:warming] || self[:cooling]
189
+ schedule.in '10s' do
190
+ power?
191
+ end
192
+ end
193
+ when :mute
194
+ self[:mute] = pjt_data[-1] == 1
195
+ when :input
196
+ self[:input] = INPUTS[pjt_data]
197
+ when :contrast, :brightness, :color, :hue, :sharpness
198
+ self[COMMANDS[pjt_command]] = pjt_data[-1]
199
+ when :error_status
200
+
201
+ end
202
+ end
203
+ end
204
+ else
205
+ # Command failed..
206
+ self[:last_error] = pjt_data
207
+ logger.debug "Command #{pjt_command} failed with Major 0x#{pjt_data[0].to_s(16)} and Minor 0x#{pjt_data[1].to_s(16)}"
208
+ return :abort
209
+ end
210
+
211
+ :success
212
+ end
213
+
214
+
215
+ protected
216
+
217
+
218
+ # Called by the Abstract Tokenizer to confirm we have the
219
+ # whole message.
220
+ def check_complete(byte_str)
221
+ bytes = str_to_array(byte_str)
222
+
223
+ # Min message length is 8 bytes
224
+ return false if bytes.length < 8
225
+
226
+ # Check we have the data
227
+ data = bytes[8..-1]
228
+ if data.length == bytes[7]
229
+ return true
230
+ elsif data.length > bytes[7]
231
+ # Let the tokeniser know we only want the following number of bytes
232
+ return 7 + bytes[7]
233
+ end
234
+
235
+ # Still waiting on data
236
+ return false
237
+ end
238
+
239
+
240
+ def do_poll(*args)
241
+ power?({:priority => 0}) do
242
+ if self[:power]
243
+ input?
244
+ mute?
245
+ do_send(:get, :error_status, {:priority => 0})
246
+ end
247
+ end
248
+ end
249
+
250
+ # Constants as per manual page 13
251
+ # version, category
252
+ PjTalk_Header = [0x02, 0x0a]
253
+
254
+
255
+ # request, category, command
256
+ COMMANDS = {
257
+ power_on: [0x17, 0x2E],
258
+ power_off: [0x17, 0x2F],
259
+ input: [0x00, 0x01],
260
+ mute: [0x00, 0x30],
261
+
262
+ error_status: [0x01, 0x01],
263
+ power_status: [0x01, 0x02],
264
+
265
+ contrast: [0x00, 0x10],
266
+ brightness: [0x00, 0x11],
267
+ color: [0x00, 0x12],
268
+ hue: [0x00, 0x13],
269
+ sharpness: [0x00, 0x14],
270
+ }
271
+ COMMANDS.merge!(COMMANDS.invert)
272
+
273
+
274
+ def do_send(getset, command, param = nil, options = {})
275
+ # Check for missing params
276
+ if param.is_a? Hash
277
+ options = param
278
+ param = nil
279
+ end
280
+
281
+ reqres = getset == :get ? [0x01] : [0x00]
282
+
283
+ # Control + Mode
284
+ if param.nil?
285
+ options[:name] = command if options[:name].nil?
286
+ cmd = COMMANDS[command] + [0x00]
287
+ else
288
+ options[:name] = :"#{command}_req" if options[:name].nil?
289
+ if !param.is_a?(Array)
290
+ param = [param]
291
+ end
292
+ cmd = COMMANDS[command] + [param.length] + param
293
+ end
294
+
295
+ # Build the IDTalk header # set request every time?
296
+ pjt_cmd = PjTalk_Header + @community + reqres + cmd
297
+
298
+ send(pjt_cmd, options)
299
+ end
300
+ end