somfy_sdn 1.0.6 → 1.0.11

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: e0d95ac8d2a5acf1ceb0e03051645d8b243f0feffd89da389757df490a85fd6b
4
- data.tar.gz: 86cd04871922fbad94f4b1c7fc5a99da211a1f4cffd398ee69c1cda8ffdcc681
3
+ metadata.gz: ef81905b4b164a5e09d771e07b286f09df6d40cd259b8253c2a0ef1f01f649e4
4
+ data.tar.gz: 4d6f3ad72d6b2e8f06c530a8657d684f4f0b12c9d54eb13e998841b18a8e37dd
5
5
  SHA512:
6
- metadata.gz: 430f8b9cab0a245abe7b150203556376de5afbe409a7f5272634880157b831aed32434baf9758e55bb00ba2afbd732f9e3e8b19605e9e2bb51619a2dc5080405
7
- data.tar.gz: b51a53a9cabd2d2efadcff644b7c7bdfce958744228f6092c318d7bfec4447302570e20de8d36a78e09496e3c5a5a3ec1cf6e32bbbeedb29abcc12bad0a27d64
6
+ metadata.gz: 8bf05e27c1e7c50e6893198950d7ab91099807c9d8ad139629e89fe3fc97d6f46c87e93400e1083ecba7c64ec3638a302523804fd921125414aa62f3632e5edb
7
+ data.tar.gz: 3300c346eeb666d82c686c0e5dec4fb830fa96adabdd7dbc45f3d4cd8e609c13f2796d1ae0b6cd2b2186bcf1ae3251d180e17dd33d578ccfa29eba3f86870e4a
@@ -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,15 +3,23 @@ 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 expected_response
9
+ if name =~ /::Get([A-Za-z]+)/
10
+ const_get("Post#{$1}", true)
11
+ else
12
+ Ack
13
+ end
14
+ end
15
+
8
16
  def parse(data)
9
17
  offset = -1
10
18
  msg = length = ack_requested = message_class = nil
11
19
  # we loop here scanning for a valid message
12
20
  loop do
13
21
  offset += 1
14
- return nil if data.length - offset < 11
22
+ return [nil, 0] if data.length - offset < 11
15
23
  msg = to_number(data[offset])
16
24
  length = to_number(data[offset + 1])
17
25
  ack_requested = length & 0x80 == 0x80
@@ -39,9 +47,14 @@ module SDN
39
47
  reserved = to_number(data[offset + 2])
40
48
  src = transform_param(data.slice(offset + 3, 3))
41
49
  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
50
+ begin
51
+ result = message_class.new(reserved: reserved, ack_requested: ack_requested, src: src, dest: dest)
52
+ result.parse(data.slice(offset + 9, length - 11))
53
+ result.msg = msg if message_class == UnknownMessage
54
+ rescue ArgumentError => e
55
+ puts "discarding illegal message #{e}"
56
+ result = nil
57
+ end
45
58
  [result, offset + length]
46
59
  end
47
60
  end
@@ -50,6 +63,7 @@ module SDN
50
63
  singleton_class.include Helpers
51
64
 
52
65
  attr_reader :reserved, :ack_requested, :src, :dest
66
+ attr_writer :ack_requested
53
67
 
54
68
  def initialize(reserved: nil, ack_requested: false, src: nil, dest: nil)
55
69
  @reserved = reserved || 0x02 # message sent to Sonesse 30
@@ -72,10 +86,13 @@ module SDN
72
86
  length |= 0x80 if ack_requested
73
87
  result = transform_param(self.class.const_get(:MSG)) + transform_param(length) + result
74
88
  result.concat(checksum(result))
75
- puts "wrote #{result.map { |b| '%02x' % b }.join(' ')}"
76
89
  result.pack("C*")
77
90
  end
78
91
 
92
+ def ==(other)
93
+ self.serialize == other.serialize
94
+ end
95
+
79
96
  def inspect
