somfy_sdn 1.0.5 → 1.0.10

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: c1f060c34defbdfc40cae2d9e3f34535aa31d44d45bbcbb00ff769fc29cbb01c
4
- data.tar.gz: a83d074584f5142609c9b2752fbefbeddb7d5247f7a4a6a2c26fa80d3f679c68
3
+ metadata.gz: 57ade06be997d5f36fa76aa13c87aa00e26b1bfe07de7a7fd29b9015f249a1a3
4
+ data.tar.gz: '03462586cabf999666ac43e83036811b8a10df15515ea17d7bfd7b4d2e421e3a'
5
5
  SHA512:
6
- metadata.gz: 39226a05926b458bffb5f91ba69dbcd06d19a1d6daccf98d47c4caf2908e3d991afe5b0ee1917a4dcb0cc4d96fb03056cc9b4f017408666d7c93b32528f10e08
7
- data.tar.gz: 2b80671284b192bc6eba54c98bf615e41dfe7990be3d343127f14a5cd8c222b7a45840c52c1de8a208d8b9468bdfd3bafd8acdd3f829198a9c2321ea6485b3b3
6
+ metadata.gz: '00080b23f47bd5c11c13406d818feec587c744b132cb1b1f4cbd57f3a64cd002c0747e4bccb575315e996b7ec7b92d736a5892116c31b277f85fcbf83fd36c6f'
7
+ data.tar.gz: '01700481ff1ed2966a00287c5c9f1193b600b56518ce36da8a06dae4768daa51fac0da4635993b9eb3daf6ea8a0f068f3e8fd7aee5bd9977e3350eb46aa58d6a'
@@ -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,7 +3,7 @@ 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
8
  def parse(data)
9
9
  offset = -1
@@ -11,7 +11,7 @@ module SDN
11
11
  # we loop here scanning for a valid message
12
12
  loop do
13
13
  offset += 1
14
- return nil if data.length - offset < 11
14
+ return [nil, 0] if data.length - offset < 11
15
15
  msg = to_number(data[offset])
16
16
  length = to_number(data[offset + 1])
17
17
  ack_requested = length & 0x80 == 0x80
@@ -39,9 +39,14 @@ module SDN
39
39
  reserved = to_number(data[offset + 2])
40
40
  src = transform_param(data.slice(offset + 3, 3))
41
41
  dest = transform_param(data.slice(offset + 6, 3))
42
- result = message_class.new(reserved: reserved, ack_requested: ack_requested, src: src, dest: dest)
43
- result.parse(data.slice(offset + 9, length - 11))
44
- result.msg = msg if message_class == UnknownMessage
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
49
+ end
45
50
  [result, offset + length]
46
51
  end
47
52
  end
@@ -50,6 +55,7 @@ module SDN
50
55
  singleton_class.include Helpers
51
56
 
52
57
  attr_reader :reserved, :ack_requested, :src, :dest
58
+ attr_writer :ack_requested
53
59
 
54
60
  def initialize(reserved: nil, ack_requested: false, src: nil, dest: nil)
55
61
  @reserved = reserved || 0x02 # message sent to Sonesse 30
@@ -72,10 +78,13 @@ module SDN
72
78
  length |= 0x80 if ack_requested
73
79
  result = transform_param(self.class.const_get(:MSG)) + transform_param(length) + result
74
80
  result.concat(checksum(result))
75
- puts "wrote #{result.map { |b| '%02x' % b }.join(' ')}"
76
81
  result.pack("C*")
77
82
  end
78
83
 
84
+ def ==(other)
85
+ self.serialize == other.serialize
86
+ end
87
+
79
88
  def inspect
80
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]
81
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,10 +1,36 @@
1
1
  require 'mqtt'
2
- require 'serialport'
3
- require 'socket'
4
2
  require 'uri'
5
3
  require 'set'
6
4
 
