somfy_sdn 1.0.4 → 1.0.9

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: 7b76beaca35ebf0ae0847fc75bf0fcd12c45e395768cca0c711557b167488e3e
4
+ data.tar.gz: 64dfc392ef651d1afc6e6a94c8c586e1e95e575c0ff9ba12c74f472cbfe88874
5
5
  SHA512:
6
- metadata.gz: a2b29a899d358c26a2640e359263785354f9eaff371ec5738d4cb78f58352d6e58fc91c24dca7f4445137eb2739e583cd3901ebda81a161f1d49b223e44617ff
7
- data.tar.gz: 5bc0d2fee7de84b8d69a6396f03ae4f4d61248b9c109c08f3e7ba1d3e35bc2d0a93e46178ee5c7963efdf6360da43969c9334e83b70ae453abe392743145a5c7
6
+ metadata.gz: 87a1138187217fb8bf7fe62e00f69e20b742d2527d4f0059b9a2d4ba42012c887947608fdbfad4123400a0968d99d9c257cb361025ecef668786d34b708a0512
7
+ data.tar.gz: e6ddbe689d8d60d252c24a1291893b05040a638ba3b86ac3a7bc1c28479a9ccdf0485e5914035818c50e1005b076d148263916ae1e7a66e05efdb075786d1d95
@@ -0,0 +1,6 @@
1
+ require 'sdn/message'
2
+ require 'sdn/mqtt_bridge'
3
+
4
+ module SDN
5
+ BROADCAST_ADDRESS = [0xff, 0xff, 0xff]
6
+ end
@@ -3,81 +3,51 @@ require 'sdn/messages/helpers'
3
3
  module SDN
4
4
  class MalformedMessage < RuntimeError; end
5
5
 
6
- class Message
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, 0] 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
35
 
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}"
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(' ')}"
38
+
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))
42
+ begin
43
+ result = message_class.new(reserved: reserved, ack_requested: ack_requested, src: src, dest: dest)
44
+ result.parse(data.slice(offset + 9, length - 11))
45
+ result.msg = msg if message_class == UnknownMessage
46
+ rescue ArgumentError => e
47
+ puts "discarding illegal message #{e}"
48
+ result = nil
44
49
  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
-
72
- puts "read #{(data + read_sum).map { |b| '%02x' % b }.join(' ')}"
73
-
74
- reserved = to_number(data[2])
75
- src = transform_param(data[3..5])
76
- dest = transform_param(data[6..8])
77
- result = message_class.new(reserved: reserved, ack_requested: ack_requested, src: src, dest: dest)
78
- result.parse(data[9..-1])
79
- result.msg = msg if message_class == UnknownMessage
80
- result
50
+ [result, offset + length]
81
51
  end
82
52
  end
83
53
 
@@ -85,6 +55,7 @@ module SDN
85
55
  singleton_class.include Helpers
86
56
 
87
57
  attr_reader :reserved, :ack_requested, :src, :dest
58
+ attr_writer :ack_requested
88
59
 
89
60
  def initialize(reserved: nil, ack_requested: false, src: nil, dest: nil)
90
61
  @reserved = reserved || 0x02 # message sent to Sonesse 30
@@ -107,10 +78,13 @@ module SDN
107
78
  length |= 0x80 if ack_requested
108
79
  result = transform_param(self.class.const_get(:MSG)) + transform_param(length) + result
109
80
  result.concat(checksum(result))
110
- puts "wrote #{result.map { |b| '%02x' % b }.join(' ')}"
111
81
  result.pack("C*")
112
82
  end
113
83
 
84
+ def ==(other)
85
+ self.serialize == other.serialize
86
+ end
87
+
114
88
  def inspect
115
89
  "#<%s @reserved=%02xh, @ack_requested=%s, @src=%s, @dest=%s%s>" % [self.class.name, reserved, ack_requested, print_address(src), print_address(dest), class_inspect]
116
90
  end
@@ -27,17 +27,17 @@ module SDN
27
27
  end
28
28
 
29
29
  def direction=(value)
30
- raise ArgumentError, "direction must be one of :down, :up, or :cancel" unless DIRECTION.keys.include?(value)
30
+ raise ArgumentError, "direction must be one of :down, :up, or :cancel (#{value})" unless DIRECTION.keys.include?(value)
31
31
  @direction = value
32
32
  end
33
33
 
