somfy_sdn 1.0.4 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 339d400e53c19eeaa513328963a29c4abedb1b4f046d096a478b4f07576224fb
4
- data.tar.gz: 806e109d0934740ceebfc5ea51112b91073b9782d5fbb3267aac49808f98690a
3
+ metadata.gz: c1f060c34defbdfc40cae2d9e3f34535aa31d44d45bbcbb00ff769fc29cbb01c
4
+ data.tar.gz: a83d074584f5142609c9b2752fbefbeddb7d5247f7a4a6a2c26fa80d3f679c68
5
5
  SHA512:
6
- metadata.gz: a2b29a899d358c26a2640e359263785354f9eaff371ec5738d4cb78f58352d6e58fc91c24dca7f4445137eb2739e583cd3901ebda81a161f1d49b223e44617ff
7
- data.tar.gz: 5bc0d2fee7de84b8d69a6396f03ae4f4d61248b9c109c08f3e7ba1d3e35bc2d0a93e46178ee5c7963efdf6360da43969c9334e83b70ae453abe392743145a5c7
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 readpartial(io, length, allow_empty: true)
9
- data = []
10
- while data.length < length
11
- begin
12
- data.concat(io.read_nonblock(length - data.length).bytes)
13
- rescue EOFError
14
- break
15
- rescue IO::WaitReadable
16
- break if allow_empty
17
- IO.select([io])
18
- end
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 "read #{(data + read_sum).map { |b| '%02x' % b }.join(' ')}"
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[3..5])
76
- dest = transform_param(data[6..8])
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[9..-1])
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
 
@@ -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
- def initialize(mqtt_uri, serialport, device_id: "somfy", base_topic: "homie")
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
- @write_queue = Queue.new
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
- @sdn = SerialPort.open(serialport, "baud" => 4800, "parity" => SerialPort::ODD)
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(@sdn)
112
- next unless message
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
- @write_queue.push(SDN::Message::GetMotorStatus.new(message.src))
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
- @write_queue.push(SDN::Message::GetMotorPosition.new(message.src))
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 = @write_queue.pop
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
- @write_queue.push(SDN::Message::GetNodeAddr.new)
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
- messages.each { |m| @write_queue.push(m) }
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
- @write_queue.push(message)
263
- next if follow_up.is_a?(SDN::Message::GetMotorStatus) && motor&.state == :running
264
- @write_queue.push(follow_up)
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
- # these messages are often corrupt; just don't bother for now.
445
- #@write_queue.push(SDN::Message::GetNodeLabel.new(sdn_addr))
446
- @write_queue.push(SDN::Message::GetMotorStatus.new(sdn_addr))
447
- @write_queue.push(SDN::Message::GetMotorLimits.new(sdn_addr))
448
- @write_queue.push(SDN::Message::GetMotorDirection.new(sdn_addr))
449
- @write_queue.push(SDN::Message::GetMotorRollingSpeed.new(sdn_addr))
450
- (1..16).each { |ip| @write_queue.push(SDN::Message::GetMotorIP.new(sdn_addr, ip)) }
451
- (0...16).each { |g| @write_queue.push(SDN::Message::GetGroupAddr.new(sdn_addr, g)) }
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
@@ -1,3 +1,3 @@
1
1
  module SDN
2
- VERSION = '1.0.4'
2
+ VERSION = '1.0.5'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: somfy_sdn
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer