simms 1.0.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.
- data/.rspec +1 -0
- data/History.txt +7 -0
- data/LICENSE +20 -0
- data/README.rdoc +60 -0
- data/Version.txt +1 -0
- data/lib/simms.rb +8 -0
- data/lib/simms/beacon.rb +81 -0
- data/lib/simms/bytes.rb +73 -0
- data/lib/simms/electricity_config_beacon.rb +34 -0
- data/lib/simms/electricity_data_beacon.rb +70 -0
- data/lib/simms/global_config_beacon.rb +29 -0
- data/lib/simms/packet.rb +53 -0
- data/spec/simms/beacon_spec.rb +136 -0
- data/spec/simms/bytes_spec.rb +47 -0
- data/spec/simms/packet_spec.rb +30 -0
- data/spec/spec_helper.rb +9 -0
- metadata +78 -0
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require <%= File.join(File.expand_path(File.dirname(__FILE__)), 'spec', 'spec_helper.rb') %>
|
data/History.txt
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Irongaze Consulting LLC
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
'Software'), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
= GEM: simms
|
2
|
+
|
3
|
+
Written by Rob Morris @ Irongaze Consulting LLC (http://irongaze.com)
|
4
|
+
|
5
|
+
Based on sample PHP code and documentation provided by Simms Energy (http://simmsenergy.com)
|
6
|
+
|
7
|
+
Funding for development provided by Sustainable Industrial Solutions LLC (http://sustainableis.com)
|
8
|
+
|
9
|
+
== DESCRIPTION
|
10
|
+
|
11
|
+
A set of classes to enable reading posted data from Simms Energy monitoring products (http://simmsenergy.com)
|
12
|
+
|
13
|
+
== SYNOPSIS
|
14
|
+
|
15
|
+
This gem facilitates interpreting incoming monitoring data and converting it into Ruby objects for easier
|
16
|
+
manipulation. It handles parsing the byte-stream of mesh data Simms devices provide, including
|
17
|
+
conversion of hex byte-codes into Ruby native data types such as integers, bools and floats.
|
18
|
+
|
19
|
+
To use:
|
20
|
+
|
21
|
+
# Require the library (not needed if using Rails/bundler)
|
22
|
+
>> require 'simms'
|
23
|
+
|
24
|
+
# Parse a packet to extract the contained beacon, passing in
|
25
|
+
# the timestamp and data posted to your server from the Simms device
|
26
|
+
>> beacon = Simms::Packet.parse(params['DateTime'], params['MeshData'])
|
27
|
+
|
28
|
+
# Which device sent this beacon?
|
29
|
+
>> beacon.uuid
|
30
|
+
=> '0A-28-00-48-27-88'
|
31
|
+
|
32
|
+
# See what kind of beacon we have
|
33
|
+
>> beacon.group
|
34
|
+
=> :electric
|
35
|
+
>> beacon.type
|
36
|
+
=> :data
|
37
|
+
>> beacon.data?
|
38
|
+
=> true
|
39
|
+
|
40
|
+
# Extract some data from the beacon
|
41
|
+
>> beacon.phase
|
42
|
+
=> 0
|
43
|
+
>> beacon.accumulated_real_energy
|
44
|
+
=> 15290
|
45
|
+
>> beacon.present_real_power
|
46
|
+
=> 472.08821
|
47
|
+
|
48
|
+
== REQUIREMENTS
|
49
|
+
|
50
|
+
None, though you'll need rspec to build and test the gem
|
51
|
+
|
52
|
+
== INSTALL
|
53
|
+
|
54
|
+
To install, simply run:
|
55
|
+
|
56
|
+
sudo gem install simms
|
57
|
+
|
58
|
+
RVM users can skip the sudo:
|
59
|
+
|
60
|
+
gem install simms
|
data/Version.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/lib/simms.rb
ADDED
data/lib/simms/beacon.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# Represents the data or configuration information broadcast by a single Simms monitoring
|
2
|
+
# device.
|
3
|
+
module Simms
|
4
|
+
class Beacon
|
5
|
+
|
6
|
+
# Attributes
|
7
|
+
attr_accessor :uuid, :timestamp, :command_group_id, :device_command_id
|
8
|
+
|
9
|
+
# Command group codes
|
10
|
+
CG_GLOBAL = 0xFFFF
|
11
|
+
CG_SENSOR = 0x0001
|
12
|
+
CG_ELECTRIC = 0x0002
|
13
|
+
CG_HVAC = 0x0004
|
14
|
+
CG_PULSER = 0x0005
|
15
|
+
CG_IRRIGATION = 0x0007
|
16
|
+
|
17
|
+
# Device commands
|
18
|
+
DC_DATA = 0x0001
|
19
|
+
DC_GLOBAL_CONFIG = 0x000B
|
20
|
+
DC_CONFIG = 0x0022
|
21
|
+
|
22
|
+
# Find the appropriate beacon class for a given group + command tuple
|
23
|
+
def self.class_for(group, command)
|
24
|
+
case group
|
25
|
+
|
26
|
+
when CG_GLOBAL then
|
27
|
+
case command
|
28
|
+
when DC_GLOBAL_CONFIG then
|
29
|
+
return Simms::GlobalConfigBeacon
|
30
|
+
end
|
31
|
+
|
32
|
+
when CG_ELECTRIC then
|
33
|
+
case command
|
34
|
+
when DC_DATA then
|
35
|
+
return Simms::ElectricityDataBeacon
|
36
|
+
when DC_CONFIG then
|
37
|
+
return Simms::ElectricityConfigBeacon
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
# None found!
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(bytes)
|
47
|
+
@bytes = bytes
|
48
|
+
end
|
49
|
+
|
50
|
+
def group
|
51
|
+
case @command_group_id
|
52
|
+
when CG_GLOBAL then :global
|
53
|
+
when CG_SENSOR then :sensor
|
54
|
+
when CG_ELECTRIC then :electric
|
55
|
+
when CG_HVAC then :hvac
|
56
|
+
when CG_PULSER then :pulser
|
57
|
+
when CG_IRRIGATION then :irrigation
|
58
|
+
else
|
59
|
+
:unknown
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def type
|
64
|
+
case @device_command_id
|
65
|
+
when DC_DATA then :data
|
66
|
+
when DC_CONFIG, DC_GLOBAL_CONFIG then :config
|
67
|
+
else
|
68
|
+
:unknown
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def data?
|
73
|
+
type == :data
|
74
|
+
end
|
75
|
+
|
76
|
+
def config?
|
77
|
+
type == :config
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
data/lib/simms/bytes.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Manages a set of byte codes in hex string format (eg 'AF', '0C', etc) and
|
2
|
+
# provides methods for extracting floats, ints, etc from that stream by
|
3
|
+
# position.
|
4
|
+
#
|
5
|
+
# Responsible for low-level translation of incoming data into types usable
|
6
|
+
# in Ruby.
|
7
|
+
module Simms
|
8
|
+
class Bytes
|
9
|
+
|
10
|
+
attr_accessor :data
|
11
|
+
|
12
|
+
def initialize(byte_array)
|
13
|
+
@data = byte_array
|
14
|
+
@data = @data.to_s.scan(/../) unless @data.is_a?(Array)
|
15
|
+
end
|
16
|
+
|
17
|
+
def count
|
18
|
+
@data.count
|
19
|
+
end
|
20
|
+
|
21
|
+
def empty?
|
22
|
+
count == 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def uint8(pos)
|
26
|
+
@data[pos].hex
|
27
|
+
end
|
28
|
+
|
29
|
+
def uint16(pos)
|
30
|
+
@data[pos...pos+2].reverse.join('').hex
|
31
|
+
end
|
32
|
+
|
33
|
+
def sint16(pos)
|
34
|
+
[@data[pos...pos+2].join].pack('H*').unpack('s').first
|
35
|
+
end
|
36
|
+
|
37
|
+
def uint32(pos)
|
38
|
+
@data[pos...pos+4].reverse.join('').hex
|
39
|
+
end
|
40
|
+
|
41
|
+
def sint32(pos)
|
42
|
+
[@data[pos...pos+4].join].pack('H*').unpack('l').first
|
43
|
+
end
|
44
|
+
|
45
|
+
def bool(pos)
|
46
|
+
@data[pos].hex == 1
|
47
|
+
end
|
48
|
+
|
49
|
+
def float(pos)
|
50
|
+
v = uint32(pos)
|
51
|
+
x = (v & ((1 << 23) - 1)) + (1 << 23) * (v >> 31 | 1)
|
52
|
+
exp = (v >> 23 & 0xFF) - 127
|
53
|
+
return x.to_f * (2 ** (exp - 23))
|
54
|
+
end
|
55
|
+
|
56
|
+
def slice(*args)
|
57
|
+
Bytes.new(@data.slice(*args))
|
58
|
+
end
|
59
|
+
|
60
|
+
def [](*args)
|
61
|
+
Bytes.new(@data[*args])
|
62
|
+
end
|
63
|
+
|
64
|
+
def calc_checksum
|
65
|
+
@data.inject(0) {|sum, byte| sum + byte.hex} % 0xFF
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
@data.join('')
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'simms/beacon'
|
2
|
+
|
3
|
+
module Simms
|
4
|
+
class ElectricityConfigBeacon < Simms::Beacon
|
5
|
+
|
6
|
+
def ct_amps
|
7
|
+
@bytes.uint16(6)
|
8
|
+
end
|
9
|
+
|
10
|
+
def nominal_voltage
|
11
|
+
@bytes.uint16(8)
|
12
|
+
end
|
13
|
+
|
14
|
+
def phase_type
|
15
|
+
@bytes.uint8(10)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Human-readable description of the phase type
|
19
|
+
def phase_type_description
|
20
|
+
case phase_type
|
21
|
+
when 0 then
|
22
|
+
'3 Phase With Neutral, Display L-N'
|
23
|
+
when 1 then
|
24
|
+
'3 Phase With Neutral, Display L-L'
|
25
|
+
when 2 then
|
26
|
+
'Single Split Phase A+B with Neutral, Display L-N'
|
27
|
+
when 3 then
|
28
|
+
'Single Phase A to Neutral, Display L-N'
|
29
|
+
else
|
30
|
+
'Unknown phase type!'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'simms/beacon'
|
2
|
+
|
3
|
+
# Electricity data beacons are sent at the time interval configured in the device, and will be
|
4
|
+
# have one packet sent per phase being monitored, ie for a 3-phase system, you will receive
|
5
|
+
# 3 data beacons per reporting interval per device, with the phase field set to 0, 1, or 2.
|
6
|
+
module Simms
|
7
|
+
class ElectricityDataBeacon < Simms::Beacon
|
8
|
+
|
9
|
+
# Which phase this beacon represents
|
10
|
+
def phase
|
11
|
+
@bytes.uint8(6)
|
12
|
+
end
|
13
|
+
|
14
|
+
# TODO What does this mean? What are the valid values?
|
15
|
+
def error_state
|
16
|
+
@bytes.uint8(7)
|
17
|
+
end
|
18
|
+
|
19
|
+
# May be negative, as in the case of a solar power installation. Is a counter, ie to get
|
20
|
+
# the real energy (Wh) for a given interval, you need to diff against past values
|
21
|
+
def accumulated_real_energy
|
22
|
+
@bytes.sint32(8)
|
23
|
+
end
|
24
|
+
|
25
|
+
# In Wh - see #accumulated_real_energy for counter info
|
26
|
+
def accumulated_apparent_energy
|
27
|
+
@bytes.sint32(12)
|
28
|
+
end
|
29
|
+
|
30
|
+
# In W
|
31
|
+
def present_real_power
|
32
|
+
@bytes.float(16)
|
33
|
+
end
|
34
|
+
|
35
|
+
# In W
|
36
|
+
def present_apparent_power
|
37
|
+
@bytes.float(20)
|
38
|
+
end
|
39
|
+
|
40
|
+
def volts_rms
|
41
|
+
@bytes.float(24)
|
42
|
+
end
|
43
|
+
|
44
|
+
def current_rms
|
45
|
+
@bytes.float(28)
|
46
|
+
end
|
47
|
+
|
48
|
+
# In VA
|
49
|
+
def power_factor
|
50
|
+
@bytes.float(32)
|
51
|
+
end
|
52
|
+
|
53
|
+
def max_demand
|
54
|
+
@bytes.float(36)
|
55
|
+
end
|
56
|
+
|
57
|
+
def volts_sag
|
58
|
+
@bytes.uint16(40)
|
59
|
+
end
|
60
|
+
|
61
|
+
def volts_peak
|
62
|
+
@bytes.uint16(42)
|
63
|
+
end
|
64
|
+
|
65
|
+
def current_peak
|
66
|
+
@bytes.uint16(44)
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'simms/beacon'
|
2
|
+
|
3
|
+
module Simms
|
4
|
+
class GlobalConfigBeacon < Simms::Beacon
|
5
|
+
|
6
|
+
def version_high
|
7
|
+
@bytes.uint16(6)
|
8
|
+
end
|
9
|
+
|
10
|
+
def version_low
|
11
|
+
@bytes.uint16(8)
|
12
|
+
end
|
13
|
+
|
14
|
+
def firmware_version
|
15
|
+
"#{version_high}.#{version_low}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def local_devices
|
19
|
+
@bytes.uint16(10)
|
20
|
+
end
|
21
|
+
|
22
|
+
def device_name
|
23
|
+
name = ''
|
24
|
+
@bytes.slice(24,16).data.collect {|b| b.hex}.each {|c| name << c if c > 0}
|
25
|
+
name
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
data/lib/simms/packet.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Simms
|
2
|
+
class Packet
|
3
|
+
|
4
|
+
# Action type constants
|
5
|
+
ACTION_BEACON = 0x35
|
6
|
+
|
7
|
+
# Parses an incoming packet, extracts its enclosed beacon data, instantiates
|
8
|
+
# the correct beacon class, and returns it. Raises exceptions on malformed data.
|
9
|
+
# If beacon type is unknown, raises an exception if raise_on_unknown is set to true. Otherwise,
|
10
|
+
# unknown beacons are returned as nil.
|
11
|
+
def self.parse(timestamp, data_packet, raise_on_unknown = false)
|
12
|
+
# Parse the string into hex pair bytes for the mesh packet
|
13
|
+
mesh_data = Bytes.new(data_packet)
|
14
|
+
|
15
|
+
# Extract the bytes for the embedded beacon data
|
16
|
+
beacon_data = mesh_data[34..-1]
|
17
|
+
|
18
|
+
# Verify data
|
19
|
+
if mesh_data.empty? || beacon_data.empty?
|
20
|
+
raise "Malformed packet - missing segments"
|
21
|
+
end
|
22
|
+
unless mesh_data.uint16(32) == beacon_data.count
|
23
|
+
raise "Malformed packet - length field (#{mesh_data.uint16(32)}) does not match data length (#{beacon_data.count})"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Parse timestamp - always UTC time
|
27
|
+
timestamp = Time.utc(*(timestamp.split(/[\- \:]/)))
|
28
|
+
|
29
|
+
# Get key variables from packet
|
30
|
+
uuid = mesh_data.slice(6,6).data.join('-')
|
31
|
+
group = beacon_data.uint16(0)
|
32
|
+
command = beacon_data.uint8(2)
|
33
|
+
action = beacon_data.uint8(3)
|
34
|
+
|
35
|
+
# Instantiate beacon instance (assuming it's a beacon action!)
|
36
|
+
klass = Simms::Beacon.class_for(group, command) if action == ACTION_BEACON
|
37
|
+
if klass
|
38
|
+
# Create the beacon, then set all the meta-info we have about it
|
39
|
+
beacon = klass.new(beacon_data)
|
40
|
+
beacon.command_group_id = group
|
41
|
+
beacon.device_command_id = command
|
42
|
+
beacon.timestamp = timestamp
|
43
|
+
beacon.uuid = uuid
|
44
|
+
beacon
|
45
|
+
|
46
|
+
else
|
47
|
+
raise "Unable to find beacon class for group #{group} and command #{command}" if raise_on_unknown
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
|
2
|
+
describe Simms::Beacon do
|
3
|
+
|
4
|
+
SAMPLE_TIMESTAMP = '2012-06-27 16:16:08'
|
5
|
+
SAMPLE_GC_BEACON = 'FB10040002000015ABA0B0220100FF00FF00FFFFFFFFFFFF0100790E937900022A00FFFF0B351A820000C8000100040002000015ABA0B022010053696D6D734F666669636550616E656C0000'
|
6
|
+
SAMPLE_EC_BEACON = 'FB10040002000015ABA0B0250100FF00FF00FFFFFFFFFFFF0100AA72704C00020D00020022359FCA960078000300D3'
|
7
|
+
SAMPLE_ED_BEACON = 'FB10040002000015ABA0B0220100FF00FF00FFFFFFFFFFFF01008E51CB1C00022E0002000135E58900014EF722004308280041E91144132620442D5EF542D417A740B43D693F8C8820447A007A000500'
|
8
|
+
|
9
|
+
it 'should return the correct class for a given group + command combo' do
|
10
|
+
{
|
11
|
+
[0xFFFF, 0x0B] => Simms::GlobalConfigBeacon,
|
12
|
+
[0x0002, 0x01] => Simms::ElectricityDataBeacon,
|
13
|
+
[0x0002, 0x22] => Simms::ElectricityConfigBeacon
|
14
|
+
}.each do |info, klass|
|
15
|
+
Simms::Beacon.class_for(*info).should == klass
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'when a global configuration beacon' do
|
20
|
+
before do
|
21
|
+
@beacon = Simms::Packet.parse(SAMPLE_TIMESTAMP, SAMPLE_GC_BEACON)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should know what kind of beacon it is' do
|
25
|
+
@beacon.group.should == :global
|
26
|
+
@beacon.type.should == :config
|
27
|
+
@beacon.config?.should be_true
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should extract firmware version' do
|
31
|
+
@beacon.firmware_version.should == '0.200'
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should extract local device count' do
|
35
|
+
@beacon.local_devices.should == 1
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should extract device name' do
|
39
|
+
@beacon.device_name.should == 'SimmsOfficePanel'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when an electricity configuration beacon' do
|
44
|
+
before do
|
45
|
+
@beacon = Simms::Packet.parse(SAMPLE_TIMESTAMP, SAMPLE_EC_BEACON)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should know what kind of beacon it is' do
|
49
|
+
@beacon.group.should == :electric
|
50
|
+
@beacon.type.should == :config
|
51
|
+
@beacon.config?.should be_true
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should extract CT clamp amps' do
|
55
|
+
@beacon.ct_amps.should == 150
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should extract nominal voltage' do
|
59
|
+
@beacon.nominal_voltage.should == 120
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should extract phase type' do
|
63
|
+
@beacon.phase_type.should == 3
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should provide a user-readable phase type' do
|
67
|
+
@beacon.phase_type_description.should == 'Single Phase A to Neutral, Display L-N'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'when an electricity data beacon' do
|
72
|
+
before do
|
73
|
+
@beacon = Simms::Packet.parse(SAMPLE_TIMESTAMP, SAMPLE_ED_BEACON)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should know what kind of beacon it is' do
|
77
|
+
@beacon.group.should == :electric
|
78
|
+
@beacon.type.should == :data
|
79
|
+
@beacon.data?.should be_true
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should extract the phase' do
|
83
|
+
@beacon.phase.should == 0
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should extract the error state' do
|
87
|
+
@beacon.error_state.should == 1
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'should extract the accumulated real energy (Wh)' do
|
91
|
+
@beacon.accumulated_real_energy.should == 2291534
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should extract the accumulated apparent energy' do
|
95
|
+
@beacon.accumulated_apparent_energy.should == 2623555
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should extract the present real power' do
|
99
|
+
@beacon.present_real_power.round(4).should == 583.6446
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'should extract the present apparent power' do
|
103
|
+
@beacon.present_apparent_power.round(4).should == 640.5949
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'should extract the volts rms' do
|
107
|
+
@beacon.volts_rms.round(4).should == 122.6839
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should extract the current rms' do
|
111
|
+
@beacon.current_rms.round(4).should == 5.2217
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should extract the power factor' do
|
115
|
+
@beacon.power_factor.round(4).should == 0.9111
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'should extract the max demand' do
|
119
|
+
@beacon.max_demand.round(4).should == 642.1335
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should extract the volts sag' do
|
123
|
+
@beacon.volts_sag.should == 122
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should extract the volts peak' do
|
127
|
+
@beacon.volts_peak.should == 122
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'should extract the current peak' do
|
131
|
+
@beacon.current_peak.should == 5
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
describe Simms::Bytes do
|
2
|
+
|
3
|
+
it 'should manage a set of byte strings' do
|
4
|
+
bytes = bytes('010200ABCD')
|
5
|
+
bytes.count.should == 5
|
6
|
+
bytes.data.last.should == 'CD'
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should extract unsigned 8 bit ints' do
|
10
|
+
bytes('01').uint8(0).should == 1
|
11
|
+
bytes('AC').uint8(0).should == 172
|
12
|
+
bytes('FF').uint8(0).should == 255
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should extract unsigned 16 bit ints' do
|
16
|
+
bytes('0400').uint16(0).should == 4
|
17
|
+
bytes('0000').uint16(0).should == 0
|
18
|
+
bytes('FFFF').uint16(0).should == 65535
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should extract unsigned 32 bit ints' do
|
22
|
+
bytes('4EF72200').uint32(0).should == 2291534
|
23
|
+
bytes('FFFFFFFF').uint32(0).should == 4294967295
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should extract signed 32 bit ints' do
|
27
|
+
bytes('4EF72200').sint32(0).should == 2291534
|
28
|
+
bytes('FFFFFFFF').sint32(0).should == -1
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should extract floats' do
|
32
|
+
bytes('8C882044').float(0).round(4).should == 642.1335
|
33
|
+
bytes('B43D693F').float(0).round(4).should == 0.9111
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should enable subset extraction' do
|
37
|
+
@bytes = bytes('1234567890')
|
38
|
+
@bytes[0].data.should == ['12']
|
39
|
+
@bytes[1,2].to_s.should == '3456'
|
40
|
+
end
|
41
|
+
|
42
|
+
# Helper to construct a new Bytes object from a given string
|
43
|
+
def bytes(str)
|
44
|
+
Simms::Bytes.new(str)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
describe Simms::Packet do
|
2
|
+
|
3
|
+
SAMPLE_TIMESTAMP = '2012-06-27 16:16:08'
|
4
|
+
SAMPLE_PACKET = 'FB10040002000015ABA0B0220100FF00FF00FFFFFFFFFFFF0100790E937900022A00FFFF0B351A820000C8000100040002000015ABA0B022010053696D6D734F666669636550616E656C0000'
|
5
|
+
|
6
|
+
before(:all) do
|
7
|
+
@beacon = Simms::Packet.parse(SAMPLE_TIMESTAMP, SAMPLE_PACKET)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should parse valid packets' do
|
11
|
+
Simms::Packet.parse(SAMPLE_TIMESTAMP, SAMPLE_PACKET).should_not be_nil
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should return the correct beacon type' do
|
15
|
+
@beacon.should be_a(Simms::GlobalConfigBeacon)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should correctly set the beacon\'s device UUID' do
|
19
|
+
@beacon.uuid.should == '00-15-AB-A0-B0-22'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should correctly set the beacon\'s timestamp' do
|
23
|
+
@beacon.timestamp.should == Time.utc(2012,6,27,16,16,8)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should return raise on invalid packet data' do
|
27
|
+
lambda{Simms::Packet.parse(SAMPLE_TIMESTAMP, '1234')}.should raise_error
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simms
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Rob Morris
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.6'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.6'
|
30
|
+
description: Allows accepting and parsing Simms Energy monitor data streams.
|
31
|
+
email:
|
32
|
+
- rob@irongaze.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- lib/simms/beacon.rb
|
38
|
+
- lib/simms/bytes.rb
|
39
|
+
- lib/simms/electricity_config_beacon.rb
|
40
|
+
- lib/simms/electricity_data_beacon.rb
|
41
|
+
- lib/simms/global_config_beacon.rb
|
42
|
+
- lib/simms/packet.rb
|
43
|
+
- lib/simms.rb
|
44
|
+
- spec/simms/beacon_spec.rb
|
45
|
+
- spec/simms/bytes_spec.rb
|
46
|
+
- spec/simms/packet_spec.rb
|
47
|
+
- spec/spec_helper.rb
|
48
|
+
- LICENSE
|
49
|
+
- History.txt
|
50
|
+
- Version.txt
|
51
|
+
- README.rdoc
|
52
|
+
- .rspec
|
53
|
+
homepage: http://irongaze.com
|
54
|
+
licenses:
|
55
|
+
- MIT
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 1.9.2
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
requirements: []
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 1.8.24
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: Data stream parser for Simms Energy monitors
|
78
|
+
test_files: []
|