34
34
  def duration=(value)
35
- raise ArgumentError, "duration must be in range 0x0a to 0xff" unless value || value >= 0x0a && value <= 0xff
35
+ raise ArgumentError, "duration must be in range 0x0a to 0xff (#{value})" unless value && value >= 0x0a && value <= 0xff
36
36
  @duration = value
37
37
  end
38
38
 
39
39
  def speed=(value)
40
- raise ArgumentError, "speed must be one of :up, :down, or :slow" unless SPEED.keys.include?(value)
40
+ raise ArgumentError, "speed must be one of :up, :down, or :slow (#{value})" unless SPEED.keys.include?(value)
41
41
  @speed = speed
42
42
  end
43
43
 
@@ -0,0 +1,9 @@
1
+ module SDN
2
+ class Message
3
+ module ILT2
4
+ class GetMotorPosition < SimpleRequest
5
+ MSG = 0x44
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ module SDN
2
+ class Message
3
+ module ILT2
4
+ class PostMotorPosition < Message
5
+ MSG = 0x64
6
+ PARAMS_LENGTH = 3
7
+
8
+ attr_accessor :position_pulses, :position_percent
9
+
10
+ def parse(params)
11
+ super
12
+ self.position_pulses = to_number(params[0..1])
13
+ self.position_percent = to_number(params[2]).to_f / 255 * 100
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,59 @@
1
+ module SDN
2
+ class Message
3
+ module ILT2
4
+ class SetMotorPosition < Message
5
+ MSG = 0x54
6
+ PARAMS_LENGTH = 3
7
+ TARGET_TYPE = {
8
+ up_limit: 1,
9
+ down_limit: 2,
10
+ stop: 3,
11
+ ip: 4,
12
+ next_ip_up: 5,
13
+ next_ip_down: 6,
14
+ jog_up: 10,
15
+ jog_down: 11,
16
+ position_percent: 16,
17
+ }.freeze
18
+
19
+ attr_reader :target_type, :target
20
+
21
+ def initialize(dest = nil, target_type = :up_limit, target = 0, **kwargs)
22
+ kwargs[:dest] ||= dest
23
+ super(**kwargs)
24
+ self.target_type = target_type
25
+ self.target = target
26
+ end
27
+
28
+ def parse(params)
29
+ super
30
+ self.target_type = TARGET_TYPE.invert[to_number(params[0])]
31
+ target = to_number(params[1..2])
32
+ if target_type == :position_percent
33
+ target = target.to_f / 255 * 100
34
+ end
35
+ self.target = target
36
+ end
37
+
38
+ def target_type=(value)
39
+ raise ArgumentError, "target_type must be one of :up_limit, :down_limit, :stop, :ip, :next_ip_up, :next_ip_down, :jog_up, :jog_down, or :position_percent" unless TARGET_TYPE.keys.include?(value)
40
+ @target_type = value
41
+ end
42
+
43
+ def target=(value)
44
+ if target_type == :position_percent && value
45
+ @target = [[0, value].max, 100].min
46
+ else
47
+ @target = value&. & 0xffff
48
+ end
49
+ end
50
+
51
+ def params
52
+ param = target
53
+ param = (param * 255 / 100).to_i if target_type == :position_percent
54
+ transform_param(TARGET_TYPE[target_type]) + from_number(param, 2)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,8 +1,36 @@
1
1
  require 'mqtt'
2
- require 'serialport'
2
+ require 'uri'
3
3
  require 'set'
4
4
 
5
5
  module SDN
6
+ MessageAndRetries = Struct.new(:message, :remaining_retries, :priority)
7
+
8
+ Group = Struct.new(:bridge, :addr, :positionpercent, :state, :motors) do
9
+ def initialize(*)
10
+ members.each { |k| self[k] = :nil }
11
+ super
12
+ end
13
+
14
+ def publish(attribute, value)
15
+ if self[attribute] != value
16
+ bridge.publish("#{addr}/#{attribute}", value.to_s)
17
+ self[attribute] = value
18
+ end
19
+ end
20
+
21
+ def printed_addr
22
+ Message.print_address(Message.parse_address(addr))
23
+ end
24
+
25
+ def motor_objects
26
+ bridge.motors.select { |addr, motor| motor.groups_string.include?(printed_addr) }.values
27
+ end
28
+
29
+ def motors_string
30
+ motor_objects.map { |m| SDN::Message.print_address(SDN::Message.parse_address(m.addr)) }.sort.join(',')
31
+ end
32
+ end
33
+
6
34
  Motor = Struct.new(:bridge,
7
35
  :addr,
8
36
  :label,
@@ -52,64 +80,110 @@ module SDN
52
80
  :ip16pulses,
53
81
  :ip16percent,
54
82
  :groups) do
