somfy_sdn 1.0.4 → 1.0.5
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 +4 -4
- data/lib/sdn/message.rb +33 -68
- data/lib/sdn/mqtt_bridge.rb +116 -24
- data/lib/sdn/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1f060c34defbdfc40cae2d9e3f34535aa31d44d45bbcbb00ff769fc29cbb01c
|
4
|
+
data.tar.gz: a83d074584f5142609c9b2752fbefbeddb7d5247f7a4a6a2c26fa80d3f679c68
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39226a05926b458bffb5f91ba69dbcd06d19a1d6daccf98d47c4caf2908e3d991afe5b0ee1917a4dcb0cc4d96fb03056cc9b4f017408666d7c93b32528f10e08
|
7
|
+
data.tar.gz: 2b80671284b192bc6eba54c98bf615e41dfe7990be3d343127f14a5cd8c222b7a45840c52c1de8a208d8b9468bdfd3bafd8acdd3f829198a9c2321ea6485b3b3
|
data/lib/sdn/message.rb
CHANGED
@@ -5,79 +5,44 @@ module SDN
|
|
5
5
|
|
6
6
|
class Message
|
7
7
|
class << self
|
8
|
-
def
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
8
|
+
def parse(data)
|
9
|
+
offset = -1
|
10
|
+
msg = length = ack_requested = message_class = nil
|
11
|
+
# we loop here scanning for a valid message
|
12
|
+
loop do
|
13
|
+
offset += 1
|
14
|
+
return nil if data.length - offset < 11
|
15
|
+
msg = to_number(data[offset])
|
16
|
+
length = to_number(data[offset + 1])
|
17
|
+
ack_requested = length & 0x80 == 0x80
|
18
|
+
length &= 0x7f
|
19
|
+
# impossible message
|
20
|
+
next if length < 11 || length > 32
|
21
|
+
# don't have enough data for what this message wants;
|
22
|
+
# it could be garbage on the line so keep scanning
|
23
|
+
next if length > data.length - offset
|
24
|
+
|
25
|
+
message_class = constants.find { |c| (const_get(c, false).const_get(:MSG, false) rescue nil) == msg }
|
26
|
+
message_class = const_get(message_class, false) if message_class
|
27
|
+
message_class ||= UnknownMessage
|
28
|
+
|
29
|
+
calculated_sum = checksum(data.slice(offset, length - 2))
|
30
|
+
read_sum = data.slice(offset + length - 2, 2)
|
31
|
+
next unless read_sum == calculated_sum
|
32
|
+
|
33
|
+
break
|
19
34
|
end
|
20
|
-
data
|
21
|
-
end
|
22
|
-
|
23
|
-
def parse(io)
|
24
|
-
io = StringIO.new(io) if io.is_a?(String)
|
25
|
-
data = readpartial(io, 2, allow_empty: false)
|
26
|
-
if data.length != 2
|
27
|
-
# don't have enough data yet; buffer it
|
28
|
-
io.ungetbyte(data.first) if data.length == 1
|
29
|
-
raise MalformedMessage, "Could not get message type and length"
|
30
|
-
end
|
31
|
-
msg = to_number(data.first)
|
32
|
-
length = to_number(data.last)
|
33
|
-
ack_requested = length & 0x80 == 0x80
|
34
|
-
length &= 0x7f
|
35
|
-
if length < 11 || length > 32
|
36
|
-
# only skip over one byte to try and re-sync
|
37
|
-
io.ungetbyte(data.last)
|
38
|
-
raise MalformedMessage, "Message has bogus length: #{length}"
|
39
|
-
end
|
40
|
-
data.concat(readpartial(io, length - 4))
|
41
|
-
unless data.length == length - 2
|
42
|
-
data.reverse.each { |byte| io.ungetbyte(byte) }
|
43
|
-
raise MalformedMessage, "Missing data: got #{data.length} expected #{length}"
|
44
|
-
end
|
45
|
-
|
46
|
-
message_class = constants.find { |c| (const_get(c, false).const_get(:MSG, false) rescue nil) == msg }
|
47
|
-
message_class = const_get(message_class, false) if message_class
|
48
|
-
message_class ||= UnknownMessage
|
49
|
-
|
50
|
-
bogus_checksums = [SetNodeLabel::MSG, PostNodeLabel::MSG].include?(msg)
|
51
|
-
|
52
|
-
calculated_sum = checksum(data)
|
53
|
-
read_sum = readpartial(io, 2)
|
54
|
-
if read_sum.length == 0 || (!bogus_checksums && read_sum.length == 1)
|
55
|
-
read_sum.each { |byte| io.ungetbyte(byte) }
|
56
|
-
data.reverse.each { |byte| io.ungetbyte(byte) }
|
57
|
-
raise MalformedMessage, "Missing data: got #{data.length} expected #{length}"
|
58
|
-
end
|
59
|
-
|
60
|
-
# check both the proper checksum, and a truncated checksum
|
61
|
-
unless calculated_sum == read_sum || (bogus_checksums && calculated_sum.last == read_sum.first)
|
62
|
-
raw_message = (data + read_sum).map { |b| '%02x' % b }.join(' ')
|
63
|
-
# skip over single byte to try and re-sync
|
64
|
-
data.shift
|
65
|
-
read_sum.reverse.each { |byte| io.ungetbyte(byte) }
|
66
|
-
data.reverse.each { |byte| io.ungetbyte(byte) }
|
67
|
-
raise MalformedMessage, "Checksum mismatch for #{message_class.name}: #{raw_message}"
|
68
|
-
end
|
69
|
-
# the checksum was truncated; put back the unused byte
|
70
|
-
io.ungetbyte(read_sum.last) if calculated_sum != read_sum && read_sum.length == 2
|
71
35
|
|
72
|
-
puts "
|
36
|
+
puts "discarding invalid data prior to message #{data[0...offset].map { |b| '%02x' % b }.join(' ')}" unless offset == 0
|
37
|
+
puts "read #{data.slice(offset, length).map { |b| '%02x' % b }.join(' ')}"
|
73
38
|
|
74
|
-
reserved = to_number(data[2])
|
75
|
-
src = transform_param(data
|
76
|
-
dest = transform_param(data
|
39
|
+
reserved = to_number(data[offset + 2])
|
40
|
+
src = transform_param(data.slice(offset + 3, 3))
|
41
|
+
dest = transform_param(data.slice(offset + 6, 3))
|
77
42
|
result = message_class.new(reserved: reserved, ack_requested: ack_requested, src: src, dest: dest)
|
78
|
-
result.parse(data
|
43
|
+
result.parse(data.slice(offset + 9, length - 11))
|
79
44
|
result.msg = msg if message_class == UnknownMessage
|
80
|
-
result
|
45
|
+
[result, offset + length]
|
81
46
|
end
|
82
47
|
end
|
83
48
|
|
data/lib/sdn/mqtt_bridge.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'mqtt'
|
2
2
|
require 'serialport'
|
3
|
+
require 'socket'
|
4
|
+
require 'uri'
|
3
5
|
require 'set'
|
4
6
|
|
5
7
|
module SDN
|
@@ -92,24 +94,56 @@ module SDN
|
|
92
94
|
end
|
93
95
|
|
94
96
|
class MQTTBridge
|
95
|
-
|
97
|
+
WAIT_TIME = 0.25
|
98
|
+
BROADCAST_WAIT = 5.0
|
99
|
+
|
100
|
+
def initialize(mqtt_uri, port, device_id: "somfy", base_topic: "homie")
|
96
101
|
@base_topic = "#{base_topic}/#{device_id}"
|
97
102
|
@mqtt = MQTT::Client.new(mqtt_uri)
|
98
103
|
@mqtt.set_will("#{@base_topic}/$state", "lost", true)
|
99
104
|
@mqtt.connect
|
105
|
+
|
100
106
|
@motors = {}
|
101
107
|
@groups = Set.new
|
102
|
-
|
108
|
+
|
109
|
+
@mutex = Mutex.new
|
110
|
+
@cond = ConditionVariable.new
|
111
|
+
@command_queue = []
|
112
|
+
@request_queue = []
|
113
|
+
@response_pending = false
|
114
|
+
@broadcast_pending = false
|
103
115
|
|
104
116
|
publish_basic_attributes
|
105
117
|
|
106
|
-
|
118
|
+
uri = URI.parse(port)
|
119
|
+
if uri.scheme == "tcp"
|
120
|
+
@sdn = TCPSocket.new(uri.host, uri.port)
|
121
|
+
else
|
122
|
+
@sdn = SerialPort.open(serialport, "baud" => 4800, "parity" => SerialPort::ODD)
|
123
|
+
end
|
107
124
|
|
108
125
|
read_thread = Thread.new do
|
126
|
+
buffer = ""
|
109
127
|
loop do
|
110
128
|
begin
|
111
|
-
message = SDN::Message.parse(
|
112
|
-
|
129
|
+
message, bytes_read = SDN::Message.parse(buffer.bytes)
|
130
|
+
unless message
|
131
|
+
begin
|
132
|
+
buffer.concat(@sdn.read_nonblock(1))
|
133
|
+
next
|
134
|
+
rescue IO::WaitReadable
|
135
|
+
wait = buffer.empty? ? nil : WAIT_TIME
|
136
|
+
if IO.select([@sdn], nil, nil, wait).nil?
|
137
|
+
# timed out; just discard everything
|
138
|
+
puts "timed out reading; discarding buffer: #{buffer.unpack('H*').first}"
|
139
|
+
buffer = ""
|
140
|
+
end
|
141
|
+
end
|
142
|
+
next
|
143
|
+
end
|
144
|
+
# discard how much we read
|
145
|
+
buffer = buffer[bytes_read..-1]
|
146
|
+
|
113
147
|
src = SDN::Message.print_address(message.src)
|
114
148
|
# ignore the UAI Plus and ourselves
|
115
149
|
if src != '7F.7F.7F' && !SDN::Message::is_group_address?(message.src) && !(motor = @motors[src.gsub('.', '')])
|
@@ -118,6 +152,7 @@ module SDN
|
|
118
152
|
end
|
119
153
|
|
120
154
|
puts "read #{message.inspect}"
|
155
|
+
follow_ups = []
|
121
156
|
case message
|
122
157
|
when SDN::Message::PostNodeLabel
|
123
158
|
if (motor.publish(:label, message.label))
|
@@ -129,10 +164,10 @@ module SDN
|
|
129
164
|
motor.publish(:ip, message.ip)
|
130
165
|
when SDN::Message::PostMotorStatus
|
131
166
|
if message.state == :running || motor.state == :running
|
132
|
-
|
167
|
+
follow_ups << SDN::Message::GetMotorStatus.new(message.src)
|
133
168
|
end
|
134
169
|
# this will do one more position request after it stopped
|
135
|
-
|
170
|
+
follow_ups << SDN::Message::GetMotorPosition.new(message.src)
|
136
171
|
motor.publish(:state, message.state)
|
137
172
|
motor.publish(:last_direction, message.last_direction)
|
138
173
|
motor.publish(:last_action_source, message.last_action_source)
|
@@ -153,6 +188,12 @@ module SDN
|
|
153
188
|
motor.add_group(message.group_index, message.group_address)
|
154
189
|
end
|
155
190
|
|
191
|
+
@mutex.synchronize do
|
192
|
+
signal = @response_pending || !follow_ups.empty?
|
193
|
+
@response_pending = @broadcast_pending
|
194
|
+
@request_queue.concat(follow_ups)
|
195
|
+
@cond.signal if signal
|
196
|
+
end
|
156
197
|
rescue SDN::MalformedMessage => e
|
157
198
|
puts "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
|
158
199
|
rescue => e
|
@@ -164,13 +205,43 @@ module SDN
|
|
164
205
|
write_thread = Thread.new do
|
165
206
|
begin
|
166
207
|
loop do
|
167
|
-
message =
|
208
|
+
message = nil
|
209
|
+
@mutex.synchronize do
|
210
|
+
# got woken up early by another command getting queued; spin
|
211
|
+
if @response_pending
|
212
|
+
while @response_pending
|
213
|
+
remaining_wait = @response_pending - Time.now.to_f
|
214
|
+
if remaining_wait < 0
|
215
|
+
puts "timed out waiting on response"
|
216
|
+
@response_pending = nil
|
217
|
+
@broadcast_pending = nil
|
218
|
+
else
|
219
|
+
@cond.wait(@mutex, remaining_wait)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
else
|
223
|
+
sleep 0.1
|
224
|
+
end
|
225
|
+
|
226
|
+
message = @command_queue.shift
|
227
|
+
unless message
|
228
|
+
message = @request_queue.shift
|
229
|
+
if message
|
230
|
+
@response_pending = Time.now.to_f + WAIT_TIME
|
231
|
+
if message.dest == [0xff, 0xff, 0xff]
|
232
|
+
@broadcast_pending = Time.now.to_f + BROADCAST_WAIT
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# spin until there is a message
|
238
|
+
@cond.wait(@mutex) unless message
|
239
|
+
end
|
240
|
+
next unless message
|
241
|
+
|
168
242
|
puts "writing #{message.inspect}"
|
169
243
|
@sdn.write(message.serialize)
|
170
244
|
@sdn.flush
|
171
|
-
# give more response time to a discovery message
|
172
|
-
sleep 5 if (message.is_a?(SDN::Message::GetNodeAddr) && message.dest == [0xff, 0xff, 0xff])
|
173
|
-
sleep 0.1
|
174
245
|
end
|
175
246
|
rescue => e
|
176
247
|
puts "failure writing: #{e}"
|
@@ -182,7 +253,10 @@ module SDN
|
|
182
253
|
puts "got #{value.inspect} at #{topic}"
|
183
254
|
if topic == "#{@base_topic}/discovery/discover/set" && value == "true"
|
184
255
|
# trigger discovery
|
185
|
-
@
|
256
|
+
@mutex.synchronize do
|
257
|
+
@request_queue.push(SDN::Message::GetNodeAddr.new)
|
258
|
+
@cond.signal
|
259
|
+
end
|
186
260
|
elsif (match = topic.match(%r{^#{Regexp.escape(@base_topic)}/(?<addr>\h{6})/(?<property>label|down|up|stop|positionpulses|positionpercent|ip|wink|reset|(?<speed_type>upspeed|downspeed|slowspeed)|uplimit|downlimit|direction|ip(?<ip>\d+)(?<ip_type>pulses|percent)|groups)/set$}))
|
187
261
|
addr = SDN::Message.parse_address(match[:addr])
|
188
262
|
property = match[:property]
|
@@ -255,13 +329,18 @@ module SDN
|
|
255
329
|
next if is_group
|
256
330
|
next unless motor
|
257
331
|
messages = motor.set_groups(value)
|
258
|
-
|
332
|
+
@mutex.synchronize do
|
333
|
+
messages.each { |m| @command_queue.push(m) }
|
334
|
+
@cond.signal
|
335
|
+
end
|
259
336
|
nil
|
260
337
|
end
|
261
338
|
if message
|
262
|
-
@
|
263
|
-
|
264
|
-
|
339
|
+
@mutex.synchronize do
|
340
|
+
@command_queue.push(message)
|
341
|
+
@request_queue.push(follow_up) unless @request_queue.include?(follow_up)
|
342
|
+
@cond.signal
|
343
|
+
end
|
265
344
|
end
|
266
345
|
end
|
267
346
|
end
|
@@ -275,6 +354,16 @@ module SDN
|
|
275
354
|
@mqtt.subscribe("#{@base_topic}/#{topic}")
|
276
355
|
end
|
277
356
|
|
357
|
+
def enqueue(message, queue = :command)
|
358
|
+
@mutex.synchronize do
|
359
|
+
queue = instance_variable_get(:"#{@queue}_queue")
|
360
|
+
unless queue.include?(message)
|
361
|
+
queue.push(message)
|
362
|
+
@cond.signal
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
278
367
|
def publish_basic_attributes
|
279
368
|
publish("$homie", "v4.0.0")
|
280
369
|
publish("$name", "Somfy SDN Network")
|
@@ -441,14 +530,17 @@ module SDN
|
|
441
530
|
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.to_a).join(","))
|
442
531
|
|
443
532
|
sdn_addr = SDN::Message.parse_address(addr)
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
533
|
+
@mutex.synchronize do
|
534
|
+
@request_queue.push(SDN::Message::GetNodeLabel.new(sdn_addr))
|
535
|
+
@request_queue.push(SDN::Message::GetMotorStatus.new(sdn_addr))
|
536
|
+
@request_queue.push(SDN::Message::GetMotorLimits.new(sdn_addr))
|
537
|
+
@request_queue.push(SDN::Message::GetMotorDirection.new(sdn_addr))
|
538
|
+
@request_queue.push(SDN::Message::GetMotorRollingSpeed.new(sdn_addr))
|
539
|
+
(1..16).each { |ip| @request_queue.push(SDN::Message::GetMotorIP.new(sdn_addr, ip)) }
|
540
|
+
(0...16).each { |g| @request_queue.push(SDN::Message::GetGroupAddr.new(sdn_addr, g)) }
|
541
|
+
|
542
|
+
@cond.signal
|
543
|
+
end
|
452
544
|
|
453
545
|
motor
|
454
546
|
end
|
data/lib/sdn/version.rb
CHANGED