somfy_sdn 1.0.4 → 1.0.9

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 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