55
- def initialize(*)
56
- members.each { |k| self[k] = :nil }
57
- @groups = [].fill(nil, 0, 16)
58
- super
59
- end
60
-
61
- def publish(attribute, value)
62
- if self[attribute] != value
63
- bridge.publish("#{addr}/#{attribute}", value.to_s)
64
- self[attribute] = value
65
- end
66
- end
67
-
68
- def add_group(index, address)
69
- bridge.add_group(SDN::Message.print_address(address)) if address
70
- @groups[index] = address
71
- publish(:groups, groups_string)
72
- end
73
-
74
- def set_groups(groups)
75
- return unless groups =~ /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i
76
- groups = groups.split(',').sort.uniq.map { |g| SDN::Message.parse_address(g) }
77
- groups.fill(nil, groups.length, 16 - groups.length)
78
- messages = []
79
- sdn_addr = SDN::Message.parse_address(addr)
80
- groups.each_with_index do |g, i|
81
- if @groups[i] != g
82
- messages << SDN::Message::SetGroupAddr.new(sdn_addr, i, g)
83
- messages << SDN::Message::GetGroupAddr.new(sdn_addr, i)
84
- end
85
- end
86
- messages
87
- end
88
-
89
- def groups_string
90
- @groups.compact.map { |g| SDN::Message.print_address(g) }.sort.uniq.join(',')
91
- end
83
+ def initialize(*)
84
+ members.each { |k| self[k] = :nil }
85
+ @groups = [].fill(nil, 0, 16)
86
+ super
87
+ end
88
+
89
+ def publish(attribute, value)
90
+ if self[attribute] != value
91
+ bridge.publish("#{addr}/#{attribute}", value.to_s)
92
+ self[attribute] = value
93
+ end
94
+ end
95
+
96
+ def add_group(index, address)
97
+ group = bridge.add_group(SDN::Message.print_address(address)) if address
98
+ @groups[index] = address
99
+ group&.publish(:motors, group.motors_string)
100
+ publish(:groups, groups_string)
101
+ end
102
+
103
+ def set_groups(groups)
104
+ return unless groups =~ /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i
105
+ groups = groups.split(',').sort.uniq.map { |g| SDN::Message.parse_address(g) }.select { |g| SDN::Message.is_group_address?(g) }
106
+ groups.fill(nil, groups.length, 16 - groups.length)
107
+ messages = []
108
+ sdn_addr = SDN::Message.parse_address(addr)
109
+ groups.each_with_index do |g, i|
110
+ if @groups[i] != g
111
+ messages << SDN::Message::SetGroupAddr.new(sdn_addr, i, g).tap { |m| m.ack_requested = true }
112
+ messages << SDN::Message::GetGroupAddr.new(sdn_addr, i)
113
+ end
114
+ end
115
+ messages
116
+ end
117
+
118
+ def groups_string
119
+ @groups.compact.map { |g| SDN::Message.print_address(g) }.sort.uniq.join(',')
120
+ end
121
+
122
+ def group_objects
123
+ groups_string.split(',').map { |addr| bridge.groups[addr.gsub('.', '')] }
124
+ end
92
125
  end
93
126
 
94
127
  class MQTTBridge
95
- def initialize(mqtt_uri, serialport, device_id: "somfy", base_topic: "homie")
128
+ WAIT_TIME = 0.25
129
+ BROADCAST_WAIT = 5.0
130
+
131
+ attr_reader :motors, :groups
132
+
133
+ def initialize(mqtt_uri, port, device_id: "somfy", base_topic: "homie")
96
134
  @base_topic = "#{base_topic}/#{device_id}"
97
135
  @mqtt = MQTT::Client.new(mqtt_uri)
98
136
  @mqtt.set_will("#{@base_topic}/$state", "lost", true)
99
137
  @mqtt.connect
138
+
100
139
  @motors = {}