7
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
+
8
34
  Motor = Struct.new(:bridge,
9
35
  :addr,
10
36
  :label,
@@ -54,49 +80,56 @@ module SDN
54
80
  :ip16pulses,
55
81
  :ip16percent,
56
82
  :groups) do
57
- def initialize(*)
58
- members.each { |k| self[k] = :nil }
59
- @groups = [].fill(nil, 0, 16)
60
- super
61
- end
62
-
63
- def publish(attribute, value)
64
- if self[attribute] != value
65
- bridge.publish("#{addr}/#{attribute}", value.to_s)
66
- self[attribute] = value
67
- end
68
- end
69
-
70
- def add_group(index, address)
71
- bridge.add_group(SDN::Message.print_address(address)) if address
72
- @groups[index] = address
73
- publish(:groups, groups_string)
74
- end
75
-
76
- def set_groups(groups)
77
- return unless groups =~ /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i
78
- groups = groups.split(',').sort.uniq.map { |g| SDN::Message.parse_address(g) }
79
- groups.fill(nil, groups.length, 16 - groups.length)
80
- messages = []
81
- sdn_addr = SDN::Message.parse_address(addr)
82
- groups.each_with_index do |g, i|
83
- if @groups[i] != g
84
- messages << SDN::Message::SetGroupAddr.new(sdn_addr, i, g)
85
- messages << SDN::Message::GetGroupAddr.new(sdn_addr, i)
86
- end
87
- end
88
- messages
89
- end
90
-
91
- def groups_string
92
- @groups.compact.map { |g| SDN::Message.print_address(g) }.sort.uniq.join(',')
93
- 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
94
125
  end
95
126
 
96
127
  class MQTTBridge
97
128
  WAIT_TIME = 0.25
98
129
  BROADCAST_WAIT = 5.0
99
130
 
131
+ attr_reader :motors, :groups
132
+
100
133
  def initialize(mqtt_uri, port, device_id: "somfy", base_topic: "homie")
101
134
  @base_topic = "#{base_topic}/#{device_id}"
102
135
  @mqtt = MQTT::Client.new(mqtt_uri)
@@ -104,12 +137,11 @@ module SDN
104
137
  @mqtt.connect
105
138
 
106
139
  @motors = {}
107
- @groups = Set.new
140
+ @groups = {}
108
141
 
109
142
  @mutex = Mutex.new
110
143
  @cond = ConditionVariable.new
111
- @command_queue = []
112
- @request_queue = []
144
+ @queues = [[], [], []]
113
145
  @response_pending = false
114
146
  @broadcast_pending = false
115
147
 
@@ -117,9 +149,17 @@ module SDN
117
149
 
118
150
  uri = URI.parse(port)
119
151
  if uri.scheme == "tcp"
152
+ require 'socket'
120
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)
121
160
  else
122
- @sdn = SerialPort.open(serialport, "baud" => 4800, "parity" => SerialPort::ODD)
161
+ require 'serialport'
162
+ @sdn = SerialPort.open(port, "baud" => 4800, "parity" => SerialPort::ODD)
123
163
  end
124
164
 
125
165
  read_thread = Thread.new do
@@ -127,13 +167,15 @@ module SDN
127
167
  loop do
128
168
  begin
129
169
  message, bytes_read = SDN::Message.parse(buffer.bytes)
170
+ # discard how much we read
171
+ buffer = buffer[bytes_read..-1]
130
172
  unless message
131
173
  begin
132
- buffer.concat(@sdn.read_nonblock(1))
174
+ buffer.concat(@sdn.read_nonblock(64 * 1024))
133
175
  next
134
176
  rescue IO::WaitReadable
135
177
  wait = buffer.empty? ? nil : WAIT_TIME
136
- if IO.select([@sdn], nil, nil, wait).nil?
178
+ if @sdn.wait_readable(wait).nil?
137
179
  # timed out; just discard everything
138
180
  puts "timed out reading; discarding buffer: #{buffer.unpack('H*').first}"
139
181
  buffer = ""
