onkyo_eiscp_ruby 0.0.3 → 2.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: be075bcd7a1830e7614b8384d610ea59d0b7160f
4
- data.tar.gz: 40cea299c77c937a786f387cfbf4c11e8696632c
2
+ SHA256:
3
+ metadata.gz: 8e9c2fd0e9e79716c2b0a084a6dd7859597732f86fb71c1f4bcd4462c8e9297b
4
+ data.tar.gz: df71853616b26d2f62f6490b99f263d2a5bb2590c4de4bc7b1930fab14604ea5
5
5
  SHA512:
6
- metadata.gz: 5066d43a66350cb39e84ddbae87038d4ba04057f2dd884eee1852c3dd108ed4a44842878157dfb854e7802d3e771cbe5789a70af936af0e338a36264e92332d6
7
- data.tar.gz: 6c2c8bb4ba7d3b7e9fee332d448a054cf97480194192252965b363b7efaa8d8b095ae76d2f68ab6f064fbf68ec2bac75c63b2ba53a26e891b055c0751f776cc4
6
+ metadata.gz: 5377a5163b1219647c708b0d125b1359108d3ebbd91bd4f2c8916a002aa4b83363d2531e0340deddc77a2251e2701cd26cb4fe9ab3b4a4c551a496ed2c20d6b0
7
+ data.tar.gz: fabc67566fb94c98b743c1db76173f86e3de03333ccad16dda9e5dbeb09747a2bad4ec62867593de404633065819644f0574e22c2c492e209d6f0bf17a609482
data/README.md CHANGED
@@ -1,21 +1,42 @@
1
1
  onkyo_eiscp_ruby
2
2
  ================
