lifx-lan 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|