somfy_sdn 1.0.5 → 1.0.10

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