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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +59 -0
- data/Rakefile +3 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/lib/xap.rb +30 -0
- data/lib/xap/parser.rb +6 -0
- data/lib/xap/parser/parse_xap.rb +47 -0
- data/lib/xap/parser/xap.treetop +47 -0
- data/lib/xap/parser/xap_nodes.rb +120 -0
- data/lib/xap/schema.rb +7 -0
- data/lib/xap/schema/xap_bsc.rb +416 -0
- data/lib/xap/schema/xap_bsc_device.rb +384 -0
- data/lib/xap/xap_address.rb +169 -0
- data/lib/xap/xap_dev.rb +76 -0
- data/lib/xap/xap_handler.rb +197 -0
- data/lib/xap/xap_msg.rb +194 -0
- data/lib/xap_ruby.rb +6 -0
- data/lib/xap_ruby/version.rb +3 -0
- data/xap_ruby.gemspec +43 -0
- metadata +163 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -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)
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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
|
data/bin/setup
ADDED
data/lib/xap.rb
ADDED
@@ -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'
|
data/lib/xap/parser.rb
ADDED
@@ -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
|
data/lib/xap/schema.rb
ADDED
@@ -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
|