@@ -141,8 +183,6 @@ module SDN
141
183
  end
142
184
  next
143
185
  end
144
- # discard how much we read
145
- buffer = buffer[bytes_read..-1]
146
186
 
147
187
  src = SDN::Message.print_address(message.src)
148
188
  # ignore the UAI Plus and ourselves
@@ -162,6 +202,17 @@ module SDN
162
202
  motor.publish(:positionpercent, message.position_percent)
163
203
  motor.publish(:positionpulses, message.position_pulses)
164
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
165
216
  when SDN::Message::PostMotorStatus
166
217
  if message.state == :running || motor.state == :running
167
218
  follow_ups << SDN::Message::GetMotorStatus.new(message.src)
@@ -172,6 +223,11 @@ module SDN
172
223
  motor.publish(:last_direction, message.last_direction)
173
224
  motor.publish(:last_action_source, message.last_action_source)
174
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
175
231
  when SDN::Message::PostMotorLimits
176
232
  motor.publish(:uplimit, message.up_limit)
177
233
  motor.publish(:downlimit, message.down_limit)
@@ -182,7 +238,7 @@ module SDN
182
238
  motor.publish(:downspeed, message.down_speed)
183
239
  motor.publish(:slowspeed, message.slow_speed)
184
240
  when SDN::Message::PostMotorIP
185
- motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
241
+ motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
186
242
  motor.publish(:"ip#{message.ip}percent", message.position_percent)
187
243
  when SDN::Message::PostGroupAddr
188
244
  motor.add_group(message.group_index, message.group_address)
@@ -191,9 +247,14 @@ module SDN
191
247
  @mutex.synchronize do
192
248
  signal = @response_pending || !follow_ups.empty?
193
249
  @response_pending = @broadcast_pending
194
- @request_queue.concat(follow_ups)
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
195
253
  @cond.signal if signal
196
254
  end
255
+ rescue EOFError
256
+ puts "EOF reading"
257
+ exit 2
197
258
  rescue SDN::MalformedMessage => e
198
259
  puts "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
199
260
  rescue => e
@@ -205,43 +266,63 @@ module SDN
205
266
  write_thread = Thread.new do
206
267
  begin
207
268
  loop do
208
- message = nil
269
+ message_and_retries = nil
209
270
  @mutex.synchronize do
210
271
  # got woken up early by another command getting queued; spin
211
272
  if @response_pending
273
+ puts "another message queued, but we're still waiting"
212
274
  while @response_pending
213
275
  remaining_wait = @response_pending - Time.now.to_f
214
276
  if remaining_wait < 0
215
277
  puts "timed out waiting on response"
216
278
  @response_pending = nil
217
279
  @broadcast_pending = nil
280
+ if @prior_message&.remaining_retries != 0
281
+ puts "retrying #{@prior_message.remaining_retries} more times ..."
282
+ @queues[@prior_message.priority].push(@prior_message)
283
+ @prior_message = nil
284
+ end
218
285
  else
286
+ puts "waiting #{remaining_wait} more..."
219
287
  @cond.wait(@mutex, remaining_wait)
220
288
  end
221
289
  end
222
290
  else
291
+ # minimum time between messages
292
+ puts "waiting between messages"
223
293
  sleep 0.1
224
294
  end
225
295
 
226
- message = @command_queue.shift
227
- unless message
228
- message = @request_queue.shift
229
- if message
296
+ puts "looking for next message to write"
297
+ @queues.find { |q| message_and_retries = q.shift }
298
+ if message_and_retries
299
+ if message_and_retries.message.ack_requested || message_and_retries.message.class.name =~ /^SDN::Message::Get/
230
300
  @response_pending = Time.now.to_f + WAIT_TIME
231
- if message.dest == [0xff, 0xff, 0xff]
301
+ 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)
232
302
  @broadcast_pending = Time.now.to_f + BROADCAST_WAIT
233
- end
303
+ end
234
304
  end
