sparkle_motion 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/CHANGELOG.md +7 -0
- data/LICENSE +22 -0
- data/README.md +47 -0
- data/bin/sm-discover +18 -0
- data/bin/sm-mark-lights +58 -0
- data/bin/sm-off +38 -0
- data/bin/sm-on +50 -0
- data/bin/sm-simulate +595 -0
- data/bin/sm-watch-memory +9 -0
- data/bin/sparkle-motion +48 -0
- data/config.yml +201 -0
- data/data/dynamic/bridges.csv +5 -0
- data/data/static/devices.csv +7 -0
- data/lib/sparkle_motion.rb +72 -0
- data/lib/sparkle_motion/config.rb +45 -0
- data/lib/sparkle_motion/env.rb +14 -0
- data/lib/sparkle_motion/http.rb +26 -0
- data/lib/sparkle_motion/hue/ssdp.rb +39 -0
- data/lib/sparkle_motion/launch_pad/color.rb +41 -0
- data/lib/sparkle_motion/launch_pad/widget.rb +187 -0
- data/lib/sparkle_motion/launch_pad/widgets/button.rb +29 -0
- data/lib/sparkle_motion/launch_pad/widgets/horizontal_slider.rb +43 -0
- data/lib/sparkle_motion/launch_pad/widgets/radio_group.rb +53 -0
- data/lib/sparkle_motion/launch_pad/widgets/toggle.rb +48 -0
- data/lib/sparkle_motion/launch_pad/widgets/vertical_slider.rb +43 -0
- data/lib/sparkle_motion/lazy_request_config.rb +87 -0
- data/lib/sparkle_motion/node.rb +80 -0
- data/lib/sparkle_motion/nodes/generator.rb +8 -0
- data/lib/sparkle_motion/nodes/generators/const.rb +15 -0
- data/lib/sparkle_motion/nodes/generators/perlin.rb +26 -0
- data/lib/sparkle_motion/nodes/generators/wave2.rb +20 -0
- data/lib/sparkle_motion/nodes/transform.rb +34 -0
- data/lib/sparkle_motion/nodes/transforms/contrast.rb +20 -0
- data/lib/sparkle_motion/nodes/transforms/range.rb +41 -0
- data/lib/sparkle_motion/nodes/transforms/spotlight.rb +35 -0
- data/lib/sparkle_motion/output.rb +69 -0
- data/lib/sparkle_motion/results.rb +78 -0
- data/lib/sparkle_motion/utility.rb +68 -0
- data/lib/sparkle_motion/vector2.rb +11 -0
- data/lib/sparkle_motion/version.rb +3 -0
- data/sparkle_motion.gemspec +44 -0
- metadata +178 -0
data/bin/sm-watch-memory
ADDED
data/bin/sparkle-motion
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
###############################################################################
|
3
|
+
# Visual Effects
|
4
|
+
###############################################################################
|
5
|
+
# Whether to use background sweep thread for saw-tooth pattern on hue:
|
6
|
+
export USE_SWEEP=1
|
7
|
+
# Whether to actually run main lighting threads:
|
8
|
+
export USE_LIGHTS=1
|
9
|
+
# Whether or not to run the simulation graph:
|
10
|
+
export USE_GRAPH=1
|
11
|
+
# Whether or not to use Novation LaunchPad for controls:
|
12
|
+
export USE_INPUT=1
|
13
|
+
|
14
|
+
|
15
|
+
###############################################################################
|
16
|
+
# Debugging
|
17
|
+
###############################################################################
|
18
|
+
# Run for a fixed number of iterations, or until we're killed (0):
|
19
|
+
export ITERATIONS=0
|
20
|
+
|
21
|
+
# Forcibly disable Ruby GC:
|
22
|
+
export SKIP_GC=0
|
23
|
+
|
24
|
+
# Logging verbosity. Valid values: DEBUG, INFO, WARN, ERROR. Default is INFO.
|
25
|
+
export SPARKLEMOTION_LOGLEVEL=INFO
|
26
|
+
|
27
|
+
# Whether to run a profiler:
|
28
|
+
export PROFILE_RUN= # ruby-prof|memory_profiler
|
29
|
+
|
30
|
+
# If using ruby-prof, what mode to run it in:
|
31
|
+
export RUBY_PROF_MODE=allocations # ALLOCATIONS, CPU_TIME, GC_RUNS, GC_TIME, MEMORY, PROCESS_TIME, WALL_TIME
|
32
|
+
|
33
|
+
# Dump various PNGs showing the results of given nodes in the DAG over time.
|
34
|
+
# This is VERY VERY memory intensize! Don't try to use it for a long run!
|
35
|
+
# Current nodes: perlin, stretched, shifted_0, shifted_1, shifted_2, shifted_3, spotlit, output
|
36
|
+
# ... however you probably don't care about shifted_0..shifted_2.
|
37
|
+
export DEBUG_NODES= #perlin,stretched,shifted_3,spotlit,output
|
38
|
+
|
39
|
+
|
40
|
+
###############################################################################
|
41
|
+
echo "Beginning simulation. Press ctrl-c to end."
|
42
|
+
touch /tmp/sparkle-motion.state
|
43
|
+
EXIT_FLAG=127
|
44
|
+
while [ $EXIT_FLAG != 0 ]; do
|
45
|
+
./bin/sm-simulate
|
46
|
+
EXIT_FLAG=$?
|
47
|
+
echo "Process terminated with exit code: $EXIT_FLAG"
|
48
|
+
done
|
data/config.yml
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
---
|
2
|
+
common_username: &common_username "1234567890"
|
3
|
+
# Determine HTTP request concurrency for updating lights. Per-bridge.
|
4
|
+
max_connects: 3
|
5
|
+
bridges:
|
6
|
+
"Bridge-01":
|
7
|
+
ip: "192.168.2.10"
|
8
|
+
username: *common_username
|
9
|
+
debug_hue: 0
|
10
|
+
"Bridge-02":
|
11
|
+
ip: "192.168.2.6"
|
12
|
+
username: *common_username
|
13
|
+
debug_hue: 25000
|
14
|
+
"Bridge-03":
|
15
|
+
ip: "192.168.2.7"
|
16
|
+
username: *common_username
|
17
|
+
debug_hue: 45000
|
18
|
+
"Bridge-04":
|
19
|
+
ip: "192.168.2.9"
|
20
|
+
username: *common_username
|
21
|
+
debug_hue: 12000
|
22
|
+
simulation:
|
23
|
+
# TODO: Have day/night configurations and a way to LERP between them.
|
24
|
+
#
|
25
|
+
# TODO: Tool for testing bulb positioning.
|
26
|
+
#
|
27
|
+
# TODO: Use mid-point/range notation for intensity to make tweaking easier.
|
28
|
+
#
|
29
|
+
# TODO: Duck intensity when tweaking saturation.
|
30
|
+
#
|
31
|
+
# TODO: Finish simulation visualization tool.
|
32
|
+
#
|
33
|
+
# TODO: Re-parameterize sleep option, and maybe add a way to tweak it on the fly.
|
34
|
+
#
|
35
|
+
# TODO: Add outer-loop script, and use exit codes to differentiate between restart and terminate.
|
36
|
+
#
|
37
|
+
# TODO: Way to tweak output transition time on the fly?
|
38
|
+
#
|
39
|
+
# TODO: Separate color-sweeps per bridge.
|
40
|
+
#
|
41
|
+
# TODO: Way to tweak sweep transition time on the fly?
|
42
|
+
#
|
43
|
+
# TODO: Play with X multiplier for Perlin component to see if that makes the lighting more visually interesting.
|
44
|
+
#
|
45
|
+
# TODO: Group all writes to a bridge into a single thread.
|
46
|
+
#
|
47
|
+
# TODO: Either self-tune delays, or avoid sending refreshes to each light/group too fast?
|
48
|
+
output:
|
49
|
+
transition: 0.3
|
50
|
+
sweep:
|
51
|
+
# Don't set this transition much below 1.0! ZigBee spec only allows 1
|
52
|
+
# group update/sec, but Hue Bridge/lights seem to be OK with about 1 every
|
53
|
+
# 0.75 sec...
|
54
|
+
#
|
55
|
+
# Negative values mean to use a transition time of 0 for the change, but
|
56
|
+
# wait the absolute value between steps.
|
57
|
+
transition: 1.5
|
58
|
+
# Ballpark estimation of Jen's palette:
|
59
|
+
values:
|
60
|
+
- 49500
|
61
|
+
- 49500
|
62
|
+
- 48000
|
63
|
+
- 49500
|
64
|
+
- 49500
|
65
|
+
- 51000
|
66
|
+
nodes:
|
67
|
+
# wave2:
|
68
|
+
# speed: [0.1, 1.0]
|
69
|
+
perlin:
|
70
|
+
speed: [0.1, 4.0]
|
71
|
+
contrast:
|
72
|
+
# Function: LINEAR, CUBIC, QUINTIC -- don't bother using iterations > 1
|
73
|
+
# with LINEAR, as LINEAR is a no-op.
|
74
|
+
function: cubic
|
75
|
+
iterations: 3
|
76
|
+
controls:
|
77
|
+
exit:
|
78
|
+
position: mixer
|
79
|
+
colors:
|
80
|
+
"color": dark_gray
|
81
|
+
"down": white
|
82
|
+
intensity:
|
83
|
+
widget: SparkleMotion::LaunchPad::Widgets::VerticalSlider
|
84
|
+
positions:
|
85
|
+
- [0, 4]
|
86
|
+
- [1, 4]
|
87
|
+
- [2, 4]
|
88
|
+
- [3, 4]
|
89
|
+
size: 4
|
90
|
+
values:
|
91
|
+
# Mid-point, delta (was low/high):
|
92
|
+
- [0.350, 0.050] #[0.25, 0.45]
|
93
|
+
- [0.500, 0.050] #[0.40, 0.60]
|
94
|
+
- [0.675, 0.125] #[0.55, 0.80]
|
95
|
+
- [0.875, 0.125] #[0.75, 1.00]
|
96
|
+
colors:
|
97
|
+
"on": 0x22003F
|
98
|
+
"off": 0x05000A
|
99
|
+
"down": 0x27103F
|
100
|
+
saturation:
|
101
|
+
widget: SparkleMotion::LaunchPad::Widgets::VerticalSlider
|
102
|
+
transition: 0.3
|
103
|
+
positions:
|
104
|
+
- [4, 4]
|
105
|
+
- [5, 4]
|
106
|
+
- [6, 4]
|
107
|
+
- [7, 4]
|
108
|
+
size: 4
|
109
|
+
groups:
|
110
|
+
- ["Bridge-01", 0]
|
111
|
+
- ["Bridge-02", 0]
|
112
|
+
- ["Bridge-03", 0]
|
113
|
+
- ["Bridge-04", 0]
|
114
|
+
values:
|
115
|
+
- 102
|
116
|
+
- 152
|
117
|
+
- 203
|
118
|
+
- 254
|
119
|
+
colors:
|
120
|
+
"on": 0x1C103F
|
121
|
+
"off": 0x03030C
|
122
|
+
"down": 0x10103F
|
123
|
+
spotlighting:
|
124
|
+
x: 0
|
125
|
+
y: 0
|
126
|
+
mappings:
|
127
|
+
# NOTE: Mappings defines the width/height of the widget implicitly!
|
128
|
+
# NOTE: Values are indexes into main_lights array.
|
129
|
+
#
|
130
|
+
# Excluding outermost lights, and going top-down for left-most to
|
131
|
+
# right-most light across left then right strings:
|
132
|
+
#
|
133
|
+
# Bridge 3/4:
|
134
|
+
- [14, 15, 16, 17, 18, 19, 20, 21]
|
135
|
+
# Bridge 1/2:
|
136
|
+
- [ 2, 3, 4, 5, 6, 7, 8, 9]
|
137
|
+
colors:
|
138
|
+
"on": 0x272700
|
139
|
+
"off": 0x020200
|
140
|
+
"down": 0x3F3F10
|
141
|
+
# Main Lights is the group for which the main simulation will be applied.
|
142
|
+
# It can be any number of lights from any number of bridges but you'll need to
|
143
|
+
# plan groups out for saturation controls.
|
144
|
+
main_lights:
|
145
|
+
# Strip 1:
|
146
|
+
- ["Bridge-01", 37]
|
147
|
+
- ["Bridge-01", 36]
|
148
|
+
- ["Bridge-01", 38]
|
149
|
+
- ["Bridge-01", 39]
|
150
|
+
- ["Bridge-01", 40]
|
151
|
+
- ["Bridge-01", 35]
|
152
|
+
|
153
|
+
- ["Bridge-02", 12]
|
154
|
+
- ["Bridge-02", 21]
|
155
|
+
- ["Bridge-02", 20]
|
156
|
+
- ["Bridge-02", 19]
|
157
|
+
- ["Bridge-02", 15]
|
158
|
+
- ["Bridge-02", 18]
|
159
|
+
|
160
|
+
# Strip 2:
|
161
|
+
- ["Bridge-03", 2]
|
162
|
+
- ["Bridge-03", 3]
|
163
|
+
- ["Bridge-03", 8]
|
164
|
+
- ["Bridge-03", 10]
|
165
|
+
- ["Bridge-03", 9]
|
166
|
+
- ["Bridge-03", 6]
|
167
|
+
|
168
|
+
- ["Bridge-04", 7]
|
169
|
+
- ["Bridge-04", 11]
|
170
|
+
- ["Bridge-04", 12]
|
171
|
+
- ["Bridge-04", 1]
|
172
|
+
- ["Bridge-04", 9]
|
173
|
+
- ["Bridge-04", 8]
|
174
|
+
# TODO: Come up with appropriate behaviors/extensions of behavior for dance
|
175
|
+
# TODO: floor:
|
176
|
+
# TODO: * Spotighting.
|
177
|
+
# TODO: * Saturation.
|
178
|
+
# TODO: * Intensity.
|
179
|
+
#
|
180
|
+
# Dance Lights is the group of lights above the dance floor, which will get
|
181
|
+
# their own simulation, although updates will be interleaved with the main
|
182
|
+
# lights per-bridge. It should be a positional map affording spatial
|
183
|
+
# coherence.
|
184
|
+
#
|
185
|
+
# For spotlighting/saturation/intensity purposes, it will be treated as one
|
186
|
+
# group.
|
187
|
+
dance_lights:
|
188
|
+
- ["Bridge-01", 26]
|
189
|
+
- ["Bridge-02", 11]
|
190
|
+
- ["Bridge-03", 7]
|
191
|
+
- ["Bridge-04", 5]
|
192
|
+
accent_lights:
|
193
|
+
- ["Bridge-01", 9]
|
194
|
+
- ["Bridge-01", 10]
|
195
|
+
- ["Bridge-01", 11]
|
196
|
+
- ["Bridge-01", 12]
|
197
|
+
- ["Bridge-01", 13]
|
198
|
+
- ["Bridge-01", 33]
|
199
|
+
- ["Bridge-01", 34]
|
200
|
+
- ["Bridge-02", 7]
|
201
|
+
- ["Bridge-02", 8]
|
@@ -0,0 +1,7 @@
|
|
1
|
+
Manufacturer,Type,"Model ID",Series
|
2
|
+
Philips,"Color light",LLC011,"Hue Bloom"
|
3
|
+
Philips,"Color light",LST001,"LightStrips"
|
4
|
+
Philips,"Dimmable light",LWB004,"Hue Lux"
|
5
|
+
Philips,"Extended color light",LCT001,"Hue Lamp"
|
6
|
+
Philips,"Extended color light",LCT002,"Hue Downlight"
|
7
|
+
Philips,"Extended color light",LLC020,"Hue Go"
|
@@ -0,0 +1,72 @@
|
|
1
|
+
lib = File.expand_path("../", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
# We shave 1/6th of a sec off launch time by not doing the following, but it
|
5
|
+
# presumes the user is using RVM properly to isolate the gem env, and that
|
6
|
+
# there are no git/path-based gems in the Gemfile:
|
7
|
+
# require "rubygems"
|
8
|
+
# require "bundler/setup"
|
9
|
+
|
10
|
+
require "yaml"
|
11
|
+
require "logger-better"
|
12
|
+
|
13
|
+
# System for building interesting, dynamic lighting effects for the Philips Hue,
|
14
|
+
# using the Novation Launchpad for control.
|
15
|
+
module SparkleMotion
|
16
|
+
def self.logger; @logger; end
|
17
|
+
|
18
|
+
def self.init!(name)
|
19
|
+
@logger = Logger::Better.new(STDOUT)
|
20
|
+
@logger.level = (ENV["SPARKLEMOTION_LOGLEVEL"] || "info").downcase.to_sym
|
21
|
+
@logger.progname = name
|
22
|
+
end
|
23
|
+
|
24
|
+
# Load code for talking to Philips Hue lighting system.
|
25
|
+
def self.use_hue!(discovery: false, api: false)
|
26
|
+
if api
|
27
|
+
require "sparkle_motion/results"
|
28
|
+
require "sparkle_motion/lazy_request_config"
|
29
|
+
end
|
30
|
+
|
31
|
+
if discovery
|
32
|
+
require "sparkle_motion/hue/ssdp"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Load code for graph-structured effect generation.
|
37
|
+
def self.use_graph!
|
38
|
+
# Base classes:
|
39
|
+
require "sparkle_motion/node"
|
40
|
+
require "sparkle_motion/nodes/generator"
|
41
|
+
require "sparkle_motion/nodes/transform"
|
42
|
+
|
43
|
+
# Simulation root nodes:
|
44
|
+
require "sparkle_motion/nodes/generators/const"
|
45
|
+
require "sparkle_motion/nodes/generators/perlin"
|
46
|
+
require "sparkle_motion/nodes/generators/wave2"
|
47
|
+
|
48
|
+
# Simulation transform nodes:
|
49
|
+
require "sparkle_motion/nodes/transforms/contrast"
|
50
|
+
require "sparkle_motion/nodes/transforms/range"
|
51
|
+
require "sparkle_motion/nodes/transforms/spotlight"
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.use_widgets!
|
55
|
+
require "sparkle_motion/launch_pad/widget"
|
56
|
+
require "sparkle_motion/launch_pad/widgets/horizontal_slider"
|
57
|
+
require "sparkle_motion/launch_pad/widgets/vertical_slider"
|
58
|
+
require "sparkle_motion/launch_pad/widgets/radio_group"
|
59
|
+
require "sparkle_motion/launch_pad/widgets/button"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Load code/widgets for Novation LaunchPad.
|
63
|
+
def self.use_launchpad!
|
64
|
+
require "launchpad"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
require "sparkle_motion/version"
|
69
|
+
require "sparkle_motion/utility"
|
70
|
+
require "sparkle_motion/config"
|
71
|
+
require "sparkle_motion/env"
|
72
|
+
require "sparkle_motion/http"
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# TODO: Load this on-demand, not automatically! Namespace it! AUGH!
|
2
|
+
require "sparkle_motion/vector2"
|
3
|
+
require "sparkle_motion/launch_pad/color"
|
4
|
+
|
5
|
+
def unpack_color(col)
|
6
|
+
if col.is_a?(String)
|
7
|
+
SparkleMotion::LaunchPad::Color.const_get(col.upcase).to_h
|
8
|
+
else
|
9
|
+
{ r: ((col >> 16) & 0xFF),
|
10
|
+
g: ((col >> 8) & 0xFF),
|
11
|
+
b: (col & 0xFF) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def unpack_colors_in_place!(cfg)
|
16
|
+
cfg.each do |key, val|
|
17
|
+
if val.is_a?(Array)
|
18
|
+
cfg[key] = val.map { |vv| unpack_color(vv) }
|
19
|
+
else
|
20
|
+
cfg[key] = unpack_color(val)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def unpack_vector_in_place!(cfg)
|
26
|
+
cfg.each do |key, val|
|
27
|
+
next unless val.is_a?(Array) && val.length == 2
|
28
|
+
cfg[key] = Vector2.new(x: val[0], y: val[1])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
CONFIG = YAML.load(File.read("config.yml"))
|
33
|
+
CONFIG["bridges"].map do |name, cfg|
|
34
|
+
cfg["name"] = name
|
35
|
+
end
|
36
|
+
|
37
|
+
CONFIG["simulation"]["controls"].values.each do |cfg|
|
38
|
+
next unless cfg && cfg["colors"]
|
39
|
+
unpack_colors_in_place!(cfg["colors"])
|
40
|
+
end
|
41
|
+
|
42
|
+
CONFIG["simulation"]["nodes"].values.each do |cfg|
|
43
|
+
next unless cfg
|
44
|
+
unpack_vector_in_place!(cfg)
|
45
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# TODO: Namespacing/classes/etc!
|
2
|
+
def env_int(name, allow_zero = false)
|
3
|
+
return nil unless ENV.key?(name)
|
4
|
+
tmp = ENV[name].to_i
|
5
|
+
tmp = nil if tmp == 0 && !allow_zero
|
6
|
+
tmp
|
7
|
+
end
|
8
|
+
|
9
|
+
def env_float(name)
|
10
|
+
return nil unless ENV.key?(name)
|
11
|
+
ENV[name].to_f
|
12
|
+
end
|
13
|
+
|
14
|
+
def env_bool(name); (env_int(name, true) || 1) != 0; end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# TODO: Namespacing/classes/etc!
|
2
|
+
require "curb"
|
3
|
+
require "oj"
|
4
|
+
|
5
|
+
# TODO: Try to figure out how to set Curl::CURLOPT_TCP_NODELAY => true
|
6
|
+
# TODO: Disable Curl from sending keepalives by trying HTTP/1.0.
|
7
|
+
MULTI_OPTIONS = { pipeline: false,
|
8
|
+
max_connects: (CONFIG["max_connects"] || 3) }
|
9
|
+
EASY_OPTIONS = { "timeout" => 5,
|
10
|
+
"connect_timeout" => 5,
|
11
|
+
"follow_location" => false,
|
12
|
+
"max_redirects" => 0 } # ,
|
13
|
+
# version: Curl::HTTP_1_0 }
|
14
|
+
# easy.header_str.grep(/keep-alive/)
|
15
|
+
# Force keepalive off to see if that makes any difference...
|
16
|
+
# TODO: Use this: `easy.headers["Expect"] = ''` to remove a default header we don't care about!
|
17
|
+
|
18
|
+
def hue_server(config); "http://#{config['ip']}"; end
|
19
|
+
def hue_base(config); "#{hue_server(config)}/api/#{config['username']}"; end
|
20
|
+
def hue_light_endpoint(config, light_id); "#{hue_base(config)}/lights/#{light_id}/state"; end
|
21
|
+
def hue_group_endpoint(config, group); "#{hue_base(config)}/groups/#{group}/action"; end
|
22
|
+
|
23
|
+
def with_transition_time(data, transition)
|
24
|
+
data["transitiontime"] = (transition * 10.0).round(0)
|
25
|
+
data
|
26
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "frisky/ssdp"
|
2
|
+
Frisky.logging_enabled = false # Frisky is super verbose
|
3
|
+
|
4
|
+
module SparkleMotion
|
5
|
+
module Hue
|
6
|
+
# Helpers for SSDP discovery of bridges.
|
7
|
+
class SSDP
|
8
|
+
def scan
|
9
|
+
raw = Frisky::SSDP
|
10
|
+
.search("IpBridge")
|
11
|
+
.select { |resp| ssdp_response?(resp) }
|
12
|
+
.map { |resp| ssdp_extract(resp) }
|
13
|
+
.select { |resp| resp["name"] == "upnp:rootdevice" }
|
14
|
+
Hash[raw.map { |resp| [resp["id"], resp["ipaddress"]] }]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Ensure we're *only* getting responses from a Philips Hue bridge. The
|
18
|
+
# Hue Bridge tends to be obnoxious and announce itself on *any* SSDP
|
19
|
+
# SSDP request, so we assume that we may encounter other obnoxious gear
|
20
|
+
# as well...
|
21
|
+
def ssdp_response?(resp)
|
22
|
+
(resp[:server] || "")
|
23
|
+
.split(/[,\s]+/)
|
24
|
+
.find { |token| token =~ %r{\AIpBridge/\d+(\.\d+)*\z} }
|
25
|
+
end
|
26
|
+
|
27
|
+
def ssdp_extract(resp)
|
28
|
+
{ "id" => usn_to_id(resp[:usn]),
|
29
|
+
"name" => resp[:st],
|
30
|
+
"ipaddress" => URI.parse(resp[:location]).host }
|
31
|
+
end
|
32
|
+
|
33
|
+
# TODO: With all the hassle around ID and the fact that I'm essentially
|
34
|
+
# TODO: coercing it down to just MAC address.... Just use the damned IP
|
35
|
+
# TODO: or MAC!
|
36
|
+
def usn_to_id(usn); usn.split(/:/, 3)[1].split(/-/).last; end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|