80
97
  "#<%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
98
  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,
@@ -53,50 +79,58 @@ module SDN
53
79
  :ip15percent,
54
80
  :ip16pulses,
55
81
  :ip16percent,
56
- :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
82
+ :groups,
83
+ :last_action) do
84
+ def initialize(*)
85
+ members.each { |k| self[k] = :nil }
86
+ @groups = [].fill(nil, 0, 16)
87
+ super
88
+ end
89
+
90
+ def publish(attribute, value)
91
+ if self[attribute] != value
92
+ bridge.publish("#{addr}/#{attribute}", value.to_s)
93
+ self[attribute] = value
94
+ end
95
+ end
96
+
97
+ def add_group(index, address)
98
+ group = bridge.add_group(SDN::Message.print_address(address)) if address
99
+ @groups[index] = address
100
+ group&.publish(:motors, group.motors_string)
101
+ publish(:groups, groups_string)
102
+ end
103
+
104
+ def set_groups(groups)
105
+ return unless groups =~ /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i
106
+ groups = groups.split(',').sort.uniq.map { |g| SDN::Message.parse_address(g) }.select { |g| SDN::Message.is_group_address?(g) }
107
+ groups.fill(nil, groups.length, 16 - groups.length)
108
+ messages = []
109
+ sdn_addr = SDN::Message.parse_address(addr)
110
+ groups.each_with_index do |g, i|
111
+ if @groups[i] != g
112
+ messages << SDN::Message::SetGroupAddr.new(sdn_addr, i, g).tap { |m| m.ack_requested = true }
113
+ messages << SDN::Message::GetGroupAddr.new(sdn_addr, i)
114
+ end
115
+ end
116
+ messages
117
+ end
118
+
119
+ def groups_string
120
+ @groups.compact.map { |g| SDN::Message.print_address(g) }.sort.uniq.join(',')
121
+ end
122
+
123
+ def group_objects
124
+ groups_string.split(',').map { |addr| bridge.groups[addr.gsub('.', '')] }
125
+ end
94
126
  end
95
127
 
96
128
  class MQTTBridge
97
129
  WAIT_TIME = 0.25
98
130
  BROADCAST_WAIT = 5.0
99
131
 
132
+ attr_reader :motors, :groups
133
+
100
134
  def initialize(mqtt_uri, port, device_id: "somfy", base_topic: "homie")
101
135
  @base_topic = "#{base_topic}/#{device_id}"
102
136
  @mqtt = MQTT::Client.new(mqtt_uri)
@@ -104,12 +138,11 @@ module SDN
104
138
  @mqtt.connect
105
139
 
106
140
  @motors = {}
107
- @groups = Set.new
141
+ @groups = {}
108
142
 
109
143
  @mutex = Mutex.new
110
144
  @cond = ConditionVariable.new
111
- @command_queue = []
112
- @request_queue = []
145
+ @queues = [[], [], []]
113
146
  @response_pending = false
114
147
  @broadcast_pending = false
115
148
 
@@ -117,23 +150,34 @@ module SDN
117
150
 
118
151
  uri = URI.parse(port)
119
152
  if uri.scheme == "tcp"
153
+ require 'socket'
120
154
  @sdn = TCPSocket.new(uri.host, uri.port)
155
+ elsif uri.scheme == "telnet" || uri.scheme == "rfc2217"
156
+ require 'net/telnet/rfc2217'
157
+ @sdn = Net::Telnet::RFC2217.new('Host' => uri.host,
158
+ 'Port' => uri.port || 23,
159
+ 'baud' => 4800,
160
+ 'parity' => Net::Telnet::RFC2217::ODD)
121
161
  else
122
- @sdn = SerialPort.open(port, "baud" => 4800, "parity" => SerialPort::ODD)
162
+ require 'ccutrer-serialport'
163
+ @sdn = CCutrer::SerialPort.new(port, baud: 4800, parity: :odd)
123
164
  end
