lightwaverf 0.3.3 → 0.4.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.
- data/bin/lightwaverf +10 -2
- data/lib/lightwaverf.rb +644 -146
- metadata +40 -7
data/bin/lightwaverf
CHANGED
@@ -9,12 +9,20 @@ case ARGV[0]
|
|
9
9
|
puts LightWaveRF.new.configure
|
10
10
|
when 'sequence'
|
11
11
|
puts LightWaveRF.new.sequence ARGV[1], ARGV[2]
|
12
|
+
when 'mood'
|
13
|
+
puts LightWaveRF.new.mood ARGV[1], ARGV[2], ARGV[3]
|
14
|
+
when 'learnmood'
|
15
|
+
puts LightWaveRF.new.learnmood ARGV[1], ARGV[2], ARGV[3]
|
12
16
|
when 'energy'
|
13
17
|
puts LightWaveRF.new.energy ARGV[1], ARGV[2], ARGV[3]
|
18
|
+
when 'update_timers'
|
19
|
+
puts LightWaveRF.new.update_timers ARGV[1], ARGV[2], ARGV[3]
|
14
20
|
when 'timer'
|
15
|
-
puts LightWaveRF.new.
|
21
|
+
puts LightWaveRF.new.run_timers ARGV[1], ARGV[2]
|
22
|
+
when 'run_timers'
|
23
|
+
puts LightWaveRF.new.run_timers ARGV[1], ARGV[2]
|
16
24
|
when 'update'
|
17
|
-
puts LightWaveRF.new.update_config ARGV[1], ARGV[2]
|
25
|
+
puts LightWaveRF.new.update_config ARGV[1], ARGV[2]
|
18
26
|
else
|
19
27
|
LightWaveRF.new.send ARGV[0], ARGV[1], ARGV[2], ARGV[3]
|
20
28
|
end
|
data/lib/lightwaverf.rb
CHANGED
@@ -1,17 +1,43 @@
|
|
1
|
+
# TODO:
|
2
|
+
# All day events without times - need to fix regex
|
3
|
+
# Make regex better
|
4
|
+
# Get rid of references in yaml cache file - use dup more? Or does it not matter?
|
5
|
+
# Cope with events that start and end in the same run?
|
6
|
+
# Add info about states to timer log
|
7
|
+
# Consider adding a 'random' time shift modifier to make holiday security lights more 'realistic'
|
8
|
+
# Build / update cron job automatically
|
9
|
+
|
10
|
+
|
1
11
|
require 'yaml'
|
2
12
|
require 'socket'
|
13
|
+
require 'net/http'
|
14
|
+
require 'uri'
|
15
|
+
require 'net/https'
|
16
|
+
require 'json'
|
17
|
+
require 'rexml/document'
|
18
|
+
require 'time'
|
19
|
+
require 'date'
|
3
20
|
include Socket::Constants
|
4
21
|
|
5
22
|
class LightWaveRF
|
6
23
|
|
7
24
|
@config_file = nil
|
8
25
|
@log_file = nil
|
26
|
+
@log_timer_file = nil
|
9
27
|
@config = nil
|
28
|
+
@timers = nil
|
10
29
|
|
11
30
|
# Display usage info
|
12
|
-
def usage
|
31
|
+
def usage room = nil
|
13
32
|
rooms = self.class.get_rooms self.get_config
|
14
|
-
'usage: lightwaverf ' + rooms.values.first['name'] + ' ' + rooms.values.first['device'].keys.first.to_s + ' on # where "' + rooms.keys.first + '" is a room in ' + self.get_config_file
|
33
|
+
config = 'usage: lightwaverf ' + rooms.values.first['name'] + ' ' + rooms.values.first['device'].keys.first.to_s + ' on # where "' + rooms.keys.first + '" is a room in ' + self.get_config_file
|
34
|
+
if room and rooms[room]
|
35
|
+
config += "\ntry: lightwaverf " + rooms[room]['name'] + ' all on'
|
36
|
+
rooms[room]['device'].each do | device |
|
37
|
+
config += "\ntry: lightwaverf " + rooms[room]['name'] + ' ' + device.first.to_s + ' on'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
config
|
15
41
|
end
|
16
42
|
|
17
43
|
# Display help
|
@@ -30,11 +56,11 @@ class LightWaveRF
|
|
30
56
|
# debug: (Boolean
|
31
57
|
def configure debug = false
|
32
58
|
config = self.get_config
|
33
|
-
puts 'What is the ip address of your wifi link? (' + self.get_config['host'] + '). Enter a blank line to broadcast UDP commands.'
|
34
|
-
host = STDIN.gets.chomp
|
35
|
-
if ! host.to_s.empty?
|
36
|
-
|
37
|
-
end
|
59
|
+
# puts 'What is the ip address of your wifi link? (' + self.get_config['host'] + '). Enter a blank line to broadcast UDP commands.'
|
60
|
+
# host = STDIN.gets.chomp
|
61
|
+
# if ! host.to_s.empty?
|
62
|
+
# config['host'] = host
|
63
|
+
# end
|
38
64
|
puts 'What is the address of your google calendar? (' + self.get_config['calendar'] + '). Optional!'
|
39
65
|
calendar = STDIN.gets.chomp
|
40
66
|
if ! calendar.to_s.empty?
|
@@ -47,9 +73,7 @@ class LightWaveRF
|
|
47
73
|
parts = device.split ' '
|
48
74
|
if !parts[0].to_s.empty? and !parts[1].to_s.empty?
|
49
75
|
new_room = parts.shift
|
50
|
-
|
51
|
-
config['room'] = [ ]
|
52
|
-
end
|
76
|
+
config['room'] ||= [ ]
|
53
77
|
found = false
|
54
78
|
config['room'].each do | room |
|
55
79
|
if room['name'] == new_room
|
@@ -59,7 +83,7 @@ class LightWaveRF
|
|
59
83
|
debug and ( p 'so now room is ' + room.to_s )
|
60
84
|
end
|
61
85
|
if ! found
|
62
|
-
config['room'].push 'name' => new_room, 'device' => parts
|
86
|
+
config['room'].push 'name' => new_room, 'device' => parts, 'mood' => nil
|
63
87
|
end
|
64
88
|
debug and ( p 'added ' + parts.to_s + ' to ' + new_room )
|
65
89
|
end
|
@@ -84,9 +108,64 @@ class LightWaveRF
|
|
84
108
|
@log_file || File.expand_path('~') + '/lightwaverf.log'
|
85
109
|
end
|
86
110
|
|
111
|
+
# Timer log file getter
|
112
|
+
def get_timer_log_file
|
113
|
+
@timer_log_file || File.expand_path('~') + '/lightwaverf-timer.log'
|
114
|
+
end
|
115
|
+
|
116
|
+
# Timer logger
|
117
|
+
def log_timer_event type, room = nil, device = nil, state = nil, result = false
|
118
|
+
# create log message
|
119
|
+
message = nil
|
120
|
+
case type
|
121
|
+
when 'update'
|
122
|
+
message = '### Updated timer cache'
|
123
|
+
when 'run'
|
124
|
+
message = '*** Ran timers'
|
125
|
+
when 'sequence'
|
126
|
+
message = 'Ran sequence: ' + state
|
127
|
+
when 'mood'
|
128
|
+
message = 'Set mood: ' + mood + ' in room ' + room
|
129
|
+
when 'device'
|
130
|
+
message = 'Set device: ' + device + ' in room ' + room + ' to state ' + state
|
131
|
+
end
|
132
|
+
unless message.nil?
|
133
|
+
File.open( self.get_timer_log_file, 'a' ) do | f |
|
134
|
+
f.write("\n" + Time.now.to_s + ' - ' + message + ' - ' + ( result ? 'SUCCESS!' : 'FAILED!' ))
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Timer cache file getter
|
140
|
+
def get_timer_cache_file
|
141
|
+
@log_file || File.expand_path('~') + '/lightwaverf-timer-cache.yml'
|
142
|
+
end
|
143
|
+
|
144
|
+
# Get timer cache file, create it if needed
|
145
|
+
def get_timer_cache
|
146
|
+
p 'getting timer cache...'
|
147
|
+
if ! @timers
|
148
|
+
p 'no timers!'
|
149
|
+
if ! File.exists? self.get_timer_cache_file
|
150
|
+
p 'no file!'
|
151
|
+
self.update_timers
|
152
|
+
p 'updated...'
|
153
|
+
end
|
154
|
+
@timers = YAML.load_file self.get_timer_cache_file
|
155
|
+
p 'timers now ' + @timers.to_s
|
156
|
+
end
|
157
|
+
p 'returning, timers now ' + @timers.to_s
|
158
|
+
@timers
|
159
|
+
end
|
160
|
+
|
161
|
+
# Store the timer cache
|
162
|
+
def put_timer_cache timers = { 'events' => [ ] }
|
163
|
+
File.open( self.get_timer_cache_file, 'w' ) do | handle |
|
164
|
+
handle.write YAML.dump( timers )
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
87
168
|
def put_config config = { 'room' => [ { 'name' => 'our', 'device' => [ 'light', 'lights' ] } ] }
|
88
|
-
puts 'put_config got ' + config.to_s
|
89
|
-
puts 'so writing ' + YAML.dump( config )
|
90
169
|
File.open( self.get_config_file, 'w' ) do | handle |
|
91
170
|
handle.write YAML.dump( config )
|
92
171
|
end
|
@@ -117,99 +196,122 @@ class LightWaveRF
|
|
117
196
|
# Credits:
|
118
197
|
# wonko - http://lightwaverfcommunity.org.uk/forums/topic/querying-configuration-information-from-the-lightwaverf-website/
|
119
198
|
def update_config email = nil, pin = nil, debug = false
|
120
|
-
|
199
|
+
|
121
200
|
# Login to LightWaveRF Host server
|
122
|
-
require 'net/http'
|
123
|
-
require 'uri'
|
124
201
|
uri = URI.parse 'https://lightwaverfhost.co.uk/manager/index.php'
|
125
202
|
http = Net::HTTP.new uri.host, uri.port
|
126
203
|
if uri.scheme == 'https'
|
127
|
-
require 'net/https'
|
128
204
|
http.use_ssl = true
|
129
205
|
end
|
130
206
|
data = 'pin=' + pin + '&email=' + email
|
131
|
-
headers = { 'Content-Type'=> 'application/x-www-form-urlencoded
|
207
|
+
headers = { 'Content-Type'=> 'application/x-www-form-urlencoded' }
|
132
208
|
resp, data = http.post uri.request_uri, data, headers
|
133
|
-
|
209
|
+
|
134
210
|
if resp and resp.body
|
135
|
-
|
136
|
-
# var gDeviceNames = [""]
|
137
|
-
# var gDeviceStatus = [""]
|
138
|
-
# var gRoomNames = [""]
|
139
|
-
# var gRoomStatus = [""]
|
140
|
-
# http://rubular.com/r/UH0H4b4afF
|
141
|
-
variables = Hash.new
|
142
|
-
resp.body.scan(/var (gDeviceNames|gDeviceStatus|gRoomNames|gRoomStatus)\s*=\s*([^;]*)/).each do |variable|
|
143
|
-
variables[variable[0]] = variable[1].scan(/"([^"]*)\"/)
|
144
|
-
end
|
145
|
-
debug and (p '[Info - LightWaveRF Gem] Javascript variables ' + variables.to_s)
|
146
|
-
|
147
|
-
rooms = [ ]
|
148
|
-
# Rooms - gRoomNames is a collection of 8 values, or room names
|
149
|
-
variables['gRoomNames'].each_with_index do |(roomName), roomIndex|
|
150
|
-
# Room Status - gRoomStatus is a collection of 8 values indicating the status of the corresponding room in gRoomNames
|
151
|
-
# A: Active
|
152
|
-
# I: Inactive
|
153
|
-
if variables['gRoomStatus'] and variables['gRoomStatus'][roomIndex] and variables['gRoomStatus'][roomIndex][0] == 'A'
|
154
|
-
# Devices - gDeviceNames is a collection of 80 values, structured in blocks of ten values for each room:
|
155
|
-
# Devices 1 - 6, Mood 1 - 3, All Off
|
156
|
-
roomDevices = Array.new
|
157
|
-
deviceNamesIndexStart = roomIndex*10
|
158
|
-
variables['gDeviceNames'][(deviceNamesIndexStart)..(deviceNamesIndexStart+5)].each_with_index do |(deviceName), deviceIndex|
|
159
|
-
# Device Status - gDeviceStatus is a collection of 80 values which indicate the status/type of the corresponding device in gDeviceNames
|
160
|
-
# O: On/Off Switch
|
161
|
-
# D: Dimmer
|
162
|
-
# R: Radiator(s)
|
163
|
-
# P: Open/Close
|
164
|
-
# I: Inactive (i.e. not configured)
|
165
|
-
# m: Mood (inactive)
|
166
|
-
# M: Mood (active)
|
167
|
-
# o: All Off
|
168
|
-
deviceStatusIndex = roomIndex*10+deviceIndex
|
169
|
-
if variables['gDeviceStatus'] and variables['gDeviceStatus'][deviceStatusIndex] and variables['gDeviceStatus'][deviceStatusIndex][0] != 'I'
|
170
|
-
roomDevices << deviceName
|
171
|
-
end
|
172
|
-
end
|
173
|
-
# Create a hash of the active room and active devices and add to rooms array
|
174
|
-
if roomName and roomDevices and roomDevices.any?
|
175
|
-
rooms << {'name'=>roomName,'device'=>roomDevices}
|
176
|
-
end
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
211
|
+
rooms = self.get_rooms_from resp.body, debug
|
180
212
|
# Update 'room' element in LightWaveRF Gem config file
|
181
213
|
# config['room'] is an array of hashes containing the room name and device names
|
182
214
|
# in the format { 'name' => 'Room Name', 'device' => ['Device 1', Device 2'] }
|
183
|
-
if rooms
|
215
|
+
if rooms.any?
|
184
216
|
config = self.get_config
|
185
217
|
config['room'] = rooms
|
186
|
-
|
187
|
-
|
188
|
-
end
|
189
|
-
debug and (p '[Info - LightWaveRF Gem] Updated config with ' + rooms.size.to_s + ' room(s): ' + rooms.to_s)
|
218
|
+
self.put_config config
|
219
|
+
debug and ( p '[Info - LightWaveRF Gem] Updated config with ' + rooms.size.to_s + ' room(s): ' + rooms.to_s )
|
190
220
|
else
|
191
|
-
debug and (p '[Info - LightWaveRF Gem] Unable to update config: No active rooms or devices found')
|
221
|
+
debug and ( p '[Info - LightWaveRF Gem] Unable to update config: No active rooms or devices found' )
|
192
222
|
end
|
193
223
|
else
|
194
|
-
debug and (p '[Info - LightWaveRF Gem] Unable to update config: No response from Host server')
|
224
|
+
debug and ( p '[Info - LightWaveRF Gem] Unable to update config: No response from Host server' )
|
195
225
|
end
|
196
226
|
self.get_config
|
197
227
|
end
|
198
228
|
|
229
|
+
def get_rooms_from body = '', debug = nil
|
230
|
+
variables = self.get_variables_from body, debug
|
231
|
+
rooms = [ ]
|
232
|
+
# Rooms - gRoomNames is a collection of 8 values, or room names
|
233
|
+
debug and ( puts variables['gRoomStatus'].inspect )
|
234
|
+
variables['gRoomNames'].each_with_index do | roomName, roomIndex |
|
235
|
+
# Room Status - gRoomStatus is a collection of 8 values indicating the status of the corresponding room in gRoomNames
|
236
|
+
# A: Active
|
237
|
+
# I: Inactive
|
238
|
+
if variables['gRoomStatus'] and variables['gRoomStatus'][roomIndex] and variables['gRoomStatus'][roomIndex][0] == 'A'
|
239
|
+
debug and ( puts variables['gRoomStatus'][roomIndex].inspect )
|
240
|
+
# Devices - gDeviceNames is a collection of 80 values, structured in blocks of ten values for each room:
|
241
|
+
# Devices 1 - 6, Mood 1 - 3, All Off
|
242
|
+
roomDevices = [ ]
|
243
|
+
deviceNamesIndexStart = roomIndex * 10
|
244
|
+
variables['gDeviceNames'][(deviceNamesIndexStart)..(deviceNamesIndexStart+5)].each_with_index do | deviceName, deviceIndex |
|
245
|
+
# Device Status - gDeviceStatus is a collection of 80 values which indicate the status/type of the corresponding device in gDeviceNames
|
246
|
+
# O: On/Off Switch
|
247
|
+
# D: Dimmer
|
248
|
+
# R: Radiator(s)
|
249
|
+
# P: Open/Close
|
250
|
+
# I: Inactive (i.e. not configured)
|
251
|
+
# m: Mood (inactive)
|
252
|
+
# M: Mood (active)
|
253
|
+
# o: All Off
|
254
|
+
deviceStatusIndex = roomIndex * 10 + deviceIndex
|
255
|
+
if variables['gDeviceStatus'] and variables['gDeviceStatus'][deviceStatusIndex] and variables['gDeviceStatus'][deviceStatusIndex][0] != 'I'
|
256
|
+
roomDevices << deviceName
|
257
|
+
end
|
258
|
+
end
|
259
|
+
# Create a hash of the active room and active devices and add to rooms array
|
260
|
+
if roomName and roomDevices and roomDevices.any?
|
261
|
+
rooms << { 'name' => roomName, 'device' => roomDevices }
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
rooms
|
266
|
+
end
|
267
|
+
|
268
|
+
# Get variables from the source of lightwaverfhost.co.uk
|
269
|
+
# Separated out so it can be tested
|
270
|
+
#
|
271
|
+
def get_variables_from body = '', debug = nil
|
272
|
+
# debug and ( p '[Info - LightWaveRF Gem] body was ' + body.to_s )
|
273
|
+
variables = { }
|
274
|
+
# Extract JavaScript variables from the page
|
275
|
+
# var gDeviceNames = [""]
|
276
|
+
# var gDeviceStatus = [""]
|
277
|
+
# var gRoomNames = [""]
|
278
|
+
# var gRoomStatus = [""]
|
279
|
+
# http://rubular.com/r/UH0H4b4afF
|
280
|
+
body.scan( /var (gDeviceNames|gDeviceStatus|gRoomNames|gRoomStatus)\s*=\s*([^;]*)/ ).each do | variable |
|
281
|
+
if variable[0]
|
282
|
+
variables[variable[0]] = variable[1].scan /"([^"]*)\"/
|
283
|
+
end
|
284
|
+
end
|
285
|
+
debug and ( p '[Info - LightWaveRF Gem] so variables are ' + variables.inspect )
|
286
|
+
variables
|
287
|
+
end
|
288
|
+
|
199
289
|
# Get a cleaned up version of the rooms and devices from the config file
|
200
290
|
def self.get_rooms config = { 'room' => [ ]}, debug = false
|
201
291
|
rooms = { }
|
202
292
|
r = 1
|
203
293
|
config['room'].each do | room |
|
204
294
|
debug and ( puts room['name'] + ' = R' + r.to_s )
|
205
|
-
rooms[room['name']] = { 'id' => 'R' + r.to_s, 'name' => room['name'], 'device' => { }}
|
295
|
+
rooms[room['name']] = { 'id' => 'R' + r.to_s, 'name' => room['name'], 'device' => { }, 'mood' => { }, 'learnmood' => { }}
|
206
296
|
d = 1
|
207
|
-
room['device'].
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
297
|
+
unless room['device'].nil?
|
298
|
+
room['device'].each do | device |
|
299
|
+
# @todo possibly need to complicate this to get a device name back in here
|
300
|
+
debug and ( puts ' - ' + device + ' = D' + d.to_s )
|
301
|
+
rooms[room['name']]['device'][device] = 'D' + d.to_s
|
302
|
+
d += 1
|
303
|
+
end
|
304
|
+
end
|
305
|
+
m = 1
|
306
|
+
unless room['mood'].nil?
|
307
|
+
room['mood'].each do | mood |
|
308
|
+
rooms[room['name']]['mood'][mood] = 'FmP' + m.to_s
|
309
|
+
rooms[room['name']]['learnmood'][mood] = 'FsP' + m.to_s
|
310
|
+
m += 1
|
311
|
+
end
|
212
312
|
end
|
313
|
+
# add 'all off' special mood
|
314
|
+
rooms[room['name']]['mood']['alloff'] = 'Fa'
|
213
315
|
r += 1
|
214
316
|
end
|
215
317
|
rooms
|
@@ -235,6 +337,15 @@ class LightWaveRF
|
|
235
337
|
state = 'Fa'
|
236
338
|
when 'on'
|
237
339
|
state = 'F1'
|
340
|
+
# preset dim levels
|
341
|
+
when 'low'
|
342
|
+
state = 'FdP8'
|
343
|
+
when 'mid'
|
344
|
+
state = 'FdP16'
|
345
|
+
when 'high'
|
346
|
+
state = 'FdP24'
|
347
|
+
when 'full'
|
348
|
+
state = 'FdP32'
|
238
349
|
when 1..100
|
239
350
|
state = 'FdP' + ( state * 0.32 ).round.to_s
|
240
351
|
else
|
@@ -261,7 +372,7 @@ class LightWaveRF
|
|
261
372
|
'666,!' + room['id'] + room['device'][device] + state + '|Turn ' + room['name'] + ' ' + device + '|' + state + ' via @pauly'
|
262
373
|
else
|
263
374
|
'666,!' + room['id'] + state + '|Turn ' + room['name'] + '|' + state + ' via @pauly'
|
264
|
-
end
|
375
|
+
end
|
265
376
|
end
|
266
377
|
|
267
378
|
# Set the Time Zone on the LightWaveRF WiFi Link
|
@@ -278,7 +389,7 @@ class LightWaveRF
|
|
278
389
|
debug and ( puts '[Info - LightWaveRF] timezone: response is ' + data )
|
279
390
|
return (data == "666,OK\r\n")
|
280
391
|
end
|
281
|
-
|
392
|
+
|
282
393
|
# Turn one of your devices on or off or all devices in a room off
|
283
394
|
#
|
284
395
|
# Example:
|
@@ -290,18 +401,36 @@ class LightWaveRF
|
|
290
401
|
# device: (String)
|
291
402
|
# state: (String)
|
292
403
|
def send room = nil, device = nil, state = 'on', debug = false
|
293
|
-
|
404
|
+
success = false
|
405
|
+
debug and ( p 'Executing send on device: ' + device + ' in room: ' + room + ' with state: ' + state )
|
294
406
|
rooms = self.class.get_rooms self.get_config, debug
|
295
|
-
state = 'alloff' if (device.empty? and state == 'off')
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
debug and ( p 'command is ' + command )
|
300
|
-
data = self.raw command
|
301
|
-
debug and ( p 'response is ' + data )
|
407
|
+
state = 'alloff' if ( device.empty? and state == 'off' )
|
408
|
+
|
409
|
+
unless rooms[room] and state
|
410
|
+
STDERR.puts self.usage( room );
|
302
411
|
else
|
303
|
-
|
412
|
+
# support for setting state for all devices in the room (recursive)
|
413
|
+
if device == 'all'
|
414
|
+
debug and ( p 'Processing all devices...' )
|
415
|
+
rooms[room]['device'].each do | device_name, code |
|
416
|
+
debug and ( p "Device is: " + device_name )
|
417
|
+
self.send room, device_name, state, debug
|
418
|
+
sleep 1
|
419
|
+
end
|
420
|
+
success = true
|
421
|
+
# process single device
|
422
|
+
elsif state == 'alloff' || (device and rooms[room]['device'][device])
|
423
|
+
state = self.class.get_state state
|
424
|
+
command = self.command rooms[room], device, state
|
425
|
+
debug and ( p 'command is ' + command )
|
426
|
+
data = self.raw command
|
427
|
+
debug and ( p 'response is ' + data )
|
428
|
+
success = true
|
429
|
+
else
|
430
|
+
STDERR.puts self.usage( room );
|
431
|
+
end
|
304
432
|
end
|
433
|
+
success
|
305
434
|
end
|
306
435
|
|
307
436
|
# A sequence of events
|
@@ -314,11 +443,92 @@ class LightWaveRF
|
|
314
443
|
# name: (String)
|
315
444
|
# debug: (Boolean)
|
316
445
|
def sequence name, debug = false
|
446
|
+
success = true
|
317
447
|
if self.get_config['sequence'][name]
|
318
448
|
self.get_config['sequence'][name].each do | task |
|
319
|
-
|
449
|
+
if task[0] == 'pause'
|
450
|
+
debug and ( p 'Pausing for ' + task[1].to_s + ' seconds...' )
|
451
|
+
sleep task[1].to_i
|
452
|
+
debug and ( p 'Resuming...' )
|
453
|
+
elsif task[0] == 'mood'
|
454
|
+
self.mood task[1], task[2], debug
|
455
|
+
else
|
456
|
+
self.send task[0], task[1], task[2].to_s, debug
|
457
|
+
end
|
458
|
+
sleep 1
|
459
|
+
end
|
460
|
+
success = true
|
461
|
+
end
|
462
|
+
success
|
463
|
+
end
|
464
|
+
|
465
|
+
# Set a mood in one of your rooms
|
466
|
+
#
|
467
|
+
# Example:
|
468
|
+
# >> LightWaveRF.new.mood 'living', 'movie'
|
469
|
+
#
|
470
|
+
# Arguments:
|
471
|
+
# room: (String)
|
472
|
+
# mood: (String)
|
473
|
+
def mood room = nil, mood = nil, debug = false
|
474
|
+
success = false
|
475
|
+
debug and (p 'Executing mood: ' + mood + ' in room: ' + room)
|
476
|
+
#debug and ( puts 'config is ' + self.get_config.to_s )
|
477
|
+
rooms = self.class.get_rooms self.get_config
|
478
|
+
# support for setting a mood in all rooms (recursive)
|
479
|
+
if room == 'all'
|
480
|
+
debug and ( p "Processing all rooms..." )
|
481
|
+
rooms.each do | config, each_room |
|
482
|
+
room = each_room['name']
|
483
|
+
debug and ( p "Room is: " + room )
|
484
|
+
success = self.mood room, mood, debug
|
320
485
|
sleep 1
|
321
486
|
end
|
487
|
+
success = true
|
488
|
+
# process single mood
|
489
|
+
else
|
490
|
+
if rooms[room] and mood
|
491
|
+
if rooms[room]['mood'][mood]
|
492
|
+
command = self.command rooms[room], nil, rooms[room]['mood'][mood]
|
493
|
+
debug and ( p 'command is ' + command )
|
494
|
+
self.raw command
|
495
|
+
success = true
|
496
|
+
# support for special "moods" via device looping
|
497
|
+
elsif mood[0,3] == 'all'
|
498
|
+
state = mood[3..-1]
|
499
|
+
debug and (p 'Selected state is: ' + state)
|
500
|
+
rooms[room]['device'].each do | device |
|
501
|
+
p 'Processing device: ' + device[0]
|
502
|
+
self.send room, device[0], state, debug
|
503
|
+
sleep 1
|
504
|
+
end
|
505
|
+
success = true
|
506
|
+
end
|
507
|
+
else
|
508
|
+
STDERR.puts self.usage( room );
|
509
|
+
end
|
510
|
+
end
|
511
|
+
success
|
512
|
+
end
|
513
|
+
|
514
|
+
# Learn a mood in one of your rooms
|
515
|
+
#
|
516
|
+
# Example:
|
517
|
+
# >> LightWaveRF.new.learnmood 'living', 'movie'
|
518
|
+
#
|
519
|
+
# Arguments:
|
520
|
+
# room: (String)
|
521
|
+
# mood: (String)
|
522
|
+
def learnmood room = nil, mood = nil, debug = false
|
523
|
+
debug and (p 'Learning mood: ' + mood)
|
524
|
+
#debug and ( puts 'config is ' + self.get_config.to_s )
|
525
|
+
rooms = self.class.get_rooms self.get_config
|
526
|
+
if rooms[room] and mood and rooms[room]['learnmood'][mood]
|
527
|
+
command = self.command rooms[room], nil, rooms[room]['learnmood'][mood]
|
528
|
+
debug and ( p 'command is ' + command )
|
529
|
+
self.raw command
|
530
|
+
else
|
531
|
+
STDERR.puts self.usage( room )
|
322
532
|
end
|
323
533
|
end
|
324
534
|
|
@@ -336,8 +546,7 @@ class LightWaveRF
|
|
336
546
|
data['message']['annotation'] = { 'title' => title.to_s, 'text' => note.to_s }
|
337
547
|
end
|
338
548
|
debug and ( p data )
|
339
|
-
|
340
|
-
File.open( self.get_log_file, 'a' ) do |f|
|
549
|
+
File.open( self.get_log_file, 'a' ) do | f |
|
341
550
|
f.write( data.to_json + "\n" )
|
342
551
|
end
|
343
552
|
data['message']
|
@@ -348,7 +557,7 @@ class LightWaveRF
|
|
348
557
|
response = nil
|
349
558
|
# Get host address or broadcast address
|
350
559
|
host = self.get_config['host'] || '255.255.255.255'
|
351
|
-
# Create socket
|
560
|
+
# Create socket
|
352
561
|
listener = UDPSocket.new
|
353
562
|
# Add broadcast socket options if necessary
|
354
563
|
if (host == '255.255.255.255')
|
@@ -372,81 +581,370 @@ class LightWaveRF
|
|
372
581
|
response
|
373
582
|
end
|
374
583
|
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
# interval: (Integer)
|
391
|
-
# debug: (Boolean)
|
392
|
-
#
|
393
|
-
def timer interval = 5, debug = false
|
394
|
-
require 'net/http'
|
395
|
-
require 'rexml/document'
|
396
|
-
url = LightWaveRF.new.get_config['calendar'] + '?singleevents=true&start-min=' + Date.today.strftime( '%Y-%m-%d' ) + '&start-max=' + Date.today.next.strftime( '%Y-%m-%d' )
|
584
|
+
def update_timers past = 60, future = 1440, debug = false
|
585
|
+
p '----------------'
|
586
|
+
p "Updating timers..."
|
587
|
+
|
588
|
+
# determine the window to query
|
589
|
+
now = Time.new
|
590
|
+
query_start = now - self.class.to_seconds( past )
|
591
|
+
query_end = now + self.class.to_seconds( future )
|
592
|
+
|
593
|
+
url = LightWaveRF.new.get_config['calendar']
|
594
|
+
# url += '?ctz=' + Time.new.zone
|
595
|
+
url += '?ctz=UTC'
|
596
|
+
url += '&singleevents=true'
|
597
|
+
url += '&start-min=' + query_start.strftime( '%FT%T%:z' ).sub('+', '%2B')
|
598
|
+
url += '&start-max=' + query_end.strftime( '%FT%T%:z' ).sub('+', '%2B')
|
397
599
|
debug and ( p url )
|
398
600
|
parsed_url = URI.parse url
|
399
601
|
http = Net::HTTP.new parsed_url.host, parsed_url.port
|
400
602
|
begin
|
401
603
|
http.use_ssl = true
|
402
604
|
rescue
|
403
|
-
debug and ( p 'cannot use ssl' )
|
605
|
+
debug and ( p 'cannot use ssl, tried ' + parsed_url.host + ', ' + parsed_url.port.to_s )
|
606
|
+
url.gsub! 'https:', 'http:'
|
607
|
+
debug and ( p 'so fetching ' + url )
|
608
|
+
parsed_url = URI.parse url
|
609
|
+
http = Net::HTTP.new parsed_url.host
|
404
610
|
end
|
405
611
|
request = Net::HTTP::Get.new parsed_url.request_uri
|
406
612
|
response = http.request request
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
#
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
613
|
+
|
614
|
+
# if we get a good response
|
615
|
+
debug and ( p "Response code is: " + response.code)
|
616
|
+
if response.code == '200'
|
617
|
+
debug and ( p "Retrieved calendar ok")
|
618
|
+
doc = REXML::Document.new response.body
|
619
|
+
now = Time.now.strftime '%H:%M'
|
620
|
+
|
621
|
+
events = [ ]
|
622
|
+
states = [ ]
|
623
|
+
|
624
|
+
# refresh the list of entries for the caching period
|
625
|
+
doc.elements.each 'feed/entry' do | e |
|
626
|
+
debug and ( p "-------------------")
|
627
|
+
debug and ( p "Processing entry...")
|
628
|
+
event = Hash.new
|
629
|
+
|
630
|
+
# tokenise the title
|
631
|
+
debug and ( p "Event title is: " + e.elements['title'].text )
|
632
|
+
command = e.elements['title'].text.split
|
633
|
+
command_length = command.length
|
634
|
+
debug and ( p "Number of words is: " + command_length.to_s )
|
635
|
+
if command and command.length >= 1
|
636
|
+
first_word = command[0].to_s
|
637
|
+
# determine the type of the entry
|
638
|
+
if first_word[0,1] == '#'
|
639
|
+
debug and ( p "Type is: state" )
|
640
|
+
event['type'] = 'state' # temporary type, will be overridden later
|
641
|
+
event['room'] = nil
|
642
|
+
event['device'] = nil
|
643
|
+
event['state'] = first_word[1..-1].to_s
|
644
|
+
modifier_start = command_length # can't have modifiers on states
|
645
|
+
else
|
646
|
+
case first_word
|
647
|
+
when 'mood'
|
648
|
+
debug and ( p "Type is: mood" )
|
649
|
+
event['type'] = 'mood'
|
650
|
+
event['room'] = command[1].to_s
|
651
|
+
event['device'] = nil
|
652
|
+
event['state'] = command[2].to_s
|
653
|
+
modifier_start = 3
|
654
|
+
when 'sequence'
|
655
|
+
debug and ( p "Type is: sequence" )
|
656
|
+
event['type'] = 'sequence'
|
657
|
+
event['room'] = nil
|
658
|
+
event['device'] = nil
|
659
|
+
event['state'] = command[1].to_s
|
660
|
+
modifier_start = 2
|
661
|
+
else
|
662
|
+
debug and ( p "Type is: device" )
|
663
|
+
event['type'] = 'device'
|
664
|
+
event['room'] = command[0].to_s
|
665
|
+
event['device'] = command[1].to_s
|
666
|
+
# handle optional state
|
667
|
+
if command_length > 2
|
668
|
+
third_word = command[2].to_s
|
669
|
+
first_char = third_word[0,1]
|
670
|
+
debug and ( p "First char is: " + first_char )
|
671
|
+
# if the third word does not start with a modifier flag, assume it's a state
|
672
|
+
if first_char != '@' and first_char != '!' and first_char != '+' and first_char != '-'
|
673
|
+
debug and ( p "State has been given.")
|
674
|
+
event['state'] = command[2].to_s
|
675
|
+
modifier_start = 3
|
676
|
+
else
|
677
|
+
debug and ( p "State has not been given." )
|
678
|
+
modifier_start = 2
|
679
|
+
end
|
680
|
+
else
|
681
|
+
debug and ( p "State has not been given." )
|
682
|
+
event['state'] = nil
|
683
|
+
modifier_start = 2
|
684
|
+
end
|
685
|
+
end
|
686
|
+
end
|
687
|
+
|
688
|
+
# get modifiers if they exist
|
689
|
+
time_modifier = 0
|
690
|
+
if command_length > modifier_start
|
691
|
+
debug and ( p "May have modifiers..." )
|
692
|
+
when_modifiers = Array.new
|
693
|
+
unless_modifiers = Array.new
|
694
|
+
modifier_count = command_length - modifier_start
|
695
|
+
debug and ( p "Count of modifiers is " + modifier_count.to_s )
|
696
|
+
for i in modifier_start..(command_length-1)
|
697
|
+
modifier = command[i]
|
698
|
+
if modifier[0,1] == '@'
|
699
|
+
debug and ( p "Found when modifier: " + modifier[1..-1] )
|
700
|
+
when_modifiers.push modifier[1..-1]
|
701
|
+
elsif modifier[0,1] == '!'
|
702
|
+
debug and ( p "Found unless modifier: " + modifier[1..-1] )
|
703
|
+
unless_modifiers.push modifier[1..-1]
|
704
|
+
elsif modifier[0,1] == '+'
|
705
|
+
debug and ( p "Found positive time modifier: " + modifier[1..-1] )
|
706
|
+
time_modifier = modifier[1..-1].to_i
|
707
|
+
elsif modifier[0,1] == '-'
|
708
|
+
debug and ( p "Found negative time modifier: " + modifier[1..-1] )
|
709
|
+
time_modifier = modifier[1..-1].to_i * -1
|
710
|
+
end
|
711
|
+
end
|
712
|
+
# add when/unless modifiers to the event
|
713
|
+
event['when_modifiers'] = when_modifiers
|
714
|
+
event['unless_modifiers'] = unless_modifiers
|
715
|
+
end
|
716
|
+
|
717
|
+
# parse the date string
|
718
|
+
debug and ( p "Time string is: " + e.elements['summary'].text)
|
719
|
+
event_time = /When: ([\w ]+) (\d\d:\d\d) to ([\w ]+)?(\d\d:\d\d) \n(.*)<br>(.*)/.match e.elements['summary'].text
|
720
|
+
debug and ( p "Event times are: " + event_time.to_s )
|
721
|
+
start_date = event_time[1].to_s
|
722
|
+
start_time = event_time[2].to_s
|
723
|
+
end_date = event_time[3].to_s
|
724
|
+
end_time = event_time[4].to_s
|
725
|
+
timezone = event_time[5].to_s
|
726
|
+
if end_date == '' or end_date.nil? # copy start date to end date if it wasn't given (as the same date)
|
727
|
+
end_date = start_date
|
728
|
+
end
|
729
|
+
debug and ( p "Start date: " + start_date)
|
730
|
+
debug and ( p "Start time: " + start_time)
|
731
|
+
debug and ( p "End date: " + end_date)
|
732
|
+
debug and ( p "End time: " + end_time)
|
733
|
+
debug and ( p "Timezone: " + timezone)
|
734
|
+
|
735
|
+
# convert to datetimes
|
736
|
+
start_dt = DateTime.parse(start_date.strip + ' ' + start_time.strip + ' ' + timezone.strip)
|
737
|
+
end_dt = DateTime.parse(end_date.strip + ' ' + end_time.strip + ' ' + timezone.strip)
|
738
|
+
|
739
|
+
# apply time modifier if it exists
|
740
|
+
if time_modifier != 0
|
741
|
+
debug and ( p "Adjusting timings by: " + time_modifier.to_s)
|
742
|
+
start_dt = ((start_dt.to_time) + time_modifier*60).to_datetime
|
743
|
+
end_dt = ((end_dt.to_time) + time_modifier*60).to_datetime
|
744
|
+
end
|
745
|
+
|
746
|
+
debug and ( p "Start datetime: " + start_dt.to_s)
|
747
|
+
debug and ( p "End datetime: " + end_dt.to_s)
|
748
|
+
|
749
|
+
# populate the dates
|
750
|
+
event['date'] = start_dt
|
751
|
+
# handle device entries without explicit on/off state
|
752
|
+
if event['type'] == 'device' and ( event['state'].nil? or ( event['state'] != 'on' and event['state'] != 'off' ))
|
753
|
+
debug and ( p "Duplicating event without explicit on/off state...")
|
754
|
+
# if not state was given, assume we meant 'on'
|
755
|
+
if event['state'].nil?
|
756
|
+
event['state'] = 'on'
|
757
|
+
end
|
758
|
+
end_event = event.dup # duplicate event for start and end
|
759
|
+
end_event['date'] = end_dt
|
760
|
+
end_event['state'] = 'off'
|
761
|
+
events.push event
|
762
|
+
events.push end_event
|
763
|
+
# create state plus start and end events if a state
|
764
|
+
elsif event['type'] == 'state'
|
765
|
+
debug and ( p "Processing state : " + event['state'])
|
766
|
+
# create state
|
767
|
+
state = Hash.new
|
768
|
+
state['name'] = event['state']
|
769
|
+
state['start'] = start_dt.dup
|
770
|
+
state['end'] = end_dt.dup
|
771
|
+
states.push state
|
772
|
+
# convert event to start and end sequence
|
773
|
+
event['type'] = 'sequence'
|
774
|
+
event['state'] = state['name'] + '_start'
|
775
|
+
end_event = event.dup # duplicate event for start and end
|
776
|
+
end_event['date'] = end_dt
|
777
|
+
end_event['state'] = state['name'] + '_end'
|
778
|
+
events.push event
|
779
|
+
events.push end_event
|
780
|
+
# else just add the event
|
781
|
+
else
|
782
|
+
events.push event
|
783
|
+
end
|
784
|
+
|
429
785
|
end
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
786
|
+
|
787
|
+
end
|
788
|
+
|
789
|
+
# record some timestamps
|
790
|
+
info = { }
|
791
|
+
info['updated_at'] = Time.new.strftime( '%FT%T%:z' )
|
792
|
+
info['start_time'] = query_start.strftime( '%FT%T%:z' )
|
793
|
+
info['end_time'] = query_end.strftime( '%FT%T%:z' )
|
794
|
+
|
795
|
+
# build final timer config
|
796
|
+
timers = { }
|
797
|
+
timers['info'] = info
|
798
|
+
timers['events'] = events
|
799
|
+
timers['states'] = states
|
800
|
+
|
801
|
+
p 'Timer list is: ' + YAML.dump( timers )
|
802
|
+
|
803
|
+
# store the list
|
804
|
+
put_timer_cache timers
|
805
|
+
self.log_timer_event 'update', nil, nil, nil, true
|
806
|
+
|
807
|
+
else
|
808
|
+
self.log_timer_event 'update', nil, nil, nil, false
|
809
|
+
end
|
810
|
+
end
|
811
|
+
|
812
|
+
# Convert a string to seconds, assume it is in minutes
|
813
|
+
def self.to_seconds interval = 0
|
814
|
+
match = /^(\d+)([shd])$/.match( interval.to_s )
|
815
|
+
if match
|
816
|
+
case match[2]
|
817
|
+
when 's'
|
818
|
+
return match[1].to_i
|
819
|
+
when 'h'
|
820
|
+
return match[1].to_i * 3600
|
821
|
+
when 'd'
|
822
|
+
return match[1].to_i * 86400
|
823
|
+
end
|
824
|
+
end
|
825
|
+
return interval.to_i * 60
|
826
|
+
end
|
827
|
+
|
828
|
+
def run_timers interval = 5, debug = false
|
829
|
+
p '----------------'
|
830
|
+
p "Running timers..."
|
831
|
+
get_timer_cache
|
832
|
+
debug and ( p 'Timer list is: ' + YAML.dump( @timers ))
|
833
|
+
|
834
|
+
# get the current time and end interval time
|
835
|
+
now = Time.new
|
836
|
+
start_tm = now - now.sec
|
837
|
+
end_tm = start_tm + self.class.to_seconds( interval )
|
838
|
+
|
839
|
+
# convert to datetimes
|
840
|
+
start_horizon = DateTime.parse start_tm.to_s
|
841
|
+
end_horizon = DateTime.parse end_tm.to_s
|
842
|
+
p '----------------'
|
843
|
+
p 'Start horizon is: ' + start_horizon.to_s
|
844
|
+
p 'End horizon is: ' + end_horizon.to_s
|
845
|
+
|
846
|
+
# sort the events and states (to guarantee order if longer intervals are used)
|
847
|
+
@timers['events'].sort! { | x, y | x['date'] <=> y['date'] }
|
848
|
+
@timers['states'].sort! { | x, y | x['date'] <=> y['date'] }
|
849
|
+
|
850
|
+
# array to hold events that should be executed this run
|
851
|
+
run_list = [ ]
|
852
|
+
|
853
|
+
# process each event
|
854
|
+
@timers['events'].each do | event |
|
855
|
+
debug and ( p '----------------' )
|
856
|
+
debug and ( p 'Processing event: ' + event.to_s )
|
857
|
+
debug and ( p 'Event time is: ' + event['date'].to_s )
|
858
|
+
|
859
|
+
# first, assume we'll not be running the event
|
860
|
+
run_now = false
|
861
|
+
|
862
|
+
# check that it is in the horizon time
|
863
|
+
unless event['date'] >= start_horizon and event['date'] < end_horizon
|
864
|
+
debug and ( p 'Event is NOT in horizon...ignoring')
|
865
|
+
else
|
866
|
+
debug and ( p 'Event is in horizon...')
|
867
|
+
run_now = true
|
868
|
+
|
869
|
+
# if has modifiers, check modifiers against states
|
870
|
+
unless event['when_modifiers'].nil?
|
871
|
+
debug and ( p 'Event has when modifiers. Checking they are all met...')
|
872
|
+
|
873
|
+
# determine which states apply at the time of the event
|
874
|
+
applicable_states = Array.new
|
875
|
+
@timers['states'].each do | state |
|
876
|
+
if event['date'] >= state['start'] and event['date'] < state['end']
|
877
|
+
applicable_states.push state['name']
|
878
|
+
end
|
437
879
|
end
|
880
|
+
debug and ( p 'Applicable states are: ' + applicable_states.to_s)
|
881
|
+
|
882
|
+
# check that each when modifier exists in appliable states
|
883
|
+
event['when_modifiers'].each do | modifier |
|
884
|
+
unless applicable_states.include? modifier
|
885
|
+
debug and ( p 'Event when modifier not met: ' + modifier)
|
886
|
+
run_now = false
|
887
|
+
break
|
888
|
+
end
|
889
|
+
end
|
890
|
+
|
891
|
+
# check that each unless modifier does not exist in appliable states
|
892
|
+
event['unless_modifiers'].each do | modifier |
|
893
|
+
if applicable_states.include? modifier
|
894
|
+
debug and ( p 'Event unless modifier not met: ' + modifier)
|
895
|
+
run_now = false
|
896
|
+
break
|
897
|
+
end
|
898
|
+
end
|
899
|
+
end
|
900
|
+
|
901
|
+
# if we have determined the event should run, add to the run list
|
902
|
+
if run_now
|
903
|
+
run_list.push event
|
438
904
|
end
|
439
905
|
end
|
440
906
|
end
|
441
|
-
|
907
|
+
|
908
|
+
# process the run list
|
909
|
+
p '-----------------------'
|
910
|
+
p 'Events to execute this run are: ' + run_list.to_s
|
911
|
+
|
912
|
+
triggered = [ ]
|
913
|
+
|
914
|
+
run_list.each do | event |
|
915
|
+
# execute based on type
|
916
|
+
case event['type']
|
917
|
+
when 'mood'
|
918
|
+
p 'Executing mood. Room: ' + event['room'] + ', Mood: ' + event['state']
|
919
|
+
result = self.mood event['room'], event['state'], debug
|
920
|
+
sleep 1
|
921
|
+
triggered << [ event['room'], event['device'], event['state'] ]
|
922
|
+
when 'sequence'
|
923
|
+
p 'Executing sequence. Sequence: ' + event['state']
|
924
|
+
result = self.sequence event['state'], debug
|
925
|
+
sleep 1
|
926
|
+
triggered << [ event['room'], event['device'], event['state'] ]
|
927
|
+
else
|
928
|
+
p 'Executing device. Room: ' + event['room'] + ', Device: ' + event['device'] + ', State: ' + event['state']
|
929
|
+
result = self.send event['room'], event['device'], event['state'], debug
|
930
|
+
sleep 1
|
931
|
+
triggered << [ event['room'], event['device'], event['state'] ]
|
932
|
+
end
|
933
|
+
self.log_timer_event event['type'], event['room'], event['device'], event['state'], result
|
934
|
+
end
|
935
|
+
|
936
|
+
# update energy log
|
442
937
|
title = nil
|
443
938
|
text = nil
|
444
939
|
if triggered.length > 0
|
445
940
|
debug and ( p triggered.length.to_s + ' events so annotating energy log too...' )
|
446
941
|
title = 'timer'
|
447
|
-
text = triggered.map { |e| e.join " " }.join ", "
|
942
|
+
text = triggered.map { | e | e.join " " }.join ", "
|
448
943
|
end
|
449
944
|
self.energy title, text, debug
|
945
|
+
|
946
|
+
self.log_timer_event 'run', nil, nil, nil, true
|
450
947
|
end
|
948
|
+
|
451
949
|
end
|
452
950
|
|
metadata
CHANGED
@@ -1,20 +1,53 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lightwaverf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Paul Clarke
|
9
9
|
- Ian Perrin
|
10
|
+
- Julian McLean
|
10
11
|
autorequire:
|
11
12
|
bindir: bin
|
12
13
|
cert_chain: []
|
13
|
-
date: 2013-
|
14
|
-
dependencies:
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
date: 2013-06-22 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: htmlentities
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ! '>='
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '0'
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - ! '>='
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: '0'
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: json
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
description: ! " Interact with lightwaverf wifi-link from code or the command line.\n
|
49
|
+
\ Control your lights, heating, sockets etc.\n Also set up timers using a google
|
50
|
+
calendar and log energy usage.\n"
|
18
51
|
email: pauly@clarkeology.com
|
19
52
|
executables:
|
20
53
|
- lightwaverf
|
@@ -48,5 +81,5 @@ rubyforge_project:
|
|
48
81
|
rubygems_version: 1.8.23
|
49
82
|
signing_key:
|
50
83
|
specification_version: 3
|
51
|
-
summary: Home automation
|
84
|
+
summary: Home automation with lightwaverf
|
52
85
|
test_files: []
|