saal 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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>