101
- @groups = Set.new
102
- @write_queue = Queue.new
140
+ @groups = {}
141
+
142
+ @mutex = Mutex.new
143
+ @cond = ConditionVariable.new
144
+ @queues = [[], [], []]
145
+ @response_pending = false
146
+ @broadcast_pending = false
103
147
 
104
148
  publish_basic_attributes
105
149
 
106
- @sdn = SerialPort.open(serialport, "baud" => 4800, "parity" => SerialPort::ODD)
150
+ uri = URI.parse(port)
151
+ if uri.scheme == "tcp"
152
+ require 'socket'
153
+ @sdn = TCPSocket.new(uri.host, uri.port)
154
+ elsif uri.scheme == "telnet" || uri.scheme == "rfc2217"
155
+ require 'net/telnet/rfc2217'
156
+ @sdn = Net::Telnet::RFC2217.new('Host' => uri.host,
157
+ 'Port' => uri.port || 23,
158
+ 'baud' => 4800,
159
+ 'parity' => Net::Telnet::RFC2217::ODD)
160
+ else
161
+ require 'serialport'
162
+ @sdn = SerialPort.open(port, "baud" => 4800, "parity" => SerialPort::ODD)
163
+ end
107
164
 
108
165
  read_thread = Thread.new do
166
+ buffer = ""
109
167
  loop do
110
168
  begin
111
- message = SDN::Message.parse(@sdn)
112
- next unless message
169
+ message, bytes_read = SDN::Message.parse(buffer.bytes)
170
+ # discard how much we read
171
+ buffer = buffer[bytes_read..-1]
172
+ unless message
173
+ begin
174
+ buffer.concat(@sdn.read_nonblock(64 * 1024))
175
+ next
176
+ rescue IO::WaitReadable
177
+ wait = buffer.empty? ? nil : WAIT_TIME
178
+ if @sdn.wait_readable(wait).nil?
179
+ # timed out; just discard everything
180
+ puts "timed out reading; discarding buffer: #{buffer.unpack('H*').first}"
181
+ buffer = ""
182
+ end
183
+ end
184
+ next
185
+ end
186
+
113
187
  src = SDN::Message.print_address(message.src)
114
188
  # ignore the UAI Plus and ourselves
115
189
  if src != '7F.7F.7F' && !SDN::Message::is_group_address?(message.src) && !(motor = @motors[src.gsub('.', '')])
@@ -118,6 +192,7 @@ module SDN
118
192
  end
119
193
 
120
194
  puts "read #{message.inspect}"
195
+ follow_ups = []
121
196
  case message
122
197
  when SDN::Message::PostNodeLabel
123
198
  if (motor.publish(:label, message.label))
@@ -127,16 +202,32 @@ module SDN
127
202
  motor.publish(:positionpercent, message.position_percent)
128
203
  motor.publish(:positionpulses, message.position_pulses)
129
204
  motor.publish(:ip, message.ip)
205
+ motor.group_objects.each do |group|
206
+ positions = group.motor_objects.map(&:positionpercent)
207
+ position = nil
208
+ # calculate an average, but only if we know a position for
209
+ # every shade
210
+ if !positions.include?(:nil) && !positions.include?(nil)
211
+ position = positions.inject(&:+) / positions.length
212
+ end
213
+
214
+ group.publish(:positionpercent, position)
215
+ end
130
216
  when SDN::Message::PostMotorStatus
131
217
  if message.state == :running || motor.state == :running
132
- @write_queue.push(SDN::Message::GetMotorStatus.new(message.src))
218
+ follow_ups << SDN::Message::GetMotorStatus.new(message.src)
133
219
  end
134
220
  # this will do one more position request after it stopped
135
- @write_queue.push(SDN::Message::GetMotorPosition.new(message.src))
221
+ follow_ups << SDN::Message::GetMotorPosition.new(message.src)
136
222
  motor.publish(:state, message.state)
137
223
  motor.publish(:last_direction, message.last_direction)
138
224
  motor.publish(:last_action_source, message.last_action_source)
139
225
  motor.publish(:last_action_cause, message.last_action_cause)
226
+ motor.group_objects.each do |group|
227
+ states = group.motor_objects.map(&:state).uniq
228
+ state = states.length == 1 ? states.first : 'mixed'
229
+ group.publish(:state, state)
230
+ end
140
231
  when SDN::Message::PostMotorLimits
141
232
  motor.publish(:uplimit, message.up_limit)
142
233
  motor.publish(:downlimit, message.down_limit)
