aca-device-modules 1.0.3 → 1.0.4

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,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