str_dn_2030 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fd3349718acb66b742700b924e4254fea002a00d
4
+ data.tar.gz: 74c3b4739709f818f31b37fd4a365a443d88fe5c
5
+ SHA512:
6
+ metadata.gz: ecf0ed5e95d92abe4ec9531506f545626dd291d835bbbeb515505d60f8cd0acb3cd85a4a90044a25ba80a3693ec5c3d402502484219b540eb530d1ec0be8a2bb
7
+ data.tar.gz: e9afbea4adfe919f75f2af5ecf288522799b336b6f131546c529a7386977feccac1cd3e0fa12ba8e6f939084cf4cdf804635fda775a7a20348f65d311e60adfb
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'sinatra'
4
+ gem 'puma'
5
+
6
+ # Specify your gem's dependencies in str_dn_2030.gemspec
7
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Shota Fukumori (sora_h)
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,114 @@
1
+ # StrDn2030
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ ``` ruby
8
+ gem 'str_dn_2030'
9
+ gem 'sinatra' # if you want to use str_dn_2030/web
10
+ ```
11
+
12
+ Or install it yourself as:
13
+
14
+ $ gem install str_dn_2030
15
+
16
+ ## Usage
17
+
18
+ ### as a Ruby library
19
+
20
+ #### Connect
21
+
22
+ ``` ruby
23
+ require 'str_dn_2030'
24
+
25
+ remote = StrDn2030.new('x.x.x.x') # pass your amp's IP address
26
+ remote.connect
27
+
28
+ zone = remote.zones(0) # main zone
29
+ ```
30
+
31
+ #### See status
32
+
33
+ ``` ruby
34
+ p zone.volume
35
+ p zone.powered_on?
36
+ p zone.muted?
37
+ p zone.headphone?
38
+ ```
39
+
40
+ #### Control input
41
+
42
+ ``` ruby
43
+ input = zone.active_input
44
+ p input.name
45
+ p input.preset_name
46
+
47
+ p zone.inputs #=> Hash
48
+
49
+ zone.inputs['Chrome'].activate!
50
+ zone.active_input = zone.inputs['Apple TV']
51
+ ```
52
+
53
+ #### Control volume
54
+
55
+ ``` ruby
56
+ zone.volume = 30
57
+ zone.mute = true
58
+ zone.mute = false
59
+ ```
60
+
61
+ ### HTTP interface
62
+
63
+ ``` ruby
64
+ # config.ru
65
+ require 'str_dn_2030'
66
+ require 'str_dn_2030/web'
67
+
68
+ remote = StrDn2030::Remote.new('x.x.x.x')
69
+ remote.connect
70
+ StrDn2030::Web.set :remote, remote
71
+
72
+ run StrDn2030::Web
73
+ ```
74
+
75
+ ```
76
+ curl http://localhost:9292/zones/0
77
+ ```
78
+
79
+ ```
80
+ curl http://localhost:9292/zones/0/inputs
81
+ ```
82
+
83
+ ```
84
+ curl http://localhost:9292/zones/0/volume
85
+ curl -X PUT \
86
+ -H 'Content-Type: application/json' \
87
+ -d '{"volume": 25}' \
88
+ http://localhost:9292/zones/0/volume
89
+ curl -X PUT \
90
+ -H 'Content-Type: application/json' \
91
+ -d '{"mute": true}' \
92
+ http://localhost:9292/zones/0/volume
93
+ ```
94
+
95
+ ```
96
+ curl 'http://localhost:9292/zones/0/inputs/Apple+TV'
97
+ curl -X POST 'http://localhost:9292/zones/0/inputs/Apple+TV/activate'
98
+ ```
99
+
100
+ ```
101
+ curl http://localhost:9292/zones/0/active
102
+ curl -X PUT \
103
+ -H 'Content-Type: application/json' \
104
+ -d '{"input": "Apple TV"}' \
105
+ http://localhost:9292/zones/0/active
106
+ ```
107
+
108
+ ## Contributing
109
+
110
+ 1. Fork it ( https://github.com/sorah/str_dn_2030/fork )
111
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
112
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
113
+ 4. Push to the branch (`git push origin my-new-feature`)
114
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,12 @@
1
+ require 'str_dn_2030'
2
+ require 'str_dn_2030/web'
3
+
4
+ remote = StrDn2030::Remote.new(
5
+ ENV['STRDN2030_HOST'],
6
+ ENV['STRDN2030_PORT'] ? ENV['STRDN2030_PORT'].to_i : 33335
7
+ )
8
+ remote.connect
9
+
10
+ StrDn2030::Web.set :remote, remote
11
+ StrDn2030::Web.set :max_volume, ENV['STRDN2030_MAX_VOLUME'] ? ENV['STRDN2030_MAX_VOLUME'].to_i : 33
12
+ run StrDn2030::Web
@@ -0,0 +1,292 @@
1
+ require 'str_dn_2030/version'
2
+ require 'str_dn_2030/input'
3
+ require 'str_dn_2030/zone'
4
+ require 'socket'
5
+ require 'thread'
6
+
7
+ module StrDn2030
8
+ class Remote
9
+ VOLUME_STATUS_REGEXP = /\x02\x06\xA8\x92(?<zone>.)(?<type>.)(?<volume>..)./nm
10
+ STATUS_REGEXP = /\x02\x07\xA8\x82(?<zone>.)(?<ch>.)(?<ch2>.)(?<flag1>.)(?<unused>.)./nm
11
+
12
+ INPUT_REGEXP = /(?<index>.)(?<audio>.)(?<video>.)(?<icon>.)(?<preset_name>.{8})(?<name>.{8})(?<skip>.)/mn
13
+ INPUTLIST_REGEXP = Regexp.new(
14
+ '\x02\xD7\xA8\x8B(?<zone>.)' \
15
+ "(?<inputs>(?:#{INPUT_REGEXP}){10})" \
16
+ '\x00\x00.',
17
+ Regexp::MULTILINE,
18
+ 'n'
19
+ )
20
+ def initialize(host, port = 33335)
21
+ @host, @port = host, port
22
+
23
+ @socket = nil
24
+ @receiver_thread = nil
25
+
26
+ @lock = Mutex.new
27
+ @receiver_lock = Mutex.new
28
+ @hook = nil
29
+
30
+ @listeners = {}
31
+ @listeners_lock = Mutex.new
32
+
33
+ @inputs = {}
34
+ @statuses = {}
35
+ end
36
+
37
+ attr_reader :inputs, :host, :port
38
+
39
+ def inspect
40
+ "#<#{self.class.name}: #{@host}:#{@port}>"
41
+ end
42
+
43
+ def hook(&block)
44
+ if block_given?
45
+ @hook = block
46
+ else
47
+ @hook
48
+ end
49
+ end
50
+
51
+ def listen(type, filter = nil)
52
+ Thread.current[:strdn2030_filter] = filter
53
+ @listeners_lock.synchronize {
54
+ @listeners[type] ||= []
55
+ @listeners[type] << Thread.current
56
+ }
57
+
58
+ sleep
59
+
60
+ data = Thread.current[:strdn2030_data]
61
+ Thread.current[:strdn2030_data] = nil
62
+ data
63
+ end
64
+
65
+
66
+ def connected?
67
+ !!@socket
68
+ end
69
+
70
+ def connect
71
+ disconnect if connected?
72
+
73
+ @lock.synchronize do
74
+ @socket = TCPSocket.open(@host, @port)
75
+ start_receiver
76
+ reload_input
77
+ self
78
+ end
79
+ end
80
+
81
+ def disconnect
82
+ @lock.synchronize do
83
+ return unless @socket
84
+ @socket.close unless @socket.closed?
85
+ @socket = nil
86
+ stop_receiver
87
+ end
88
+ end
89
+
90
+ def reload
91
+ reload_input
92
+ @statuses = {}
93
+ self
94
+ end
95
+
96
+ def zone(zone_id)
97
+ Zone.new(self, zone_id.is_a?(String) ? zone_id.ord : zone_id.to_i)
98
+ end
99
+
100
+ #### These are private api
101
+
102
+ def status_get(zone_id = 0)
103
+ @statuses[zone_id] || begin
104
+ zone = zone_id.chr('ASCII-8BIT')
105
+ send "\x02\x03\xA0\x82".b + zone + "\x00".b
106
+ listen(:status, zone_id)
107
+ end
108
+ end
109
+
110
+ def volume_get(zone_id, type = "\x03".b)
111
+ zone = zone_id.chr('ASCII-8BIT')
112
+ send "\x02\x04\xa0\x92".b + zone + type + "\x00".b
113
+ listen(:volume, zone_id)
114
+ end
115
+
116
+ def volume_set(zone_id, other, type = "\x03".b)
117
+ zone = zone_id.chr('ASCII-8BIT')
118
+ send "\x02\x06\xa0\x52".b + zone + type + [other.to_i].pack('s>') + "\x00".b
119
+ listen(:success)
120
+ other
121
+ end
122
+
123
+ def active_input_get(zone_id)
124
+ current = status_get(zone_id)[:ch][:video]
125
+ inputs[zone_id][current] || self.reload_input.inputs[zone_id][current]
126
+ end
127
+
128
+ def active_input_set(zone_id, other)
129
+ new_input = if other.is_a?(Input)
130
+ raise ArgumentError, "#{other.inspect} is not in zone #{zone_id}" unless other.zone == zone_id
131
+ other
132
+ else
133
+ inputs[zone_id][other] || self.reload_input.inputs[zone_id][other]
134
+ end
135
+
136
+ raise ArgumentError, "#{other.inspect} not exists" unless new_input
137
+
138
+ zone = zone_id.chr('ASCII-8BIT')
139
+ send "\x02\x04\xa0\x42".b + zone + new_input.video + "\x00".b
140
+ listen(:success)
141
+ other
142
+ end
143
+
144
+ def mute_set(zone_id, new_mute)
145
+ zone = zone_id.chr('ASCII-8BIT')
146
+ send "\x02\x04\xa0\x53".b + zone + (new_mute ? "\x01".b : "\x00".b) + "\x00".b
147
+ listen(:success)
148
+ new_mute
149
+ end
150
+
151
+ def reload_input
152
+ @inputs = {}
153
+ get_input_list(0, 0)
154
+ listen(:input_list, 0)
155
+ get_input_list(0, 1)
156
+ listen(:input_list, 0)
157
+ get_input_list(1, 0)
158
+ listen(:input_list, 1)
159
+ get_input_list(1, 1)
160
+ listen(:input_list, 1)
161
+ self
162
+ end
163
+
164
+ private
165
+
166
+ def receiver_alive?
167
+ @receiver_thread && @receiver_thread.alive?
168
+ end
169
+
170
+ def start_receiver(if_dead=false)
171
+ return if if_dead && receiver_alive?
172
+ stop_receiver
173
+ @receiver_lock.synchronize do
174
+ debug [:start_receiver]
175
+ @receiver_thread = Thread.new(@socket, &method(:receiver))
176
+ @receiver_thread.abort_on_exception = true
177
+ end
178
+ end
179
+
180
+ def stop_receiver
181
+ @receiver_lock.synchronize do
182
+ debug [:stop_receiver]
183
+ @receiver_thread.kill if receiver_alive?
184
+ @receiver_thread = nil
185
+ end
186
+ end
187
+
188
+ def get_input_list(zone_id, page = 0)
189
+ send("\x02\x04\xa0\x8b".b + zone_id.chr('ASCII-8BIT') + page.chr('ASCII-8BIT') + "\x00".b)
190
+ end
191
+
192
+ def send(str)
193
+ start_receiver(true)
194
+ debug [:send, str]
195
+ @socket.write str
196
+ end
197
+
198
+ def receiver(socket)
199
+ buffer = "".b
200
+
201
+ hit = false
202
+ handle = lambda do |pattern, &handler|
203
+ if m = buffer.match(pattern)
204
+ hit = true
205
+ buffer.replace(m.pre_match + m.post_match)
206
+ handler[m]
207
+ end
208
+ end
209
+
210
+ while chunk = socket.read(1)
211
+ hit = false
212
+ buffer << chunk.b
213
+
214
+ handle.(STATUS_REGEXP, &method(:handle_status))
215
+ handle.(VOLUME_STATUS_REGEXP, &method(:handle_volume_status))
216
+ handle.(INPUTLIST_REGEXP, &method(:handle_input_list))
217
+
218
+ handle.(/\A\xFD/n) { delegate(:success) }
219
+ handle.(/\A\xFE/n) { delegate(:error) }
220
+
221
+ debug([:buffer_remain, buffer]) if hit && !buffer.empty?
222
+ end
223
+ end
224
+
225
+ def delegate(name, subtype = nil, *args)
226
+ wake_listener name, subtype, *args
227
+ @hook.call(name, *args) if @hook
228
+ end
229
+
230
+ def wake_listener(type, subtype, data = nil)
231
+ @listeners_lock.synchronize do
232
+ @listeners[type] ||= []
233
+ @listeners[type].each do |th|
234
+ next if th[:strdn2030_filter] && !(th[:strdn2030_filter] === subtype)
235
+ th[:strdn2030_data] = data
236
+ th.wakeup
237
+ end
238
+ @listeners[type].clear
239
+ end
240
+ end
241
+
242
+ def handle_status(m)
243
+ flag1 = m['flag1'].ord
244
+ flags = {
245
+ raw: [m['flag1']].map{ |_| _.ord.to_s(2).rjust(8,' ') },
246
+ power: flag1[0] == 1,
247
+ mute: flag1[1] == 1,
248
+ headphone: flag1[2] == 1,
249
+ unknown_6: flag1[5],
250
+ }
251
+ data = @statuses[m['zone'].ord] = {
252
+ zone: m['zone'], ch: {audio: m['ch'], video: m['ch2']}, flags: flags
253
+ }
254
+
255
+ delegate(:status, m['zone'].ord, data)
256
+ end
257
+
258
+ def handle_volume_status(m)
259
+ data = {zone: m['zone'], type: m['type'], volume: m['volume'].unpack('s>').first}
260
+ delegate(:volume, m['zone'].ord, data)
261
+ end
262
+
263
+ def handle_input_list(m)
264
+ #p m
265
+ inputs = {}
266
+ zone = m['zone'].ord
267
+ m['inputs'].scan(INPUT_REGEXP).each do |input_line|
268
+ idx, audio, video, icon, preset_name, name, skip_flags = input_line
269
+
270
+ input = inputs[audio] = inputs[video] = Input.new(
271
+ self,
272
+ zone,
273
+ idx,
274
+ audio,
275
+ video,
276
+ icon,
277
+ preset_name,
278
+ name,
279
+ skip_flags,
280
+ )
281
+ inputs[input.name] = input
282
+ end
283
+ (@inputs[zone] ||= {}).merge! inputs
284
+ delegate :input_list, zone, inputs
285
+ end
286
+
287
+ def debug(*args)
288
+ p(*args) if ENV["STR_DN_2030_DEBUG"]
289
+ end
290
+ end
291
+ end
292
+
@@ -0,0 +1,83 @@
1
+ module StrDn2030
2
+ class Input
3
+ def initialize(parent, zone, index, audio, video, icon, preset_name, name, skip)
4
+ @parent = parent
5
+ @zone = zone
6
+ @index = index.dup.b.freeze
7
+ @audio = audio.dup.b.freeze
8
+ @video = video.dup.b.freeze
9
+ @icon = icon.dup.b.freeze
10
+ @preset_name = preset_name.strip.freeze
11
+ @name = name.strip.freeze
12
+ @skip_flags = skip.dup.b.freeze
13
+ end
14
+
15
+ attr_reader :parent,
16
+ :zone, :index,
17
+ :audio, :video,
18
+ :icon,
19
+ :preset_name, :name,
20
+ :skip_flags
21
+
22
+ def inspect
23
+ "#<#{self.class.name}: #{name} @ #{parent.host}:#{parent.port}/#{zone}>"
24
+ end
25
+
26
+ def activate!
27
+ parent.zone(self.zone).active_input = self
28
+ nil
29
+ end
30
+
31
+ def active?
32
+ parent.zone(self.zone).active_input == self
33
+ end
34
+
35
+ def skipped?
36
+ skip[:watch] && skip[:listen]
37
+ end
38
+
39
+ def watch_skipped?
40
+ skip[:watch]
41
+ end
42
+
43
+ def listen_skipped?
44
+ skip[:listen]
45
+ end
46
+
47
+ def skipped_any?
48
+ skip[:watch] || skip[:listen]
49
+ end
50
+
51
+ def skip
52
+ @skip ||= begin
53
+ {raw: skip_flags}.tap do |_|
54
+ _.merge!({
55
+ "\x11".b => {watch: true, listen: true},
56
+ "\x21".b => {watch: true, listen: true},
57
+ "\x30".b => {watch: false, listen: false},
58
+ "\x10".b => {watch: false, listen: true},
59
+ "\x20".b => {watch: true, listen: false},
60
+ }[skip_flags.b] || {})
61
+ end
62
+ end
63
+ end
64
+
65
+ def ==(other)
66
+ other.is_a?(self.class) && self.video == other.video && self.audio == other.audio
67
+ end
68
+
69
+ def as_json
70
+ {
71
+ zone: zone,
72
+ index: index,
73
+ audio: audio,
74
+ video: video,
75
+ icon: icon,
76
+ preset_name: preset_name,
77
+ name: name,
78
+ skip: skip,
79
+ active: active?,
80
+ }
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,3 @@
1
+ module StrDn2030
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,154 @@
1
+ require 'str_dn_2030'
2
+ require 'sinatra/base'
3
+ require 'json'
4
+
5
+ module StrDn2030
6
+ class Web < Sinatra::Base
7
+ set :max_volume, nil
8
+
9
+ helpers do
10
+ def remote
11
+ self.class.remote
12
+ end
13
+
14
+ def max_volume
15
+ self.class.max_volume
16
+ end
17
+ end
18
+
19
+ get '/' do
20
+ 'strdn2030'
21
+ end
22
+
23
+ post '/reload' do
24
+ remote.reload
25
+ status 204
26
+ end
27
+
28
+ get '/zones/:zone' do
29
+ content_type :json
30
+ zone = remote.zone(params[:zone].to_i)
31
+ {
32
+ zone: params[:zone],
33
+ volume: zone.volume,
34
+ active_input: zone.active_input.as_json,
35
+ mute: zone.muted?,
36
+ power: zone.powered_on?,
37
+ headphone: zone.headphone?,
38
+ }.to_json
39
+ end
40
+
41
+ get '/zones/:zone/inputs' do
42
+ content_type :json
43
+ zone = remote.zone(params[:zone].to_i)
44
+ inputs = Hash[zone.inputs.values.uniq.map { |input| [input.name, input.as_json] }]
45
+ {
46
+ inputs: inputs
47
+ }.to_json
48
+ end
49
+
50
+ get '/zones/:zone/inputs/:input' do
51
+ content_type :json
52
+ zone = remote.zone(params[:zone].to_i)
53
+ input = zone.inputs[params[:input]]
54
+
55
+ unless input
56
+ status 404
57
+ return {error: '404'}.to_json
58
+ end
59
+
60
+ input.as_json.to_json
61
+ end
62
+
63
+ post '/zones/:zone/inputs/:input/activate' do
64
+ zone = remote.zone(params[:zone].to_i)
65
+ input = zone.inputs[params[:input]]
66
+
67
+ unless input
68
+ status 404
69
+ content_type :json
70
+ return {error: '404'}.to_json
71
+ end
72
+
73
+ input.activate!
74
+
75
+ status 204
76
+ ''
77
+ end
78
+
79
+ get '/zones/:zone/active' do
80
+ content_type :json
81
+ zone = remote.zone(params[:zone].to_i)
82
+ zone.active_input.as_json.to_json
83
+ end
84
+
85
+ put '/zones/:zone/active' do
86
+ content_type :json
87
+
88
+ zone = remote.zone(params[:zone].to_i)
89
+ json_params = if request.content_type == 'application/json'
90
+ JSON.parse(request.body.read)
91
+ else
92
+ {}
93
+ end
94
+ query = json_params['input'] || params[:input]
95
+ input = zone.inputs[query]
96
+
97
+ unless input
98
+ pattern = Regexp.new(query, 'i')
99
+ input = zone.inputs.values.uniq.find { |_| pattern === _.name }
100
+ end
101
+
102
+ unless input
103
+ status 400
104
+ return {error: 'no input found'}.to_json
105
+ end
106
+
107
+ input.activate!
108
+
109
+ status 200
110
+ {input: input.as_json}.to_json
111
+ end
112
+
113
+ get '/zones/:zone/volume' do
114
+ content_type :json
115
+ zone = remote.zone(params[:zone].to_i)
116
+
117
+ {
118
+ zone: params[:zone],
119
+ volume: zone.volume,
120
+ mute: zone.muted?,
121
+ headphone: zone.headphone?,
122
+ }.to_json
123
+ end
124
+
125
+ put '/zones/:zone/volume' do
126
+ zone = remote.zone(params[:zone].to_i)
127
+ json_params = if request.content_type == 'application/json'
128
+ JSON.parse(request.body.read)
129
+ else
130
+ {}
131
+ end
132
+
133
+ volume = json_params['volume'] || params[:volume]
134
+ if volume
135
+ volume = volume.to_i
136
+ if max_volume && (max_volume < volume)
137
+ content_type :json
138
+ status 400
139
+ return {error: "over max volume #{max_volume}, given #{volume}"}.to_json
140
+ end
141
+
142
+ zone.volume = volume
143
+ end
144
+
145
+ mute = json_params.key?('mute') ? json_params['mute'] : params[:mute]
146
+ unless mute.nil?
147
+ p mute
148
+ zone.mute = mute
149
+ end
150
+
151
+ status 204
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,58 @@
1
+ module StrDn2030
2
+ class Remote
3
+ class Zone
4
+ def initialize(parent, zone_id, volume_type = "\x03".b)
5
+ @parent = parent
6
+ @zone_id = zone_id
7
+ @zone = zone_id.chr('ASCII-8BIT').freeze
8
+ @volume_type = volume_type.dup.b.freeze
9
+ end
10
+
11
+ attr_reader :parent, :zone_id, :zone, :volume_type
12
+ alias id zone_id
13
+
14
+ def reload
15
+ parent.reload; self
16
+ end
17
+
18
+ def inputs
19
+ parent.inputs[zone_id]
20
+ end
21
+
22
+ def powered_on?
23
+ parent.status_get(zone_id)[:flags][:power]
24
+ end
25
+
26
+ def muted?
27
+ parent.status_get(zone_id)[:flags][:mute]
28
+ end
29
+
30
+ def mute=(other)
31
+ parent.mute_set(zone_id, other)
32
+ end
33
+
34
+ def headphone?
35
+ parent.status_get(zone_id)[:flags][:headphone]
36
+ end
37
+
38
+ def volume
39
+ parent.volume_get(zone_id, volume_type)[:volume]
40
+ end
41
+
42
+ def volume=(other)
43
+ parent.volume_set(zone_id, other, volume_type)
44
+ end
45
+
46
+ def active_video
47
+ parent.active_input_get(zone_id)
48
+ end
49
+
50
+ def active_video=(other)
51
+ parent.active_input_set(zone_id, other)
52
+ end
53
+
54
+ alias active_input active_video
55
+ alias active_input= active_video=
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'str_dn_2030/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "str_dn_2030"
8
+ spec.version = StrDn2030::VERSION
9
+ spec.authors = ["Shota Fukumori (sora_h)"]
10
+ spec.email = ["her@sorah.jp"]
11
+ spec.summary = %q{Control your STR-DN-2030}
12
+ spec.description = %q{Control STR-DN-2030 via TCP.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake"
23
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: str_dn_2030
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shota Fukumori (sora_h)
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Control STR-DN-2030 via TCP.
42
+ email:
43
+ - her@sorah.jp
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - Gemfile
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - config.ru
54
+ - lib/str_dn_2030.rb
55
+ - lib/str_dn_2030/input.rb
56
+ - lib/str_dn_2030/version.rb
57
+ - lib/str_dn_2030/web.rb
58
+ - lib/str_dn_2030/zone.rb
59
+ - str_dn_2030.gemspec
60
+ homepage: ''
61
+ licenses:
62
+ - MIT
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 2.2.2
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Control your STR-DN-2030
84
+ test_files: []