xap_ruby 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: 4d218f4cd8c25a7216bbf37f0be337a1c6146f8d
4
+ data.tar.gz: 1a30d854e84114a1cae501a8148702b28418c01a
5
+ SHA512:
6
+ metadata.gz: 6d1374d05a86b88265bfba24ed697e76a495c6afae3702421c8e7b366ffbdf94aff3bf3024ff6c3ee5bb9a132f2520ed13174ddf8917b83e47dd798abccf2cf3
7
+ data.tar.gz: 523b3a5c72fd055f868aa4333862c92986baedf01f4420bef5180e9d650e674d3e3a4b631878ccc831799937d45f20e4d86d3119ebc713dc54046a0bc1aaaec4
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .ruby-version
11
+ .ruby-gemset
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in xap_ruby.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012-2016, Mike Bourgeous (and any Git contributors)
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,59 @@
1
+ xap\_ruby
2
+ =========
3
+ This gem provides basic xAP Automation protocol support for EventMachine
4
+ applications. It was developed for use in Nitrogen Logic controller software.
5
+ There are no automated tests and the code could be improved in many ways, but it
6
+ may still be useful to someone.
7
+
8
+ This is a Ruby library written from scratch for communicating with a home
9
+ automation network using the xAP protocol. Supports sending and receiving
10
+ arbitrary xAP messages, triggering callbacks on certain received messages,
11
+ etc. Also includes an implementation of an xAP Basic Status and Control
12
+ device. Incoming xAP messages are parsed using an ad-hoc parser based on
13
+ Ruby's String#split() and Array#map() (a validating Treetop parser is also
14
+ available). Network events are handled using EventMachine.
15
+
16
+ This library strives to support all address wildcard modes and data types
17
+ specified by the xAP specification as correctly as possible.
18
+
19
+ Read the examples under `test/` to understand how to create your own applications
20
+ using xap\_ruby. All user-facing classes should have documenting comments.
21
+
22
+ xAP
23
+ ---
24
+ xAP is a broadcast UDP protocol for interfacing disparate home automation
25
+ systems and devices. Despite its weaknesses, xAP support is available for many
26
+ DIY and enthusiast automation systems. For more information on xAP, visit
27
+ http://www.xapautomation.org/.
28
+
29
+ Installation
30
+ ------------
31
+
32
+ Add this line to your application's Gemfile:
33
+
34
+ ```ruby
35
+ gem 'xap_ruby'
36
+ ```
37
+
38
+ Testing and Examples
39
+ --------------------
40
+ There are no automated tests. You can test the code by running
41
+ test/bscdev\_test.rb to simulate an xAP BSC device, then running
42
+ test/xap\_receive.sh and test/xap\_query.sh on another machine.
43
+
44
+ * test/bsdev\_test.rb creates a dummy xAP Basic Status and Control device.
45
+ * test/parser\_test.rb tests the Treetop parser with a variety of xAP data types.
46
+ * test/xap\_query.sh uses netcat to send a network-wide xAP query message.
47
+ * test/xap\_receive.rb prints all xAP messages received from the network.
48
+
49
+ Users of xap\_ruby
50
+ ------------------
51
+ Submit a pull request if you use this library somewhere and would like a
52
+ mention here.
53
+
54
+ * [Nitrogen Logic](http://www.nitrogenlogic.com/) - [Depth Camera Controller](http://www.nitrogenlogic.com/products/depth_controller.html)
55
+
56
+ Copyright
57
+ ---------
58
+ (C)2012-2017 Mike Bourgeous (and any Git contributors), licensed under
59
+ two-clause BSD (see LICENSE)
@@ -0,0 +1,3 @@
1
+ namespace :gem do
2
+ require "bundler/gem_tasks"
3
+ end
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "xap_ruby"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,30 @@
1
+ # Basic class and function definitions for the xAP protocol
2
+ # (C)2012-2016 Mike Bourgeous
3
+
4
+ require 'eventmachine'
5
+ require_relative 'xap_ruby'
6
+
7
+ # Basic functions for working with the xAP protocol.
8
+ module Xap
9
+ # Generates a random xAP UID of the form 'FF(01..FE)(01..FE)00'.
10
+ def self.random_uid
11
+ a = Random.rand(253) + 1
12
+ b = Random.rand(253) + 1
13
+ sprintf "FF%02X%02X00", a, b
14
+ end
15
+
16
+ # Prints a message using the global puts, prefixed with 'xAP: [time]'
17
+ # TODO: Use Logger
18
+ def self.log msg
19
+ puts "#{Time.now.strftime('%Y-%m-%d %H:%M:%S.%6N %z')} - xAP - #{msg}"
20
+ end
21
+ end
22
+
23
+ require_relative 'xap/parser'
24
+
25
+ require_relative 'xap/xap_address'
26
+ require_relative 'xap/xap_msg'
27
+ require_relative 'xap/xap_dev'
28
+ require_relative 'xap/xap_handler'
29
+
30
+ require_relative 'xap/schema'
@@ -0,0 +1,6 @@
1
+ module Xap
2
+ module Parser
3
+ end
4
+ end
5
+
6
+ require_relative 'parser/parse_xap'
@@ -0,0 +1,47 @@
1
+ # Treetop parser for xAP messages
2
+ # (C)2012 Mike Bourgeous
3
+ #
4
+ # References:
5
+ # http://thingsaaronmade.com/blog/a-quick-intro-to-writing-a-parser-using-treetop.html
6
+ # http://treetop.rubyforge.org/syntactic_recognition.html
7
+ # http://treetop.rubyforge.org/using_in_ruby.html
8
+ # http://www.xapautomation.org/index.php?title=Protocol_definition
9
+
10
+ require 'treetop'
11
+ require_relative 'xap_nodes'
12
+
13
+ module Xap
14
+ module Parser
15
+ module ParseXap
16
+ path = File.expand_path(File.dirname(__FILE__))
17
+ Treetop.load(File.join(path, 'xap.treetop'))
18
+ @@parser = XapTreetopParser.new
19
+
20
+ # Returns a Treetop node tree for the given xAP message
21
+ def self.parse(data)
22
+ tree = @@parser.parse(data, :root => :message)
23
+
24
+ if !tree
25
+ raise Exception, "Parse error: #{@@parser.failure_reason.inspect} (index #{@@parser.index})"
26
+ end
27
+
28
+ tree
29
+ end
30
+
31
+ # Returns a hash that is equivalent to calling parse(data).to_hash(),
32
+ # but much faster. However, this method does not do any explicit
33
+ # checking for invalid messages or values.
34
+ def self.simple_parse(data)
35
+ Hash[*data.split(/}\n?/).map {|v|
36
+ bl = v.split("\n{\n")
37
+ bl[1] = Hash[*bl[1].to_s.split("\n").map {|v2|
38
+ pair = v2.split(/[=!]/, 2)
39
+ pair[1] = [pair[1]].pack 'H*' if v2 =~ /^[^=!]+!/
40
+ pair
41
+ }.flatten!]
42
+ bl
43
+ }.flatten!(1)]
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,47 @@
1
+ # Treetop grammar file for parsing xAP
2
+ # (C)2012 Mike Bourgeous
3
+
4
+ grammar XapTreetop
5
+ rule keyword
6
+ # The xAP message grammar in the spec doesn't say that periods
7
+ # can be included in keywords, but the example messages include
8
+ # periods in keywords. TODO: handle odd number of hex digits
9
+ [-A-Za-z0-9_ ]+ ( '.' [-A-Za-z0-9_ ]+ )* <Keyword>
10
+ end
11
+
12
+ rule ascii
13
+ [^\n]* <AsciiValue>
14
+ end
15
+
16
+ rule hex
17
+ [A-Z0-9]* <HexValue>
18
+ end
19
+
20
+ rule space
21
+ [\s]+
22
+ end
23
+
24
+ rule eol
25
+ "\n"
26
+ end
27
+
28
+ rule value
29
+ delim:'=' val:ascii / delim:'!' val:hex <Value>
30
+ end
31
+
32
+ rule kvp
33
+ keyword value eol <KeyValuePair>
34
+ end
35
+
36
+ rule pairs
37
+ kvp* <Pairs>
38
+ end
39
+
40
+ rule block
41
+ keyword eol '{' eol pairs '}' eol <MessageBlock>
42
+ end
43
+
44
+ rule message
45
+ block* <Message>
46
+ end
47
+ end
@@ -0,0 +1,120 @@
1
+ # Treetop node extensions for parsing xAP
2
+ # (C)2012 Mike Bourgeous
3
+
4
+ module XapTreetop
5
+ class Keyword < Treetop::Runtime::SyntaxNode
6
+ end
7
+
8
+ # When value in KVP is prefixed by =
9
+ class AsciiValue < Treetop::Runtime::SyntaxNode
10
+ def raw_value
11
+ self.text_value
12
+ end
13
+ end
14
+
15
+ # When value in KVP is prefixed by !
16
+ class HexValue < Treetop::Runtime::SyntaxNode
17
+ def raw_value
18
+ [self.text_value].pack 'H*'
19
+ end
20
+ end
21
+
22
+ class Value < Treetop::Runtime::SyntaxNode
23
+ end
24
+
25
+ class KeyValuePair < Treetop::Runtime::SyntaxNode
26
+ def key
27
+ keyword.text_value
28
+ end
29
+
30
+ def val
31
+ value.val.raw_value
32
+ end
33
+
34
+ def to_s
35
+ s = "#{key}"
36
+ if is_hex?
37
+ s << '!'
38
+ else
39
+ s << '='
40
+ end
41
+ s << value.val.text_value
42
+ s
43
+ end
44
+
45
+ def is_hex?
46
+ value.val.is_a? HexValue
47
+ end
48
+ end
49
+
50
+ class Pairs < Treetop::Runtime::SyntaxNode
51
+ def to_hash
52
+ h = {}
53
+ elements.each do |el|
54
+ if el.is_a? KeyValuePair
55
+ h[el.key] = el.val
56
+ end
57
+ end
58
+ h
59
+ end
60
+
61
+ def to_s
62
+ s = ''
63
+ elements.each do |el|
64
+ if el.is_a? KeyValuePair
65
+ s << "#{el.to_s}\n"
66
+ end
67
+ end
68
+ s
69
+ end
70
+ end
71
+
72
+ class MessageBlock < Treetop::Runtime::SyntaxNode
73
+ def name
74
+ keyword.text_value
75
+ end
76
+
77
+ def values
78
+ pairs.to_hash
79
+ end
80
+
81
+ def to_s
82
+ s = "#{keyword.text_value}\n{\n"
83
+ s << pairs.to_s
84
+ s << "}\n"
85
+ s
86
+ end
87
+ end
88
+
89
+ class Message < Treetop::Runtime::SyntaxNode
90
+ # Returns the name of the first message block
91
+ def first_block
92
+ elements.each do |el|
93
+ if el.is_a? MessageBlock
94
+ return el.keyword.text_value
95
+ end
96
+ end
97
+ nil
98
+ end
99
+
100
+ def to_hash
101
+ h = {}
102
+ elements.each do |el|
103
+ if el.is_a? MessageBlock
104
+ h[el.keyword.text_value] = el.values
105
+ end
106
+ end
107
+ h
108
+ end
109
+
110
+ def to_s
111
+ s = ""
112
+ elements.each do |el|
113
+ if el.is_a? MessageBlock
114
+ s << el.to_s
115
+ end
116
+ end
117
+ s
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,7 @@
1
+ module Xap
2
+ module Schema
3
+ end
4
+ end
5
+
6
+ require_relative 'schema/xap_bsc'
7
+ require_relative 'schema/xap_bsc_device'
@@ -0,0 +1,416 @@
1
+ # Support for the xAP Basic Status and Control schema.
2
+ # (C)2012 Mike Bourgeous
3
+ #
4
+ # References:
5
+ # http://www.xapautomation.org/index.php?title=Basic_Status_and_Control_Schema
6
+
7
+ module Xap
8
+ module Schema
9
+ class XapBscBlock
10
+ attr_accessor :state, :level, :text, :display_text, :id
11
+
12
+ # is_input - Whether this is an input block or an output block
13
+ # index - If not nil, the block's index (0-based)
14
+ # hash - The block's hash of key-value pairs from @blocks -- this will
15
+ # be modified to
16
+ def initialize is_input, index, hash
17
+ @is_input = is_input
18
+ @index = index
19
+ @hash = hash
20
+
21
+ @hash.clone.each do |k, v|
22
+ case k.downcase
23
+ when 'state'
24
+ @hash.delete k
25
+ set_state v
26
+ when 'level'
27
+ @hash.delete k
28
+ set_level v
29
+ when 'text'
30
+ @hash.delete k
31
+ self.text = v
32
+ when 'displaytext'
33
+ @hash.delete k
34
+ self.display_text = v
35
+ when 'id'
36
+ @hash.delete k
37
+ self.id = v.upcase
38
+ end
39
+ end
40
+ end
41
+
42
+ # Sets this block's State field. Once the state is set, it cannot be
43
+ # unset, only changed. Pass true for 'ON', false for 'OFF', 'toggle'
44
+ # for 'toggle', or nil or any other value for '?'.
45
+ def state= s
46
+ @state = s
47
+ @hash['State'] = case s
48
+ when true
49
+ 'ON'
50
+ when false
51
+ 'OFF'
52
+ else
53
+ if s.is_a?(String) && s.downcase == 'toggle'
54
+ @state = 'toggle'
55
+ 'toggle'
56
+ else
57
+ @state = '?'
58
+ '?'
59
+ end
60
+ end
61
+ end
62
+
63
+ # Sets this block's Level field. Once the level is set, it cannot be
64
+ # unset, only changed. Examples: pass [ 1, 5 ] to specify '1/5'. Pass
65
+ # [ 35, '%' ] to specify '35%'. Pass [ 25 ] to specify 25 in an
66
+ # endpoint's native range.
67
+ def level= num_denom_array
68
+ raise 'num_denom_array must be an Array.' unless num_denom_array.is_a? Array
69
+ numerator, denominator = num_denom_array
70
+ @level = [ numerator, denominator ]
71
+ if denominator == '%'
72
+ @hash['Level'] = "#{numerator.to_i}%"
73
+ else
74
+ @hash['Level'] = "#{numerator.to_i}/#{denominator.to_i}"
75
+ end
76
+ end
77
+
78
+ # Sets this block's Text field. Once the text is set, it cannot be
79
+ # unset, only changed.
80
+ def text= t
81
+ raise 'Text must not include newlines.' if t.include? "\n"
82
+ @text = t
83
+ @hash['Text'] = t
84
+ end
85
+
86
+ # Sets this block's DisplayText field. Once the display text is set,
87
+ # it cannot be unset, only changed.
88
+ def display_text= t
89
+ raise 'Display text must not include newlines.' if t.include? "\n"
90
+ @display_text = t
91
+ @hash['DisplayText'] = t
92
+ end
93
+
94
+ # Sets this block's ID field. The given ID must be a String containing
95
+ # either two uppercase hex digits or a single asterisk. Once the ID is
96
+ # set, it cannot be unset, only changed.
97
+ def id= i
98
+ raise 'ID must be two uppercase hex digits or *.' unless i =~ /^([0-9A-Z][0-9A-Z]|\*)$/
99
+ @id = i
100
+ @hash['ID'] = i
101
+ end
102
+
103
+ # Returns 'input.state(.nn)' for input messages, 'output.state(.nn)' for output messages
104
+ def blockname
105
+ s = @is_input ? 'input.state' : 'output.state'
106
+ s << ".#{@index + 1}" if @index
107
+ s
108
+ end
109
+
110
+ # Returns a human-readable string description of this block.
111
+ def inspect
112
+ "Name: #{blockname} ID: #{id} State: #{state} Level: #{level} Text: #{text} DisplayText: #{display_text}"
113
+ end
114
+
115
+ protected
116
+ # Sets state based on the state text: "ON", "OFF", or "?"
117
+ def set_state s
118
+ case s.upcase
119
+ when 'ON'
120
+ self.state = true
121
+ when 'OFF'
122
+ self.state = false
123
+ when 'TOGGLE'
124
+ self.state = 'toggle'
125
+ when '?'
126
+ self.state = '?'
127
+ else
128
+ # Don't set state for anything else
129
+ end
130
+ end
131
+
132
+ # Sets level based on the level text: "x%", "y/z"
133
+ def set_level l
134
+ if l.include? '/'
135
+ self.level = l.split('/').map { |v| v.to_i }
136
+ elsif l.end_with? '%'
137
+ self.level = [ l.to_i, '%' ]
138
+ else
139
+ self.level = [ l.to_i ]
140
+ end
141
+ end
142
+ end
143
+
144
+ class XapBscMessage < XapUnsupportedMessage
145
+ def self.parse hash
146
+ self.new hash, nil, nil, nil, nil
147
+ end
148
+
149
+ def initialize msgclass, src_addr, src_uid, target_addr, is_input
150
+ super msgclass, src_addr, src_uid, target_addr
151
+
152
+ if msgclass.is_a?(Hash)
153
+ raise 'xAP BSC messages must have at least one block' if @blocks.length == 0
154
+ @is_input = @blocks.keys[0].downcase.start_with? 'input'
155
+ else
156
+ @is_input = is_input
157
+ end
158
+
159
+ @bsc_blocks = []
160
+ idx = 0
161
+ @blocks.each do |k, v|
162
+ kdown = k.downcase
163
+ if kdown.start_with?('input') || kdown.start_with?('output')
164
+ @bsc_blocks << XapBscBlock.new(@is_input, idx, v)
165
+ end
166
+ idx += 1 if idx
167
+ end
168
+ end
169
+
170
+ # Returns a human-readable string description of this message.
171
+ def inspect
172
+ s = "#{self.class.name}: #{@bsc_blocks.length} blocks recognized, #{@blocks.length} total\n"
173
+ s << "Blocks: \n"
174
+ @bsc_blocks.each do |blk|
175
+ s << "\t#{blk.inspect}\n"
176
+ end
177
+ s << "Regenerated message:\n\t"
178
+ s << super.lines.to_a.join("\t")
179
+ end
180
+
181
+ # Yields each XapBscBlock in sequence.
182
+ def each_block &block
183
+ @bsc_blocks.each do |b|
184
+ yield b
185
+ end
186
+ end
187
+ end
188
+
189
+ class XapBscCommand < XapBscMessage
190
+ register_class self, 'xAPBSC.cmd'
191
+
192
+ # Initializes an xAP BSC command message with the given source address
193
+ # and UID and target address. Any subsequent arguments are ignored.
194
+ def initialize src_addr, src_uid, target_addr, *args
195
+ if src_addr.is_a?(Hash)
196
+ super src_addr, src_uid, nil, nil, nil
197
+ else
198
+ super 'xAPBSC.cmd', src_addr, src_uid, target_addr, false
199
+ end
200
+ raise 'All xAP BSC command messages must have a target address.' if @target_addr.nil?
201
+ check_block 0
202
+ end
203
+
204
+ # Gets the State value of the index-th block (0-based). Returns true
205
+ # for 'ON', false for 'OFF', 'toggle' for 'toggle', '?' for '?' or any
206
+ # other value, and nil for undefined. Throws an error if index is out
207
+ # of range.
208
+ def get_state index
209
+ @bsc_blocks[index].state
210
+ end
211
+
212
+ # Sets the State value of the index-th block (0-based). Pass true for
213
+ # 'ON', false for 'OFF', 'toggle' for 'toggle', or any other value for
214
+ # '?'. The block will be created if it is not present. It is up to
215
+ # the caller to avoid creating gaps in the block indexes.
216
+ def set_state index, value
217
+ check_block index
218
+ @bsc_blocks[index].state = value
219
+ end
220
+
221
+ # Gets the Level value of the index-th block (0-based). Returns an
222
+ # array with the numerator and '%' if the message contains a percentage
223
+ # level, the numerator and denominator if the message contains a ranged
224
+ # level, or just the numerator if the message contains a non-ranged
225
+ # level. Throws an error if index is out of range.
226
+ def get_level index
227
+ @bsc_blocks[index].level
228
+ end
229
+
230
+ # Sets the Level value of the index-th block (0-based). The value
231
+ # parameter must be an array containing the numerator and '%' for a
232
+ # percentage level, the numerator and the denominator for a ranged
233
+ # level, or the numerator alone for command messages that will set an
234
+ # endpoint's level using its native range. The block will be created
235
+ # if it is not present. It is up to the caller to avoid creating gaps
236
+ # in the block indexes.
237
+ def set_level index, value
238
+ check_block index
239
+ @bsc_blocks[index].level = value
240
+ end
241
+
242
+ # Gets the Text value of the index-th block (0-based). Throws an error
243
+ # if index is out of range.
244
+ def get_text index
245
+ @bsc_blocks[index].text
246
+ end
247
+
248
+ # Sets the Text value of the index-th block (0-based). The block will
249
+ # be created if it is not present. It is up to the caller to avoid
250
+ # creating gaps in the block indexes.
251
+ def set_text index, value
252
+ check_block index
253
+ @bsc_blocks[index].text = value
254
+ end
255
+
256
+ # Gets the DisplayText value of the index-th block (0-based). Throws an error
257
+ # if index is out of range.
258
+ def get_display_text index
259
+ @bsc_blocks[index].display_text
260
+ end
261
+
262
+ # Sets the DisplayText value of the index-th block (0-based). The block will
263
+ # be created if it is not present. It is up to the caller to avoid
264
+ # creating gaps in the block indexes.
265
+ def set_display_text index, value
266
+ check_block index
267
+ @bsc_blocks[index].display_text = value
268
+ end
269
+
270
+ # Gets the ID value of the index-th block (0-based). Throws an error
271
+ # if index is out of range.
272
+ def get_id index
273
+ @bsc_blocks[index].id
274
+ end
275
+
276
+ # Sets the ID value of the index-th block (0-based). The ID given must
277
+ # be either two uppercase hex digits or a single asterisk. The block
278
+ # will be created if it is not present. It is up to the caller to
279
+ # avoid creating gaps in the block indexes.
280
+ def set_id index, value
281
+ check_block index
282
+ @bsc_blocks[index].id = value
283
+ end
284
+
285
+ private
286
+ def check_block index
287
+ unless @bsc_blocks[index]
288
+ h = {}
289
+ blk = XapBscBlock.new @is_input, index, h
290
+ @bsc_blocks[index] = blk
291
+ @blocks[blk.blockname] = h
292
+ end
293
+ end
294
+ end
295
+
296
+ class XapBscQuery < XapBscMessage
297
+ register_class self, 'xAPBSC.query'
298
+
299
+ # Initializes an xAP BSC query message with the given source address
300
+ # and UID and target address. Any subsequent arguments are ignored.
301
+ def initialize src_addr, src_uid, target_addr, *args
302
+ if src_addr.is_a?(Hash)
303
+ super src_addr, src_uid, nil, nil, nil
304
+ else
305
+ super 'xAPBSC.query', src_addr, src_uid, target_addr
306
+ @blocks['request'] = {}
307
+ end
308
+ raise 'All xAP BSC query messages must have a target address.' if @target_addr.nil?
309
+ end
310
+ end
311
+
312
+ # Shared functionality between info and event messages.
313
+ class XapBscResponse < XapBscMessage
314
+ attr_accessor :state, :level, :text, :display_text
315
+
316
+ # Initializes an xAP BSC event or info message with the given source
317
+ # address and UID. If is_input is truthy, this will be an input.state
318
+ # message; if is_input is falsy, this will be an output.state message.
319
+ # Any subsequent arguments are ignored.
320
+ def initialize src_addr, src_uid, is_input, *args
321
+ if src_addr.is_a?(Hash)
322
+ super src_addr, src_uid, nil, nil, nil
323
+ else
324
+ super self.class.classname, src_addr, src_uid, nil, is_input
325
+ @is_input = !!is_input
326
+
327
+ h = {}
328
+ blk = XapBscBlock.new @is_input, nil, h
329
+ @bsc_blocks[0] = blk
330
+ @blocks[blk.blockname] = h
331
+ end
332
+ end
333
+
334
+ # Sets the State field in the message's (input|output).status block.
335
+ # Once the state is set, it cannot be unset, only changed. Pass true
336
+ # for 'ON', false for 'OFF', 'toggle' for 'toggle', or nil for '?'.
337
+ def state= s
338
+ raise 'Do not use State=toggle for response messages.' if s.is_a?(String) && s.casecmp('toggle') == 0
339
+ @bsc_blocks[0].state = s
340
+ end
341
+
342
+ # Sets the Level field in the message's (input|output).status block.
343
+ # Once the level is set, it cannot be unset, only changed. xAPBSC.info
344
+ # and xAPBSC.event messages should not use percentage or non-ranged
345
+ # responses. Example: pass [ 1, 5 ] to specify '1/5'.
346
+ def level= num_denom_array
347
+ if num_denom_array[1] == nil || num_denom_array[1] == '%'
348
+ raise "Do not use percentages or non-ranged levels for response messages (#{num_denom_array})."
349
+ end
350
+ @bsc_blocks[0].level = num_denom_array
351
+ end
352
+
353
+ # Sets the Text field in the message's (input|output).status block.
354
+ # Once the text is set, it cannot be unset, only changed.
355
+ def text= t
356
+ @bsc_blocks[0].text = t
357
+ end
358
+
359
+ # Sets the DisplayText field in the message's (input|output).status
360
+ # block. Once the display text is set, it cannot be unset, only
361
+ # changed.
362
+ def display_text= t
363
+ @bsc_blocks[0].display_text = t
364
+ end
365
+
366
+ # Gets the message's State value. Returns true for 'ON', false for
367
+ # 'OFF', 'toggle' for 'toggle', '?' for '?' or any other value, and nil
368
+ # for undefined.
369
+ def state
370
+ @bsc_blocks[0].state
371
+ end
372
+
373
+ # Gets the Level value, if any. Returns a two-element array with the
374
+ # numerator and '%' if the message contains a percentage level, or the
375
+ # numerator and denominator if the message contains a ranged level.
376
+ def level
377
+ @bsc_blocks[0].level
378
+ end
379
+
380
+ # Gets the message's Text value, if any.
381
+ def text
382
+ @bsc_blocks[0].text
383
+ end
384
+
385
+ # Gets the message's DisplayText value, if any.
386
+ def display_text
387
+ @bsc_blocks[0].display_text
388
+ end
389
+ end
390
+
391
+ class XapBscEvent < XapBscResponse
392
+ @@classname = 'xAPBSC.event'
393
+ register_class self, @@classname
394
+
395
+ def self.classname
396
+ @@classname
397
+ end
398
+ end
399
+
400
+ # The xAP standard seems kind of silly for having separate info and event
401
+ # messages, especially since info messages may be sent at device startup, and
402
+ # the result of a command message must be an info message if the command
403
+ # message didn't change anything, or an event message otherwise. Overall, the
404
+ # xAP protocol is excessively chatty. But, it seems a lot of DIY home
405
+ # automation systems support it, so it's best to use the weak protocol you have
406
+ # rather than the perfect one you don't.
407
+ class XapBscInfo < XapBscResponse
408
+ @@classname = 'xAPBSC.info'
409
+ register_class self, @@classname
410
+
411
+ def self.classname
412
+ @@classname
413
+ end
414
+ end
415
+ end
416
+ end