saal 0.2.2

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.
@@ -0,0 +1,53 @@
1
+ module SAAL
2
+ class OutlierCache
3
+ # By feeding values into this cache the outliers are identified. The cache
4
+ # is conservative and only flags down values that it is sure are outliers.
5
+ # The cache considers itself "live" when the values in the cache are all
6
+ # within a few percent of each other and will then flag down outliers. When
7
+ # the cache is not live all values will be considered good.
8
+
9
+ COMP_CACHE_SIZE = 11 # Should be even so the median is well calculated
10
+
11
+ # These are conservative settings that can be made stricted if the cache is
12
+ # not rejecting enough values or is often not "live"
13
+ # Sets how close the central values have to be for the cache to be "live"
14
+ MAX_CACHE_DEVIATION = 0.05
15
+ # Sets how off the read value can be from the cache median to be accepted
16
+ MAX_VALUE_DEVIATION = 0.25
17
+
18
+ def initialize
19
+ @compcache = []
20
+ end
21
+
22
+ def live
23
+ @compcache.size == COMP_CACHE_SIZE && valid_cache
24
+ end
25
+
26
+ def validate(value)
27
+ ret = compare_with_cache(value)
28
+ @compcache.shift if @compcache.size == COMP_CACHE_SIZE
29
+ @compcache.push value
30
+ ret
31
+ end
32
+
33
+ private
34
+ def compare_with_cache(value)
35
+ return true if !live
36
+ @compcache.sort!
37
+ median = @compcache[COMP_CACHE_SIZE/2]
38
+ (value.to_f/median.to_f - 1.0).abs < MAX_VALUE_DEVIATION
39
+ end
40
+
41
+ def valid_cache
42
+ @compcache.sort!
43
+ central = @compcache[1..(@compcache.size-2)]
44
+ sum = central.inject(0.0){|sum,el| sum+el}
45
+ return false if sum == 0.0
46
+ average = sum/central.size
47
+ central.each do |el|
48
+ return false if (el.to_f/average.to_f - 1.0).abs > MAX_CACHE_DEVIATION
49
+ end
50
+ true
51
+ end
52
+ end
53
+ end
data/lib/owsensor.rb ADDED
@@ -0,0 +1,21 @@
1
+ module SAAL
2
+ class OWSensor < SensorUnderlying
3
+ attr_reader :serial
4
+ def initialize(defs, opts={})
5
+ @serial = defs['serial']
6
+ @connect_opts = {}
7
+ @connect_opts[:server] = defs['server'] if defs['server']
8
+ @connect_opts[:port] = defs['port'] if defs['port']
9
+ @owconn = opts[:owconn]
10
+ end
11
+
12
+ def read(uncached = false)
13
+ @owconn ||= OWNet::Connection.new(@connect_opts)
14
+ begin
15
+ @owconn.read((uncached ? '/uncached' : '')+@serial)
16
+ rescue Exception
17
+ nil
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/saal.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'yaml'
2
+ require "mysql"
3
+ require 'ownet'
4
+ require 'nokogiri'
5
+ require 'erb'
6
+
7
+ module SAAL
8
+ CONFDIR = "/etc/saal/"
9
+ SENSORSCONF = CONFDIR+"sensors.yml"
10
+ DBCONF = CONFDIR+"database.yml"
11
+
12
+ VERSION = '0.2.2'
13
+ end
14
+
15
+ require File.dirname(__FILE__)+'/dbstore.rb'
16
+ require File.dirname(__FILE__)+'/sensors.rb'
17
+ require File.dirname(__FILE__)+'/sensor.rb'
18
+ require File.dirname(__FILE__)+'/owsensor.rb'
19
+ require File.dirname(__FILE__)+'/daemon.rb'
20
+ require File.dirname(__FILE__)+'/chart_data.rb'
21
+ require File.dirname(__FILE__)+'/outliercache.rb'
22
+ require File.dirname(__FILE__)+'/dinrelay.rb'
23
+
data/lib/sensor.rb ADDED
@@ -0,0 +1,100 @@
1
+ module SAAL
2
+ class UnimplementedMethod < RuntimeError
3
+ end
4
+
5
+ class SensorUnderlying
6
+ def writeable?; false; end
7
+ def self.writeable!
8
+ define_method(:writeable?){true}
9
+ end
10
+ end
11
+
12
+ class Sensor
13
+ MAX_READ_TRIES = 5
14
+
15
+ attr_reader :name, :description
16
+ def initialize(dbstore, name, underlying, defs, opts={})
17
+ @dbstore = dbstore
18
+ @name = name
19
+ @underlying = underlying
20
+ @description = defs['name']
21
+ @mock_opts = {}
22
+
23
+ # Reading correction settings
24
+ @max_value = defs['max_value']
25
+ @max_correctable = defs['max_correctable']
26
+ @min_value = defs['min_value']
27
+ @min_correctable = defs['min_correctable']
28
+
29
+ # Outliercache
30
+ @outliercache = opts[:no_outliercache] ? nil : OutlierCache.new
31
+ end
32
+
33
+ def writeable?
34
+ @underlying.writeable?
35
+ end
36
+
37
+ def read
38
+ outlier_proof_read(false)
39
+ end
40
+
41
+ def read_uncached
42
+ outlier_proof_read(true)
43
+ end
44
+
45
+ def write(value)
46
+ if @mock_opts[:value]
47
+ @mock_opts[:value] = value
48
+ else
49
+ @underlying.write(value)
50
+ end
51
+ end
52
+
53
+ def average(from, to)
54
+ return @mock_opts[:average] if @mock_opts[:average]
55
+ @dbstore.average(@name, from, to)
56
+ end
57
+
58
+ def minimum(from, to)
59
+ return @mock_opts[:minimum] if @mock_opts[:minimum]
60
+ @dbstore.minimum(@name, from, to)
61
+ end
62
+
63
+ def maximum(from, to)
64
+ return @mock_opts[:maximum] if @mock_opts[:maximum]
65
+ @dbstore.maximum(@name, from, to)
66
+ end
67
+
68
+ def store_value
69
+ value = read_uncached
70
+ @dbstore.write(@name, Time.now.utc.to_i, value) if value
71
+ end
72
+
73
+ def mock_set(opts)
74
+ @mock_opts.merge!(opts)
75
+ end
76
+
77
+ private
78
+ def outlier_proof_read(uncached)
79
+ return @mock_opts[:value] if @mock_opts[:value]
80
+ tries = 0
81
+ value = nil
82
+ begin
83
+ tries += 1
84
+ value = @underlying.read(uncached)
85
+ break if value && @outliercache && @outliercache.validate(value)
86
+ end while tries < MAX_READ_TRIES
87
+ normalize(value)
88
+ end
89
+
90
+ def normalize(value)
91
+ if @max_value and value > @max_value
92
+ (@max_correctable and value <= @max_correctable) ? @max_value : nil
93
+ elsif @min_value and value < @min_value
94
+ (@min_correctable and value >= @min_correctable) ? @min_value : nil
95
+ else
96
+ value
97
+ end
98
+ end
99
+ end
100
+ end
data/lib/sensors.rb ADDED
@@ -0,0 +1,48 @@
1
+ module SAAL
2
+ class UnknownSensorType < RuntimeError
3
+ end
4
+
5
+ class Sensors
6
+ include Enumerable
7
+
8
+ def initialize(conffile=SAAL::SENSORSCONF, dbconffile=SAAL::DBCONF)
9
+ @defs = YAML::load(File.new(conffile))
10
+ @dbstore = DBStore.new(dbconffile)
11
+ @sensors = {}
12
+ @defs.each do |name, defs|
13
+ self.class.sensors_from_defs(@dbstore, name, defs).each{|s| @sensors[s.name] = s}
14
+ end
15
+
16
+ end
17
+
18
+ # Implements the get methods to fetch a specific sensor
19
+ def method_missing(name, *args)
20
+ name = name.to_s
21
+ if args.size == 0 && @sensors.include?(name)
22
+ @sensors[name]
23
+ else
24
+ raise NoMethodError, "undefined method \"#{name}\" for #{self}"
25
+ end
26
+ end
27
+
28
+ def each
29
+ @sensors.each{|name, sensor| yield sensor}
30
+ end
31
+
32
+ def self.sensors_from_defs(dbstore, name, defs, opts={})
33
+ if defs['onewire']
34
+ return [Sensor.new(dbstore, name, OWSensor.new(defs['onewire'], opts),
35
+ defs, opts)]
36
+ elsif defs['dinrelay']
37
+ og = DINRelay::OutletGroup.new(defs['dinrelay'])
38
+ outlet_names = defs['dinrelay']['outlets'] || []
39
+ return outlet_names.map do |num, oname|
40
+ Sensor.new(dbstore, oname, DINRelay::Outlet.new(num.to_i, og), defs, opts)
41
+ end
42
+ else
43
+ raise UnknownSensorType, "Couldn't figure out a valid sensor type "
44
+ "from the configuration for #{name}"
45
+ end
46
+ end
47
+ end
48
+ end
data/saal.gemspec ADDED
@@ -0,0 +1,77 @@
1
+ Gem::Specification.new do |s|
2
+ s.specification_version = 2 if s.respond_to? :specification_version=
3
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
+ s.rubygems_version = '1.3.5'
5
+
6
+ s.platform = Gem::Platform::RUBY
7
+
8
+ s.name = 'saal'
9
+ s.version = '0.2.2'
10
+ s.date = '2010-12-29'
11
+
12
+ s.summary = "Thin abstraction layer for interfacing and recording sensors (currently onewire) and actuators (currently dinrelay)"
13
+ s.description = <<EOF
14
+ A daemon and libraries to create an abstraction layer that interfaces with
15
+ sensors and actuators, recording their state, responding to requests
16
+ for current and historical values, and allowing changes of state.
17
+ EOF
18
+
19
+ s.authors = ["Pedro Côrte-Real"]
20
+ s.email = 'pedro@pedrocr.net'
21
+ s.homepage = 'https://github.com/pedrocr/saal'
22
+
23
+ s.require_paths = %w[lib]
24
+
25
+ s.has_rdoc = true
26
+ s.rdoc_options = ['-S', '-w 2', '-N', '-c utf8']
27
+ s.extra_rdoc_files = %w[README.rdoc LICENSE]
28
+
29
+ s.executables = Dir.glob("bin/*").map{|f| f.gsub('bin/','')}
30
+
31
+ s.add_dependency('ownet', [">= 0.1.0"])
32
+ s.add_dependency('nokogiri')
33
+ s.add_dependency('mysql')
34
+
35
+ # = MANIFEST =
36
+ s.files = %w[
37
+ LICENSE
38
+ README.rdoc
39
+ Rakefile
40
+ TODO
41
+ bin/.gitignore
42
+ bin/dinrelayset
43
+ bin/dinrelaystatus
44
+ bin/saal_chart
45
+ bin/saal_daemon
46
+ bin/saal_dump_database
47
+ bin/saal_import_mysql
48
+ bin/saal_readall
49
+ lib/chart_data.rb
50
+ lib/daemon.rb
51
+ lib/dbstore.rb
52
+ lib/dinrelay.rb
53
+ lib/outliercache.rb
54
+ lib/owsensor.rb
55
+ lib/saal.rb
56
+ lib/sensor.rb
57
+ lib/sensors.rb
58
+ saal.gemspec
59
+ test/chart_data_test.rb
60
+ test/daemon_test.rb
61
+ test/dbstore_test.rb
62
+ test/dinrelay.html.erb
63
+ test/dinrelay_test.rb
64
+ test/nonexistant_sensor.yml
65
+ test/outliercache_test.rb
66
+ test/sensor_test.rb
67
+ test/sensors_test.rb
68
+ test/test_db.yml
69
+ test/test_dinrelay_sensors.yml
70
+ test/test_helper.rb
71
+ test/test_sensor_cleanups.yml
72
+ test/test_sensors.yml
73
+ ]
74
+ # = MANIFEST =
75
+
76
+ s.test_files = s.files.select { |path| path =~ /^test\/.*\.rb/ }
77
+ end
@@ -0,0 +1,39 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+ require 'time'
3
+
4
+ MOCK_AVERAGES = [40.001,30.002,nil,60.004,300.005]
5
+ MOCK_MAX = 215.3
6
+ MOCK_MIN = 35.2
7
+ NORMALIZED_MOCK_AVERAGES = [2.7,0.0,-1.0,13.8,100.0]
8
+
9
+ class MockSensor
10
+ attr_reader :asked_averages
11
+ def initialize
12
+ @averages = MOCK_AVERAGES.dup
13
+ @asked_averages = []
14
+ end
15
+ def average(from, to)
16
+ @asked_averages << [from,to];
17
+ @averages.shift
18
+ end
19
+ end
20
+
21
+ class TestChartData < Test::Unit::TestCase
22
+ def test_get_data
23
+ sensor = MockSensor.new
24
+ c = SAAL::ChartData.new(sensor)
25
+ assert_equal MOCK_AVERAGES, c.get_data(0, 1000, 5)
26
+ assert_equal([[0,199],[200,399],[400,599],[600,799],[800,1000]],
27
+ sensor.asked_averages)
28
+ end
29
+
30
+ def test_normalize_data
31
+ sensor = MockSensor.new
32
+ c = SAAL::ChartData.new(sensor)
33
+ d = c.get_data(0, 1000, 5)
34
+ assert_equal NORMALIZED_MOCK_AVERAGES,
35
+ c.normalize_data(d, MOCK_MIN, MOCK_MAX)
36
+ assert_equal([[0,199],[200,399],[400,599],[600,799],[800,1000]],
37
+ sensor.asked_averages)
38
+ end
39
+ end
@@ -0,0 +1,44 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+ require 'time'
3
+
4
+ class TestDaemon < Test::Unit::TestCase
5
+ def test_working_daemon
6
+ db_setup
7
+ nsecs = 0.5
8
+ interval = 0.00001
9
+ with_fake_owserver do
10
+ d = SAAL::Daemon.new(:keep_stdin => true,
11
+ :interval => interval,
12
+ :dbconf => TEST_DBCONF,
13
+ :sensorconf => TEST_SENSORS_FILE)
14
+ pid = d.run
15
+ sleep nsecs # Potential timing bug when the system is under load
16
+ Process.kill("TERM", pid)
17
+ Process.waitpid(pid)
18
+ end
19
+
20
+ db_test_query("SELECT * FROM sensor_reads") do |res|
21
+ assert res.num_rows > 0, "No sensor reads in DB"
22
+ end
23
+ end
24
+
25
+ def test_empty_reads_daemon
26
+ db_setup
27
+ nsecs = 0.5
28
+ interval = 0.00001
29
+ with_fake_owserver do
30
+ d = SAAL::Daemon.new(:keep_stdin => true,
31
+ :interval => interval,
32
+ :dbconf => TEST_DBCONF,
33
+ :sensorconf => TEST_NONEXIST_SENSOR_FILE)
34
+ pid = d.run
35
+ sleep nsecs # Potential timing bug when the system is under load
36
+ Process.kill("TERM", pid)
37
+ Process.waitpid(pid)
38
+ end
39
+
40
+ db_test_query("SELECT * FROM sensor_reads") do |res|
41
+ assert res.num_rows == 0
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,70 @@
1
+ require File.dirname(__FILE__)+'/test_helper.rb'
2
+
3
+ class TestFileStore < Test::Unit::TestCase
4
+ def test_insert
5
+ db_setup
6
+ test_time = 1196024160
7
+ test_value = 7.323
8
+
9
+ @dbstore.write(:test_sensor, test_time, test_value)
10
+
11
+ db_test_query("SELECT * FROM sensor_reads") do |res|
12
+ assert_equal 1, res.num_rows
13
+ assert_equal ["test_sensor", test_time.to_s, test_value.to_s], res.fetch_row
14
+ end
15
+ end
16
+
17
+ def test_insert_nil
18
+ db_setup
19
+
20
+ assert_raise(ArgumentError) {@dbstore.write(:test_sensor, 1, nil)}
21
+ assert_raise(ArgumentError) {@dbstore.write(:test_sensor, 0, 1)}
22
+ assert_raise(ArgumentError) {@dbstore.write(:test_sensor, nil, 1)}
23
+ end
24
+
25
+ def test_average
26
+ db_setup
27
+ test_values = [[10, 7.323],[12, 5.432],[23, -2.125], [44, 0.123]]
28
+ test_average = (5.432 - 2.125)/2.0
29
+ test_values.each do |values|
30
+ @dbstore.write(:test_sensor, *values)
31
+ end
32
+
33
+ assert_instance_of Float, @dbstore.average(:test_sensor, 11, 25)
34
+ assert_in_delta test_average, @dbstore.average(:test_sensor, 11, 25), 0.001
35
+ assert_in_delta test_average, @dbstore.average(:test_sensor, 12, 25), 0.001
36
+ assert_in_delta test_average, @dbstore.average(:test_sensor, 12, 23), 0.001
37
+
38
+ # when there are no points it's nil
39
+ assert_nil @dbstore.average(:test_sensor, 50, 60)
40
+ end
41
+
42
+ def test_min_max
43
+ db_setup
44
+ test_values = [[10, 7.323],[12, 5.432],[23, -2.125], [44, 0.123]]
45
+ test_values.each do |values|
46
+ @dbstore.write(:test_sensor, *values)
47
+ end
48
+
49
+ [[:minimum, -2.125], [:maximum, 5.432]].each do |func, value|
50
+ assert_instance_of Float, @dbstore.send(func, :test_sensor, 11, 25)
51
+ assert_in_delta value, @dbstore.send(func,:test_sensor, 11, 25), 0.0001
52
+ assert_in_delta value, @dbstore.send(func,:test_sensor, 12, 25), 0.0001
53
+ assert_in_delta value, @dbstore.send(func,:test_sensor, 12, 23), 0.0001
54
+
55
+ # when there are no points it's nil
56
+ assert_nil @dbstore.send(func,:test_sensor, 50, 60)
57
+ end
58
+ end
59
+
60
+ def test_enumerable
61
+ db_setup
62
+ test_time = 1196024160
63
+ test_value = 7.323
64
+ n = 5
65
+
66
+ n.times {@dbstore.write(:test_sensor, test_time, test_value)}
67
+ assert_equal [["test_sensor", test_time, test_value]]*n,
68
+ @dbstore.map{|sensor,time,value| [sensor,time,value]}
69
+ end
70
+ end
@@ -0,0 +1,143 @@
1
+ <html><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-15">
2
+ <meta name="ROBOTS" content="NOINDEX, NOFOLLOW">
3
+
4
+ <meta http-equiv="Refresh" content="300">
5
+ <title>Outlet Control - Lite Power Controller</title></head>
6
+ <!-- state=00 lock=00 -->
7
+
8
+ <body alink="#0000FF" vlink="#0000FF">
9
+ <font face="Arial, Helvetica, Sans-Serif">
10
+ <table width="100%" cellspacing="0" cellpadding="0">
11
+ <tbody><tr>
12
+ <td valign="top" width="17%" height="100%">
13
+
14
+ <!-- menu -->
15
+ <table width="100%" height="100%" align="center" border="0" cellspacing="1" cellpadding="0">
16
+ <tbody><tr><td valign="top" bgcolor="#F4F4F4">
17
+ <table width="100%" cellpadding="1" cellspacing="5">
18
+
19
+ <tbody><tr><td align="center">
20
+
21
+ <table><tbody><tr><td><a href="http://www.digital-loggers.com/8.html"><img src="./index_files/logo.gif" width="195" height="65" border="0"></a></td>
22
+
23
+ <td><b><font size="-1">Ethernet Power Controller</font></b></td></tr></tbody></table>
24
+ <hr>
25
+ </td></tr>
26
+
27
+
28
+
29
+ <tr><td nowrap=""><b><a href="./index_files/index.html">Outlet Control</a></b></td></tr>
30
+ <tr><td nowrap=""><b><a href="http://localhost:8080/admin.htm">Setup</a></b></td></tr>
31
+
32
+
33
+
34
+
35
+
36
+ <tr><td nowrap=""><b><a href="http://localhost:8080/ap.htm">AutoPing</a></b></td></tr>
37
+ <tr><td nowrap=""><b><a href="http://localhost:8080/syslog.htm">System Log</a></b></td></tr>
38
+ <tr><td nowrap=""><b><a href="http://localhost:8080/logout">Logout</a></b></td></tr>
39
+ <tr><td nowrap=""><b><a href="http://localhost:8080/help/">Help</a></b></td></tr>
40
+
41
+ <tr><td><hr></td></tr>
42
+
43
+
44
+ <tr><td><b><a href="http://www.digital-loggers.com/5.html">Manual</a></b></td></tr>
45
+
46
+ <tr><td><b><a href="http://www.digital-loggers.com/6.html">FAQ</a></b></td></tr>
47
+
48
+ <tr><td><b><a href="http://www.digital-loggers.com/7.html">Product Information</a></b></td></tr>
49
+
50
+ <tr><td><b><a href="http://www.digital-loggers.com/8.html">Digital Loggers, Inc.</a></b></td></tr>
51
+
52
+
53
+ </tbody></table>
54
+ </td></tr>
55
+
56
+
57
+ <tr><td valign="bottom" height="100%" bgcolor="#F4F4F4">
58
+ <small>Version 1.2.C (Apr 12 2009 / 14:28:13) 43858FBE-7E60D1A3</small>
59
+ </td></tr>
60
+ <tr><td valign="bottom" height="100%" bgcolor="#F4F4F4">
61
+ <small>S/N:0000131097</small>
62
+ </td></tr>
63
+
64
+ </tbody></table>
65
+ <!-- /menu -->
66
+
67
+ </td>
68
+ <td valign="top" width="83%">
69
+
70
+ <!-- heading table -->
71
+ <table width="100%" align="center" border="0" cellspacing="1" cellpadding="3">
72
+ <tbody><tr><td bgcolor="red">&nbsp;</td></tr><tr><td align="center"><h1>We recommend that you change the password!</h1></td></tr><tr><td bgcolor="red">&nbsp;</td></tr>
73
+ <tr>
74
+ <th bgcolor="#DDDDFF" align="left">
75
+ Controller: Lite Power Controller
76
+ </th>
77
+ </tr>
78
+
79
+ <tr bgcolor="#FFFFFF" align="left">
80
+ <td>
81
+ Uptime: 115:48:32 <!-- 416912s up -->
82
+ </td>
83
+ </tr>
84
+
85
+ </tbody></table>
86
+ <!-- /heading table -->
87
+
88
+ <br>
89
+
90
+ <!-- individual control table -->
91
+ <table width="100%" align="center" border="0" cellspacing="1" cellpadding="3">
92
+
93
+ <tbody><tr>
94
+ <td bgcolor="#DDDDFF" colspan="5" align="left">
95
+ Individual Control
96
+ </td>
97
+ </tr>
98
+
99
+ <!-- heading rows -->
100
+ <tr bgcolor="#DDDDDD">
101
+ <th>#</th>
102
+ <th align="left">Name</th>
103
+ <th align="left">State</th>
104
+ <th align="left" colspan="2">Action</th>
105
+ </tr>
106
+ <!-- /heading rows -->
107
+
108
+ <% (1..8).each do |num| %>
109
+ <% state = outlets[num] %>
110
+ <% color = state == "ON" ? "green" : "red" %>
111
+ <% reverse = state == "ON" ? "OFF" : "ON" %>
112
+ <tr bgcolor="#F4F4F4"><td align="center">8</td>
113
+ <td>Outlet <%= num %></td><td>
114
+ <b><font color="<%= color %>"><%= state %></font></b></td><td>
115
+ <a href="http://example.com/outlet?<%= num %>=<%= reverse %>">Switch <%= reverse %></a>
116
+ </td><td>
117
+ <!-- <a href=outlet?<%= num %>=CCL>Cycle</a> -->
118
+ </td></tr>
119
+ <% end %>>
120
+
121
+ </tbody></table>
122
+ <!-- /individual control table -->
123
+
124
+ <br>
125
+
126
+ <table width="100%" align="center" border="0" cellspacing="1" cellpadding="3">
127
+ <tbody><tr><td bgcolor="#DDDDFF" align="left">Master Control</td></tr>
128
+
129
+ <tr><td bgcolor="#F4F4F4" align="left"><a href="http://localhost:8080/outlet?a=OFF">All outlets OFF</a></td></tr>
130
+ <tr><td bgcolor="#F4F4F4" align="left"><a href="http://localhost:8080/outlet?a=ON">All outlets ON</a></td></tr>
131
+ <tr><td bgcolor="#F4F4F4" align="left"><a href="http://localhost:8080/outlet?a=CCL">Cycle all outlets</a></td></tr>
132
+ <tr><td align="center">Sequence delay: 1 sec.</td></tr>
133
+
134
+ </tbody></table>
135
+
136
+
137
+ </td>
138
+ </tr>
139
+ </tbody></table>
140
+
141
+
142
+
143
+ </font></body><style type="text/css">embed[type*="application/x-shockwave-flash"],embed[src*=".swf"],object[type*="application/x-shockwave-flash"],object[codetype*="application/x-shockwave-flash"],object[src*=".swf"],object[codebase*="swflash.cab"],object[classid*="D27CDB6E-AE6D-11cf-96B8-444553540000"],object[classid*="d27cdb6e-ae6d-11cf-96b8-444553540000"],object[classid*="D27CDB6E-AE6D-11cf-96B8-444553540000"]{ display: none !important;}</style></html>