lifx-lan 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 +17 -0
- data/.travis.yml +8 -0
- data/.yardopts +3 -0
- data/CHANGES.md +45 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +23 -0
- data/README.md +15 -0
- data/Rakefile +20 -0
- data/bin/lifx-snoop +50 -0
- data/examples/auto-off/auto-off.rb +34 -0
- data/examples/blink/blink.rb +19 -0
- data/examples/identify/identify.rb +69 -0
- data/examples/travis-build-light/build-light.rb +57 -0
- data/lib/bindata_ext/bool.rb +30 -0
- data/lib/bindata_ext/record.rb +11 -0
- data/lib/lifx-lan.rb +27 -0
- data/lib/lifx/lan/client.rb +149 -0
- data/lib/lifx/lan/color.rb +199 -0
- data/lib/lifx/lan/config.rb +17 -0
- data/lib/lifx/lan/firmware.rb +60 -0
- data/lib/lifx/lan/gateway_connection.rb +185 -0
- data/lib/lifx/lan/light.rb +440 -0
- data/lib/lifx/lan/light_collection.rb +111 -0
- data/lib/lifx/lan/light_target.rb +185 -0
- data/lib/lifx/lan/logging.rb +14 -0
- data/lib/lifx/lan/message.rb +168 -0
- data/lib/lifx/lan/network_context.rb +188 -0
- data/lib/lifx/lan/observable.rb +66 -0
- data/lib/lifx/lan/protocol/address.rb +25 -0
- data/lib/lifx/lan/protocol/device.rb +387 -0
- data/lib/lifx/lan/protocol/header.rb +24 -0
- data/lib/lifx/lan/protocol/light.rb +142 -0
- data/lib/lifx/lan/protocol/message.rb +19 -0
- data/lib/lifx/lan/protocol/metadata.rb +23 -0
- data/lib/lifx/lan/protocol/payload.rb +12 -0
- data/lib/lifx/lan/protocol/sensor.rb +31 -0
- data/lib/lifx/lan/protocol/type.rb +204 -0
- data/lib/lifx/lan/protocol/wan.rb +51 -0
- data/lib/lifx/lan/protocol/wifi.rb +102 -0
- data/lib/lifx/lan/protocol_path.rb +85 -0
- data/lib/lifx/lan/required_keyword_arguments.rb +12 -0
- data/lib/lifx/lan/routing_manager.rb +114 -0
- data/lib/lifx/lan/routing_table.rb +48 -0
- data/lib/lifx/lan/seen.rb +25 -0
- data/lib/lifx/lan/site.rb +97 -0
- data/lib/lifx/lan/tag_manager.rb +111 -0
- data/lib/lifx/lan/tag_table.rb +49 -0
- data/lib/lifx/lan/target.rb +24 -0
- data/lib/lifx/lan/thread.rb +13 -0
- data/lib/lifx/lan/timers.rb +29 -0
- data/lib/lifx/lan/transport.rb +46 -0
- data/lib/lifx/lan/transport/tcp.rb +91 -0
- data/lib/lifx/lan/transport/udp.rb +87 -0
- data/lib/lifx/lan/transport_manager.rb +43 -0
- data/lib/lifx/lan/transport_manager/lan.rb +169 -0
- data/lib/lifx/lan/utilities.rb +36 -0
- data/lib/lifx/lan/version.rb +5 -0
- data/lifx-lan.gemspec +26 -0
- data/spec/color_spec.rb +43 -0
- data/spec/gateway_connection_spec.rb +30 -0
- data/spec/integration/client_spec.rb +42 -0
- data/spec/integration/light_spec.rb +56 -0
- data/spec/integration/tags_spec.rb +42 -0
- data/spec/light_collection_spec.rb +37 -0
- data/spec/message_spec.rb +183 -0
- data/spec/protocol_path_spec.rb +109 -0
- data/spec/routing_manager_spec.rb +25 -0
- data/spec/routing_table_spec.rb +23 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/transport/udp_spec.rb +44 -0
- data/spec/transport_spec.rb +14 -0
- metadata +187 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'lifx/lan/light_target'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
# LightCollection represents a collection of {Light}s, which can either refer to
|
6
|
+
# all lights on a {NetworkContext}, or lights
|
7
|
+
class LightCollection
|
8
|
+
include LightTarget
|
9
|
+
include Enumerable
|
10
|
+
include RequiredKeywordArguments
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
class TagNotFound < ArgumentError; end
|
14
|
+
|
15
|
+
# Refers to {NetworkContext} the instance belongs to
|
16
|
+
# @return [NetworkContext]
|
17
|
+
attr_reader :context
|
18
|
+
|
19
|
+
# Tag of the collection. `nil` represents all lights
|
20
|
+
# @return [String]
|
21
|
+
attr_reader :tag
|
22
|
+
|
23
|
+
# Creates a {LightCollection} instance. Should not be used directly.
|
24
|
+
# @api private
|
25
|
+
# @param context: [NetworkContext] NetworkContext this collection belongs to
|
26
|
+
# @param tag: [String] Tag
|
27
|
+
def initialize(context: required!(:context), tag: nil)
|
28
|
+
@context = context
|
29
|
+
@tag = tag
|
30
|
+
end
|
31
|
+
|
32
|
+
# Queues a {Protocol::Payload} to be sent to bulbs in the collection
|
33
|
+
# @param payload [Protocol::Payload] Payload to be sent
|
34
|
+
# @param acknowledge: [Boolean] whether recipients should acknowledge message
|
35
|
+
# @param at_time: [Integer] Unix epoch in milliseconds to run the payload. Only applicable to certain payload types.
|
36
|
+
# @api private
|
37
|
+
# @return [LightCollection] self for chaining
|
38
|
+
def send_message(payload, acknowledge: false, at_time: nil)
|
39
|
+
if tag
|
40
|
+
context.send_message(target: Target.new(tag: tag), payload: payload, acknowledge: acknowledge, at_time: at_time)
|
41
|
+
else
|
42
|
+
context.send_message(target: Target.new(broadcast: true), payload: payload, acknowledge: acknowledge, at_time: at_time)
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a {Light} with device id matching `id`
|
48
|
+
# @param id [String] Device ID
|
49
|
+
# @return [Light]
|
50
|
+
def with_id(id)
|
51
|
+
lights.find { |l| l.id == id}
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns a {Light} with its label matching `label`
|
55
|
+
# @param label [String, Regexp] Label
|
56
|
+
# @return [Light]
|
57
|
+
def with_label(label)
|
58
|
+
if label.is_a?(Regexp)
|
59
|
+
lights.find { |l| l.label(fetch: false) =~ label }
|
60
|
+
else
|
61
|
+
lights.find { |l| l.label(fetch: false) == label }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns a {LightCollection} of {Light}s tagged with `tag`
|
66
|
+
# @param tag [String] Tag
|
67
|
+
# @return [LightCollection]
|
68
|
+
def with_tag(tag)
|
69
|
+
if context.tags.include?(tag)
|
70
|
+
self.class.new(context: context, tag: tag)
|
71
|
+
else
|
72
|
+
raise TagNotFound.new("No such tag '#{tag}'")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns an Array of {Light}s
|
77
|
+
# @return [Array<Light>]
|
78
|
+
def lights
|
79
|
+
if tag
|
80
|
+
context.all_lights.select { |l| l.tags.include?(tag) }
|
81
|
+
else
|
82
|
+
context.all_lights
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
DEFAULT_ALIVE_THRESHOLD = 30 # seconds
|
87
|
+
# Returns an Array of {Light}s considered alive
|
88
|
+
# @param threshold: The maximum number of seconds a {Light} was last seen to be considered alive
|
89
|
+
# @return [Array<Light>] Lights considered alive
|
90
|
+
def alive(threshold: DEFAULT_ALIVE_THRESHOLD)
|
91
|
+
lights.select { |l| l.seconds_since_seen <= threshold }
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns an Array of {Light}s considered stale
|
95
|
+
# @param threshold: The minimum number of seconds since a {Light} was last seen to be considered stale
|
96
|
+
# @return [Array<Light>] Lights considered stale
|
97
|
+
def stale(threshold: DEFAULT_ALIVE_THRESHOLD)
|
98
|
+
lights.select { |l| l.seconds_since_seen > threshold }
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns a nice string representation of itself
|
102
|
+
# @return [String]
|
103
|
+
def to_s
|
104
|
+
%Q{#<#{self.class.name} lights=#{lights}#{tag ? " tag=#{tag}" : ''}>}
|
105
|
+
end
|
106
|
+
alias_method :inspect, :to_s
|
107
|
+
|
108
|
+
def_delegators :lights, :empty?, :each
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module LIFX
|
2
|
+
module LAN
|
3
|
+
# LightTarget is a module that contains Light commands that can work
|
4
|
+
# with either a single {Light} or multiple Lights via a {LightCollection}
|
5
|
+
module LightTarget
|
6
|
+
MSEC_PER_SEC = 1000
|
7
|
+
|
8
|
+
# Attempts to set the color of the light(s) to `color` asynchronously.
|
9
|
+
# This method cannot guarantee that the message was received.
|
10
|
+
# @param color [Color] The color to be set
|
11
|
+
# @param duration: [Numeric] Transition time in seconds
|
12
|
+
# @return [Light, LightCollection] self for chaining
|
13
|
+
def set_color(color, duration: LIFX::Config.default_duration)
|
14
|
+
send_message(Protocol::Light::SetColor.new(
|
15
|
+
color: color.to_hsbk,
|
16
|
+
duration: (duration * MSEC_PER_SEC).to_i,
|
17
|
+
stream: 0,
|
18
|
+
))
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
# Attempts to apply a waveform to the light(s) asynchronously.
|
23
|
+
# @note Don't use this directly.
|
24
|
+
# @api private
|
25
|
+
def set_waveform(color, waveform: required!(:waveform),
|
26
|
+
cycles: required!(:cycles),
|
27
|
+
stream: 0,
|
28
|
+
transient: true,
|
29
|
+
period: 1.0,
|
30
|
+
skew_ratio: 0.5,
|
31
|
+
acknowledge: false)
|
32
|
+
send_message(Protocol::Light::SetWaveform.new(
|
33
|
+
color: color.to_hsbk,
|
34
|
+
waveform: waveform,
|
35
|
+
cycles: cycles,
|
36
|
+
stream: stream,
|
37
|
+
transient: transient,
|
38
|
+
period: (period * 1_000).to_i,
|
39
|
+
skew_ratio: (skew_ratio * 65535).round - 32768,
|
40
|
+
), acknowledge: acknowledge)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Attempts to make the light(s) pulse `color` and then back to its original color. Asynchronous.
|
44
|
+
# @param color [Color] Color to pulse
|
45
|
+
# @param duty_cycle: [Float] Ratio of a cycle the light(s) is set to `color`
|
46
|
+
# @param cycles: [Integer] Number of cycles
|
47
|
+
# @param transient: [Boolean] If false, the light will remain at the color the waveform is at when it ends
|
48
|
+
# @param period: [Integer] Number of seconds a cycle. Must be above 1.0 (?)
|
49
|
+
# @param stream: [Integer] Unused
|
50
|
+
def pulse(color, cycles: 1,
|
51
|
+
duty_cycle: 0.5,
|
52
|
+
transient: true,
|
53
|
+
period: 1.0,
|
54
|
+
stream: 0)
|
55
|
+
set_waveform(color, waveform: Protocol::Light::Waveform::PULSE,
|
56
|
+
cycles: cycles,
|
57
|
+
skew_ratio: 1 - duty_cycle,
|
58
|
+
stream: stream,
|
59
|
+
transient: transient,
|
60
|
+
period: period)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Attempts to make the light(s) transition to `color` and back in a smooth sine wave. Asynchronous.
|
64
|
+
# @param color [Color] Color
|
65
|
+
# @param cycles: [Integer] Number of cycles
|
66
|
+
# @param peak: [Float] Defines the peak point of the wave. Defaults to 0.5 which is a standard sine
|
67
|
+
# @param transient: [Boolean] If false, the light will remain at the color the waveform is at when it ends
|
68
|
+
# @param period: [Integer] Number of seconds a cycle. Must be above 1.0 (?)
|
69
|
+
# @param stream: [Integer] Unused
|
70
|
+
def sine(color, cycles: 1,
|
71
|
+
period: 1.0,
|
72
|
+
peak: 0.5,
|
73
|
+
transient: true,
|
74
|
+
stream: 0)
|
75
|
+
set_waveform(color, waveform: Protocol::Light::Waveform::SINE,
|
76
|
+
cycles: cycles,
|
77
|
+
skew_ratio: peak,
|
78
|
+
stream: stream,
|
79
|
+
transient: transient,
|
80
|
+
period: period)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Attempts to make the light(s) transition to `color` smoothly, then immediately back to its original color. Asynchronous.
|
84
|
+
# @param color [Color] Color
|
85
|
+
# @param cycles: [Integer] Number of cycles
|
86
|
+
# @param transient: [Boolean] If false, the light will remain at the color the waveform is at when it ends
|
87
|
+
# @param period: [Integer] Number of seconds a cycle. Must be above 1.0 (?)
|
88
|
+
# @param stream: [Integer] Unused
|
89
|
+
def half_sine(color, cycles: 1,
|
90
|
+
period: 1.0,
|
91
|
+
transient: true,
|
92
|
+
stream: 0)
|
93
|
+
set_waveform(color, waveform: Protocol::Light::Waveform::HALF_SINE,
|
94
|
+
cycles: cycles,
|
95
|
+
stream: stream,
|
96
|
+
transient: transient,
|
97
|
+
period: period)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Attempts to make the light(s) transition to `color` linearly and back. Asynchronous.
|
101
|
+
# @param color [Color] Color to pulse
|
102
|
+
# @param cycles: [Integer] Number of cycles
|
103
|
+
# @param transient: [Boolean] If false, the light will remain at the color the waveform is at when it ends
|
104
|
+
# @param period: [Integer] Number of seconds a cycle. Must be above 1.0 (?)
|
105
|
+
# @param stream: [Integer] Unused
|
106
|
+
def triangle(color, cycles: 1,
|
107
|
+
period: 1.0,
|
108
|
+
peak: 0.5,
|
109
|
+
transient: true,
|
110
|
+
stream: 0)
|
111
|
+
set_waveform(color, waveform: Protocol::Light::Waveform::TRIANGLE,
|
112
|
+
cycles: cycles,
|
113
|
+
skew_ratio: peak,
|
114
|
+
stream: stream,
|
115
|
+
transient: transient,
|
116
|
+
period: period)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Attempts to make the light(s) transition to `color` linearly, then instantly back. Asynchronous.
|
120
|
+
# @param color [Color] Color to saw wave
|
121
|
+
# @param cycles: [Integer] Number of cycles
|
122
|
+
# @param transient: [Boolean] If false, the light will remain at the color the waveform is at when it ends
|
123
|
+
# @param period: [Integer] Number of seconds a cycle. Must be above 1.0 (?)
|
124
|
+
# @param stream: [Integer] Unused
|
125
|
+
def saw(color, cycles: 1,
|
126
|
+
period: 1.0,
|
127
|
+
transient: true,
|
128
|
+
stream: 0)
|
129
|
+
set_waveform(color, waveform: Protocol::Light::Waveform::SAW,
|
130
|
+
cycles: cycles,
|
131
|
+
stream: stream,
|
132
|
+
transient: transient,
|
133
|
+
period: period)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Attempts to set the power state to `state` asynchronously.
|
137
|
+
# This method cannot guarantee the message was received.
|
138
|
+
# @param state [:on, :off]
|
139
|
+
# @return [Light, LightCollection] self for chaining
|
140
|
+
def set_power(state)
|
141
|
+
level = case state
|
142
|
+
when :on
|
143
|
+
1
|
144
|
+
when :off
|
145
|
+
0
|
146
|
+
else
|
147
|
+
raise ArgumentError.new("Must pass in either :on or :off")
|
148
|
+
end
|
149
|
+
send_message(Protocol::Device::SetPower.new(level: level))
|
150
|
+
self
|
151
|
+
end
|
152
|
+
|
153
|
+
# Attempts to turn the light(s) on asynchronously.
|
154
|
+
# This method cannot guarantee the message was received.
|
155
|
+
# @return [Light, LightCollection] self for chaining
|
156
|
+
def turn_on
|
157
|
+
set_power(:on)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Attempts to turn the light(s) off asynchronously.
|
161
|
+
# This method cannot guarantee the message was received.
|
162
|
+
# @return [Light, LightCollection] self for chaining
|
163
|
+
def turn_off
|
164
|
+
set_power(:off)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Requests light(s) to report their state
|
168
|
+
# This method cannot guarantee the message was received.
|
169
|
+
# @return [Light, LightCollection] self for chaining
|
170
|
+
def refresh
|
171
|
+
send_message(Protocol::Light::Get.new)
|
172
|
+
self
|
173
|
+
end
|
174
|
+
|
175
|
+
NSEC_IN_SEC = 1_000_000_000
|
176
|
+
# Attempts to set the device time on the targets
|
177
|
+
# @api private
|
178
|
+
# @param time [Time] The time to set
|
179
|
+
# @return [void]
|
180
|
+
def set_time(time = Time.now)
|
181
|
+
send_message(Protocol::Device::SetTime.new(time: (time.to_f * NSEC_IN_SEC).round))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'lifx/lan/protocol_path'
|
3
|
+
|
4
|
+
module LIFX
|
5
|
+
module LAN
|
6
|
+
# @api private
|
7
|
+
class Message
|
8
|
+
include Logging
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
class MessageError < StandardError; end
|
12
|
+
class UnpackError < MessageError; end
|
13
|
+
class PackError < MessageError; end
|
14
|
+
|
15
|
+
class InvalidFrame < UnpackError; end
|
16
|
+
class UnsupportedProtocolVersion < UnpackError; end
|
17
|
+
class NotAddressableFrame < UnpackError; end
|
18
|
+
class NoPayload < PackError; end
|
19
|
+
class UnmappedPayload < MessageError; end
|
20
|
+
class InvalidFields < PackError; end
|
21
|
+
|
22
|
+
PROTOCOL_VERSION = 1024
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def unpack(data)
|
26
|
+
raise InvalidFrame if data.length < 2
|
27
|
+
|
28
|
+
header = Protocol::Header.read(data)
|
29
|
+
raise UnsupportedProtocolVersion.new("Expected #{PROTOCOL_VERSION} but got #{header.protocol} instead") if header.protocol != PROTOCOL_VERSION
|
30
|
+
raise NotAddressableFrame if header.addressable == 0
|
31
|
+
|
32
|
+
message = Protocol::Message.read(data)
|
33
|
+
path = ProtocolPath.new(raw_site: message.raw_site, raw_target: message.raw_target, tagged: message.tagged)
|
34
|
+
payload_class = message_type_for_id(message.type_.snapshot)
|
35
|
+
if payload_class.nil?
|
36
|
+
if Config.log_invalid_messages
|
37
|
+
logger.error("Message.unpack: Unrecognised payload ID: #{message.type_}")
|
38
|
+
logger.error("Message.unpack: Message: #{message}")
|
39
|
+
end
|
40
|
+
return nil # FIXME
|
41
|
+
raise UnmappedPayload.new("Unrecognised payload ID: #{message.type_}")
|
42
|
+
end
|
43
|
+
begin
|
44
|
+
payload = payload_class.read(message.payload)
|
45
|
+
rescue => ex
|
46
|
+
if message.raw_site == "\x00" * 6
|
47
|
+
logger.info("Message.unpack: Ignoring malformed message from virgin bulb")
|
48
|
+
else
|
49
|
+
if Config.log_invalid_messages
|
50
|
+
logger.error("Message.unpack: Exception while unpacking payload of type #{payload_class}: #{ex}")
|
51
|
+
logger.error("Message.unpack: Data: #{data.inspect}")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
new(path, message, payload)
|
56
|
+
rescue => ex
|
57
|
+
if Config.log_invalid_messages
|
58
|
+
logger.debug("Message.unpack: Exception while unpacking #{data.inspect}")
|
59
|
+
logger.debug("Message.unpack: #{ex} - #{ex.backtrace.join("\n")}")
|
60
|
+
end
|
61
|
+
raise ex
|
62
|
+
end
|
63
|
+
|
64
|
+
def message_type_for_id(type_id)
|
65
|
+
Protocol::TYPE_ID_TO_CLASS[type_id]
|
66
|
+
end
|
67
|
+
|
68
|
+
def type_id_for_message_class(klass)
|
69
|
+
Protocol::CLASS_TO_TYPE_ID[klass]
|
70
|
+
end
|
71
|
+
|
72
|
+
def valid_fields
|
73
|
+
@valid_fields ||= Protocol::Message.new.field_names.map(&:to_sym)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
LIFX::LAN::Protocol::Message.fields.each do |field|
|
78
|
+
define_method(field.name) do
|
79
|
+
@message.send(field.name).snapshot
|
80
|
+
end
|
81
|
+
|
82
|
+
define_method("#{field.name}=") do |value|
|
83
|
+
@message.send("#{field.name}=", value)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
alias_method :tagged?, :tagged
|
88
|
+
alias_method :addressable?, :addressable
|
89
|
+
|
90
|
+
def_delegators :path, :device_id, :site_id, :tagged
|
91
|
+
|
92
|
+
attr_accessor :path, :payload
|
93
|
+
def initialize(*args)
|
94
|
+
if args.count == 3
|
95
|
+
@path, @message, @payload = args
|
96
|
+
elsif (hash = args.first).is_a?(Hash)
|
97
|
+
path = hash.delete(:path) || ProtocolPath.new
|
98
|
+
payload = hash.delete(:payload)
|
99
|
+
|
100
|
+
check_valid_fields!(hash)
|
101
|
+
|
102
|
+
@message = Protocol::Message.new(hash)
|
103
|
+
self.payload = payload
|
104
|
+
self.path = path
|
105
|
+
@message.tagged = path.tagged? if path
|
106
|
+
else
|
107
|
+
@message = Protocol::Message.new
|
108
|
+
end
|
109
|
+
@message.msg_size = @message.num_bytes
|
110
|
+
@message.protocol = PROTOCOL_VERSION
|
111
|
+
rescue => ex
|
112
|
+
raise MessageError.new("Unable to initialize message with args: #{args.inspect} - #{ex}")
|
113
|
+
end
|
114
|
+
|
115
|
+
def payload=(payload)
|
116
|
+
@payload = payload
|
117
|
+
type_id = self.class.type_id_for_message_class(payload.class)
|
118
|
+
if type_id.nil?
|
119
|
+
raise UnmappedPayload.new("Unmapped payload class #{payload.class}")
|
120
|
+
end
|
121
|
+
@message.type_ = type_id
|
122
|
+
@message.payload = payload.pack
|
123
|
+
end
|
124
|
+
|
125
|
+
def pack
|
126
|
+
raise NoPayload if !payload
|
127
|
+
@message.raw_site = path.raw_site
|
128
|
+
@message.raw_target = path.raw_target
|
129
|
+
@message.tagged = path.tagged?
|
130
|
+
@message.msg_size = @message.num_bytes
|
131
|
+
@message.pack
|
132
|
+
end
|
133
|
+
|
134
|
+
def to_s
|
135
|
+
hash = {site: path.site_id}
|
136
|
+
if path.tagged?
|
137
|
+
hash[:tags] = path.tag_ids
|
138
|
+
hash[:tags] = 'all' if hash[:tags].empty?
|
139
|
+
else
|
140
|
+
hash[:device] = path.device_id
|
141
|
+
end
|
142
|
+
hash[:type_] = payload.class.to_s.sub('LIFX::Protocol::', '')
|
143
|
+
hash[:addressable] = addressable? ? 'true' : 'false'
|
144
|
+
hash[:tagged] = path.tagged? ? 'true' : 'false'
|
145
|
+
hash[:at_time] = @message.at_time if @message.at_time && @message.at_time > 0
|
146
|
+
hash[:protocol] = protocol
|
147
|
+
hash[:payload] = payload.snapshot if payload
|
148
|
+
attrs = hash.map { |k, v| "#{k}=#{v}" }.join(' ')
|
149
|
+
%Q{#<LIFX::LAN::Message #{attrs}>}
|
150
|
+
end
|
151
|
+
|
152
|
+
def to_hex
|
153
|
+
pack.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
|
154
|
+
end
|
155
|
+
|
156
|
+
alias_method :inspect, :to_s
|
157
|
+
|
158
|
+
protected
|
159
|
+
|
160
|
+
def check_valid_fields!(hash)
|
161
|
+
invalid_fields = hash.keys - self.class.valid_fields
|
162
|
+
if invalid_fields.count > 0
|
163
|
+
raise InvalidFields.new("Invalid fields for Message: #{invalid_fields.join(', ')}")
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|