3
3
  [![Gem Version](https://badge.fury.io/rb/onkyo_eiscp_ruby.png)](http://badge.fury.io/rb/onkyo_eiscp_ruby)
4
+ [![GitHub version](https://badge.fury.io/gh/mikerodrigues%2Fonkyo_eiscp_ruby.png)](http://badge.fury.io/gh/mikerodrigues%2Fonkyo_eiscp_ruby)
4
5
 
5
- A Ruby implementation of eISCP for controlling Onkyo receivers.
6
+ *A Ruby implementation of eISCP for controlling Onkyo receivers.*
7
+
8
+ **This code is not really still under development and using it might still make you sick.**
9
+
10
+ **The python version linked below sees much more activity.**
11
+
12
+ Automatically discover receivers in the broadcast domain
13
+
14
+ Send commands to receivers and parse returned messages
15
+
16
+ Open a TCP socket to receive solicited and non-solicited status updates.
6
17
 
7
- **This code is still under heavy development and using it might make you sick.**
8
- Create ISCP messages and eISCP packets
9
- Automatically discover receiver's in the broadcast domain
10
- Send/Recieve eISCP messages
11
- Open a TCP socket to send commands and receive solicited and non-solicited status updates.
12
18
  Mock reciever (currently only responds to discovery)
13
19
 
20
+ Human-readable commands
21
+
14
22
  **Inspired by https://github.com/miracle2k/onkyo-eiscp
15
23
 
16
24
  **Protocol information from http://michael.elsdoerfer.name/onkyo/ISCP-V1.21_2011.xls
17
25
 
26
+ What's missing?
27
+ ---------------
28
+ * Command validation
29
+
30
+ * Parsing of all human readable commands (run the tests to see some commands that aren't parsable in human readable form yet.
18
31
 
32
+ * Reasonable variants for human-readable commands (ex. `main-volume` or`volume
33
+ ` as opposed to `master-volume`)
34
+
35
+ * Model compatability checking
36
+
37
+ * Logging
38
+
39
+ * Exhaustive testing and documentation
19
40
 
20
41
  Using the Library
21
42
  -----------------
@@ -23,53 +44,182 @@ Using the Library
23
44
 
24
45
  require 'eiscp'
25
46
 
26
- * Discover local receivers
27
-
47
+ * You might want to `include EISCP` if you know you won't pollute your namespace
48
+ with Constants under `EISCP` (`Dictionary`, `Message`, `Parser`, `Receiver`,
49
+ `VERSION`)
50
+
51
+ * You can do most everything through the `Receiver` and `Message` objects. If you
52
+ want to accept user input you will probably want to use the Parser module. Be
53
+ sure to check out the RDocs or dig through the source code. I try to keep it
54
+ well commented/documented, and there's more functionality to the library than
55
+ is shown here:
56
+
57
+ * The `Message` object is pretty self explanatory. `Message.new` is mostly used
58
+ internally, but you're better off using `Parser.parse` to create them. You
59
+ probably will want to interact with `Message` objects to get information:
60
+
61
+ ```ruby
62
+ msg = EISCP::Message.new(command: 'PWR', value: '01')
63
+ msg.zone => 'main'
64
+ msg.command => "PWR"
65
+ msg.value => "01"
66
+ msg.command_name => "system-power"
67
+ msg.command_description => "System Power Command"
68
+ msg.value_name => "on"
69
+ msg.value_description => "sets System On"
70
+ ```
71
+
72
+ * Discover local receivers (returns an `Array` of `Receiver` objects)
73
+
74
+ ```ruby
28
75
  EISCP::Receiver.discover
76
+ ```
29
77
 
30
- * Create Receiver object from first discovered
78
+ * Create `Receiver` object from first discovered Receiver on the LAN
31
79
 
32
- Receiver.new
80
+ ```ruby
81
+ receiver = EISCP::Receiver.new
82
+ ```
33
83
 
34
- * Open a TCP connection to monitor solicited updates
84
+ * Or create one manually by IP address or hostname
35
85
 
36
- receiver = Receiver.new('10.0.0.1')
37
- receiver.connect
86
+ ```ruby
87
+ receiver = EISCP::Receiver.new('10.0.0.132')
88
+ ```
38
89
 
39
- * You can also pass a block and operate on received packet strings:
90
+ * When you create a `Receiver` object with a callback block it will
91
+ connect and call your block on each message received.:
40
92
 
41
- receiver.connect do |data|
42
- puts EISCP::Receiver.parse(data).iscp_message
93
+ ```ruby
94
+ receiver = EISCP::Receiver.new do |msg|
95
+ puts msg.command
96
+ puts msg.value
43
97
  end
98
+ ```
44
99
 
45
- * Turn on the receiver
100
+ * Receivers created without a block will not connect automatically. You can use
101
+ the `connect` method to create a socket and connect to the receiver.
46
102
 
47
- message = EISCP::Message.parse("PWR", "01")
48
- message.send(message.to_eiscp)
103
+ ```ruby
104
+ receiver.connect
105
+ ```
49
106
 
50
- * New 'parse' method makes creating EISCP objects more flexible.
51
- This parses messages from command line or raw eiscp data from the socket
52
-
53
- iscp_message = EISCP::Message.parse "PWR01"
54
- iscp_message = EISCP::Message.parse "PWR 01"
55
- iscp_message = EISCP::Message.parse "!1PWR01"
56
- iscp_message = EISCP::Message.parse "!1PWR 01"
107
+ * You can also set or change the callback block later. This will kill the
108
+ existing callback thread, recreate the socket if necessary and start
109
+ a new callback thread using the provided block:
57
110
 
58
- * Parsing raw socket data
111
+ ```ruby
112
+ receiver.connect do |msg|
113
+ puts "Received: #{msg.command_name}:#{msg.value_name}"
114
+ end
115
+ ```
116
+
117
+ * You can also disconnect, which will close the socket and kill the connection
118
+ thread:
119
+
120
+ ```ruby
121
+ receiver.disconnect
122
+ ```
123
+
124
+ * Get information about the Receiver:
125
+
126
+ ```ruby
127
+ receiver.model => "TX-NR609"
128
+ receiver.host => "10.0.0.111"
129
+ receiver.port => 60128
130
+ receiver.mac_address => "001122334455"
131
+ receiver.area => "DX"
132
+ ```
133
+
134
+ * Receivers now have a `@state` hash that contains a mapping of commands and
135
+ values received. You can use this see the Receiver's last known state without
136
+ querying. Use the `#update_state` method to run every 'QSTN' command in the
137
+ Dictionary and update the state hash, but do note that it will take a few
138
+ seconds to finish:
139
+
140
+ ```ruby
141
+ receiver.update_state
142
+ receiver.state["MVL"] => "22"
143
+ receiver.human_readable_state["master-volume"] => 34
144
+ ```
145
+
146
+
147
+ * You can use `CommandMethods` to easily send a message and return the reply as
148
+ a Message object. Once `Receiver#connect`is called, a method is defined for each command listed in the
149
+ `Dictionary` using the `@command_name` attribute which is 'human readable'.
150
+ You can check the included yaml file or look at the output of
151
+ `EISCP::Dictionary.commands`. Here a few examples:
152
+
153
+ ```ruby
154
+ # Turn on receiver
155
+ receiver.system_power "on"
156
+
157
+ # Query current input source
158
+ receiver.input_selector "query"
159
+
160
+ # Turn the master volume up one level
161
+ receiver.master_volume "level-up"
162
+
163
+ # Set the master volume to 45
164
+ receiver.master_volume "45"
165
+ ```
166
+
167
+ * Parse ISCP and human readable strings:
168
+
169
+ ```ruby
170
+ # Parse various ISCP strings
171
+ iscp_message = EISCP::Parser.parse "PWR01"
172
+ iscp_message = EISCP::Parser.parse "PWR 01"
173
+ iscp_message = EISCP::Parser.parse "!1PWR01"
174
+ iscp_message = EISCP::Parser.parse "!1PWR 01"
175
+
176
+ # Parse human readable,
177
+ EISCP::Parser.parse("main-volume 34")
178
+ ```
179
+
180
+ * `Parser.parse` is also used internally by `Receiver` to parse raw eISCP socket
181
+ data.
59
182
 
60
- iscp_message_from_raw_eiscp = EISCP::Message.parse iscp_message.to_eiscp
61
183
 
62
184
  Using the Binaries
63
185
  ------------------
64
186
 
65
187
  * Discover local receivers
66
188
 
67
- $ onkyo.rb -d
189
+ `$ onkyo.rb -d`
190
+
191
+ * Send a human-readable command
192
+
193
+ `$ onkyo.rb system-power on # uses Parser.parse`
194
+
195
+ * Or send a raw command
68
196
 
69
- * Connect to the first discovered receiver to see status updates
197
+ `$ onkyo.rb PWRQSTN # Also tries to use Parser.parse`
70
198
 
71
- $ onkyo.rb -c
199
+ * Monitor the first discovered receiver to see status updates
200
+
201
+ `$ onkyo.rb -m`
72
202
 
73
203
  * Start the mock server (only responds to 'ECNQSTN')
74
204
 
75
- $ onkyo-server.rb
205
+ `$ onkyo-server.rb`
206
+
207
+ * Turn off the first receiver discovered:
208
+
209
+ `$ onkyo.rb system-power off`
210
+
211
+ * List all known commands and values:
212
+
213
+ `$ onkyo.rb -L`
214
+
215
+ * List all known commands known to work with discovered models:
216
+
217
+ `$ onkyo.rb -l`
218
+
219
+ Contributing
220
+ ------------
221
+
222
+ * Open an issue describing bug or feature
223
+ * Fork repo
224
+ * Create a branch
225
+ * Send pull request
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.3
1
+ 2.1.2
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require_relative './receiver'
5
+ require_relative './message'
6
+
7
+ # Mock server that only responds to ECNQSTN.
8
+
9
+ module EISCP
10
+ # This class acts as an Onkyo Stereo. It can be used for testing
11
+ #
12
+ class MockReceiver
13
+ DISCOVERY_IP = '255.255.255.255'
14
+ ONKYO_DISCOVERY_RESPONSE = Message.new('ECN', "TX-NR609/60128/DX/14DAE9E967C8\x19\r\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
15
+
16
+ # Create/start the server object.
17
+
18
+ def initialize
19
+ Socket.udp_server_loop(DISCOVERY_IP, Receiver::ONKYO_PORT) do |msg, src|
20
+ src.reply "ISCP\x00\x00\x00\x10\x00\x00\x00&\x01\x00\x00\x00!1ECNTX-NR609/60128/DX/14DAE9E967C8\x19\r\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
21
+ puts msg
22
+ end
23
+ end
24
+ end
25
+ end
data/bin/onkyo.rb CHANGED
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'eiscp'
4
5
  require 'optparse'
5
6
  require 'ostruct'
7
+
8
+ # This object parses ARGV and returns an @option hash
9
+ #
6
10
  class Options
7
- DEFAULT_OPTIONS = { verbose: true, all: false }
11
+ DEFAULT_OPTIONS = { verbose: true, all: false }.freeze
8
12
  USAGE = ' Usage: onkyo_rb [options]'
9
13
 
10
14
  def self.parse(args)
11
15
  @options = OpenStruct.new
12
16
 
13
17
  options = OptionParser.new do |opts|
14
-
15
18
  opts.banner = USAGE
16
19
 
17
20
  opts.on '-d', '--discover', 'Find Onkyo Receivers on the local broadcast domain' do |d|
@@ -34,53 +37,110 @@ class Options
34
37
  @options.list_all = l
35
38
  end
36
39
 
37
- opts.on '-c', '--connect', 'Connect to the first discovered reciever and show updates' do |c|
38
- @options.connect = c
40
+ opts.on '-m', '--monitor', 'Connect to the first discovered reciever and monitor updates' do |m|
41
+ @options.monitor = m
39
42
  end
40
-
41
43
  end
42
44
 
43
45
  options.parse!(args)
44
46
 
45
- if @options == nil && ARGV == []
46
- puts options
47
- end
47
+ puts options if @options.nil? && ARGV == []
48
48
 
49
49
  if @options.discover
50
- EISCP::Receiver.discover.each do |receiver|
51
- puts EISCP::Message.parse(receiver[0]).to_iscp
52
- end
50
+ EISCP::Receiver.discover.each { |rec| puts "#{rec.host}:#{rec.port} - #{rec.model} - #{rec.mac_address}" }
53
51
  exit 0
54
52
  end
55
53
 
56
54
  if @options.help
57
55
  puts options
58
- exit 0
56
+ exit 0
57
+ end
58
+
59
+ if @options.monitor
60
+ begin
61
+ rec = EISCP::Receiver.new do |reply|
62
+ puts "#{Time.now} #{rec.host} "\
63
+ "#{reply.zone}: "\
64
+ "#{reply.command_description || reply.command} "\
65
+ "-> #{reply.value_description || reply.value}"
66
+ end
67
+ rec.thread.join
68
+ rescue Interrupt
69
+ raise 'Exiting...'
70
+ rescue Exception => e
71
+ puts 'bummer...'
72
+ puts e
73
+ end
59
74
  end
60
75
 
61
- if @options.connect
62
- eiscp = EISCP::Receiver.new(EISCP::Receiver.discover[0][1])
63
- eiscp.connect do |data|
64
- puts msg = EISCP::Receiver.parse(data).to_iscp
76
+ if @options.list
77
+ models = []
78
+ modelsets = []
79
+ EISCP::Receiver.discover.each do |rec|
80
+ models << rec.model
81
+ end
82
+ models.each do |model|
83
+ EISCP::Dictionary.modelsets.each do |modelset, list|
84
+ modelsets << modelset unless list.select { |x| x.match model }.empty?
85
+ end
86
+ end
87
+ EISCP::Dictionary.zones.each do |zone|
88
+ EISCP::Dictionary.commands[zone].each do |command, command_hash|
89
+ puts 'Command - Description'
90
+ puts "\n"
91
+ puts " '#{Dictionary.command_to_name(command)}' - "\
92
+ "#{Dictionary.description_from_command(command)}"
93
+ puts "\n"
94
+ puts ' Value - Description>'
95
+ puts "\n"
96
+ command_hash[:values].each do |_value, attr_hash|
97
+ if modelsets.include? attr_hash[:models]
98
+ puts " '#{EISCP::Dictionary.command_value_to_value_name(command, _value)}' - "\
99
+ " #{attr_hash[:description]}"
100
+ end
101
+ end
102
+ puts "\n"
103
+ end
65
104
  end
66
105
  end
67
106
 
107
+ if @options.list_all
108
+ EISCP::Dictionary.zones.each do |zone|
109
+ EISCP::Dictionary.commands[zone].each do |command, command_hash|
110
+ puts 'Command - Description'
111
+ puts "\n"
112
+ puts " '#{Dictionary.command_to_name(command)}' - "\
113
+ "#{Dictionary.description_from_command(command)}"
114
+ puts "\n"
115
+ puts ' Value - Description>'
116
+ puts "\n"
117
+ command_hash[:values].each do |_value, attr_hash|
118
+ puts " '#{EISCP::Dictionary.command_value_to_value_name(command, _value)}' - "\
119
+ " #{attr_hash[:description]}"
120
+ end
121
+ puts "\n"
122
+ end
123
+ end
124
+ exit 0
125
+ end
126
+
68
127
  if ARGV == []
69
128
  puts options
70
129
  exit 0
71
130
  end
72
131
  end
73
-
74
132
  end
75
133
 
134
+ include EISCP
76
135
 
77
136
  @options = Options.parse(ARGV)
78
137
 
79
-
80
- receiver = EISCP::Receiver.new(EISCP::Receiver.discover[0][1])
81
- message = (EISCP::Message.parse(ARGV.join(" ")).to_eiscp)
82
- puts receiver.send_recv message
83
-
84
-
85
-
86
-
138
+ receiver = EISCP::Receiver.discover[0]
139
+ receiver.connect
140
+ begin
141
+ command = EISCP::Parser.parse(ARGV.join(' '))
142
+ rescue StandardError
143
+ raise "Couldn't parse command"
144
+ end
145
+ reply = receiver.send_recv(command)
146
+ puts "#{Time.now}: Response from #{receiver.host}: #{reply.zone.capitalize} #{reply.command_description || reply.command} -> #{reply.value_description || reply.value}"