124
165
 
125
166
  read_thread = Thread.new do
126
167
  buffer = ""
168
+
127
169
  loop do
128
170
  begin
129
171
  message, bytes_read = SDN::Message.parse(buffer.bytes)
172
+ # discard how much we read
173
+ buffer = buffer[bytes_read..-1]
130
174
  unless message
131
175
  begin
132
- buffer.concat(@sdn.read_nonblock(1))
176
+ buffer.concat(@sdn.read_nonblock(64 * 1024))
133
177
  next
134
- rescue IO::WaitReadable
178
+ rescue IO::WaitReadable, EOFError
135
179
  wait = buffer.empty? ? nil : WAIT_TIME
136
- if IO.select([@sdn], nil, nil, wait).nil?
180
+ if @sdn.wait_readable(wait).nil?
137
181
  # timed out; just discard everything
138
182
  puts "timed out reading; discarding buffer: #{buffer.unpack('H*').first}"
139
183
  buffer = ""
@@ -141,8 +185,6 @@ module SDN
141
185
  end
142
186
  next
143
187
  end
144
- # discard how much we read
145
- buffer = buffer[bytes_read..-1]
146
188
 
147
189
  src = SDN::Message.print_address(message.src)
148
190
  # ignore the UAI Plus and ourselves
@@ -162,8 +204,24 @@ module SDN
162
204
  motor.publish(:positionpercent, message.position_percent)
163
205
  motor.publish(:positionpulses, message.position_pulses)
164
206
  motor.publish(:ip, message.ip)
207
+ motor.group_objects.each do |group|
208
+ positions = group.motor_objects.map(&:positionpercent)
209
+ position = nil
210
+ # calculate an average, but only if we know a position for
211
+ # every shade
212
+ if !positions.include?(:nil) && !positions.include?(nil)
213
+ position = positions.inject(&:+) / positions.length
214
+ end
215
+
216
+ group.publish(:positionpercent, position)
217
+ end
165
218
  when SDN::Message::PostMotorStatus
166
- if message.state == :running || motor.state == :running
219
+ if message.state == :running || motor.state == :running ||
220
+ # if it's explicitly stopped, but we didn't ask it to, it's probably
221
+ # changing directions so keep querying
222
+ (message.state == :stopped &&
223
+ message.last_action_cause == :explicit_command &&
224
+ !(motor.last_action == SDN::Message::Stop || motor.last_action.nil?))
167
225
  follow_ups << SDN::Message::GetMotorStatus.new(message.src)
168
226
  end
169
227
  # this will do one more position request after it stopped
@@ -172,6 +230,11 @@ module SDN
172
230
  motor.publish(:last_direction, message.last_direction)
173
231
  motor.publish(:last_action_source, message.last_action_source)
174
232
  motor.publish(:last_action_cause, message.last_action_cause)
233
+ motor.group_objects.each do |group|
234
+ states = group.motor_objects.map(&:state).uniq
235
+ state = states.length == 1 ? states.first : 'mixed'
236
+ group.publish(:state, state)
237
+ end
175
238
  when SDN::Message::PostMotorLimits
176
239
  motor.publish(:uplimit, message.up_limit)
177
240
  motor.publish(:downlimit, message.down_limit)
@@ -182,18 +245,26 @@ module SDN
182
245
  motor.publish(:downspeed, message.down_speed)
183
246
  motor.publish(:slowspeed, message.slow_speed)
184
247
  when SDN::Message::PostMotorIP
185
- motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
248
+ motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
186
249
  motor.publish(:"ip#{message.ip}percent", message.position_percent)
187
250
  when SDN::Message::PostGroupAddr
188
251
  motor.add_group(message.group_index, message.group_address)
189
252
  end
190
253
 
191
254
  @mutex.synchronize do
