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
data/lib/lifx-lan.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "lifx/lan/version"
|
2
|
+
require "bindata"
|
3
|
+
require "bindata_ext/bool"
|
4
|
+
require "bindata_ext/record"
|
5
|
+
|
6
|
+
require "lifx/lan/required_keyword_arguments"
|
7
|
+
require "lifx/lan/utilities"
|
8
|
+
require "lifx/lan/logging"
|
9
|
+
|
10
|
+
require "lifx/lan/thread"
|
11
|
+
|
12
|
+
require "lifx/lan/protocol/payload"
|
13
|
+
%w(device light sensor wan wifi message).each { |f| require "lifx/lan/protocol/#{f}" }
|
14
|
+
require "lifx/lan/protocol/type"
|
15
|
+
require "lifx/lan/message"
|
16
|
+
require "lifx/lan/transport"
|
17
|
+
|
18
|
+
require "lifx/lan/config"
|
19
|
+
require "lifx/lan/client"
|
20
|
+
|
21
|
+
module LIFX
|
22
|
+
module LAN
|
23
|
+
NULL_SITE_ID = "000000000000"
|
24
|
+
|
25
|
+
class TimeoutError < StandardError; end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
require 'lifx/lan/network_context'
|
5
|
+
require 'lifx/lan/light_collection'
|
6
|
+
|
7
|
+
module LIFX
|
8
|
+
module LAN
|
9
|
+
# {LIFX::LAN::Client} is the top level interface to the library. It mainly maps
|
10
|
+
# methods to the backing {NetworkContext} instance.
|
11
|
+
class Client
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# Returns a {Client} set up for accessing devices on the LAN
|
15
|
+
#
|
16
|
+
# @return [Client] A LAN LIFX::Client
|
17
|
+
def lan
|
18
|
+
@lan ||= new(transport_manager: TransportManager::LAN.new)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
extend Forwardable
|
23
|
+
include Utilities
|
24
|
+
include RequiredKeywordArguments
|
25
|
+
|
26
|
+
# Refers to the client's network context.
|
27
|
+
# @return [NetworkContext] Enclosed network context
|
28
|
+
attr_reader :context
|
29
|
+
|
30
|
+
# @param transport_manager: [TransportManager] Specify the {TransportManager}
|
31
|
+
def initialize(transport_manager: required!('transport_manager'))
|
32
|
+
@context = NetworkContext.new(transport_manager: transport_manager)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Default timeout in seconds for discovery
|
36
|
+
DISCOVERY_DEFAULT_TIMEOUT = 10
|
37
|
+
|
38
|
+
# This method tells the {NetworkContext} to look for devices asynchronously.
|
39
|
+
# @return [Client] self
|
40
|
+
def discover
|
41
|
+
@context.discover
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop_discovery
|
45
|
+
@context.stop_discovery
|
46
|
+
end
|
47
|
+
|
48
|
+
class DiscoveryTimeout < Timeout::Error; end
|
49
|
+
# This method tells the {NetworkContext} to look for devices, and will block
|
50
|
+
# until there's at least one device.
|
51
|
+
#
|
52
|
+
# @example Wait until at least three lights have been found
|
53
|
+
# client.discover! { |c| c.lights.count >= 3 }
|
54
|
+
#
|
55
|
+
# @param timeout: [Numeric] How long to try to wait for before returning
|
56
|
+
# @param condition_interval: [Numeric] Seconds between evaluating the block
|
57
|
+
# @yield [Client] This block is evaluated every `condition_interval` seconds. If true, method returns. If no block is supplied, it will block until it finds at least one light.
|
58
|
+
# @raise [DiscoveryTimeout] If discovery times out
|
59
|
+
# @return [Client] self
|
60
|
+
def discover!(timeout: DISCOVERY_DEFAULT_TIMEOUT, condition_interval: 0.1, &block)
|
61
|
+
block ||= -> { self.lights.count > 0 }
|
62
|
+
try_until -> { block.arity == 1 ? block.call(self) : block.call },
|
63
|
+
timeout: timeout,
|
64
|
+
timeout_exception: DiscoveryTimeout,
|
65
|
+
condition_interval: condition_interval,
|
66
|
+
action_interval: 1 do
|
67
|
+
discover
|
68
|
+
refresh
|
69
|
+
end
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
# Sends a request to refresh devices and tags.
|
74
|
+
# @return [void]
|
75
|
+
def refresh
|
76
|
+
@context.refresh
|
77
|
+
end
|
78
|
+
|
79
|
+
# This method takes a block consisting of multiple asynchronous color or power changing targets
|
80
|
+
# and it will try to schedule them so they run at the same time.
|
81
|
+
#
|
82
|
+
# You cannot nest `sync` calls, nor call synchronous methods inside a `sync` block.
|
83
|
+
#
|
84
|
+
# Due to messaging rate constraints, the amount of messages determine the delay before
|
85
|
+
# the commands are executed. This method also assumes all the lights have the same time.
|
86
|
+
# @example This example sets all the lights to a random colour at the same time.
|
87
|
+
# client.sync do
|
88
|
+
# client.lights.each do |light|
|
89
|
+
# light.set_color(rand(4) * 90, 1, 1)
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# @note This method is in alpha and might go away. Use tags for better group messaging.
|
94
|
+
# @yield Block of commands to synchronize
|
95
|
+
# @return [Float] Number of seconds until commands are executed
|
96
|
+
def sync(**kwargs, &block)
|
97
|
+
@context.sync(**kwargs, &block)
|
98
|
+
end
|
99
|
+
|
100
|
+
# This is the same as {#sync}, except it will block until the commands have been executed.
|
101
|
+
# @see #sync
|
102
|
+
# @return [Float] Number of seconds slept
|
103
|
+
def sync!(&block)
|
104
|
+
sync(&block).tap do |delay|
|
105
|
+
sleep(delay)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [LightCollection] Lights available to the client
|
110
|
+
# @see [NetworkContext#lights]
|
111
|
+
def lights
|
112
|
+
context.lights
|
113
|
+
end
|
114
|
+
|
115
|
+
# @return [Array<String>] All tags visible to the client
|
116
|
+
# @see [NetworkContext#tags]
|
117
|
+
def tags
|
118
|
+
context.tags
|
119
|
+
end
|
120
|
+
|
121
|
+
# @return [Array<String>] Tags that are currently unused by known devices
|
122
|
+
# @see [NetworkContext#unused_tags]
|
123
|
+
def unused_tags
|
124
|
+
context.unused_tags
|
125
|
+
end
|
126
|
+
|
127
|
+
# Purges unused tags from the system.
|
128
|
+
# Should only use when all devices are on the network, otherwise
|
129
|
+
# offline devices using their tags will not be tagged correctly.
|
130
|
+
# @return [Array<String>] Tags that were purged
|
131
|
+
def purge_unused_tags!
|
132
|
+
context.purge_unused_tags!
|
133
|
+
end
|
134
|
+
|
135
|
+
# Blocks until all messages have been sent to the gateways
|
136
|
+
# @param timeout: [Numeric] When specified, flush will wait `timeout:` seconds before throwing `Timeout::Error`
|
137
|
+
# @raise [TimeoutError] if `timeout:` was exceeded while waiting for send queue to flush
|
138
|
+
# @return [void]
|
139
|
+
def flush(timeout: nil)
|
140
|
+
context.flush(timeout: timeout)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Stops everything and cleans up.
|
144
|
+
def stop
|
145
|
+
context.stop
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
module LIFX
|
2
|
+
module LAN
|
3
|
+
module Colors
|
4
|
+
DEFAULT_KELVIN = 3500
|
5
|
+
|
6
|
+
{
|
7
|
+
red: 0,
|
8
|
+
orange: 36,
|
9
|
+
yellow: 60,
|
10
|
+
green: 120,
|
11
|
+
cyan: 195,
|
12
|
+
blue: 250,
|
13
|
+
purple: 280,
|
14
|
+
pink: 325
|
15
|
+
}.each do |color, hue|
|
16
|
+
define_method(color) do |saturation: 1.0, brightness: 1.0, kelvin: DEFAULT_KELVIN|
|
17
|
+
Color.new(hue, saturation, brightness, kelvin)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Helper to create a white {Color}
|
22
|
+
# @param brightness: [Float] Valid range: `0..1`
|
23
|
+
# @param kelvin: [Integer] Valid range: `2500..9000`
|
24
|
+
# @return [Color]
|
25
|
+
def white(brightness: 1.0, kelvin: DEFAULT_KELVIN)
|
26
|
+
Color.new(0, 0, brightness, kelvin)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Helper to create a random {Color}
|
30
|
+
def random_color(hue: rand(360), saturation: rand, brightness: rand, kelvin: DEFAULT_KELVIN)
|
31
|
+
Color.new(hue, saturation, brightness, kelvin)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# LIFX::Color represents a color intervally by HSBK (Hue, Saturation, Brightness/Value, Kelvin).
|
36
|
+
# It has methods to construct a LIFX::Color instance from various color representations.
|
37
|
+
class Color < Struct.new(:hue, :saturation, :brightness, :kelvin)
|
38
|
+
extend Colors
|
39
|
+
UINT16_MAX = 65535
|
40
|
+
KELVIN_MIN = 2500
|
41
|
+
KELVIN_MAX = 9000
|
42
|
+
|
43
|
+
class << self
|
44
|
+
# Helper method to create from HSB/HSV
|
45
|
+
# @param hue [Float] Valid range: `0..360`
|
46
|
+
# @param saturation [Float] Valid range: `0..1`
|
47
|
+
# @param brightness [Float] Valid range: `0..1`
|
48
|
+
# @return [Color]
|
49
|
+
def hsb(hue, saturation, brightness)
|
50
|
+
new(hue, saturation, brightness, DEFAULT_KELVIN)
|
51
|
+
end
|
52
|
+
alias_method :hsv, :hsb
|
53
|
+
|
54
|
+
# Helper method to create from HSBK/HSVK
|
55
|
+
# @param hue [Float] Valid range: `0..360`
|
56
|
+
# @param saturation [Float] Valid range: `0..1`
|
57
|
+
# @param brightness [Float] Valid range: `0..1`
|
58
|
+
# @param kelvin [Integer] Valid range: `2500..9000`
|
59
|
+
# @return [Color]
|
60
|
+
def hsbk(hue, saturation, brightness, kelvin)
|
61
|
+
new(hue, saturation, brightness, kelvin)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Helper method to create from HSL
|
65
|
+
# @param hue [Float] Valid range: `0..360`
|
66
|
+
# @param saturation [Float] Valid range: `0..1`
|
67
|
+
# @param luminance [Float] Valid range: `0..1`
|
68
|
+
# @return [Color]
|
69
|
+
def hsl(hue, saturation, luminance)
|
70
|
+
# From: http://ariya.blogspot.com.au/2008/07/converting-between-hsl-and-hsv.html
|
71
|
+
l = luminance * 2
|
72
|
+
saturation *= (l <= 1) ? l : 2 - l
|
73
|
+
brightness = (l + saturation) / 2
|
74
|
+
saturation = (2 * saturation) / (l + saturation)
|
75
|
+
new(hue, saturation, brightness, DEFAULT_KELVIN)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Helper method to create from RGB.
|
79
|
+
# @note RGB is not the recommended way to create colors
|
80
|
+
# @param r [Integer] Red. Valid range: `0..255`
|
81
|
+
# @param g [Integer] Green. Valid range: `0..255`
|
82
|
+
# @param b [Integer] Blue. Valid range: `0..255`
|
83
|
+
# @return [Color]
|
84
|
+
def rgb(r, g, b)
|
85
|
+
r = r / 255.0
|
86
|
+
g = g / 255.0
|
87
|
+
b = b / 255.0
|
88
|
+
|
89
|
+
max = [r, g, b].max
|
90
|
+
min = [r, g, b].min
|
91
|
+
|
92
|
+
h = s = v = max
|
93
|
+
d = max - min
|
94
|
+
s = max.zero? ? 0 : d / max
|
95
|
+
|
96
|
+
if max == min
|
97
|
+
h = 0
|
98
|
+
else
|
99
|
+
case max
|
100
|
+
when r
|
101
|
+
h = (g - b) / d + (g < b ? 6 : 0)
|
102
|
+
when g
|
103
|
+
h = (b - r) / d + 2
|
104
|
+
when b
|
105
|
+
h = (r - g) / d + 4
|
106
|
+
end
|
107
|
+
h = h * 60
|
108
|
+
end
|
109
|
+
|
110
|
+
new(h, s, v, DEFAULT_KELVIN)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Creates an instance from a {Protocol::Light::Hsbk} struct
|
114
|
+
# @api private
|
115
|
+
# @param hsbk [Protocol::Light::Hsbk]
|
116
|
+
# @return [Color]
|
117
|
+
def from_struct(hsbk)
|
118
|
+
new(
|
119
|
+
(hsbk.hue.to_f / UINT16_MAX) * 360,
|
120
|
+
(hsbk.saturation.to_f / UINT16_MAX),
|
121
|
+
(hsbk.brightness.to_f / UINT16_MAX),
|
122
|
+
hsbk.kelvin
|
123
|
+
)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def initialize(hue, saturation, brightness, kelvin)
|
128
|
+
hue = hue % 360
|
129
|
+
super(hue, saturation, brightness, kelvin)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns a new Color with the hue changed while keeping other attributes
|
133
|
+
# @param hue [Float] Hue in degrees. `0..360`
|
134
|
+
# @return [Color]
|
135
|
+
def with_hue(hue)
|
136
|
+
Color.new(hue, saturation, brightness, kelvin)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns a new Color with the saturaiton changed while keeping other attributes
|
140
|
+
# @param saturaiton [Float] Saturation as float. `0..1`
|
141
|
+
# @return [Color]
|
142
|
+
def with_saturation(saturation)
|
143
|
+
Color.new(hue, saturation, brightness, kelvin)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns a new Color with the brightness changed while keeping other attributes
|
147
|
+
# @param brightness [Float] Brightness as float. `0..1`
|
148
|
+
# @return [Color]
|
149
|
+
def with_brightness(brightness)
|
150
|
+
Color.new(hue, saturation, brightness, kelvin)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns a new Color with the kelvin changed while keeping other attributes
|
154
|
+
# @param kelvin [Integer] Kelvin. `2500..9000`
|
155
|
+
# @return [Color]
|
156
|
+
def with_kelvin(kelvin)
|
157
|
+
Color.new(hue, saturation, brightness, kelvin)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns a struct for use by the protocol
|
161
|
+
# @api private
|
162
|
+
# @return [Protocol::Light::Hsbk]
|
163
|
+
def to_hsbk
|
164
|
+
Protocol::Light::Hsbk.new(
|
165
|
+
hue: (hue / 360.0 * UINT16_MAX).to_i,
|
166
|
+
saturation: (saturation * UINT16_MAX).to_i,
|
167
|
+
brightness: (brightness * UINT16_MAX).to_i,
|
168
|
+
kelvin: [KELVIN_MIN, kelvin.to_i, KELVIN_MAX].sort[1]
|
169
|
+
)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns hue, saturation, brightness and kelvin in an array
|
173
|
+
# @return [Array<Float, Float, Float, Integer>]
|
174
|
+
def to_a
|
175
|
+
[hue, saturation, brightness, kelvin]
|
176
|
+
end
|
177
|
+
|
178
|
+
DEFAULT_SIMILAR_THRESHOLD = 0.001 # 0.1% variance
|
179
|
+
# Checks if colours are equal to 0.1% variance
|
180
|
+
# @param other [Color] Color to compare to
|
181
|
+
# @param threshold: [Float] 0..1. Threshold to consider it similar
|
182
|
+
# @return [Boolean]
|
183
|
+
def similar_to?(other, threshold: DEFAULT_SIMILAR_THRESHOLD)
|
184
|
+
return false unless other.is_a?(Color)
|
185
|
+
conditions = []
|
186
|
+
|
187
|
+
conditions << (((hue - other.hue).abs < (threshold * 360)) || begin
|
188
|
+
# FIXME: Surely there's a better way.
|
189
|
+
hues = [hue, other.hue].sort
|
190
|
+
hues[0] += 360
|
191
|
+
(hues[0] - hues[1]).abs < (threshold * 360)
|
192
|
+
end)
|
193
|
+
conditions << ((saturation - other.saturation).abs < threshold)
|
194
|
+
conditions << ((brightness - other.brightness).abs < threshold)
|
195
|
+
conditions.all?
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'configatron/core'
|
2
|
+
require 'logger'
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
Config = Configatron::Store.new
|
6
|
+
|
7
|
+
Config.default_duration = 1
|
8
|
+
Config.message_wait_timeout = 3
|
9
|
+
Config.message_retry_interval = 0.5
|
10
|
+
Config.broadcast_ip = '255.255.255.255'
|
11
|
+
Config.allowed_transports = [:udp, :tcp]
|
12
|
+
Config.log_invalid_messages = true
|
13
|
+
Config.logger = Logger.new(STDERR).tap do |logger|
|
14
|
+
logger.level = Logger::WARN
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module LIFX
|
4
|
+
module LAN
|
5
|
+
# LIFX::LAN::Firmware handles decoding firmware payloads
|
6
|
+
# @private
|
7
|
+
class Firmware < Struct.new(:build_time, :major, :minor)
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
def initialize(payload)
|
11
|
+
self.build_time = decode_time(payload.build)
|
12
|
+
self.major = (payload.version >> 0x10)
|
13
|
+
self.minor = (payload.version & 0xFF)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"#<Firmware version=#{self.major}.#{self.minor}>"
|
18
|
+
end
|
19
|
+
alias_method :inspect, :to_s
|
20
|
+
|
21
|
+
def <=>(obj)
|
22
|
+
case obj
|
23
|
+
when String
|
24
|
+
major, minor = obj.split('.', 2).map(&:to_i)
|
25
|
+
[self.major, self.minor] <=> [major, minor]
|
26
|
+
when Firmware
|
27
|
+
[self.major, self.minor] <=> [obj.major, obj.minor]
|
28
|
+
else
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def decode_time(int)
|
37
|
+
if int < 1300000000000000000
|
38
|
+
year = byte(int, 56) + 2000
|
39
|
+
month = bytes(int, 48, 40, 32).map(&:chr).join
|
40
|
+
day = byte(int, 24)
|
41
|
+
hour = byte(int, 16)
|
42
|
+
min = byte(int, 8)
|
43
|
+
sec = byte(int, 0)
|
44
|
+
# Don't want to pull in DateTime just for DateTime.new
|
45
|
+
Time.parse("%s %d %04d, %02d:%02d:%02d" % [month, day, year, hour, min, sec])
|
46
|
+
else
|
47
|
+
Time.at(int / 1000000000)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def byte(n, pos)
|
52
|
+
0xFF & (n >> pos)
|
53
|
+
end
|
54
|
+
|
55
|
+
def bytes(n, *range)
|
56
|
+
range.map {|r| byte(n, r)}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|