@@ -147,12 +238,23 @@ module SDN
147
238
  motor.publish(:downspeed, message.down_speed)
148
239
  motor.publish(:slowspeed, message.slow_speed)
149
240
  when SDN::Message::PostMotorIP
150
- motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
241
+ motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
151
242
  motor.publish(:"ip#{message.ip}percent", message.position_percent)
152
243
  when SDN::Message::PostGroupAddr
153
244
  motor.add_group(message.group_index, message.group_address)
154
245
  end
155
246
 
247
+ @mutex.synchronize do
248
+ signal = @response_pending || !follow_ups.empty?
249
+ @response_pending = @broadcast_pending
250
+ follow_ups.each do |follow_up|
251
+ @queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
252
+ end
253
+ @cond.signal if signal
254
+ end
255
+ rescue EOFError
256
+ puts "EOF reading"
257
+ exit 2
156
258
  rescue SDN::MalformedMessage => e
157
259
  puts "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
158
260
  rescue => e
@@ -164,13 +266,57 @@ module SDN
164
266
  write_thread = Thread.new do
165
267
  begin
166
268
  loop do
167
- message = @write_queue.pop
269
+ message_and_retries = nil
270
+ @mutex.synchronize do
271
+ # got woken up early by another command getting queued; spin
272
+ if @response_pending
273
+ while @response_pending
274
+ remaining_wait = @response_pending - Time.now.to_f
275
+ if remaining_wait < 0
276
+ puts "timed out waiting on response"
277
+ @response_pending = nil
278
+ @broadcast_pending = nil
279
+ if @prior_message&.remaining_retries != 0
280
+ puts "retrying #{@prior_message.remaining_retries} more times ..."
281
+ @queues[@prior_message.priority].push(@prior_message)
282
+ @prior_message = nil
283
+ end
284
+ else
285
+ @cond.wait(@mutex, remaining_wait)
286
+ end
287
+ end
288
+ else
289
+ # minimum time between messages
290
+ sleep 0.1
291
+ end
292
+
293
+ @queues.find { |q| message_and_retries = q.shift }
294
+ if message_and_retries
295
+ if message_and_retries.message.ack_requested || message_and_retries.class.name =~ /^SDN::Message::Get/
296
+ @response_pending = Time.now.to_f + WAIT_TIME
297
+ if message_and_retries.message.dest == BROADCAST_ADDRESS || SDN::Message::is_group_address?(message_and_retries.message.src) && message_and_retries.message.is_a?(SDN::Message::GetNodeAddr)
298
+ @broadcast_pending = Time.now.to_f + BROADCAST_WAIT
299
+ end
300
+ end
301
+ end
302
+
303
+ # wait until there is a message
304
+ @cond.wait(@mutex) unless message_and_retries
305
+ end
306
+ next unless message_and_retries
307
+
308
+ message = message_and_retries.message
168
309
  puts "writing #{message.inspect}"
169
- @sdn.write(message.serialize)
310
+ serialized = message.serialize
311
+ @sdn.write(serialized)
170
312
  @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
313
+ puts "wrote #{serialized.unpack("C*").map { |b| '%02x' % b }.join(' ')}"
314
+ if @response_pending
315
+ message_and_retries.remaining_retries -= 1
316
+ @prior_message = message_and_retries
317
+ else
318
+ @prior_message = nil
319
+ end
174
320
  end
175
321
  rescue => e
176
322
  puts "failure writing: #{e}"
@@ -182,8 +328,11 @@ module SDN
182
328
  puts "got #{value.inspect} at #{topic}"
183
329
  if topic == "#{@base_topic}/discovery/discover/set" && value == "true"
184
330
  # trigger discovery