192
- signal = @response_pending || !follow_ups.empty?
193
- @response_pending = @broadcast_pending
194
- @request_queue.concat(follow_ups)
255
+ correct_response = @response_pending && message.src == @prior_message&.message&.dest && message.is_a?(@prior_message&.message&.class&.expected_response)
256
+ signal = correct_response || !follow_ups.empty?
257
+ puts "correct response #{correct_response}"
258
+ puts "pending: #{@response_pending} #{@broadcast_pending}"
259
+ @response_pending = @broadcast_pending if correct_response
260
+ follow_ups.each do |follow_up|
261
+ @queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
262
+ end
195
263
  @cond.signal if signal
196
264
  end
265
+ rescue EOFError
266
+ puts "EOF reading"
267
+ exit 2
197
268
  rescue SDN::MalformedMessage => e
198
269
  puts "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
199
270
  rescue => e
@@ -205,43 +276,63 @@ module SDN
205
276
  write_thread = Thread.new do
206
277
  begin
207
278
  loop do
208
- message = nil
279
+ message_and_retries = nil
209
280
  @mutex.synchronize do
210
281
  # got woken up early by another command getting queued; spin
211
282
  if @response_pending
283
+ puts "another message queued, but we're still waiting"
212
284
  while @response_pending
213
285
  remaining_wait = @response_pending - Time.now.to_f
214
286
  if remaining_wait < 0
215
287
  puts "timed out waiting on response"
216
288
  @response_pending = nil
217
289
  @broadcast_pending = nil
290
+ if @prior_message&.remaining_retries != 0
291
+ puts "retrying #{@prior_message.remaining_retries} more times ..."
292
+ @queues[@prior_message.priority].push(@prior_message)
293
+ @prior_message = nil
294
+ end
218
295
  else
296
+ puts "waiting #{remaining_wait} more..."
219
297
  @cond.wait(@mutex, remaining_wait)
220
298
  end
221
299
  end
222
300
  else
301
+ # minimum time between messages
302
+ puts "waiting between messages"
223
303
  sleep 0.1
224
304
  end
225
305
 
226
- message = @command_queue.shift
227
- unless message
228
- message = @request_queue.shift
229
- if message
306
+ puts "looking for next message to write"
307
+ @queues.find { |q| message_and_retries = q.shift }
308
+ if message_and_retries
309
+ if message_and_retries.message.ack_requested || message_and_retries.message.class.name =~ /^SDN::Message::Get/
230
310
  @response_pending = Time.now.to_f + WAIT_TIME
231
- if message.dest == [0xff, 0xff, 0xff]
311
+ 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
312
  @broadcast_pending = Time.now.to_f + BROADCAST_WAIT
233
- end
313
+ end
234
314
  end
235
315
  end
236
316
 
237
- # spin until there is a message
238
- @cond.wait(@mutex) unless message
317
+ # wait until there is a message
318
+ if @response_pending
319
+ message_and_retries.remaining_retries -= 1
320
+ @prior_message = message_and_retries
321
+ elsif message_and_retries
322
+ @prior_message = nil
323
+ else
324
+ @cond.wait(@mutex)
325
+ end
239
326
  end
240
- next unless message
327
+ next unless message_and_retries
241
328
 
329
+ message = message_and_retries.message
242
330
  puts "writing #{message.inspect}"
243
- @sdn.write(message.serialize)
244
- @sdn.flush
331
+ puts "(and waiting for response)" if @response_pending
332
+ serialized = message.serialize
333
+ @sdn.write(serialized)
334
+ @sdn.flush if @sdn.respond_to?(:flush)
335
+ puts "wrote #{serialized.unpack("C*").map { |b| '%02x' % b }.join(' ')}"
245
336
  end
246
337
  rescue => e
247
338
  puts "failure writing: #{e}"
@@ -254,10 +345,10 @@ module SDN
254
345
  if topic == "#{@base_topic}/discovery/discover/set" && value == "true"
255
346
  # trigger discovery
256
347
  @mutex.synchronize do
