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.
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: []