str_dn_2030 0.1.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,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: []