lightwaverf 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/bin/lightwaverf +10 -2
  2. data/lib/lightwaverf.rb +644 -146
  3. 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.timer ARGV[1], ARGV[2]
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], ARGV[3]
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
- config['host'] = host
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
- if ! config['room']
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
- # Extract JavaScript variables from the page
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 and rooms.any?
215
+ if rooms.any?
184
216
  config = self.get_config
185
217
  config['room'] = rooms
186
- File.open( self.get_config_file, 'w' ) do | handle |
187
- handle.write YAML.dump( config )
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'].each do | device |
208
- # @todo possibly need to complicate this to get a device name back in here
209
- debug and ( puts ' - ' + device + ' = D' + d.to_s )
210
- rooms[room['name']]['device'][device] = 'D' + d.to_s
211
- d += 1
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
- debug and ( puts 'config is ' + self.get_config.to_s )
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
- state = self.class.get_state state
297
- if rooms[room] and state and (state == 'Fa' || (device and rooms[room]['device'][device]))
298
- command = self.command rooms[room], device, state
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
- STDERR.puts self.usage
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
- self.send task[0], task[1], task[2], debug
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
- require 'json'
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
- # Use a google calendar as a timer?
376
- # Needs a google calendar, with its url in your config file, with events like "lounge light on" etc
377
- #
378
- # Run this as a cron job every 5 mins, ie
379
- # */5 * * * * /usr/local/bin/lightwaverf timer 5 > /tmp/timer.out 2>&1
380
- #
381
- # Example:
382
- # >> LightWaveRF.new.timer
383
- # >> LightWaveRF.new.state 10
384
- #
385
- # Sample calendar:
386
- # https://www.google.com/calendar/feeds/aar79qh62fej54nprq6334s7ck%40group.calendar.google.com/public/basic
387
- # https://www.google.com/calendar/embed?src=aar79qh62fej54nprq6334s7ck%40group.calendar.google.com&ctz=Europe/London
388
- #
389
- # Arguments:
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
- doc = REXML::Document.new response.body
408
- now = Time.now.strftime '%H:%M'
409
- interval_end_time = ( Time.now + interval.to_i * 60 ).strftime '%H:%M'
410
- triggered = []
411
- doc.elements.each 'feed/entry' do | e |
412
- command = /(\w+) (\w+)( (\w+))?/.match e.elements['title'].text # look for events with a title like 'lounge light on'
413
- if command
414
- room = command[1].to_s
415
- device = command[2].to_s
416
- status = command[4]
417
- timer = /When: ([\w ]+) (\d\d:\d\d) to ([\w ]+)?(\d\d:\d\d)/.match e.elements['summary'].text
418
- if timer
419
- event_time = timer[2].to_s
420
- event_end_time = timer[4]
421
- else
422
- STDERR.puts 'did not get When: in ' + e.elements['summary'].text
423
- end
424
- # @todo fix events that start and end in this period
425
- if status
426
- event_times = { event_time => status }
427
- else
428
- event_times = { event_time => 'on', event_end_time => 'off' }
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)&nbsp;\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
- event_times.each do | t, s |
431
- debug and ( p e.elements['title'].text + ' - ' + now + ' < ' + t + ' < ' + interval_end_time + ' ?' )
432
- if t >= now and t < interval_end_time
433
- debug and ( p 'so going to turn the ' + room + ' ' + device + ' ' + s.to_s + ' now!' )
434
- self.send room, device, s.to_s
435
- sleep 1
436
- triggered << [ room, device, s ]
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
- triggered.length.to_s + " events triggered"
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.3.3
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-04-17 00:00:00.000000000 Z
14
- dependencies: []
15
- description: Interact with lightwaverf wifi link from code or the command line. Control
16
- your lights, heating, sockets etc. Also set up timers using a google calendar and
17
- log energy usage.
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: []