185
- @write_queue.push(SDN::Message::GetNodeAddr.new)
186
- 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$}))
331
+ @mutex.synchronize do
332
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeAddr.new, 1, 2))
333
+ @cond.signal
334
+ end
335
+ elsif (match = topic.match(%r{^#{Regexp.escape(@base_topic)}/(?<addr>\h{6})/(?<property>discover|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
336
  addr = SDN::Message.parse_address(match[:addr])
188
337
  property = match[:property]
189
338
  # not homie compliant; allows linking the positionpercent property
@@ -192,10 +341,15 @@ module SDN
192
341
  property = value.downcase
193
342
  value = "true"
194
343
  end
195
- motor = @motors[SDN::Message.print_address(addr).gsub('.', '')]
344
+ mqtt_addr = SDN::Message.print_address(addr).gsub('.', '')
345
+ motor = @motors[mqtt_addr]
196
346
  is_group = SDN::Message.is_group_address?(addr)
347
+ group = @groups[mqtt_addr]
197
348
  follow_up = SDN::Message::GetMotorStatus.new(addr)
198
349
  message = case property
350
+ when 'discover'
351
+ follow_up = nil
352
+ SDN::Message::GetNodeAddr.new(addr) if value == "true"
199
353
  when 'label'
200
354
  follow_up = SDN::Message::GetNodeLabel.new(addr)
201
355
  SDN::Message::SetNodeLabel.new(addr, value) unless is_group
@@ -255,13 +409,19 @@ module SDN
255
409
  next if is_group
256
410
  next unless motor
257
411
  messages = motor.set_groups(value)
258
- messages.each { |m| @write_queue.push(m) }
412
+ @mutex.synchronize do
413
+ messages.each { |m| @queues[0].push(MessageAndRetries.new(m, 5, 0)) }
414
+ @cond.signal
415
+ end
259
416
  nil
260
417
  end
261
418
  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)
419
+ message.ack_requested = true if motor && message.class.name !~ /^SDN::Message::Get/
420
+ @mutex.synchronize do
421
+ @queues[0].push(MessageAndRetries.new(message, message.ack_requested ? 5 : 1, 0))
422
+ @queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
423
+ @cond.signal
424
+ end
265
425
  end
266
426
  end
267
427
  end
@@ -275,6 +435,16 @@ module SDN
275
435
  @mqtt.subscribe("#{@base_topic}/#{topic}")
276
436
  end
277
437
 
438
+ def enqueue(message, queue = :command)
439
+ @mutex.synchronize do
440
+ queue = instance_variable_get(:"#{@queue}_queue")
441
+ unless queue.include?(message)
442
+ queue.push(message)
443
+ @cond.signal
444
+ end
445
+ end
446
+ end
447
+
278
448
  def publish_basic_attributes
279
449
  publish("$homie", "v4.0.0")
280
450
  publish("$name", "Somfy SDN Network")
@@ -290,7 +460,7 @@ module SDN
290
460
  publish("discovery/discover/$settable", "true")
291
461
  publish("discovery/discover/$retained", "false")
292
462
 
293
- subscribe("discovery/discover/set")
463
+ subscribe("+/discover/set")
294
464
  subscribe("+/label/set")
295
465
  subscribe("+/down/set")
296
466
  subscribe("+/up/set")
@@ -318,7 +488,12 @@ module SDN
318
488
  def publish_motor(addr)
319
489
  publish("#{addr}/$name", addr)
320
490
  publish("#{addr}/$type", "Sonesse 30 Motor")
321
- publish("#{addr}/$properties", "label,down,up,stop,positionpulses,positionpercent,ip,wink,reset,state,last_direction,last_action_source,last_action_cause,uplimit,downlimit,direction,upspeed,downspeed,slowspeed,#{(1..16).map { |ip| "ip#{ip}pulses,ip#{ip}percent" }.join(',')},groups")
491
+ publish("#{addr}/$properties", "discover,label,down,up,stop,positionpulses,positionpercent,ip,wink,reset,state,last_direction,last_action_source,last_action_cause,uplimit,downlimit,direction,upspeed,downspeed,slowspeed,#{(1..16).map { |ip| "ip#{ip}pulses,ip#{ip}percent" }.join(',')},groups")
492
+
493
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
494
+ publish("#{addr}/discover/$datatype", "boolean")
495
+ publish("#{addr}/discover/$settable", "true")
496
+ publish("#{addr}/discover/$retained", "false")
322
497
 
323
498
  publish("#{addr}/label/$name", "Node label")
324
499
  publish("#{addr}/label/$datatype", "string")
@@ -438,28 +613,37 @@ module SDN
438
613
 
439
614
  motor = Motor.new(self, addr)
440
615
  @motors[addr] = motor
441
- publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.to_a).join(","))
616
+ publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
442
617
 
443
618
  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)) }
