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