emu_power 1.2 → 1.3
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 +4 -4
- data/lib/emu_power.rb +3 -4
- data/lib/emu_power/api.rb +115 -74
- data/lib/emu_power/commands.rb +92 -32
- data/lib/emu_power/notifications.rb +392 -0
- metadata +6 -7
- data/lib/emu_power/stream_parser.rb +0 -118
- data/lib/emu_power/types.rb +0 -212
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a735ca710b544606634af881808df7df97cac003dcd9425d0e6ad1800d94f5a
|
4
|
+
data.tar.gz: 407c3060e5fbe93825f4ddc86675a5bbf8c377d6758561345690ca7c8919114b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c37948b89801d604e9e92309c2c15c1c657c3d452be30d6243cabc9e05d5212a33b6e1b766c73cbe0b5850956b589fc6428ef69d1e0b11942648258a2338297b
|
7
|
+
data.tar.gz: 7e241b00ec429803aa74f12b81edde4cd5a30c968fcbc29f3ce618632fbad7d0d15bd862be58d89b7197c05bc9edf28c585eb6083dfe02fd737241ef745aefb0
|
data/lib/emu_power.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
class EmuPower
|
2
2
|
end
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
require 'emu_power/commands'
|
4
|
+
require_relative 'emu_power/api'
|
5
|
+
require_relative 'emu_power/notifications'
|
6
|
+
require_relative 'emu_power/commands'
|
data/lib/emu_power/api.rb
CHANGED
@@ -2,133 +2,174 @@
|
|
2
2
|
# unit. This API is asynchronous, and allows event handlers
|
3
3
|
# to be registered for the various message types.
|
4
4
|
|
5
|
-
|
6
|
-
require 'emu_power/types'
|
7
|
-
|
5
|
+
require_relative 'notifications'
|
8
6
|
require 'serialport'
|
9
|
-
require 'nokogiri'
|
10
7
|
|
11
8
|
class EmuPower::Api
|
12
9
|
|
13
10
|
LINE_TERMINATOR = "\r\n"
|
14
11
|
|
15
|
-
|
16
|
-
def initialize(tty, history_length = 10)
|
12
|
+
attr_accessor :debug_mode
|
17
13
|
|
18
|
-
|
14
|
+
# Initialize the serial connection and set up internal structures.
|
15
|
+
def initialize(tty, debug: false)
|
19
16
|
|
20
|
-
@
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
17
|
+
@port = SerialPort.new(tty, 115200, 8, 1, SerialPort::NONE)
|
18
|
+
|
19
|
+
# Get rid of any existing buffered data - we only want to operate on
|
20
|
+
# fresh notifications.
|
21
|
+
@port.flush_input
|
22
|
+
@port.flush_output
|
23
|
+
|
24
|
+
@debug_mode = debug
|
25
|
+
|
26
|
+
reset_callbacks!
|
26
27
|
|
27
28
|
end
|
28
29
|
|
29
|
-
# Register the callback for specific notification events. Expects
|
30
|
-
#
|
31
|
-
# the callback will be
|
32
|
-
#
|
33
|
-
#
|
30
|
+
# Register the callback for specific notification events. Expects either an
|
31
|
+
# EmuPower::Notifications::Notification subclass, or :global, or :fallback. If :global
|
32
|
+
# is passed, the callback will be fired on every notification. If :fallback is
|
33
|
+
# passed, the callback will be fired for every notification that does not have
|
34
|
+
# a specific callback registered already.
|
34
35
|
def callback(klass, &block)
|
35
|
-
|
36
|
+
|
37
|
+
if klass == :global || klass == 'global'
|
38
|
+
@global_callback = block
|
39
|
+
elsif klass == :fallback || klass == 'fallback'
|
40
|
+
@fallback_callback = block
|
41
|
+
elsif EmuPower::Notifications::Notification.subclasses.include?(klass)
|
42
|
+
@callbacks[klass] = block
|
43
|
+
else
|
44
|
+
klass_list = EmuPower::Notifications::Notification.subclasses.map(&:name).join(', ')
|
45
|
+
raise ArgumentError.new("Class must be :global, :fallback, or one of #{klass_list}")
|
46
|
+
end
|
47
|
+
|
36
48
|
return true
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
# Reset all callbacks to the default no-op state.
|
53
|
+
def reset_callbacks!
|
54
|
+
@global_callback = nil
|
55
|
+
@fallback_callback = nil
|
56
|
+
@callbacks = {}
|
37
57
|
end
|
38
58
|
|
39
59
|
# Send a command to the device. Expects an instance of one of the
|
40
60
|
# command classes defined in commands.rb. The serial connection
|
41
61
|
# must be started before this can be used.
|
42
62
|
def issue_command(obj)
|
43
|
-
|
44
|
-
return false
|
63
|
+
|
64
|
+
return false if @thread.nil? || !obj.respond_to?(:to_command)
|
65
|
+
|
45
66
|
xml = obj.to_command
|
46
67
|
@port.write(xml)
|
68
|
+
|
47
69
|
return true
|
70
|
+
|
48
71
|
end
|
49
72
|
|
50
|
-
# Begin polling for serial data. We spawn a new thread to
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
|
73
|
+
# Begin polling for serial data. We spawn a new thread to handle this so we don't
|
74
|
+
# block input. This method blocks until the reader thread terminates, which in most
|
75
|
+
# cases is never. This should usually be called at the end of a program after all
|
76
|
+
# callbacks are registered. If blocking is set to false, returns immediately and
|
77
|
+
# lets the caller handle the spawned thread. Non-blocking mode should mostly be
|
78
|
+
# used for development purposes; most production scripts should use blocking mode
|
79
|
+
# and callbacks.
|
80
|
+
def start_serial(blocking = true)
|
55
81
|
|
56
82
|
return false unless @thread.nil?
|
57
83
|
|
58
|
-
parser = construct_parser
|
59
|
-
|
60
84
|
@thread = Thread.new do
|
85
|
+
|
86
|
+
# Define boundary tags
|
87
|
+
root_elements = EmuPower::Notifications.notify_roots
|
88
|
+
start_tags = root_elements.map { |v| "<#{v}>" }
|
89
|
+
stop_tags = root_elements.map { |v| "</#{v}>" }
|
90
|
+
|
91
|
+
current_notify = ''
|
92
|
+
|
93
|
+
# Build up complete XML fragments line-by-line and dispatch callbacks
|
61
94
|
loop do
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
95
|
+
|
96
|
+
line = @port.readline(LINE_TERMINATOR).strip
|
97
|
+
|
98
|
+
if start_tags.include?(line)
|
99
|
+
current_notify = line
|
100
|
+
|
101
|
+
elsif stop_tags.include?(line)
|
102
|
+
|
103
|
+
xml = current_notify + line
|
104
|
+
current_notify = ''
|
105
|
+
|
106
|
+
begin
|
107
|
+
obj = EmuPower::Notifications.construct(xml)
|
108
|
+
rescue StandardError
|
109
|
+
puts "Failed to construct object for XML fragment: #{xml}" if @debug_mode
|
110
|
+
next
|
111
|
+
end
|
112
|
+
|
113
|
+
if obj
|
114
|
+
puts obj if @debug_mode
|
115
|
+
perform_callbacks(obj)
|
116
|
+
else
|
117
|
+
puts "Incomplete XML stream: #{xml}" if @debug_mode
|
118
|
+
end
|
119
|
+
|
120
|
+
else
|
121
|
+
current_notify += line
|
69
122
|
end
|
123
|
+
|
70
124
|
end
|
71
125
|
end
|
72
126
|
|
73
127
|
if blocking
|
74
|
-
|
128
|
+
|
129
|
+
# Block until thread is terminated, and ensure we clean up after ourselves.
|
130
|
+
begin
|
131
|
+
@thread.join
|
132
|
+
ensure
|
133
|
+
stop_serial if @thread
|
134
|
+
end
|
135
|
+
|
75
136
|
else
|
76
|
-
return
|
137
|
+
return @thread
|
77
138
|
end
|
78
139
|
|
79
140
|
end
|
80
141
|
|
81
|
-
#
|
82
|
-
#
|
142
|
+
# Terminate the reader thread. The start_serial method will return
|
143
|
+
# once this is called. This will usually be called from a signal
|
144
|
+
# trap or similar, since the main program will usually be blocked
|
145
|
+
# by start_serial.
|
83
146
|
def stop_serial
|
147
|
+
|
84
148
|
return false if @thread.nil?
|
149
|
+
|
85
150
|
@thread.terminate
|
86
151
|
@thread = nil
|
87
|
-
return true
|
88
|
-
end
|
89
152
|
|
90
|
-
|
91
|
-
def history_for(klass)
|
92
|
-
return @histories[klass].compact
|
93
|
-
end
|
153
|
+
return true
|
94
154
|
|
95
|
-
# Get the most recent object for the given type
|
96
|
-
def current(klass)
|
97
|
-
return history_for(klass)[0]
|
98
155
|
end
|
99
156
|
|
100
157
|
private
|
101
158
|
|
102
|
-
#
|
103
|
-
def
|
104
|
-
|
105
|
-
container = EmuPower::Types.construct(obj)
|
159
|
+
# Dispatch the appropriate callback
|
160
|
+
def perform_callbacks(obj)
|
106
161
|
|
107
|
-
|
108
|
-
puts "BAD OBJECT #{obj}"
|
109
|
-
else
|
110
|
-
push_history(container)
|
111
|
-
@callbacks[container.class]&.call(container)
|
112
|
-
@callbacks[:global]&.call(container)
|
113
|
-
end
|
162
|
+
klass = obj.class
|
114
163
|
|
115
|
-
|
164
|
+
# Fire global callback
|
165
|
+
@global_callback&.call(obj)
|
116
166
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
167
|
+
klass_specific = @callbacks[klass]
|
168
|
+
if klass_specific
|
169
|
+
klass_specific.call(obj)
|
170
|
+
else
|
171
|
+
@fallback_callback&.call(obj)
|
121
172
|
end
|
122
|
-
end
|
123
|
-
|
124
|
-
# Helper for inserting object into appropriate history queue
|
125
|
-
def push_history(obj)
|
126
|
-
|
127
|
-
type = obj.class
|
128
|
-
|
129
|
-
old = @histories[type].pop
|
130
|
-
@histories[type].prepend(obj)
|
131
|
-
return old
|
132
173
|
|
133
174
|
end
|
134
175
|
|
data/lib/emu_power/commands.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
# Collection of command types for controlling various
|
2
|
-
#
|
1
|
+
# Collection of command types for controlling various functions on the EMU
|
2
|
+
# device. These should be constructed and passed as arguments to the API
|
3
|
+
# object's issue_command method.
|
3
4
|
|
4
5
|
class EmuPower::Commands
|
5
6
|
|
@@ -32,49 +33,108 @@ class EmuPower::Commands
|
|
32
33
|
|
33
34
|
end
|
34
35
|
|
35
|
-
class
|
36
|
+
# Helper class for defining basic commands easily. Uses the current class
|
37
|
+
# name to define the Command Name element of the output XML.
|
38
|
+
class BasicCommand < Command
|
36
39
|
def initialize
|
37
|
-
|
40
|
+
|
41
|
+
class_name = self.class.name.split('::').last
|
42
|
+
command_name = class_name
|
43
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
44
|
+
.gsub(/([a-z\d])([A-Z])/,'\1_\2')
|
45
|
+
.tr("-", "_")
|
46
|
+
.downcase
|
47
|
+
super(command_name)
|
48
|
+
|
38
49
|
end
|
39
50
|
end
|
40
51
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
52
|
+
|
53
|
+
# Restart the EMU device.
|
54
|
+
class Restart < BasicCommand
|
45
55
|
end
|
46
56
|
|
47
|
-
|
48
|
-
|
49
|
-
super('get_instantaneous_demand')
|
50
|
-
end
|
57
|
+
# Get the current time. Triggers a TimeCluster notification.
|
58
|
+
class GetTime < BasicCommand
|
51
59
|
end
|
52
60
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
end
|
61
|
+
# Get current messages from the device. Triggers a MessageCluster
|
62
|
+
# notification for each message.
|
63
|
+
class GetMessage < BasicCommand
|
57
64
|
end
|
58
65
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
end
|
66
|
+
# Request information about the meter network. Triggers a
|
67
|
+
# NetworkInfo notification.
|
68
|
+
class GetNetworkInfo < BasicCommand
|
63
69
|
end
|
64
70
|
|
65
|
-
#
|
71
|
+
# Request information about the connection between the EMU-2
|
72
|
+
# and the meter. Triggers a ConnectionStatus notification.
|
73
|
+
class GetConnectionStatus < BasicCommand
|
74
|
+
end
|
66
75
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
end
|
76
|
+
# Request a list of all connected meters. This triggers one
|
77
|
+
# MeterList notification for each connected meter.
|
78
|
+
class GetMeterList < BasicCommand
|
71
79
|
end
|
72
80
|
|
73
|
-
#
|
81
|
+
# Get detailed info on a specific meter. If more than one
|
82
|
+
# meter is connected, a MAC must be passed to identify
|
83
|
+
# the target. Triggers a MeterInfo notification.
|
84
|
+
# TODO: Allow passing meter MAC argument
|
85
|
+
class GetMeterInfo < BasicCommand
|
86
|
+
end
|
87
|
+
|
88
|
+
# Request information about the EMU-2 device. Triggers a
|
89
|
+
# DeviceInfo notification.
|
90
|
+
class GetDeviceInfo < BasicCommand
|
91
|
+
end
|
74
92
|
|
75
|
-
#
|
76
|
-
#
|
77
|
-
|
93
|
+
# Get the current fast poll period. Triggers a FastPollStatus
|
94
|
+
# notification.
|
95
|
+
class GetFastPollStatus < BasicCommand
|
96
|
+
end
|
97
|
+
|
98
|
+
# Get the running total since the last CloseCurrentPeriod
|
99
|
+
# command was issued. Triggers a CurrentPeriodUsage notify
|
100
|
+
class GetCurrentPeriodUsage < BasicCommand
|
101
|
+
end
|
102
|
+
|
103
|
+
# Close out the current billing period. Does not trigger
|
104
|
+
# any notifications.
|
105
|
+
class CloseCurrentPeriod < BasicCommand
|
106
|
+
end
|
107
|
+
|
108
|
+
# Get the previous billing period's usage. Triggers a TODO
|
109
|
+
class GetLastPeriodUsage < BasicCommand
|
110
|
+
end
|
111
|
+
|
112
|
+
# Get the current power draw in kilowatts. Triggers an
|
113
|
+
# InstantaneousDemand notification.
|
114
|
+
class GetInstantaneousDemand < BasicCommand
|
115
|
+
end
|
116
|
+
|
117
|
+
# Get the current meter reading. This is independent of the
|
118
|
+
# current period usage.
|
119
|
+
class GetCurrentSummationDelivered < BasicCommand
|
120
|
+
end
|
121
|
+
|
122
|
+
# Get the current electricity rate. This is either provided
|
123
|
+
# by the meter, or set manually on the device during setup.
|
124
|
+
# This triggers a PriceCluster notification.
|
125
|
+
class GetCurrentPrice < BasicCommand
|
126
|
+
end
|
127
|
+
|
128
|
+
# Get the current block prices. This triggers a BlockPriceDetail
|
129
|
+
# notification, and is only applicable to block-based billing
|
130
|
+
# schemes.
|
131
|
+
class GetPriceBlocks < BasicCommand
|
132
|
+
end
|
133
|
+
|
134
|
+
# Set the notification schedule on the EMU. Note: this only seems to be effective shortly after the
|
135
|
+
# unit starts up, while the modes of the schedule are all 'default'. After that, the meter seems to
|
136
|
+
# push a schedule configuration and set the mode to 'rest', which overwrites the existing schedule
|
137
|
+
# and ignores subsequent SetSchedule commands.
|
78
138
|
class SetSchedule < Command
|
79
139
|
|
80
140
|
EVENTS = %w[time message price summation demand scheduled_prices profile_data billing_period block_period]
|
@@ -89,7 +149,8 @@ class EmuPower::Commands
|
|
89
149
|
|
90
150
|
end
|
91
151
|
|
92
|
-
#
|
152
|
+
# Get the current event schedule. This triggers one ScheduleInfo notification for
|
153
|
+
# each of the listed event types.
|
93
154
|
class GetSchedule < Command
|
94
155
|
|
95
156
|
EVENTS = %w[time message price summation demand scheduled_prices profile_data billing_period block_period]
|
@@ -104,8 +165,7 @@ class EmuPower::Commands
|
|
104
165
|
end
|
105
166
|
|
106
167
|
end
|
107
|
-
end
|
108
168
|
|
109
|
-
|
169
|
+
end
|
110
170
|
|
111
171
|
end
|
@@ -0,0 +1,392 @@
|
|
1
|
+
# Notification types. Provides convenience calculators and
|
2
|
+
# accessors for the notifications sent by the EMU device.
|
3
|
+
|
4
|
+
require 'nori'
|
5
|
+
|
6
|
+
class EmuPower::Notifications
|
7
|
+
|
8
|
+
# Base class for notifications
|
9
|
+
class Notification
|
10
|
+
|
11
|
+
# Timestamp of Jan 1st 2000. Used to shift epoch to standard timestamp.
|
12
|
+
UNIX_TIME_OFFSET = 946684800
|
13
|
+
|
14
|
+
attr_accessor :raw
|
15
|
+
attr_accessor :device_mac
|
16
|
+
attr_accessor :meter_mac
|
17
|
+
attr_accessor :timestamp
|
18
|
+
|
19
|
+
def initialize(hash)
|
20
|
+
|
21
|
+
@raw = hash
|
22
|
+
|
23
|
+
# All messages may contain this metadata
|
24
|
+
@device_mac = @raw['DeviceMacId']
|
25
|
+
@meter_mac = @raw['MeterMacId']
|
26
|
+
|
27
|
+
# The EMU sets timestamps relative to Jan 1st 2000 UTC. We convert
|
28
|
+
# these into more standard Unix epoch timestamps by adding the
|
29
|
+
# appropriate offset.
|
30
|
+
@timestamp = parse_timestamp('TimeStamp')
|
31
|
+
|
32
|
+
# Build out type-specific fields
|
33
|
+
build(hash)
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
def build(hash)
|
38
|
+
# Overridden by subclasses
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_timestamp(prop)
|
42
|
+
v = @raw[prop]
|
43
|
+
return nil if v == nil
|
44
|
+
return Integer(v) + 946684800
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_hex(prop)
|
48
|
+
v = @raw[prop]
|
49
|
+
return nil if v.nil?
|
50
|
+
return Integer(v)
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_bool(prop)
|
54
|
+
v = @raw[prop]
|
55
|
+
return nil if v.nil?
|
56
|
+
return (@raw[prop] == 'Y') ? true : false
|
57
|
+
end
|
58
|
+
|
59
|
+
# Calculate real total from divisors and multipliers
|
60
|
+
def parse_amount(prop, mul_prop = 'Multiplier', div_prop = 'Divisor')
|
61
|
+
|
62
|
+
multiplier = parse_hex(mul_prop)
|
63
|
+
divisor = parse_hex(div_prop)
|
64
|
+
v = parse_hex(prop)
|
65
|
+
|
66
|
+
return 0.0 if v.nil? || multiplier.nil? || divisor.nil?
|
67
|
+
return multiplier * v / Float(divisor)
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_s
|
72
|
+
"#{self.class.root_name} Notification: #{@raw.to_s}"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Name of the XML root object corresponding to this type
|
76
|
+
def self.root_name
|
77
|
+
return self.name.split('::').last
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.subclasses
|
81
|
+
return ObjectSpace.each_object(::Class).select do |k|
|
82
|
+
k < self
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
# Dispatch to the appropriate container class based
|
89
|
+
# on the type. Expects a data hash. Returns nil on
|
90
|
+
# bad message.
|
91
|
+
def self.construct(xml)
|
92
|
+
|
93
|
+
hash = Nori.new.parse(xml)
|
94
|
+
|
95
|
+
# Extract the root of the hash and dispatch to the appropriate
|
96
|
+
# container class.
|
97
|
+
type, data = hash.first
|
98
|
+
|
99
|
+
return nil unless notify_roots.include?(type)
|
100
|
+
|
101
|
+
klass = self.const_get(type)
|
102
|
+
return klass.new(data)
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
# Helper to get the element names of all types
|
107
|
+
def self.notify_roots
|
108
|
+
return Notification.subclasses.map(&:root_name)
|
109
|
+
end
|
110
|
+
|
111
|
+
###
|
112
|
+
# Begin notification objects
|
113
|
+
###
|
114
|
+
|
115
|
+
class TimeCluster < Notification
|
116
|
+
|
117
|
+
attr_accessor :utc_time
|
118
|
+
attr_accessor :local_time
|
119
|
+
|
120
|
+
def build(hash)
|
121
|
+
self.utc_time = parse_timestamp('UTCTime')
|
122
|
+
self.local_time = parse_timestamp('LocalTime')
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
class MessageCluster < Notification
|
128
|
+
|
129
|
+
attr_accessor :id
|
130
|
+
attr_accessor :text
|
131
|
+
attr_accessor :priority
|
132
|
+
attr_accessor :start_time
|
133
|
+
attr_accessor :duration
|
134
|
+
attr_accessor :confirmation_required
|
135
|
+
attr_accessor :confirmed
|
136
|
+
attr_accessor :queue
|
137
|
+
|
138
|
+
def build(hash)
|
139
|
+
self.id = hash['Id']
|
140
|
+
self.text = hash['Text']
|
141
|
+
self.priority = hash['Priority']
|
142
|
+
self.start_time = parse_timestamp('StartTime')
|
143
|
+
self.duration = parse_hex('Duration')
|
144
|
+
self.confirmation_required = parse_bool('ConfirmationRequired')
|
145
|
+
self.confirmed = parse_bool('Confirmed')
|
146
|
+
self.queue = hash['Queue']
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
class NetworkInfo < Notification
|
152
|
+
|
153
|
+
attr_accessor :coordinator_mac
|
154
|
+
attr_accessor :status
|
155
|
+
attr_accessor :description
|
156
|
+
attr_accessor :pan_id
|
157
|
+
attr_accessor :channel
|
158
|
+
attr_accessor :short_address
|
159
|
+
attr_accessor :link_strength
|
160
|
+
|
161
|
+
def build(hash)
|
162
|
+
self.coordinator_mac = parse_hex('CoordMacId')
|
163
|
+
self.status = hash['Status']
|
164
|
+
self.description = hash['Description']
|
165
|
+
self.pan_id = hash['ExtPanId']
|
166
|
+
self.channel = hash['Channel']
|
167
|
+
self.short_address = hash['ShortAddr']
|
168
|
+
self.link_strength = parse_hex('LinkStrength')
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
class ConnectionStatus < Notification
|
174
|
+
|
175
|
+
attr_accessor :status
|
176
|
+
attr_accessor :description
|
177
|
+
attr_accessor :pan_id
|
178
|
+
attr_accessor :channel
|
179
|
+
attr_accessor :short_address
|
180
|
+
attr_accessor :link_strength
|
181
|
+
|
182
|
+
def build(hash)
|
183
|
+
self.status = hash['Status']
|
184
|
+
self.description = hash['Description']
|
185
|
+
self.pan_id = hash['ExtPanId']
|
186
|
+
self.channel = hash['Channel']
|
187
|
+
self.short_address = hash['ShortAddr']
|
188
|
+
self.link_strength = parse_hex('LinkStrength')
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
# Note: This has no fields except DeviceMacId and MeterMacId
|
194
|
+
class MeterList < Notification
|
195
|
+
end
|
196
|
+
|
197
|
+
class MeterInfo < Notification
|
198
|
+
|
199
|
+
attr_accessor :type
|
200
|
+
attr_accessor :nickname
|
201
|
+
attr_accessor :account
|
202
|
+
attr_accessor :auth
|
203
|
+
attr_accessor :host
|
204
|
+
attr_accessor :enabled
|
205
|
+
|
206
|
+
def build(hash)
|
207
|
+
self.type = parse_hex('Type')
|
208
|
+
self.nickname = hash['Nickname']
|
209
|
+
self.account = hash['Account']
|
210
|
+
self.auth = hash['Auth']
|
211
|
+
self.host = hash['Host']
|
212
|
+
self.enabled = parse_bool('Enabled')
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
class DeviceInfo < Notification
|
218
|
+
|
219
|
+
attr_accessor :install_code
|
220
|
+
attr_accessor :link_key
|
221
|
+
attr_accessor :firmware_version
|
222
|
+
attr_accessor :hardware_version
|
223
|
+
attr_accessor :image_type
|
224
|
+
attr_accessor :manufacturer
|
225
|
+
attr_accessor :model
|
226
|
+
attr_accessor :date_code
|
227
|
+
|
228
|
+
def build(hash)
|
229
|
+
self.install_code = hash['InstallCode']
|
230
|
+
self.link_key = hash['LinkKey']
|
231
|
+
self.firmware_version = hash['FWVersion']
|
232
|
+
self.hardware_version = hash['HWVersion']
|
233
|
+
self.image_type = hash['ImageType']
|
234
|
+
self.manufacturer = hash['Manufacturer']
|
235
|
+
self.model = hash['ModelId']
|
236
|
+
self.date_code = hash['DateCode']
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
class FastPollStatus < Notification
|
242
|
+
|
243
|
+
attr_accessor :frequency
|
244
|
+
attr_accessor :end_time
|
245
|
+
|
246
|
+
def build(hash)
|
247
|
+
self.frequency = parse_hex('Frequency')
|
248
|
+
self.end_time = parse_timestamp('EndTime')
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
252
|
+
|
253
|
+
class CurrentPeriodUsage < Notification
|
254
|
+
|
255
|
+
attr_accessor :usage
|
256
|
+
attr_accessor :digits_right
|
257
|
+
attr_accessor :digits_left
|
258
|
+
attr_accessor :suppress_leading_zeroes
|
259
|
+
attr_accessor :start_date
|
260
|
+
|
261
|
+
def build(hash)
|
262
|
+
self.usage = parse_amount('CurrentUsage')
|
263
|
+
self.digits_right = parse_hex('DigitsRight')
|
264
|
+
self.digits_left = parse_hex('DigitsLeft')
|
265
|
+
self.suppress_leading_zeroes = parse_bool('SuppressLeadingZero')
|
266
|
+
self.start_date = parse_timestamp('StartDate')
|
267
|
+
|
268
|
+
end
|
269
|
+
|
270
|
+
end
|
271
|
+
|
272
|
+
class LastPeriodUsage < Notification
|
273
|
+
|
274
|
+
attr_accessor :usage
|
275
|
+
attr_accessor :digits_right
|
276
|
+
attr_accessor :digits_left
|
277
|
+
attr_accessor :suppress_leading_zeroes
|
278
|
+
attr_accessor :start_date
|
279
|
+
attr_accessor :end_date
|
280
|
+
|
281
|
+
def build(hash)
|
282
|
+
self.usage = parse_amount('LastUsage')
|
283
|
+
self.digits_right = parse_hex('DigitsRight')
|
284
|
+
self.digits_left = parse_hex('DigitsLeft')
|
285
|
+
self.suppress_leading_zeroes = parse_bool('SuppressLeadingZero')
|
286
|
+
self.start_date = parse_timestamp('StartDate')
|
287
|
+
self.end_date = parse_timestamp('EndDate')
|
288
|
+
end
|
289
|
+
|
290
|
+
end
|
291
|
+
|
292
|
+
class InstantaneousDemand < Notification
|
293
|
+
|
294
|
+
attr_accessor :demand
|
295
|
+
attr_accessor :digits_right
|
296
|
+
attr_accessor :digits_left
|
297
|
+
attr_accessor :suppress_leading_zeroes
|
298
|
+
|
299
|
+
def build(hash)
|
300
|
+
self.demand = parse_amount('Demand')
|
301
|
+
self.digits_right = parse_hex('DigitsRight')
|
302
|
+
self.digits_left = parse_hex('DigitsLeft')
|
303
|
+
self.suppress_leading_zeroes = parse_bool('SuppressLeadingZero')
|
304
|
+
end
|
305
|
+
|
306
|
+
end
|
307
|
+
|
308
|
+
class CurrentSummationDelivered < Notification
|
309
|
+
|
310
|
+
attr_accessor :delivered
|
311
|
+
attr_accessor :received
|
312
|
+
attr_accessor :digits_right
|
313
|
+
attr_accessor :digits_left
|
314
|
+
attr_accessor :suppress_leading_zeroes
|
315
|
+
|
316
|
+
def build(hash)
|
317
|
+
self.delivered = parse_amount('SummationDelivered')
|
318
|
+
self.received = parse_amount('SummationReceived')
|
319
|
+
self.digits_right = parse_hex('DigitsRight')
|
320
|
+
self.digits_left = parse_hex('DigitsLeft')
|
321
|
+
self.suppress_leading_zeroes = parse_bool('SuppressLeadingZero')
|
322
|
+
end
|
323
|
+
|
324
|
+
end
|
325
|
+
|
326
|
+
class PriceCluster < Notification
|
327
|
+
|
328
|
+
attr_accessor :price
|
329
|
+
attr_accessor :currency_code # This is an ISO 3-digit currency code. 840 is USD
|
330
|
+
attr_accessor :trailing_digits
|
331
|
+
attr_accessor :tier
|
332
|
+
attr_accessor :start_time
|
333
|
+
attr_accessor :duration
|
334
|
+
attr_accessor :label
|
335
|
+
|
336
|
+
def build(hash)
|
337
|
+
self.price = parse_hex('Price')
|
338
|
+
self.currency_code = parse_hex('Currency')
|
339
|
+
self.trailing_digits = parse_hex('TrailingDigits')
|
340
|
+
self.tier = parse_hex('Tier')
|
341
|
+
self.start_time = parse_timestamp('StartTime')
|
342
|
+
self.duration = parse_hex('Duration')
|
343
|
+
self.label = hash['RateLabel']
|
344
|
+
end
|
345
|
+
|
346
|
+
end
|
347
|
+
|
348
|
+
class BlockPriceDetail < Notification
|
349
|
+
|
350
|
+
attr_accessor :current_start
|
351
|
+
attr_accessor :current_duration
|
352
|
+
attr_accessor :block_consumption
|
353
|
+
attr_accessor :number_of_blocks
|
354
|
+
attr_accessor :currency_code
|
355
|
+
attr_accessor :trailing_digits
|
356
|
+
|
357
|
+
def build(hash)
|
358
|
+
self.current_start = parse_timestamp('CurrentStart')
|
359
|
+
self.current_duration = parse_hex('CurrentDuration')
|
360
|
+
self.block_consumption = parse_amount(
|
361
|
+
'BlockPeriodConsumption',
|
362
|
+
'BlockPeriodConsumptionMultiplier',
|
363
|
+
'BlockPeriodConsumptionDivisor'
|
364
|
+
)
|
365
|
+
|
366
|
+
# Note: Not sure if multiplier/divisor are supposed to tie in here
|
367
|
+
self.number_of_blocks = parse_amount('NumberOfBlocks')
|
368
|
+
|
369
|
+
self.currency_code = parse_hex('Currency')
|
370
|
+
self.trailing_digits = parse_hex('TrailingDigits')
|
371
|
+
|
372
|
+
end
|
373
|
+
|
374
|
+
end
|
375
|
+
|
376
|
+
class ScheduleInfo < Notification
|
377
|
+
|
378
|
+
attr_accessor :mode
|
379
|
+
attr_accessor :event
|
380
|
+
attr_accessor :frequency
|
381
|
+
attr_accessor :enabled
|
382
|
+
|
383
|
+
def build(hash)
|
384
|
+
self.mode = hash['Mode']
|
385
|
+
self.event = hash['Event']
|
386
|
+
self.frequency = parse_hex('Frequency')
|
387
|
+
self.enabled = parse_bool('Enabled')
|
388
|
+
end
|
389
|
+
|
390
|
+
end
|
391
|
+
|
392
|
+
end
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: emu_power
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '1.
|
4
|
+
version: '1.3'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Steven Bertolucci
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-02-
|
11
|
+
date: 2021-02-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: nori
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '2.6'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '2.6'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: serialport
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -47,8 +47,7 @@ files:
|
|
47
47
|
- lib/emu_power.rb
|
48
48
|
- lib/emu_power/api.rb
|
49
49
|
- lib/emu_power/commands.rb
|
50
|
-
- lib/emu_power/
|
51
|
-
- lib/emu_power/types.rb
|
50
|
+
- lib/emu_power/notifications.rb
|
52
51
|
- readme.md
|
53
52
|
homepage: https://github.com/Steve0320/EmuPower
|
54
53
|
licenses:
|
@@ -1,118 +0,0 @@
|
|
1
|
-
# SAX parser implementation for processing a stream of
|
2
|
-
# XML fragments.
|
3
|
-
|
4
|
-
require 'nokogiri'
|
5
|
-
|
6
|
-
class EmuPower::StreamParser < Nokogiri::XML::SAX::Document
|
7
|
-
|
8
|
-
FAKEROOT = 'FAKEROOT'
|
9
|
-
|
10
|
-
def initialize(io, line_terminator, roots, &block)
|
11
|
-
|
12
|
-
@line_terminator = line_terminator
|
13
|
-
@io = io
|
14
|
-
|
15
|
-
# Use a push parser so we can fake a single root element
|
16
|
-
@parser = Nokogiri::XML::SAX::PushParser.new(Parser.new(FAKEROOT, roots, &block))
|
17
|
-
|
18
|
-
# This is the "root" of the document. We intentionally never close this
|
19
|
-
# so that the parser doesn't get mad when it encounters multiple real
|
20
|
-
# root elements.
|
21
|
-
@parser << "<#{FAKEROOT}>"
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
# Push all new lines from the io into the parser. The parser
|
26
|
-
# will fire the callback given on construction once a whole
|
27
|
-
# object has been processed.
|
28
|
-
def parse
|
29
|
-
lines = @io.readlines(@line_terminator)
|
30
|
-
lines.each { |l| @parser << l }
|
31
|
-
end
|
32
|
-
|
33
|
-
# Nokogiri parser definition. Processes a flat XML
|
34
|
-
# stream with multiple roots.
|
35
|
-
class Parser < Nokogiri::XML::SAX::Document
|
36
|
-
|
37
|
-
# Initialize the set of root tags to consider
|
38
|
-
def initialize(fakeroot, roots, &block)
|
39
|
-
|
40
|
-
@roots = roots
|
41
|
-
|
42
|
-
@current_object = nil
|
43
|
-
@current_property = nil
|
44
|
-
@current_root = nil
|
45
|
-
|
46
|
-
@callback = block
|
47
|
-
|
48
|
-
# All element parsers ignore this tag. This is only
|
49
|
-
# used to persuade Nokogiri to parse multiple roots
|
50
|
-
# in a single stream without getting mad.
|
51
|
-
@fakeroot = fakeroot
|
52
|
-
|
53
|
-
end
|
54
|
-
|
55
|
-
# For each tag, initialize a root element if we don't already have
|
56
|
-
# one. Otherwise, consider it a property of the current element.
|
57
|
-
def start_element(name, attrs = [])
|
58
|
-
|
59
|
-
return if name == @fakeroot
|
60
|
-
return if @current_object == nil && !@roots.include?(name)
|
61
|
-
|
62
|
-
if @current_object == nil
|
63
|
-
@current_root = name
|
64
|
-
@current_object = { "MessageType" => name }
|
65
|
-
else
|
66
|
-
@current_property = name
|
67
|
-
end
|
68
|
-
|
69
|
-
end
|
70
|
-
|
71
|
-
# Populate the content of the current element
|
72
|
-
def characters(str)
|
73
|
-
if @current_object != nil && @current_property != nil
|
74
|
-
|
75
|
-
#cur = @current_object[@current_property]
|
76
|
-
#return if cur == str
|
77
|
-
|
78
|
-
# Wrap into array if we already have a value (XML permits duplicates)
|
79
|
-
#cur = [cur] unless cur == nil
|
80
|
-
|
81
|
-
#if cur.kind_of?(Array)
|
82
|
-
# cur << str
|
83
|
-
#else
|
84
|
-
# cur = str
|
85
|
-
#end
|
86
|
-
|
87
|
-
#@current_object[@current_property] = cur
|
88
|
-
|
89
|
-
@current_object[@current_property] = str
|
90
|
-
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
# Close out the current tag and clear context
|
95
|
-
def end_element(name, attrs = [])
|
96
|
-
|
97
|
-
return if name == @fakeroot
|
98
|
-
|
99
|
-
if @current_root == name
|
100
|
-
|
101
|
-
if @callback != nil
|
102
|
-
@callback.call(@current_object)
|
103
|
-
else
|
104
|
-
puts "DEBUG: #{@current_object}"
|
105
|
-
end
|
106
|
-
|
107
|
-
@current_object = nil
|
108
|
-
@current_root = nil
|
109
|
-
|
110
|
-
elsif @current_object != nil
|
111
|
-
@current_property = nil
|
112
|
-
end
|
113
|
-
|
114
|
-
end
|
115
|
-
|
116
|
-
end
|
117
|
-
|
118
|
-
end
|
data/lib/emu_power/types.rb
DELETED
@@ -1,212 +0,0 @@
|
|
1
|
-
# Notification types. Provides convenience calculators and
|
2
|
-
# accessors for the notifications sent by the EMU device.
|
3
|
-
class EmuPower::Types
|
4
|
-
|
5
|
-
# Base class for notifications
|
6
|
-
class Notification
|
7
|
-
|
8
|
-
UNIX_TIME_OFFSET = 946684800
|
9
|
-
|
10
|
-
attr_accessor :raw
|
11
|
-
attr_accessor :device_mac
|
12
|
-
attr_accessor :meter_mac
|
13
|
-
attr_accessor :timestamp
|
14
|
-
|
15
|
-
def initialize(hash)
|
16
|
-
@raw = hash
|
17
|
-
@device_mac = @raw['DeviceMacId']
|
18
|
-
@meter_mac = @raw['MeterMacId']
|
19
|
-
build(hash)
|
20
|
-
end
|
21
|
-
|
22
|
-
def build(hash)
|
23
|
-
end
|
24
|
-
|
25
|
-
# The EMU sets timestamps relative to Jan 1st 2000 UTC. We convert
|
26
|
-
# these into more standard Unix epoch timestamps by adding the
|
27
|
-
# appropriate offset.
|
28
|
-
def timestamp
|
29
|
-
parse_timestamp('TimeStamp')
|
30
|
-
end
|
31
|
-
|
32
|
-
def parse_timestamp(prop)
|
33
|
-
v = @raw[prop]
|
34
|
-
return nil if v == nil
|
35
|
-
return Integer(v) + 946684800
|
36
|
-
end
|
37
|
-
|
38
|
-
def parse_hex(prop)
|
39
|
-
v = @raw[prop]
|
40
|
-
return nil if v.nil?
|
41
|
-
return Integer(v)
|
42
|
-
end
|
43
|
-
|
44
|
-
def parse_bool(prop)
|
45
|
-
v = @raw[prop]
|
46
|
-
return nil if v.nil?
|
47
|
-
return (@raw[prop] == 'Y') ? true : false
|
48
|
-
end
|
49
|
-
|
50
|
-
# Name of the XML root object corresponding to this type
|
51
|
-
def self.root_name
|
52
|
-
return self.name.split('::').last
|
53
|
-
end
|
54
|
-
|
55
|
-
def self.subclasses
|
56
|
-
return ObjectSpace.each_object(::Class).select do |k|
|
57
|
-
k < self
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
end
|
62
|
-
|
63
|
-
# TODO
|
64
|
-
class ConnectionStatus < Notification
|
65
|
-
end
|
66
|
-
|
67
|
-
# TODO
|
68
|
-
class DeviceInfo < Notification
|
69
|
-
end
|
70
|
-
|
71
|
-
class ScheduleInfo < Notification
|
72
|
-
|
73
|
-
attr_accessor :mode
|
74
|
-
attr_accessor :event
|
75
|
-
attr_accessor :frequency
|
76
|
-
attr_accessor :enabled
|
77
|
-
|
78
|
-
def build(hash)
|
79
|
-
self.mode = hash['Mode']
|
80
|
-
self.event = hash['Event']
|
81
|
-
self.frequency = parse_hex('Frequency')
|
82
|
-
self.enabled = parse_bool('Enabled')
|
83
|
-
end
|
84
|
-
|
85
|
-
end
|
86
|
-
|
87
|
-
# TODO
|
88
|
-
class MeterList < Notification
|
89
|
-
end
|
90
|
-
|
91
|
-
# TODO
|
92
|
-
class MeterInfo < Notification
|
93
|
-
end
|
94
|
-
|
95
|
-
# TODO
|
96
|
-
class NetworkInfo < Notification
|
97
|
-
end
|
98
|
-
|
99
|
-
class TimeCluster < Notification
|
100
|
-
|
101
|
-
attr_accessor :utc_time
|
102
|
-
attr_accessor :local_time
|
103
|
-
|
104
|
-
def build(hash)
|
105
|
-
self.utc_time = parse_timestamp('UTCTime')
|
106
|
-
self.local_time = parse_timestamp('LocalTime')
|
107
|
-
end
|
108
|
-
|
109
|
-
end
|
110
|
-
|
111
|
-
# TODO
|
112
|
-
class MessageCluster < Notification
|
113
|
-
end
|
114
|
-
|
115
|
-
# TODO
|
116
|
-
class PriceCluster < Notification
|
117
|
-
end
|
118
|
-
|
119
|
-
class InstantaneousDemand < Notification
|
120
|
-
|
121
|
-
attr_accessor :raw_demand
|
122
|
-
attr_accessor :multiplier
|
123
|
-
attr_accessor :divisor
|
124
|
-
attr_accessor :digits_right
|
125
|
-
attr_accessor :digits_left
|
126
|
-
attr_accessor :suppress_leading_zeroes
|
127
|
-
|
128
|
-
def build(hash)
|
129
|
-
self.raw_demand = parse_hex('Demand')
|
130
|
-
self.multiplier = parse_hex('Multiplier')
|
131
|
-
self.divisor = parse_hex('Divisor')
|
132
|
-
self.digits_right = parse_hex('DigitsRight')
|
133
|
-
self.digits_left = parse_hex('DigitsLeft')
|
134
|
-
self.suppress_leading_zeroes = parse_bool('SuppressLeadingZero')
|
135
|
-
end
|
136
|
-
|
137
|
-
# Return computed demand in KW. This may return nil if data is missing.
|
138
|
-
def demand
|
139
|
-
return 0 if self.divisor == 0
|
140
|
-
return nil if self.multiplier.nil? || self.raw_demand.nil? || self.divisor.nil?
|
141
|
-
return self.multiplier * self.raw_demand / Float(self.divisor)
|
142
|
-
end
|
143
|
-
|
144
|
-
end
|
145
|
-
|
146
|
-
class CurrentSummationDelivered < Notification
|
147
|
-
|
148
|
-
attr_accessor :raw_delivered
|
149
|
-
attr_accessor :raw_received
|
150
|
-
attr_accessor :multiplier
|
151
|
-
attr_accessor :divisor
|
152
|
-
attr_accessor :digits_right
|
153
|
-
attr_accessor :digits_left
|
154
|
-
attr_accessor :suppress_leading_zeroes
|
155
|
-
|
156
|
-
def build(hash)
|
157
|
-
|
158
|
-
self.raw_delivered = parse_hex('SummationDelivered')
|
159
|
-
self.raw_received = parse_hex('SummationReceived')
|
160
|
-
self.multiplier = parse_hex('Multiplier')
|
161
|
-
self.divisor = parse_hex('Divisor')
|
162
|
-
self.digits_right = parse_hex('DigitsRight')
|
163
|
-
self.digits_left = parse_hex('DigitsLeft')
|
164
|
-
self.suppress_leading_zeroes = parse_bool('SuppressLeadingZero')
|
165
|
-
|
166
|
-
end
|
167
|
-
|
168
|
-
def delivered
|
169
|
-
return 0 if self.raw_delivered == 0
|
170
|
-
return nil if self.multiplier.nil? || self.raw_delivered.nil? || self.divisor.nil?
|
171
|
-
return self.multiplier * self.raw_delivered / Float(self.divisor)
|
172
|
-
end
|
173
|
-
|
174
|
-
def received
|
175
|
-
return 0 if self.divisor == 0
|
176
|
-
return nil if self.multiplier.nil? || self.raw_received.nil? || self.divisor.nil?
|
177
|
-
return self.multiplier * self.raw_received / Float(self.divisor)
|
178
|
-
end
|
179
|
-
|
180
|
-
end
|
181
|
-
|
182
|
-
# TODO
|
183
|
-
class CurrentPeriodUsage < Notification
|
184
|
-
end
|
185
|
-
|
186
|
-
# TODO
|
187
|
-
class LastPeriodUsage < Notification
|
188
|
-
end
|
189
|
-
|
190
|
-
# TODO
|
191
|
-
class ProfileData < Notification
|
192
|
-
end
|
193
|
-
|
194
|
-
# Dispatch to the appropriate container class based
|
195
|
-
# on the type. Expects a data hash. Returns nil on
|
196
|
-
# bad message.
|
197
|
-
def self.construct(data)
|
198
|
-
|
199
|
-
type = data['MessageType']
|
200
|
-
return nil if type == nil || !notify_roots.include?(type)
|
201
|
-
|
202
|
-
klass = self.const_get(type)
|
203
|
-
return klass.new(data)
|
204
|
-
|
205
|
-
end
|
206
|
-
|
207
|
-
# Helper to get the element names of all types
|
208
|
-
def self.notify_roots
|
209
|
-
return Notification.subclasses.map(&:root_name)
|
210
|
-
end
|
211
|
-
|
212
|
-
end
|