somfy_sdn 1.0.6 → 1.0.11

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