235
305
  end
236
306
 
237
- # spin until there is a message
238
- @cond.wait(@mutex) unless message
307
+ # wait until there is a message
308
+ if @response_pending
309
+ message_and_retries.remaining_retries -= 1
310
+ @prior_message = message_and_retries
311
+ elsif message_and_retries
312
+ @prior_message = nil
313
+ else
314
+ @cond.wait(@mutex)
315
+ end
239
316
  end
240
- next unless message
317
+ next unless message_and_retries
241
318
 
319
+ message = message_and_retries.message
242
320
  puts "writing #{message.inspect}"
243
- @sdn.write(message.serialize)
321
+ puts "(and waiting for response)" if @response_pending
322
+ serialized = message.serialize
323
+ @sdn.write(serialized)
244
324
  @sdn.flush
325
+ puts "wrote #{serialized.unpack("C*").map { |b| '%02x' % b }.join(' ')}"
245
326
  end
246
327
  rescue => e
247
328
  puts "failure writing: #{e}"
@@ -254,10 +335,10 @@ module SDN
254
335
  if topic == "#{@base_topic}/discovery/discover/set" && value == "true"
255
336
  # trigger discovery
256
337
  @mutex.synchronize do
257
- @request_queue.push(SDN::Message::GetNodeAddr.new)
338
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeAddr.new, 1, 2))
258
339
  @cond.signal
259
340
  end
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$}))
341
+ 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$}))
261
342
  addr = SDN::Message.parse_address(match[:addr])
262
343
  property = match[:property]
263
344
  # not homie compliant; allows linking the positionpercent property
@@ -266,10 +347,15 @@ module SDN
266
347
  property = value.downcase
267
348
  value = "true"
268
349
  end
269
- motor = @motors[SDN::Message.print_address(addr).gsub('.', '')]
350
+ mqtt_addr = SDN::Message.print_address(addr).gsub('.', '')
351
+ motor = @motors[mqtt_addr]
270
352
  is_group = SDN::Message.is_group_address?(addr)
353
+ group = @groups[mqtt_addr]
271
354
  follow_up = SDN::Message::GetMotorStatus.new(addr)
272
355
  message = case property
356
+ when 'discover'
357
+ follow_up = nil
358
+ SDN::Message::GetNodeAddr.new(addr) if value == "true"
273
359
  when 'label'
274
360
  follow_up = SDN::Message::GetNodeLabel.new(addr)
275
361
  SDN::Message::SetNodeLabel.new(addr, value) unless is_group
@@ -330,15 +416,16 @@ module SDN
330
416
  next unless motor
331
417
  messages = motor.set_groups(value)
332
418
  @mutex.synchronize do
333
- messages.each { |m| @command_queue.push(m) }
419
+ messages.each { |m| @queues[0].push(MessageAndRetries.new(m, 5, 0)) }
334
420
  @cond.signal
335
421
  end
336
422
  nil
337
423
  end
338
424
  if message
425
+ message.ack_requested = true if message.class.name !~ /^SDN::Message::Get/
339
426
  @mutex.synchronize do
340
- @command_queue.push(message)
341
- @request_queue.push(follow_up) unless @request_queue.include?(follow_up)
427
+ @queues[0].push(MessageAndRetries.new(message, 5, 0))
428
+ @queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
342
429
  @cond.signal
343
430
  end
344
431
  end
@@ -379,7 +466,7 @@ module SDN
379
466
  publish("discovery/discover/$settable", "true")
380
467
  publish("discovery/discover/$retained", "false")
381
468
 
382
- subscribe("discovery/discover/set")
469
+ subscribe("+/discover/set")
383
470
  subscribe("+/label/set")
384
471
  subscribe("+/down/set")
385
472
  subscribe("+/up/set")
@@ -407,7 +494,12 @@ module SDN
407
494
  def publish_motor(addr)
408
495
  publish("#{addr}/$name", addr)
409
496
  publish("#{addr}/$type", "Sonesse 30 Motor")