257
- @request_queue.push(SDN::Message::GetNodeAddr.new)
348
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeAddr.new, 1, 2))
258
349
  @cond.signal
259
350
  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$}))
351
+ 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
352
  addr = SDN::Message.parse_address(match[:addr])
262
353
  property = match[:property]
263
354
  # not homie compliant; allows linking the positionpercent property
@@ -266,10 +357,15 @@ module SDN
266
357
  property = value.downcase
267
358
  value = "true"
268
359
  end
269
- motor = @motors[SDN::Message.print_address(addr).gsub('.', '')]
360
+ mqtt_addr = SDN::Message.print_address(addr).gsub('.', '')
361
+ motor = @motors[mqtt_addr]
270
362
  is_group = SDN::Message.is_group_address?(addr)
363
+ group = @groups[mqtt_addr]
271
364
  follow_up = SDN::Message::GetMotorStatus.new(addr)
272
365
  message = case property
366
+ when 'discover'
367
+ follow_up = nil
368
+ SDN::Message::GetNodeAddr.new(addr) if value == "true"
273
369
  when 'label'
274
370
  follow_up = SDN::Message::GetNodeLabel.new(addr)
275
371
  SDN::Message::SetNodeLabel.new(addr, value) unless is_group
@@ -330,15 +426,21 @@ module SDN
330
426
  next unless motor
331
427
  messages = motor.set_groups(value)
332
428
  @mutex.synchronize do
333
- messages.each { |m| @command_queue.push(m) }
429
+ messages.each { |m| @queues[0].push(MessageAndRetries.new(m, 5, 0)) }
334
430
  @cond.signal
335
431
  end
336
432
  nil
337
433
  end
434
+
435
+ if motor
436
+ motor.last_action = message.class if [Message::MoveTo, Message::Move, Message::Wink, Message::Stop].include?(message.class)
437
+ end
438
+
338
439
  if message
440
+ message.ack_requested = true if message.class.name !~ /^SDN::Message::Get/
339
441
  @mutex.synchronize do
340
- @command_queue.push(message)
341
- @request_queue.push(follow_up) unless @request_queue.include?(follow_up)
442
+ @queues[0].push(MessageAndRetries.new(message, 5, 0))
443
+ @queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
342
444
  @cond.signal
343
445
  end
344
446
  end
@@ -379,7 +481,7 @@ module SDN
379
481
  publish("discovery/discover/$settable", "true")
380
482
  publish("discovery/discover/$retained", "false")
381
483
 
382
- subscribe("discovery/discover/set")
484
+ subscribe("+/discover/set")
383
485
  subscribe("+/label/set")
384
486
  subscribe("+/down/set")
385
487
  subscribe("+/up/set")
@@ -407,7 +509,12 @@ module SDN
407
509
  def publish_motor(addr)
408
510
  publish("#{addr}/$name", addr)
409
511
  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")
512
+ 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")
513
+
514
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
515
+ publish("#{addr}/discover/$datatype", "boolean")
516
+ publish("#{addr}/discover/$settable", "true")
517
+ publish("#{addr}/discover/$retained", "false")
411
518
 
412
519
  publish("#{addr}/label/$name", "Node label")
413
520
  publish("#{addr}/label/$datatype", "string")
@@ -527,17 +634,17 @@ module SDN
527
634
 
528
635
  motor = Motor.new(self, addr)
529
636
  @motors[addr] = motor
530
- publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.to_a).join(","))
637
+ publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
531
638
 
532
639
  sdn_addr = SDN::Message.parse_address(addr)
533
640
  @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)) }
