balboa_worldwide_app 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/bwa_client +43 -0
- data/bin/bwa_mqtt_bridge +188 -0
- data/bin/bwa_proxy +14 -0
- data/bin/bwa_server +14 -0
- data/lib/balboa_worldwide_app.rb +1 -0
- data/lib/bwa/client.rb +117 -0
- data/lib/bwa/crc.rb +8 -0
- data/lib/bwa/discovery.rb +43 -0
- data/lib/bwa/message.rb +119 -0
- data/lib/bwa/messages/configuration.rb +8 -0
- data/lib/bwa/messages/configuration_request.rb +12 -0
- data/lib/bwa/messages/control_configuration.rb +17 -0
- data/lib/bwa/messages/control_configuration_request.rb +20 -0
- data/lib/bwa/messages/filter_cycles.rb +42 -0
- data/lib/bwa/messages/ready.rb +8 -0
- data/lib/bwa/messages/set_temperature.rb +26 -0
- data/lib/bwa/messages/set_temperature_scale.rb +28 -0
- data/lib/bwa/messages/set_time.rb +30 -0
- data/lib/bwa/messages/status.rb +149 -0
- data/lib/bwa/messages/toggle_item.rb +40 -0
- data/lib/bwa/proxy.rb +55 -0
- data/lib/bwa/server.rb +93 -0
- data/lib/bwa/version.rb +3 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7daff60d63c09a23f5628f69102fa722745c343d5c3641d7d87f09e47be7549a
|
4
|
+
data.tar.gz: b70859344d26e0f7a54b3a8ffea6389637683c5b5f31de5a0defb8bb52176c0a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '085e33a524d9410cbd069592804a716448031eece734bf5d2209318ef387ef9b2cda6fe941f4956681015889ca70f6f4429805eabfbc56e0bd23322262c3f850'
|
7
|
+
data.tar.gz: 749b99bc33e6df19dec5f276206a62556310be025daec5b8c71f061f8fd45510d7a6f58f201655f8b9cfc0bfaf826cf7c7b5b3d5b43755bbedc44ed87bdd70b5
|
data/bin/bwa_client
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bwa/client'
|
4
|
+
require 'bwa/discovery'
|
5
|
+
|
6
|
+
def watch(spa)
|
7
|
+
loop do
|
8
|
+
begin
|
9
|
+
message = spa.poll
|
10
|
+
next if message.is_a?(BWA::Messages::Ready)
|
11
|
+
puts message.raw_data.unpack("H*").first.scan(/[0-9a-f]{2}/).join(' ')
|
12
|
+
puts message.inspect
|
13
|
+
if block_given?
|
14
|
+
break if yield
|
15
|
+
end
|
16
|
+
rescue BWA::InvalidMessage => e
|
17
|
+
puts e.message
|
18
|
+
puts e.raw_data.unpack("H*").first.scan(/[0-9a-f]{2}/).join(' ')
|
19
|
+
break
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
if ARGV.empty?
|
25
|
+
spas = BWA::Discovery.discover
|
26
|
+
if spas.empty?
|
27
|
+
$stderr.puts "Could not find spa!"
|
28
|
+
exit 1
|
29
|
+
end
|
30
|
+
spa_ip = spas.first.first
|
31
|
+
else
|
32
|
+
spa_ip = ARGV[0]
|
33
|
+
end
|
34
|
+
|
35
|
+
spa = BWA::Client.new(spa_ip)
|
36
|
+
|
37
|
+
spa.request_configuration
|
38
|
+
spa.request_control_info
|
39
|
+
watch(spa) do
|
40
|
+
spa.last_status
|
41
|
+
end
|
42
|
+
|
43
|
+
watch(spa)
|
data/bin/bwa_mqtt_bridge
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'mqtt'
|
4
|
+
|
5
|
+
require 'bwa/client'
|
6
|
+
require 'bwa/discovery'
|
7
|
+
|
8
|
+
class MQTTBridge
|
9
|
+
def initialize(mqtt_uri, bwa, device_id: "bwa", base_topic: "homie")
|
10
|
+
@base_topic = "#{base_topic}/#{device_id}"
|
11
|
+
@mqtt = MQTT::Client.new(mqtt_uri)
|
12
|
+
@mqtt.set_will("#{@base_topic}/$state", "lost", true)
|
13
|
+
@mqtt.connect
|
14
|
+
@bwa = bwa
|
15
|
+
@attributes = {}
|
16
|
+
|
17
|
+
publish_basic_attributes
|
18
|
+
|
19
|
+
bwa_thread = Thread.new do
|
20
|
+
loop do
|
21
|
+
begin
|
22
|
+
message = @bwa.poll
|
23
|
+
next if message.is_a?(BWA::Messages::Ready)
|
24
|
+
|
25
|
+
case message
|
26
|
+
when BWA::Messages::Status
|
27
|
+
# make sure time is in sync
|
28
|
+
now = Time.now
|
29
|
+
if message.hour != now.hour || message.minute != now.min
|
30
|
+
@bwa.set_time(now.hour, now.min, message.twenty_four_hour_time)
|
31
|
+
end
|
32
|
+
publish_attribute("spa/priming", message.priming)
|
33
|
+
publish_attribute("spa/heatingmode", message.heating_mode)
|
34
|
+
publish_attribute("spa/temperaturescale", message.temperature_scale)
|
35
|
+
publish_attribute("spa/24htime", message.twenty_four_hour_time)
|
36
|
+
publish_attribute("spa/heating", message.heating)
|
37
|
+
publish_attribute("spa/temperaturerange", message.temperature_range)
|
38
|
+
publish_attribute("spa/circpump", message.circ_pump)
|
39
|
+
publish_attribute("spa/pump1", message.pump1)
|
40
|
+
publish_attribute("spa/pump2", message.pump2)
|
41
|
+
publish_attribute("spa/light1", message.light1)
|
42
|
+
publish_attribute("spa/currenttemperature", message.current_temperature)
|
43
|
+
publish_attribute("spa/currenttemperature/$unit", "º#{message.temperature_scale.to_s[0].upcase}")
|
44
|
+
publish_attribute("spa/settemperature", message.set_temperature)
|
45
|
+
publish_attribute("spa/settemperature/$unit", "º#{message.temperature_scale.to_s[0].upcase}")
|
46
|
+
if message.temperature_scale == :celsius
|
47
|
+
publish_attribute("spa/currenttemperature/$format", message.temperature_range == :high ? "26:40" : "10:26")
|
48
|
+
publish_attribute("spa/settemperature/$format", message.temperature_range == :high ? "26:40" : "10:26")
|
49
|
+
else
|
50
|
+
publish_attribute("spa/currenttemperature/$format", message.temperature_range == :high ? "80:104" : "26:40")
|
51
|
+
publish_attribute("spa/settemperature/$format", message.temperature_range == :high ? "80:104" : "26:40")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
@mqtt.get do |topic, value|
|
59
|
+
puts "got #{value.inspect} at #{topic}"
|
60
|
+
case topic[@base_topic.length + 1..-1]
|
61
|
+
when "heatingmode"
|
62
|
+
next unless %w{ready rest ready_in_rest}.include?(value)
|
63
|
+
# @bwa.set_heating_mode(value.to_sym)
|
64
|
+
when "temperaturescale"
|
65
|
+
next unless %w{fahrenheit celsius}.include?(value)
|
66
|
+
# @bwa.set_temperature_scale(value.to_sym)
|
67
|
+
when "spa/24htime/set"
|
68
|
+
next unless %w{true false}.include?(value)
|
69
|
+
now = Time.now
|
70
|
+
@bwa.set_time(now.hour, now.min, value == 'true')
|
71
|
+
when "temperaturerange"
|
72
|
+
next unless %w{low high}.include?(value)
|
73
|
+
# @bwa.set_temperature_range(value.to_sym)
|
74
|
+
when %r{^spa/(pump[12])/set}
|
75
|
+
@bwa.send(:"set_#{$1}", value.to_i)
|
76
|
+
when "spa/light1/set"
|
77
|
+
next unless %w{true false}.include?(value)
|
78
|
+
@bwa.set_light1(value == 'true')
|
79
|
+
when "spa/settemperature/set"
|
80
|
+
@bwa.set_temperature(value.to_i)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def publish(topic, value)
|
86
|
+
@mqtt.publish("#{@base_topic}/#{topic}", value, true)
|
87
|
+
end
|
88
|
+
|
89
|
+
def publish_attribute(attr, value)
|
90
|
+
if !@attributes.key?(attr) || @attributes[attr] != value
|
91
|
+
publish(attr, value.to_s)
|
92
|
+
@attributes[attr] = value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def subscribe(topic)
|
97
|
+
@mqtt.subscribe("#{@base_topic}/#{topic}")
|
98
|
+
end
|
99
|
+
|
100
|
+
def publish_basic_attributes
|
101
|
+
publish("$homie", "v4.0.0")
|
102
|
+
publish("$name", "BWA Spa")
|
103
|
+
publish("$state", "init")
|
104
|
+
publish("$nodes", "spa")
|
105
|
+
|
106
|
+
publish("spa/$name", "BWA Spa")
|
107
|
+
publish("spa/$type", "spa")
|
108
|
+
publish("spa/$properties", "priming,heatingmode,temperaturescale,24htime,heating,temperaturerange,circpump,pump1,pump2,light1,currenttemperature,settemperature")
|
109
|
+
|
110
|
+
publish("spa/priming/$name", "Is the pump priming")
|
111
|
+
publish("spa/priming/$datatype", "boolean")
|
112
|
+
|
113
|
+
publish("spa/heatingmode/$name", "Current heating mode")
|
114
|
+
publish("spa/heatingmode/$datatype", "enum")
|
115
|
+
publish("spa/heatingmode/$format", "ready,rest,ready_in_rest")
|
116
|
+
publish("spa/heatingmode/$settable", "true")
|
117
|
+
subscribe("spa/heatingmode/set")
|
118
|
+
|
119
|
+
publish("spa/temperaturescale/$name", "Temperature scale")
|
120
|
+
publish("spa/temperaturescale/$datatype", "enum")
|
121
|
+
publish("spa/temperaturescale/$format", "fahrenheit,celsius")
|
122
|
+
publish("spa/temperaturescale/$settable", "true")
|
123
|
+
subscribe("spa/temperaturescale/set")
|
124
|
+
|
125
|
+
publish("spa/24htime/$name", "Clock is 24 hour time")
|
126
|
+
publish("spa/24htime/$datatype", "boolean")
|
127
|
+
publish("spa/24htime/$settable", "true")
|
128
|
+
subscribe("spa/24htime/set")
|
129
|
+
|
130
|
+
publish("spa/heating/$name", "Heater is currently running")
|
131
|
+
publish("spa/heating/$datatype", "boolean")
|
132
|
+
|
133
|
+
publish("spa/temperaturerange/$name", "Current temperature range")
|
134
|
+
publish("spa/temperaturerange/$datatype", "enum")
|
135
|
+
publish("spa/temperaturerange/$format", "high,low")
|
136
|
+
publish("spa/temperaturerange/$settable", "true")
|
137
|
+
subscribe("spa/temperaturerange/set")
|
138
|
+
|
139
|
+
publish("spa/circpump/$name", "Circ pump is currently running")
|
140
|
+
publish("spa/circpump/$datatype", "boolean")
|
141
|
+
|
142
|
+
publish("spa/pump1/$name", "Pump 1 speed")
|
143
|
+
publish("spa/pump1/$datatype", "integer")
|
144
|
+
publish("spa/pump1/$format", "0:2")
|
145
|
+
publish("spa/pump1/$settable", "true")
|
146
|
+
subscribe("spa/pump1/set")
|
147
|
+
|
148
|
+
publish("spa/pump2/$name", "Pump 2 speed")
|
149
|
+
publish("spa/pump2/$datatype", "integer")
|
150
|
+
publish("spa/pump2/$format", "0:2")
|
151
|
+
publish("spa/pump2/$settable", "true")
|
152
|
+
subscribe("spa/pump2/set")
|
153
|
+
|
154
|
+
publish("spa/light1/$name", "Light 1")
|
155
|
+
publish("spa/light1/$datatype", "boolean")
|
156
|
+
publish("spa/light1/$settable", "true")
|
157
|
+
subscribe("spa/light1/set")
|
158
|
+
|
159
|
+
publish("spa/currenttemperature/$name", "Current temperature")
|
160
|
+
publish("spa/currenttemperature/$datatype", "integer")
|
161
|
+
|
162
|
+
publish("spa/settemperature/$name", "Set Temperature")
|
163
|
+
publish("spa/settemperature/$datatype", "integer")
|
164
|
+
publish("spa/settemperature/$settable", "true")
|
165
|
+
subscribe("spa/settemperature/set")
|
166
|
+
|
167
|
+
publish("$state", "ready")
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
mqtt_uri = ARGV.shift
|
172
|
+
|
173
|
+
if ARGV.empty?
|
174
|
+
spas = BWA::Discovery.discover
|
175
|
+
if spas.empty?
|
176
|
+
$stderr.puts "Could not find spa!"
|
177
|
+
exit 1
|
178
|
+
end
|
179
|
+
spa_ip = spas.first.first
|
180
|
+
else
|
181
|
+
spa_ip = ARGV[0]
|
182
|
+
end
|
183
|
+
|
184
|
+
spa = BWA::Client.new(spa_ip)
|
185
|
+
|
186
|
+
spa.request_configuration
|
187
|
+
|
188
|
+
MQTTBridge.new(mqtt_uri, spa)
|
data/bin/bwa_proxy
ADDED
data/bin/bwa_server
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bwa/client'
|
data/lib/bwa/client.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'bwa/message'
|
2
|
+
|
3
|
+
module BWA
|
4
|
+
class Client
|
5
|
+
attr_reader :last_status, :last_filter_configuration
|
6
|
+
|
7
|
+
def initialize(host, port = 4257)
|
8
|
+
if host =~ %r{^/dev}
|
9
|
+
require 'serialport'
|
10
|
+
@io = SerialPort.open(host, "baud" => 115200)
|
11
|
+
@queue = []
|
12
|
+
else
|
13
|
+
require 'socket'
|
14
|
+
@io = TCPSocket.new(host, port)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def poll
|
19
|
+
message = nil
|
20
|
+
while message.nil?
|
21
|
+
begin
|
22
|
+
message = Message.parse(@io)
|
23
|
+
if message.is_a?(Messages::Ready) && (msg = @queue.shift)
|
24
|
+
@io.write(msg)
|
25
|
+
end
|
26
|
+
rescue BWA::InvalidMessage => e
|
27
|
+
unless e.message =~ /Incorrect data length/
|
28
|
+
puts e.message
|
29
|
+
puts e.raw_data.unpack("H*").first.scan(/[0-9a-f]{2}/).join(' ')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
@last_status = message.dup if message.is_a?(Messages::Status)
|
34
|
+
@last_filter_configuration = message.dup if message.is_a?(Messages::FilterCycles)
|
35
|
+
message
|
36
|
+
end
|
37
|
+
|
38
|
+
def messages_pending?
|
39
|
+
!!IO.select([@io], nil, nil, 0)
|
40
|
+
end
|
41
|
+
|
42
|
+
def drain_message_queue
|
43
|
+
poll while messages_pending?
|
44
|
+
end
|
45
|
+
|
46
|
+
def send_message(message)
|
47
|
+
length = message.length + 2
|
48
|
+
full_message = "#{length.chr}#{message}".force_encoding(Encoding::ASCII_8BIT)
|
49
|
+
checksum = CRC.checksum(full_message)
|
50
|
+
full_message = "\x7e#{full_message}#{checksum.chr}\x7e".force_encoding(Encoding::ASCII_8BIT)
|
51
|
+
if @queue
|
52
|
+
@queue.push(full_message)
|
53
|
+
else
|
54
|
+
@io.write(full_message)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def request_configuration
|
59
|
+
send_message("\x0a\xbf\x04")
|
60
|
+
end
|
61
|
+
|
62
|
+
def request_control_info
|
63
|
+
send_message("\x0a\xbf\x22\x02\x00\x00")
|
64
|
+
end
|
65
|
+
|
66
|
+
def request_filter_configuration
|
67
|
+
send_message("\x0a\xbf\x22\x01\x00\x00")
|
68
|
+
end
|
69
|
+
|
70
|
+
def toggle_item(args)
|
71
|
+
send_message("\x0a\xbf\x11#{args}")
|
72
|
+
end
|
73
|
+
|
74
|
+
def toggle_light1
|
75
|
+
toggle_item("\x11\x00")
|
76
|
+
end
|
77
|
+
|
78
|
+
def toggle_pump1
|
79
|
+
toggle_item("\x04\x00")
|
80
|
+
end
|
81
|
+
|
82
|
+
def toggle_pump2
|
83
|
+
toggle_item("\x05\x00")
|
84
|
+
end
|
85
|
+
|
86
|
+
(1..2).each do |i|
|
87
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
88
|
+
def set_pump#{i}(desired)
|
89
|
+
return unless last_status
|
90
|
+
times = (desired - last_status.pump#{i}) % 3
|
91
|
+
times.times do
|
92
|
+
toggle_pump#{i}
|
93
|
+
sleep(0.1)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
RUBY
|
97
|
+
end
|
98
|
+
|
99
|
+
def set_light1(desired)
|
100
|
+
return unless last_status
|
101
|
+
return if last_status.light1 == desired
|
102
|
+
toggle_light1
|
103
|
+
end
|
104
|
+
|
105
|
+
# high range is 80-104 for F, 26-40 for C (by 0.5)
|
106
|
+
# low range is 50-80 for F, 10-26 for C (by 0.5)
|
107
|
+
def set_temperature(desired)
|
108
|
+
desired *= 2 if last_status && last_status.temperature_scale == :celsius || desired < 50
|
109
|
+
send_message("\x0a\xbf\x20#{desired.chr}")
|
110
|
+
end
|
111
|
+
|
112
|
+
def set_time(hour, minute, twenty_four_hour_time = false)
|
113
|
+
hour |= 0x80 if twenty_four_hour_time
|
114
|
+
send_message("\x0a\xbf\x21".force_encoding(Encoding::ASCII_8BIT) + hour.chr + minute.chr)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/bwa/crc.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module BWA
|
4
|
+
class Discovery
|
5
|
+
class << self
|
6
|
+
def discover(timeout = 5, exhaustive = false)
|
7
|
+
socket = UDPSocket.new
|
8
|
+
socket.bind("0.0.0.0", 0)
|
9
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
|
10
|
+
socket.sendmsg("Discovery: Who is out there?", 0, Socket.sockaddr_in(30303, '255.255.255.255'))
|
11
|
+
spas = {}
|
12
|
+
loop do
|
13
|
+
if IO.select([socket], nil, nil, timeout)
|
14
|
+
msg, ip = socket.recvfrom(64)
|
15
|
+
ip = ip[2]
|
16
|
+
name, mac = msg.split("\r\n")
|
17
|
+
name.strip!
|
18
|
+
if mac.start_with?("00-15-27-")
|
19
|
+
spas[ip] = name
|
20
|
+
break unless exhaustive
|
21
|
+
end
|
22
|
+
else
|
23
|
+
break
|
24
|
+
end
|
25
|
+
end
|
26
|
+
spas
|
27
|
+
end
|
28
|
+
|
29
|
+
def advertise
|
30
|
+
socket = UDPSocket.new
|
31
|
+
socket.bind("0.0.0.0", 30303)
|
32
|
+
msg = "BWGSPA\r\n00-15-27-00-00-01\r\n"
|
33
|
+
loop do
|
34
|
+
data, addr = socket.recvfrom(32)
|
35
|
+
next unless data == 'Discovery: Who is out there?'
|
36
|
+
ip = addr.last
|
37
|
+
puts "Advertising to #{ip}"
|
38
|
+
socket.sendmsg(msg, 0, Socket.sockaddr_in(addr[1], ip))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/bwa/message.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'bwa/crc'
|
2
|
+
|
3
|
+
module BWA
|
4
|
+
class InvalidMessage < RuntimeError
|
5
|
+
attr_reader :raw_data
|
6
|
+
|
7
|
+
def initialize(message, data)
|
8
|
+
@raw_data = data
|
9
|
+
super(message)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Message
|
14
|
+
class << self
|
15
|
+
def inherited(klass)
|
16
|
+
@messages ||= []
|
17
|
+
@messages << klass
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse(io)
|
21
|
+
io = StringIO.new(io) if io.is_a?(String)
|
22
|
+
data = ''
|
23
|
+
# skim through until a start-of-message indicator
|
24
|
+
until data[0] == '~'
|
25
|
+
data = io.read(1)
|
26
|
+
end
|
27
|
+
|
28
|
+
data.concat(io.read(1))
|
29
|
+
length = data[-1].ord
|
30
|
+
|
31
|
+
if length < 5
|
32
|
+
bytes = data.bytes
|
33
|
+
bytes.shift
|
34
|
+
bytes.reverse.each { |byte| io.ungetbyte(byte) }
|
35
|
+
raise InvalidMessage.new("Message has bogus length: #{length}", data)
|
36
|
+
end
|
37
|
+
|
38
|
+
data.concat(io.read(length))
|
39
|
+
if data.length != length + 2
|
40
|
+
data.bytes.reverse.each { |b| io.ungetbyte(b) }
|
41
|
+
raise InvalidMessage.new("Incorrect data length (received #{data.length - 2}, expected #{length})", data)
|
42
|
+
end
|
43
|
+
|
44
|
+
message_type = data[2..4]
|
45
|
+
klass = @messages.find { |k| k::MESSAGE_TYPE == message_type }
|
46
|
+
|
47
|
+
unless data[-1] == '~'
|
48
|
+
bytes = data.bytes
|
49
|
+
bytes.shift
|
50
|
+
bytes.reverse.each { |b| io.ungetbyte(b) }
|
51
|
+
raise InvalidMessage.new("Missing trailing message indicator", data)
|
52
|
+
end
|
53
|
+
|
54
|
+
unless CRC.checksum(data[1...-2]) == data[-2].ord
|
55
|
+
bytes = data.bytes
|
56
|
+
bytes.shift
|
57
|
+
bytes.reverse.each { |b| io.ungetbyte(b) }
|
58
|
+
raise InvalidMessage.new("Invalid checksum", data)
|
59
|
+
end
|
60
|
+
|
61
|
+
return nil if [
|
62
|
+
"\xfe\xbf\x00".force_encoding(Encoding::ASCII_8BIT),
|
63
|
+
"\x10\xbf\xe1".force_encoding(Encoding::ASCII_8BIT),
|
64
|
+
"\x10\xbf\x07".force_encoding(Encoding::ASCII_8BIT)].include?(message_type)
|
65
|
+
|
66
|
+
raise InvalidMessage.new("Unrecognized message #{message_type.unpack("H*").first}", data) unless klass
|
67
|
+
raise InvalidMessage.new("Unrecognized data length (#{length}) for message #{klass}", data) unless length - 5 == klass::MESSAGE_LENGTH
|
68
|
+
|
69
|
+
message = klass.new
|
70
|
+
message.parse(data[5..-2])
|
71
|
+
message.instance_variable_set(:@raw_data, data)
|
72
|
+
message
|
73
|
+
end
|
74
|
+
|
75
|
+
def format_time(hour, minute, twenty_four_hour_time = true)
|
76
|
+
if twenty_four_hour_time
|
77
|
+
print_hour = "%02d" % hour
|
78
|
+
else
|
79
|
+
print_hour = hour % 12
|
80
|
+
print_hour = 12 if print_hour == 0
|
81
|
+
am_pm = (hour >= 12 ? "PM" : "AM")
|
82
|
+
end
|
83
|
+
"#{print_hour}:#{"%02d" % minute}#{am_pm}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def format_duration(hours, minutes)
|
87
|
+
"#{hours}:#{"%02d" % minutes}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
attr_reader :raw_data
|
92
|
+
|
93
|
+
def parse(_data)
|
94
|
+
end
|
95
|
+
|
96
|
+
def serialize(message = "")
|
97
|
+
length = message.length + 5
|
98
|
+
full_message = "#{length.chr}#{self.class::MESSAGE_TYPE}#{message}".force_encoding(Encoding::ASCII_8BIT)
|
99
|
+
checksum = CRC.checksum(full_message)
|
100
|
+
"\x7e#{full_message}#{checksum.chr}\x7e".force_encoding(Encoding::ASCII_8BIT)
|
101
|
+
end
|
102
|
+
|
103
|
+
def inspect
|
104
|
+
"#<#{self.class.name} #{raw_data.unpack("H*").first}>"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
require 'bwa/messages/configuration'
|
110
|
+
require 'bwa/messages/configuration_request'
|
111
|
+
require 'bwa/messages/control_configuration'
|
112
|
+
require 'bwa/messages/control_configuration_request'
|
113
|
+
require 'bwa/messages/filter_cycles'
|
114
|
+
require 'bwa/messages/ready'
|
115
|
+
require 'bwa/messages/set_temperature'
|
116
|
+
require 'bwa/messages/set_temperature_scale'
|
117
|
+
require 'bwa/messages/set_time'
|
118
|
+
require 'bwa/messages/status'
|
119
|
+
require 'bwa/messages/toggle_item'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module BWA
|
2
|
+
module Messages
|
3
|
+
class ControlConfiguration < Message
|
4
|
+
MESSAGE_TYPE = "\x0a\xbf\x24".force_encoding(Encoding::ASCII_8BIT)
|
5
|
+
MESSAGE_LENGTH = 21
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module BWA
|
11
|
+
module Messages
|
12
|
+
class ControlConfiguration2 < Message
|
13
|
+
MESSAGE_TYPE = "\x0a\xbf\x2e".force_encoding(Encoding::ASCII_8BIT)
|
14
|
+
MESSAGE_LENGTH = 6
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module BWA
|
2
|
+
module Messages
|
3
|
+
class ControlConfigurationRequest < Message
|
4
|
+
MESSAGE_TYPE = "\x0a\xbf\x22".force_encoding(Encoding::ASCII_8BIT)
|
5
|
+
MESSAGE_LENGTH = 3
|
6
|
+
|
7
|
+
attr_accessor :type
|
8
|
+
|
9
|
+
def initialize(type = 1)
|
10
|
+
self.type = type
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(data)
|
14
|
+
self.type = data == "\x02\x00\x00" ? 1 : 2
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module BWA
|
2
|
+
module Messages
|
3
|
+
class FilterCycles < Message
|
4
|
+
attr_reader :filter1_hour, :filter1_minute, :filter1_duration_hours, :filter1_duration_minutes,
|
5
|
+
:filter2_enabled,
|
6
|
+
:filter2_hour, :filter2_minute, :filter2_duration_hours, :filter2_duration_minutes
|
7
|
+
|
8
|
+
MESSAGE_TYPE = "\x0a\xbf\x23".force_encoding(Encoding::ASCII_8BIT)
|
9
|
+
MESSAGE_LENGTH = 8
|
10
|
+
|
11
|
+
def parse(data)
|
12
|
+
@filter1_hour = data[0].ord
|
13
|
+
@filter1_minute = data[1].ord
|
14
|
+
@filter1_duration_hours = data[2].ord
|
15
|
+
@filter1_duration_minutes = data[3].ord
|
16
|
+
|
17
|
+
f2_hour = data[4].ord
|
18
|
+
@filter2_enabled = !!(f2_hour & 0x80 == 0x80)
|
19
|
+
@filter2_hour = f2_hour & 0x7f
|
20
|
+
@filter2_minute = data[5].ord
|
21
|
+
@filter2_duration_hours = data[6].ord
|
22
|
+
@filter2_duration_minutes = data[7].ord
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
result = "#<BWA::Messages::FilterCycles "
|
27
|
+
|
28
|
+
result << "filter1 "
|
29
|
+
result << self.class.format_duration(filter1_duration_hours, filter1_duration_minutes)
|
30
|
+
result << "@"
|
31
|
+
result << self.class.format_time(filter1_hour, filter1_minute)
|
32
|
+
|
33
|
+
result << " filter2(#{@filter2_enabled ? 'enabled' : 'disabled'}) "
|
34
|
+
result << self.class.format_duration(filter2_duration_hours, filter2_duration_minutes)
|
35
|
+
result << "@"
|
36
|
+
result << self.class.format_time(filter2_hour, filter2_minute)
|
37
|
+
|
38
|
+
result << ">"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module BWA
|
2
|
+
module Messages
|
3
|
+
class SetTemperature < Message
|
4
|
+
MESSAGE_TYPE = "\x0a\xbf\x20".force_encoding(Encoding::ASCII_8BIT)
|
5
|
+
MESSAGE_LENGTH = 1
|
6
|
+
|
7
|
+
attr_accessor :temperature
|
8
|
+
|
9
|
+
def initialize(temperature = nil)
|
10
|
+
self.temperature = temperature
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(data)
|
14
|
+
self.temperature = data[0].ord
|
15
|
+
end
|
16
|
+
|
17
|
+
def serialize
|
18
|
+
super(temperature.chr)
|
19
|
+
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
"#<BWA::Messages::SetTemperature #{temperature}º>"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module BWA
|
2
|
+
module Messages
|
3
|
+
class SetTemperatureScale < Message
|
4
|
+
MESSAGE_TYPE = "\x0a\xbf\x27".force_encoding(Encoding::ASCII_8BIT)
|
5
|
+
MESSAGE_LENGTH = 2
|
6
|
+
|
7
|
+
attr_accessor :scale
|
8
|
+
|
9
|
+
def initialize(scale = nil)
|
10
|
+
self.scale = scale
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(data)
|
14
|
+
self.scale = data[1].ord == 0x00 ? :fahrenheit : :celsius
|
15
|
+
end
|
16
|
+
|
17
|
+
def serialize
|
18
|
+
data = "\x01\x00"
|
19
|
+
data[1] = (scale == :fahrenheit ? 0x00 : 0x01).chr
|
20
|
+
super(data)
|
21
|
+
end
|
22
|
+
|
23
|
+
def inspect
|
24
|
+
"#<BWA::Messages::SetTemperatureScale º#{scale.to_s[0].upcase}>"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module BWA
|
2
|
+
module Messages
|
3
|
+
class SetTime < Message
|
4
|
+
MESSAGE_TYPE = "\x0a\xbf\x21".force_encoding(Encoding::ASCII_8BIT)
|
5
|
+
MESSAGE_LENGTH = 2
|
6
|
+
|
7
|
+
attr_accessor :hour, :minute, :twenty_four_hour_time
|
8
|
+
|
9
|
+
def initialize(hour = nil, minute = nil, twenty_four_hour_time = nil)
|
10
|
+
self.hour, self.minute, self.twenty_four_hour_time = hour, minute, twenty_four_hour_time
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(data)
|
14
|
+
self.hour = data[0].ord & 0x7f
|
15
|
+
self.minute = data[1].ord
|
16
|
+
self.twenty_four_hour_time = !!(data[0].ord & 0x80)
|
17
|
+
end
|
18
|
+
|
19
|
+
def serialize
|
20
|
+
hour_encoded = hour
|
21
|
+
hour_encoded |= 0x80 if twenty_four_hour_time
|
22
|
+
super("#{hour_encoded.chr}#{minute.chr}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
"#<BWA::Messages::SetTime #{Status.format_time(hour, minute, twenty_four_hour_time)}>"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module BWA
|
2
|
+
module Messages
|
3
|
+
class Status < Message
|
4
|
+
attr_accessor :priming,
|
5
|
+
:heating_mode,
|
6
|
+
:temperature_scale,
|
7
|
+
:twenty_four_hour_time,
|
8
|
+
:heating,
|
9
|
+
:temperature_range,
|
10
|
+
:hour, :minute,
|
11
|
+
:circ_pump,
|
12
|
+
:pump1, :pump2,
|
13
|
+
:light1,
|
14
|
+
:current_temperature, :set_temperature
|
15
|
+
|
16
|
+
MESSAGE_TYPE = "\xff\xaf\x13".force_encoding(Encoding::ASCII_8BIT)
|
17
|
+
MESSAGE_LENGTH = 24
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
self.priming = false
|
21
|
+
self.heating_mode = :ready
|
22
|
+
@temperature_scale = :fahrenheit
|
23
|
+
self.twenty_four_hour_time = false
|
24
|
+
self.heating = false
|
25
|
+
self.temperature_range = :high
|
26
|
+
self.hour = self.minute = 0
|
27
|
+
self.circ_pump = false
|
28
|
+
self.pump1 = self.pump2 = 0
|
29
|
+
self.light1 = false
|
30
|
+
self.set_temperature = 100
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse(data)
|
34
|
+
flags = data[1].ord
|
35
|
+
self.priming = (flags & 0x01 == 0x01)
|
36
|
+
flags = data[5].ord
|
37
|
+
self.heating_mode = case flags & 0x03
|
38
|
+
when 0x00; :ready
|
39
|
+
when 0x01; :rest
|
40
|
+
when 0x02; :ready_in_rest
|
41
|
+
end
|
42
|
+
flags = data[9].ord
|
43
|
+
self.temperature_scale = (flags & 0x01 == 0x01) ? :celsius : :fahrenheit
|
44
|
+
self.twenty_four_hour_time = (flags & 0x02 == 0x02)
|
45
|
+
flags = data[10].ord
|
46
|
+
self.heating = (flags & 0x30 != 0)
|
47
|
+
self.temperature_range = (flags & 0x04 == 0x04) ? :high : :low
|
48
|
+
flags = data[11].ord
|
49
|
+
self.pump1 = flags & 0x03
|
50
|
+
self.pump2 = (flags / 4) & 0x03
|
51
|
+
flags = data[13].ord
|
52
|
+
self.circ_pump = (flags & 0x02 == 0x02)
|
53
|
+
flags = data[14].ord
|
54
|
+
self.light1 = (flags & 0x03 == 0x03)
|
55
|
+
self.hour = data[3].ord
|
56
|
+
self.minute = data[4].ord
|
57
|
+
self.current_temperature = data[2].ord
|
58
|
+
self.current_temperature = nil if self.current_temperature == 0xff
|
59
|
+
self.set_temperature = data[20].ord
|
60
|
+
if temperature_scale == :celsius
|
61
|
+
self.current_temperature /= 2.0
|
62
|
+
self.set_temperature /= 2.0
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def serialize
|
67
|
+
data = "\x00" * 24
|
68
|
+
data[1] = (priming ? 0x01 : 0x00).chr
|
69
|
+
data[5] = (case heating_mode
|
70
|
+
when :ready; 0x00
|
71
|
+
when :rest; 0x01
|
72
|
+
when :ready_in_rest; 0x02
|
73
|
+
end).chr
|
74
|
+
flags = 0
|
75
|
+
flags |= 0x01 if temperature_scale == :celsius
|
76
|
+
flags |= 0x02 if twenty_four_hour_time
|
77
|
+
data[9] = flags.chr
|
78
|
+
flags = 0
|
79
|
+
flags |= 0x30 if heating
|
80
|
+
flags |= 0x04 if temperature_range == :high
|
81
|
+
data[10] = flags.chr
|
82
|
+
flags = 0
|
83
|
+
flags |= pump1
|
84
|
+
flags |= pump2 * 4
|
85
|
+
data[11] = flags.chr
|
86
|
+
flags = 0
|
87
|
+
flags |= 0x02 if circ_pump
|
88
|
+
data[13] = flags.chr
|
89
|
+
flags = 0
|
90
|
+
flags |= 0x03 if light1
|
91
|
+
data[14] = flags.chr
|
92
|
+
data[3] = hour.chr
|
93
|
+
data[4] = minute.chr
|
94
|
+
if temperature_scale == :celsius
|
95
|
+
data[2] = (current_temperature ? (current_temperature * 2).to_i : 0xff).chr
|
96
|
+
data[20] = (set_temperature * 2).to_i.chr
|
97
|
+
else
|
98
|
+
data[2] = (current_temperature&.to_i || 0xff).chr
|
99
|
+
data[20] = set_temperature.to_i.chr
|
100
|
+
end
|
101
|
+
|
102
|
+
super(data)
|
103
|
+
end
|
104
|
+
|
105
|
+
def temperature_scale=(value)
|
106
|
+
if value != @temperature_scale
|
107
|
+
if value == :fahrenheit
|
108
|
+
if current_temperature
|
109
|
+
self.current_temperature *= 9.0/5
|
110
|
+
self.current_temperature += 32
|
111
|
+
self.current_temperature = current_temperature.round
|
112
|
+
end
|
113
|
+
self.set_temperature *= 9.0/5
|
114
|
+
self.set_temperature += 32
|
115
|
+
self.set_temperature = set_temperature.round
|
116
|
+
else
|
117
|
+
if current_temperature
|
118
|
+
self.current_temperature -= 32
|
119
|
+
self.current_temperature *= 5.0/90
|
120
|
+
self.current_temperature = (current_temperature * 2).round / 2.0
|
121
|
+
end
|
122
|
+
self.set_temperature -= 32
|
123
|
+
self.set_temperature *= 5.0/9
|
124
|
+
self.set_temperature = (set_temperature * 2).round / 2.0
|
125
|
+
end
|
126
|
+
end
|
127
|
+
@temperature_scale = value
|
128
|
+
end
|
129
|
+
|
130
|
+
def inspect
|
131
|
+
result = "#<BWA::Messages::Status "
|
132
|
+
items = []
|
133
|
+
|
134
|
+
items << "priming" if priming
|
135
|
+
items << self.class.format_time(hour, minute, twenty_four_hour_time)
|
136
|
+
items << "#{current_temperature || '--'}/#{set_temperature}º#{temperature_scale.to_s[0].upcase}"
|
137
|
+
items << heating_mode
|
138
|
+
items << "heating" if heating
|
139
|
+
items << temperature_range
|
140
|
+
items << "circ_pump" if circ_pump
|
141
|
+
items << "pump1=#{pump1}" unless pump1 == 0
|
142
|
+
items << "pump2=#{pump2}" unless pump2 == 0
|
143
|
+
items << "light1" if light1
|
144
|
+
|
145
|
+
result << items.join(' ') << ">"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module BWA
|
2
|
+
module Messages
|
3
|
+
class ToggleItem < Message
|
4
|
+
MESSAGE_TYPE = "\x0a\xbf\x11".force_encoding(Encoding::ASCII_8BIT)
|
5
|
+
MESSAGE_LENGTH = 2
|
6
|
+
|
7
|
+
attr_accessor :item
|
8
|
+
|
9
|
+
def initialize(item = nil)
|
10
|
+
self.item = item
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(data)
|
14
|
+
self.item = case data[0].ord
|
15
|
+
when 0x04; :pump1
|
16
|
+
when 0x05; :pump2
|
17
|
+
when 0x11; :light1
|
18
|
+
when 0x50; :temperature_range
|
19
|
+
when 0x51; :heating_mode
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def serialize
|
24
|
+
data = "\x00\x00"
|
25
|
+
data[0] = (case setting
|
26
|
+
when :pump1; 0x04
|
27
|
+
when :pump2; 0x05
|
28
|
+
when :light1; 0x11
|
29
|
+
when :temperature_range; 0x50
|
30
|
+
when :heating_mode; 0x51
|
31
|
+
end).chr
|
32
|
+
super(data)
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect
|
36
|
+
"#<BWA::Messages::ToggleItem #{item}>"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/bwa/proxy.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'bwa/message'
|
3
|
+
|
4
|
+
module BWA
|
5
|
+
class Proxy
|
6
|
+
def initialize(host, port: 4257, listen_port: 4257)
|
7
|
+
@host, @port = host, port
|
8
|
+
@listen_socket = TCPServer.open(port)
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
loop do
|
13
|
+
client_socket = @listen_socket.accept
|
14
|
+
server_socket = TCPSocket.new(@host, @port)
|
15
|
+
t1 = Thread.new do
|
16
|
+
shuffle_messages(client_socket, server_socket, "Client")
|
17
|
+
end
|
18
|
+
t2 = Thread.new do
|
19
|
+
shuffle_messages(server_socket, client_socket, "Server")
|
20
|
+
end
|
21
|
+
t1.join
|
22
|
+
t2.join
|
23
|
+
break
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def shuffle_messages(socket1, socket2, tag)
|
28
|
+
leftover_data = "".force_encoding(Encoding::ASCII_8BIT)
|
29
|
+
loop do
|
30
|
+
if leftover_data.length < 2 || leftover_data.length < leftover_data[1].ord + 2
|
31
|
+
begin
|
32
|
+
leftover_data += socket1.recv(128)
|
33
|
+
rescue Errno::EBADF
|
34
|
+
# we closed it on ourselves
|
35
|
+
break
|
36
|
+
end
|
37
|
+
end
|
38
|
+
if leftover_data.empty?
|
39
|
+
socket2.close
|
40
|
+
break
|
41
|
+
end
|
42
|
+
data_length = leftover_data[1].ord
|
43
|
+
data = leftover_data[0...(data_length + 2)]
|
44
|
+
leftover_data = leftover_data[(data_length + 2)..-1] || ''
|
45
|
+
begin
|
46
|
+
message = Message.parse(data)
|
47
|
+
puts "#{tag}: #{message.inspect}"
|
48
|
+
rescue InvalidMessage => e
|
49
|
+
puts "#{tag}: #{e}"
|
50
|
+
end
|
51
|
+
socket2.send(data, 0)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/bwa/server.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'bwa/message'
|
3
|
+
|
4
|
+
module BWA
|
5
|
+
class Server
|
6
|
+
def initialize(port = 4257)
|
7
|
+
@listen_socket = TCPServer.open(port)
|
8
|
+
@status = Messages::Status.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
loop do
|
13
|
+
socket = @listen_socket.accept
|
14
|
+
#Thread.new do
|
15
|
+
run_client(socket)
|
16
|
+
#end
|
17
|
+
break
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def send_message(socket, message)
|
22
|
+
length = message.length + 2
|
23
|
+
full_message = "#{length.chr}#{message}".force_encoding(Encoding::ASCII_8BIT)
|
24
|
+
checksum = CRC.checksum(full_message)
|
25
|
+
socket.send("\x7e#{full_message}#{checksum.chr}\x7e".force_encoding(Encoding::ASCII_8BIT), 0)
|
26
|
+
end
|
27
|
+
|
28
|
+
def run_client(socket)
|
29
|
+
puts "Received connection from #{socket.remote_address.inspect}"
|
30
|
+
|
31
|
+
send_status(socket)
|
32
|
+
loop do
|
33
|
+
if IO.select([socket], nil, nil, 1)
|
34
|
+
data = socket.recv(128)
|
35
|
+
break if data.empty?
|
36
|
+
begin
|
37
|
+
message = Message.parse(data)
|
38
|
+
puts message.raw_data.unpack("H*").first.scan(/[0-9a-f]{2}/).join(' ')
|
39
|
+
puts message.inspect
|
40
|
+
|
41
|
+
case message
|
42
|
+
when Messages::ConfigurationRequest
|
43
|
+
send_configuration(socket)
|
44
|
+
when Messages::ControlConfigurationRequest
|
45
|
+
message.type == 1 ? send_control_configuration(socket) : send_control_configuration2(socket)
|
46
|
+
when Messages::SetTemperature
|
47
|
+
temperature = message.temperature
|
48
|
+
temperature /= 2.0 if @status.temperature_scale == :celsius
|
49
|
+
@status.set_temperature = temperature
|
50
|
+
when Messages::SetTemperatureScale
|
51
|
+
@status.temperature_scale = message.scale
|
52
|
+
when Messages::ToggleItem
|
53
|
+
case message.item
|
54
|
+
when :heating_mode
|
55
|
+
@status.heating_mode = (@status.heating_mode == :rest ? :ready : :rest)
|
56
|
+
when :temperature_range
|
57
|
+
@status.temperature_range = (@status.temperature_range == :low ? :high : :low)
|
58
|
+
when :pump1
|
59
|
+
@status.pump1 = (@status.pump1 + 1) % 3
|
60
|
+
when :pump2
|
61
|
+
@status.pump2 = (@status.pump2 + 1) % 3
|
62
|
+
when :light1
|
63
|
+
@status.light1 = !@status.light1
|
64
|
+
end
|
65
|
+
end
|
66
|
+
rescue BWA::InvalidMessage => e
|
67
|
+
puts e.message
|
68
|
+
puts e.raw_data.unpack("H*").first.scan(/[0-9a-f]{2}/).join(' ')
|
69
|
+
end
|
70
|
+
else
|
71
|
+
send_status(socket)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def send_status(socket)
|
77
|
+
puts "sending #{@status.inspect}"
|
78
|
+
socket.send(@status.serialize, 0)
|
79
|
+
end
|
80
|
+
|
81
|
+
def send_configuration(socket)
|
82
|
+
send_message(socket, "\x0a\xbf\x94\x02\x02\x80\x00\x15\x27\x10\xab\xd2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x15\x27\xff\xff\x10\xab\xd2")
|
83
|
+
end
|
84
|
+
|
85
|
+
def send_control_configuration(socket)
|
86
|
+
send_message(socket, "\x0a\xbf\x24\x64\xdc\x11\x00\x42\x46\x42\x50\x32\x30\x20\x20\x01\x3d\x12\x38\x2e\x01\x0a\x04\x00")
|
87
|
+
end
|
88
|
+
|
89
|
+
def send_control_configuration2(socket)
|
90
|
+
send_message(socket, "\x0a\xbf\x2e\x0a\x00\x01\xd0\x00\x44")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/bwa/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: balboa_worldwide_app
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cody Cutrer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-05-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: digest-crc
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: serialport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.3.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.3.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mqtt
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.5.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.5.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '9.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '9.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '13.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '13.0'
|
83
|
+
description:
|
84
|
+
email: cody@cutrer.com'
|
85
|
+
executables:
|
86
|
+
- bwa_mqtt_bridge
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- bin/bwa_client
|
91
|
+
- bin/bwa_mqtt_bridge
|
92
|
+
- bin/bwa_proxy
|
93
|
+
- bin/bwa_server
|
94
|
+
- lib/balboa_worldwide_app.rb
|
95
|
+
- lib/bwa/client.rb
|
96
|
+
- lib/bwa/crc.rb
|
97
|
+
- lib/bwa/discovery.rb
|
98
|
+
- lib/bwa/message.rb
|
99
|
+
- lib/bwa/messages/configuration.rb
|
100
|
+
- lib/bwa/messages/configuration_request.rb
|
101
|
+
- lib/bwa/messages/control_configuration.rb
|
102
|
+
- lib/bwa/messages/control_configuration_request.rb
|
103
|
+
- lib/bwa/messages/filter_cycles.rb
|
104
|
+
- lib/bwa/messages/ready.rb
|
105
|
+
- lib/bwa/messages/set_temperature.rb
|
106
|
+
- lib/bwa/messages/set_temperature_scale.rb
|
107
|
+
- lib/bwa/messages/set_time.rb
|
108
|
+
- lib/bwa/messages/status.rb
|
109
|
+
- lib/bwa/messages/toggle_item.rb
|
110
|
+
- lib/bwa/proxy.rb
|
111
|
+
- lib/bwa/server.rb
|
112
|
+
- lib/bwa/version.rb
|
113
|
+
homepage: https://github.com/ccutrer/bwa
|
114
|
+
licenses:
|
115
|
+
- MIT
|
116
|
+
metadata: {}
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options: []
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubygems_version: 3.0.3
|
133
|
+
signing_key:
|
134
|
+
specification_version: 4
|
135
|
+
summary: Library for communication with Balboa Water Group's WiFi module or RS-485
|
136
|
+
test_files: []
|