xap_ruby 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: 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