641
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeLabel.new(sdn_addr), 5, 2))
642
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorStatus.new(sdn_addr), 5, 2))
643
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorLimits.new(sdn_addr), 5, 2))
644
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorDirection.new(sdn_addr), 5, 2))
645
+ @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorRollingSpeed.new(sdn_addr), 5, 2))
646
+ (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
647
+ (0...16).each { |g| @queues[2].push(MessageAndRetries.new(SDN::Message::GetGroupAddr.new(sdn_addr, g), 5, 2)) }
541
648
 
542
649
  @cond.signal
543
650
  end
@@ -547,11 +654,17 @@ module SDN
547
654
 
548
655
  def add_group(addr)
549
656
  addr = addr.gsub('.', '')
550
- return if @groups.include?(addr)
657
+ group = @groups[addr]
658
+ return group if group
551
659
 
552
660
  publish("#{addr}/$name", addr)
553
661
  publish("#{addr}/$type", "Shade Group")
554
- publish("#{addr}/$properties", "down,up,stop,positionpulses,positionpercent,ip,wink,reset")
662
+ publish("#{addr}/$properties", "discover,down,up,stop,positionpulses,positionpercent,ip,wink,reset,state,motors")
663
+
664
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
665
+ publish("#{addr}/discover/$datatype", "boolean")
666
+ publish("#{addr}/discover/$settable", "true")
667
+ publish("#{addr}/discover/$retained", "false")
555
668
 
556
669
  publish("#{addr}/down/$name", "Move in down direction")
557
670
  publish("#{addr}/down/$datatype", "boolean")
@@ -566,7 +679,7 @@ module SDN
566
679
  publish("#{addr}/stop/$name", "Cancel adjustments")
567
680
  publish("#{addr}/stop/$datatype", "boolean")
568
681
  publish("#{addr}/stop/$settable", "true")
569
- publish("#{addr}/stop/$retained", "false")
682
+ publish("#{addr}/stop/$retained", "false")
570
683
 
571
684
  publish("#{addr}/positionpulses/$name", "Position from up limit (in pulses)")
572
685
  publish("#{addr}/positionpulses/$datatype", "integer")
@@ -590,8 +703,16 @@ module SDN
590
703
  publish("#{addr}/wink/$settable", "true")
591
704
  publish("#{addr}/wink/$retained", "false")
592
705
 
593
- @groups << addr
594
- publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.to_a).join(","))
706
+ publish("#{addr}/state/$name", "State of the motors; only set if all motors are in the same state")
707
+ publish("#{addr}/state/$datatype", "enum")
708
+ publish("#{addr}/state/$format", SDN::Message::PostMotorStatus::STATE.keys.join(','))
709
+
710
+ publish("#{addr}/motors/$name", "Motors that are members of this group")
711
+ publish("#{addr}/motors/$datatype", "string")
712
+
713
+ group = @groups[addr] = Group.new(self, addr)
714
+ publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
715
+ group
595
716
  end
596
717
  end
597
718
  end
@@ -1,3 +1,3 @@
1
1
  module SDN
2
- VERSION = '1.0.6'
2
+ VERSION = '1.0.11'
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.6
4
+ version: 1.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-13 00:00:00.000000000 Z
11
+ date: 2020-09-25 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: ccutrer-serialport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.0.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.0
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: byebug
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -66,7 +80,7 @@ dependencies:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
82
  version: '13.0'
69
- description:
83
+ description:
70
84
  email: cody@cutrer.com'
71
85
  executables:
72
86
  - sdn_mqtt_bridge
@@ -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
@@ -87,7 +105,7 @@ homepage: https://github.com/ccutrer/somfy_sdn
87
105
  licenses:
88
106
  - MIT
89
107
  metadata: {}
90
- post_install_message:
108
+ post_install_message:
91
109
  rdoc_options: []
92
110
  require_paths:
93
111
  - lib
@@ -102,8 +120,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
102
120
  - !ruby/object:Gem::Version
103
121
  version: '0'
104
122
  requirements: []
105
- rubygems_version: 3.0.3
106
- signing_key:
123
+ rubygems_version: 3.1.2
124
+ signing_key:
107
125
  specification_version: 4
108
126
  summary: Library for communication with Somfy SDN RS-485 motorized shades
109
127
  test_files: []