410
- 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")
497
+ 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")
498
+
499
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
500
+ publish("#{addr}/discover/$datatype", "boolean")
501
+ publish("#{addr}/discover/$settable", "true")
502
+ publish("#{addr}/discover/$retained", "false")
411
503
 
412
504
  publish("#{addr}/label/$name", "Node label")
413
505
  publish("#{addr}/label/$datatype", "string")
@@ -527,17 +619,17 @@ module SDN
527
619
 
528
620
  motor = Motor.new(self, addr)
529
621
  @motors[addr] = motor
530
- publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.to_a).join(","))
622
+ publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
531
623
 
532
624
  sdn_addr = SDN::Message.parse_address(addr)
533
625
  @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)) }
626
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeLabel.new(sdn_addr), 5, 2))
627
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorStatus.new(sdn_addr), 5, 2))
628
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorLimits.new(sdn_addr), 5, 2))
629
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorDirection.new(sdn_addr), 5, 2))
630
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorRollingSpeed.new(sdn_addr), 5, 2))
631
+ (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
632
+ (0...16).each { |g| @queues[2].push(MessageAndRetries.new(SDN::Message::GetGroupAddr.new(sdn_addr, g), 5, 2)) }
541
633
 
542
634
  @cond.signal
543
635
  end
@@ -547,11 +639,17 @@ module SDN
547
639
 
548
640
  def add_group(addr)
549
641
  addr = addr.gsub('.', '')
550
- return if @groups.include?(addr)
642
+ group = @groups[addr]
643
+ return group if group
551
644
 
552
645
  publish("#{addr}/$name", addr)
553
646
  publish("#{addr}/$type", "Shade Group")
554
- publish("#{addr}/$properties", "down,up,stop,positionpulses,positionpercent,ip,wink,reset")
647
+ publish("#{addr}/$properties", "discover,down,up,stop,positionpulses,positionpercent,ip,wink,reset,state,motors")
648
+
649
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
650
+ publish("#{addr}/discover/$datatype", "boolean")
651
+ publish("#{addr}/discover/$settable", "true")
652
+ publish("#{addr}/discover/$retained", "false")
555
653
 
556
654
  publish("#{addr}/down/$name", "Move in down direction")
557
655
  publish("#{addr}/down/$datatype", "boolean")
@@ -566,7 +664,7 @@ module SDN
566
664
  publish("#{addr}/stop/$name", "Cancel adjustments")
567
665
  publish("#{addr}/stop/$datatype", "boolean")
568
666
  publish("#{addr}/stop/$settable", "true")
569
- publish("#{addr}/stop/$retained", "false")
667
+ publish("#{addr}/stop/$retained", "false")
570
668
 
571
669
  publish("#{addr}/positionpulses/$name", "Position from up limit (in pulses)")
572
670
  publish("#{addr}/positionpulses/$datatype", "integer")
@@ -590,8 +688,16 @@ module SDN
590
688
  publish("#{addr}/wink/$settable", "true")
591
689
  publish("#{addr}/wink/$retained", "false")
592
690
 
593
- @groups << addr
594
- publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.to_a).join(","))
691
+ publish("#{addr}/state/$name", "State of the motors; only set if all motors are in the same state")
692
+ publish("#{addr}/state/$datatype", "enum")
693
+ publish("#{addr}/state/$format", SDN::Message::PostMotorStatus::STATE.keys.join(','))
694
+
695
+ publish("#{addr}/motors/$name", "Motors that are members of this group")
696
+ publish("#{addr}/motors/$datatype", "string")
697
+
698
+ group = @groups[addr] = Group.new(self, addr)
699
+ publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
700
+ group
595
701
  end
596
702
  end
597
703
  end
@@ -1,3 +1,3 @@
1
1
  module SDN
2
- VERSION = '1.0.5'
2
+ VERSION = '1.0.10'
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.5
4
+ version: 1.0.10
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