619
+ @mutex.synchronize do
620
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeLabel.new(sdn_addr), 5, 2))
621
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorStatus.new(sdn_addr), 5, 2))
622
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorLimits.new(sdn_addr), 5, 2))
623
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorDirection.new(sdn_addr), 5, 2))
624
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorRollingSpeed.new(sdn_addr), 5, 2))
625
+ (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
626
+ (0...16).each { |g| @queues[2].push(MessageAndRetries.new(SDN::Message::GetGroupAddr.new(sdn_addr, g), 5, 2)) }
627
+
628
+ @cond.signal
629
+ end
452
630
 
453
631
  motor
454
632
  end
455
633
 
456
634
  def add_group(addr)
457
635
  addr = addr.gsub('.', '')
458
- return if @groups.include?(addr)
636
+ group = @groups[addr]
637
+ return group if group
459
638
 
460
639
  publish("#{addr}/$name", addr)
461
640
  publish("#{addr}/$type", "Shade Group")
462
- publish("#{addr}/$properties", "down,up,stop,positionpulses,positionpercent,ip,wink,reset")
641
+ publish("#{addr}/$properties", "discover,down,up,stop,positionpulses,positionpercent,ip,wink,reset,state,motors")
642
+
643
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
644
+ publish("#{addr}/discover/$datatype", "boolean")
645
+ publish("#{addr}/discover/$settable", "true")
646
+ publish("#{addr}/discover/$retained", "false")
463
647
 
464
648
  publish("#{addr}/down/$name", "Move in down direction")
465
649
  publish("#{addr}/down/$datatype", "boolean")
@@ -474,7 +658,7 @@ module SDN
474
658
  publish("#{addr}/stop/$name", "Cancel adjustments")
475
659
  publish("#{addr}/stop/$datatype", "boolean")
476
660
  publish("#{addr}/stop/$settable", "true")
477
- publish("#{addr}/stop/$retained", "false")
661
+ publish("#{addr}/stop/$retained", "false")
478
662
 
479
663
  publish("#{addr}/positionpulses/$name", "Position from up limit (in pulses)")
480
664
  publish("#{addr}/positionpulses/$datatype", "integer")
@@ -498,8 +682,16 @@ module SDN
498
682
  publish("#{addr}/wink/$settable", "true")
499
683
  publish("#{addr}/wink/$retained", "false")
500
684
 
501
- @groups << addr
502
- publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.to_a).join(","))
685
+ publish("#{addr}/state/$name", "State of the motors; only set if all motors are in the same state")
686
+ publish("#{addr}/state/$datatype", "enum")
687
+ publish("#{addr}/state/$format", SDN::Message::PostMotorStatus::STATE.keys.join(','))
688
+
689
+ publish("#{addr}/motors/$name", "Motors that are members of this group")
690
+ publish("#{addr}/motors/$datatype", "string")
691
+
692
+ group = @groups[addr] = Group.new(self, addr)
693
+ publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
694
+ group
503
695
  end
504
696
  end
505
697
  end
@@ -1,3 +1,3 @@
1
1
  module SDN
2
- VERSION = '1.0.4'
2
+ VERSION = '1.0.9'
3
3
  end
@@ -1,2 +1 @@
1
- require 'sdn/message'
2
- require 'sdn/mqtt_bridge'
1
+ require 'sdn'
metadata CHANGED
@@ -1,43 +1,57 @@
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.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-13 00:00:00.000000000 Z
11
+ date: 2020-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: serialport
14
+ name: mqtt
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.3.1
19
+ version: 0.5.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 1.3.1
26
+ version: 0.5.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: mqtt
28
+ name: net-telnet-rfc2217
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.5.0
33
+ version: 0.0.3
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.5.0
40
+ version: 0.0.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: serialport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.3.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.3.1
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: byebug
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -74,10 +88,14 @@ extensions: []
74
88
  extra_rdoc_files: []
75
89
  files:
76
90
  - bin/sdn_mqtt_bridge
91
+ - lib/sdn.rb
77
92
  - lib/sdn/message.rb
78
93
  - lib/sdn/messages/control.rb
79
94
  - lib/sdn/messages/get.rb
80
95
  - lib/sdn/messages/helpers.rb
96
+ - lib/sdn/messages/ilt2/get.rb
97
+ - lib/sdn/messages/ilt2/post.rb
98
+ - lib/sdn/messages/ilt2/set.rb
81
99
  - lib/sdn/messages/post.rb
82
100
  - lib/sdn/messages/set.rb
83
101
  - lib/sdn/mqtt_bridge.rb