somfy_sdn 2.1.4 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sdn/cli/mqtt.rb CHANGED
@@ -1,12 +1,15 @@
1
- require 'mqtt'
2
- require 'uri'
3
- require 'set'
1
+ # frozen_string_literal: true
4
2
 
5
- require 'sdn/cli/mqtt/group'
6
- require 'sdn/cli/mqtt/motor'
7
- require 'sdn/cli/mqtt/read'
8
- require 'sdn/cli/mqtt/write'
9
- require 'sdn/cli/mqtt/subscriptions'
3
+ require "mqtt"
4
+ require "uri"
5
+ require "set"
6
+
7
+ require "sdn/cli/mqtt/group"
8
+ require "sdn/cli/mqtt/motor"
9
+ require "sdn/cli/mqtt/p_queue"
10
+ require "sdn/cli/mqtt/read"
11
+ require "sdn/cli/mqtt/write"
12
+ require "sdn/cli/mqtt/subscriptions"
10
13
 
11
14
  module SDN
12
15
  module CLI
@@ -22,7 +25,12 @@ module SDN
22
25
 
23
26
  attr_reader :motors, :groups
24
27
 
25
- def initialize(port, mqtt_uri, device_id: "somfy", base_topic: "homie", auto_discover: true)
28
+ def initialize(sdn,
29
+ mqtt_uri,
30
+ device_id: "somfy",
31
+ base_topic: "homie",
32
+ auto_discover: true,
33
+ known_motors: [])
26
34
  @base_topic = "#{base_topic}/#{device_id}"
27
35
  @mqtt = ::MQTT::Client.new(mqtt_uri)
28
36
  @mqtt.set_will("#{@base_topic}/$state", "lost", retain: true)
@@ -33,7 +41,7 @@ module SDN
33
41
 
34
42
  @mutex = Mutex.new
35
43
  @cond = ConditionVariable.new
36
- @queues = [[], [], []]
44
+ @queue = PQueue.new
37
45
  @response_pending = false
38
46
  @broadcast_pending = false
39
47
 
@@ -43,11 +51,16 @@ module SDN
43
51
  clear_tree(@base_topic)
44
52
  publish_basic_attributes
45
53
 
46
- @sdn = Client.new(port)
54
+ @sdn = sdn
55
+
56
+ known_motors&.each do |addr|
57
+ addr = Message.parse_address(addr)
58
+ @queue.push(MessageAndRetries.new(Message::GetMotorPosition.new(addr), 5, 0))
59
+ end
47
60
 
48
61
  Thread.abort_on_exception = true
49
- read_thread = Thread.new { read }
50
- write_thread = Thread.new { write }
62
+ Thread.new { read }
63
+ Thread.new { write }
51
64
  @mqtt.get { |packet| handle_message(packet.topic, packet.payload) }
52
65
  end
53
66
 
@@ -59,20 +72,19 @@ module SDN
59
72
  @mqtt.subscribe("#{@base_topic}/#{topic}")
60
73
  end
61
74
 
62
- def enqueue(message, queue = 0)
75
+ def enqueue(message)
63
76
  @mutex.synchronize do
64
- queue = @queues[queue]
65
- unless queue.include?(message)
66
- queue.push(message)
67
- @cond.signal
68
- end
77
+ break if @queue.include?(message)
78
+
79
+ @queue.push(message)
80
+ @cond.signal
69
81
  end
70
82
  end
71
83
 
72
84
  def clear_tree(topic)
73
85
  @mqtt.subscribe("#{topic}/#")
74
86
  @mqtt.unsubscribe("#{topic}/#", wait_for_ack: true)
75
- while !@mqtt.queue_empty?
87
+ until @mqtt.queue_empty?
76
88
  packet = @mqtt.get
77
89
  @mqtt.publish(packet.topic, nil, retain: true)
78
90
  end
@@ -136,7 +148,7 @@ module SDN
136
148
  @mqtt.batch_publish do
137
149
  publish("#{addr}/$name", addr)
138
150
  publish("#{addr}/$type", node_type.to_s)
139
- properties = %w{
151
+ properties = %w[
140
152
  discover
141
153
  label
142
154
  state
@@ -149,19 +161,17 @@ module SDN
149
161
  down-limit
150
162
  groups
151
163
  last-direction
152
- } + (1..16).map { |ip| ["ip#{ip}-pulses", "ip#{ip}-percent"] }.flatten
164
+ ] + (1..16).map { |ip| ["ip#{ip}-pulses", "ip#{ip}-percent"] }.flatten
153
165
 
154
166
  unless node_type == :st50ilt2
155
- properties.concat %w{
156
- reset
157
- last-action-source
158
- last-action-cause
159
- up-limit
160
- direction
161
- up-speed
162
- down-speed
163
- slow-speed
164
- }
167
+ properties.push("reset",
168
+ "last-action-source",
169
+ "last-action-cause",
170
+ "up-limit",
171
+ "direction",
172
+ "up-speed",
173
+ "down-speed",
174
+ "slow-speed")
165
175
  end
166
176
 
167
177
  publish("#{addr}/$properties", properties.join(","))
@@ -178,7 +188,7 @@ module SDN
178
188
 
179
189
  publish("#{addr}/state/$name", "Current state of the motor")
180
190
  publish("#{addr}/state/$datatype", "enum")
181
- publish("#{addr}/state/$format", Message::PostMotorStatus::STATE.keys.join(','))
191
+ publish("#{addr}/state/$format", Message::PostMotorStatus::STATE.keys.join(","))
182
192
 
183
193
  publish("#{addr}/control/$name", "Control motor")
184
194
  publish("#{addr}/control/$datatype", "enum")
@@ -226,22 +236,22 @@ module SDN
226
236
 
227
237
  publish("#{addr}/last-direction/$name", "Direction of last motion")
228
238
  publish("#{addr}/last-direction/$datatype", "enum")
229
- publish("#{addr}/last-direction/$format", Message::PostMotorStatus::DIRECTION.keys.join(','))
239
+ publish("#{addr}/last-direction/$format", Message::PostMotorStatus::DIRECTION.keys.join(","))
230
240
 
231
241
  unless node_type == :st50ilt2
232
242
  publish("#{addr}/reset/$name", "Recall factory settings")
233
243
  publish("#{addr}/reset/$datatype", "enum")
234
- publish("#{addr}/reset/$format", Message::SetFactoryDefault::RESET.keys.join(','))
244
+ publish("#{addr}/reset/$format", Message::SetFactoryDefault::RESET.keys.join(","))
235
245
  publish("#{addr}/reset/$settable", "true")
236
246
  publish("#{addr}/reset/$retained", "false")
237
247
 
238
248
  publish("#{addr}/last-action-source/$name", "Source of last action")
239
249
  publish("#{addr}/last-action-source/$datatype", "enum")
240
- publish("#{addr}/last-action-source/$format", Message::PostMotorStatus::SOURCE.keys.join(','))
250
+ publish("#{addr}/last-action-source/$format", Message::PostMotorStatus::SOURCE.keys.join(","))
241
251
 
242
252
  publish("#{addr}/last-action-cause/$name", "Cause of last action")
243
253
  publish("#{addr}/last-action-cause/$datatype", "enum")
244
- publish("#{addr}/last-action-cause/$format", Message::PostMotorStatus::CAUSE.keys.join(','))
254
+ publish("#{addr}/last-action-cause/$format", Message::PostMotorStatus::CAUSE.keys.join(","))
245
255
 
246
256
  publish("#{addr}/up-limit/$name", "Up limit (always = 0)")
247
257
  publish("#{addr}/up-limit/$datatype", "integer")
@@ -298,20 +308,39 @@ module SDN
298
308
 
299
309
  sdn_addr = Message.parse_address(addr)
300
310
  @mutex.synchronize do
301
- @queues[2].push(MessageAndRetries.new(Message::GetNodeLabel.new(sdn_addr), 5, 2))
311
+ # message priorities are:
312
+ # 0 - control
313
+ # 1 - follow-up (i.e. get position after control)
314
+ # 2 - get basic motor info
315
+ # 3 - get advanced motor info
316
+ # 4 - get group 1
317
+ # 5 - get ip 1
318
+ # 6 - get group 2
319
+ # 7 - get ip 2
320
+ # ...
321
+ # 50 - discover
322
+
323
+ # The Group and IP sorting makes it so you quickly get the most commonly used group
324
+ # and IP addresses, while the almost-never-used ones are pushed to the bottom of the list
325
+
326
+ @queue.push(MessageAndRetries.new(Message::GetNodeLabel.new(sdn_addr), 5, 2))
327
+
302
328
  case node_type
303
- when :st30
304
- @queues[2].push(MessageAndRetries.new(Message::GetMotorStatus.new(sdn_addr), 5, 2))
305
- @queues[2].push(MessageAndRetries.new(Message::GetMotorLimits.new(sdn_addr), 5, 2))
306
- @queues[2].push(MessageAndRetries.new(Message::GetMotorDirection.new(sdn_addr), 5, 2))
307
- @queues[2].push(MessageAndRetries.new(Message::GetMotorRollingSpeed.new(sdn_addr), 5, 2))
308
- (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(Message::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
329
+ when :st30, 0x20 # no idea why 0x20, but that's what I get
330
+ @queue.push(MessageAndRetries.new(Message::GetMotorStatus.new(sdn_addr), 5, 2))
331
+ @queue.push(MessageAndRetries.new(Message::GetMotorLimits.new(sdn_addr), 5, 2))
332
+ @queue.push(MessageAndRetries.new(Message::GetMotorDirection.new(sdn_addr), 5, 3))
333
+ @queue.push(MessageAndRetries.new(Message::GetMotorRollingSpeed.new(sdn_addr), 5, 3))
334
+ (1..16).each { |ip|
335
+ @queue.push(MessageAndRetries.new(Message::GetMotorIP.new(sdn_addr, ip), 5, 2 * ip + 3)) }
309
336
  when :st50ilt2
310
- @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorSettings.new(sdn_addr), 5, 2))
311
- @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorPosition.new(sdn_addr), 5, 2))
312
- (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
337
+ @queue.push(MessageAndRetries.new(Message::ILT2::GetMotorSettings.new(sdn_addr), 5, 2))
338
+ @queue.push(MessageAndRetries.new(Message::ILT2::GetMotorPosition.new(sdn_addr), 5, 2))
339
+ (1..16).each do |ip|
340
+ @queue.push(MessageAndRetries.new(Message::ILT2::GetMotorIP.new(sdn_addr, ip), 5, 2 * ip + 3))
341
+ end
313
342
  end
314
- (1..16).each { |g| @queues[2].push(MessageAndRetries.new(Message::GetGroupAddr.new(sdn_addr, g), 5, 2)) }
343
+ (1..16).each { |g| @queue.push(MessageAndRetries.new(Message::GetGroupAddr.new(sdn_addr, g), 5, 2 * g + 2)) }
315
344
 
316
345
  @cond.signal
317
346
  end
@@ -320,19 +349,21 @@ module SDN
320
349
  end
321
350
 
322
351
  def touch_group(group_addr)
323
- group = @groups[Message.print_address(group_addr).gsub('.', '')]
352
+ group = @groups[Message.print_address(group_addr).delete(".")]
324
353
  group&.publish(:motors, group.motors_string)
325
354
  end
326
355
 
327
356
  def add_group(addr)
328
- addr = addr.gsub('.', '')
357
+ addr = addr.delete(".")
329
358
  group = @groups[addr]
330
359
  return group if group
331
360
 
332
361
  @mqtt.batch_publish do
333
362
  publish("#{addr}/$name", addr)
334
363
  publish("#{addr}/$type", "Shade Group")
335
- publish("#{addr}/$properties", "discover,control,jog-ms,jog-pulses,position-pulses,position-percent,ip,reset,state,last-direction,motors")
364
+ publish("#{addr}/$properties",
365
+ "discover,control,jog-ms,jog-pulses,position-pulses,position-percent," \
366
+ "ip,reset,state,last-direction,motors")
336
367
 
337
368
  publish("#{addr}/discover/$name", "Trigger Motor Discovery")
338
369
  publish("#{addr}/discover/$datatype", "enum")
@@ -379,11 +410,11 @@ module SDN
379
410
 
380
411
  publish("#{addr}/state/$name", "State of the motors")
381
412
  publish("#{addr}/state/$datatype", "enum")
382
- publish("#{addr}/state/$format", Message::PostMotorStatus::STATE.keys.join(',') + ",mixed")
413
+ publish("#{addr}/state/$format", "#{Message::PostMotorStatus::STATE.keys.join(",")},mixed")
383
414
 
384
415
  publish("#{addr}/last-direction/$name", "Direction of last motion")
385
416
  publish("#{addr}/last-direction/$datatype", "enum")
386
- publish("#{addr}/last-direction/$format", Message::PostMotorStatus::DIRECTION.keys.join(',') + ",mixed")
417
+ publish("#{addr}/last-direction/$format", "#{Message::PostMotorStatus::DIRECTION.keys.join(",")},mixed")
387
418
 
388
419
  publish("#{addr}/motors/$name", "Comma separated motor addresses that are members of this group")
389
420
  publish("#{addr}/motors/$datatype", "string")
@@ -1,12 +1,14 @@
1
- require 'curses'
1
+ # frozen_string_literal: true
2
+
3
+ require "curses"
2
4
 
3
5
  module SDN
4
6
  module CLI
5
7
  class Provisioner
6
8
  attr_reader :win, :sdn, :addr, :ns
7
9
 
8
- def initialize(port, addr = nil)
9
- @sdn = Client.new(port)
10
+ def initialize(sdn, addr = nil)
11
+ @sdn = sdn
10
12
  @reversed = false
11
13
  @pulse_count = 10
12
14
 
@@ -24,10 +26,11 @@ module SDN
24
26
  message = sdn.ensure(Message::GetNodeLabel.new(addr))
25
27
 
26
28
  node_type = message.node_type
27
- @ns = ns = node_type == :st50ilt2 ? Message::ILT2 : Message
29
+ @ns = ns = (node_type == :st50ilt2) ? Message::ILT2 : Message
28
30
 
29
- print "Motor is currently labeled '#{message.label}'; what would you like to change it to (blank to leave alone)? "
30
- new_label = STDIN.gets
31
+ print "Motor is currently labeled '#{message.label}'; " \
32
+ "what would you like to change it to (blank to leave alone)? "
33
+ new_label = $stdin.gets
31
34
 
32
35
  unless new_label == "\n"
33
36
  new_label.strip!
@@ -52,8 +55,10 @@ module SDN
52
55
  Curses.nonl
53
56
  Curses.curs_set(0)
54
57
  @win = Curses.stdscr
55
-
58
+
56
59
  process
60
+ rescue Interrupt
61
+ # exitting
57
62
  rescue => e
58
63
  win.setpos(0, 0)
59
64
  win.addstr(e.inspect)
@@ -70,6 +75,7 @@ module SDN
70
75
  win.keypad = true
71
76
  print_help
72
77
  refresh
78
+ wait_for_stop
73
79
 
74
80
  loop do
75
81
  char = win.getch
@@ -93,33 +99,44 @@ module SDN
93
99
  wait_for_stop
94
100
  when Curses::Key::LEFT
95
101
  if @pos < @pulse_count
96
- sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @limit + @pulse_count - @pos, @pulse_count))
102
+ sdn.ensure(Message::ILT2::SetMotorSettings.new(addr,
103
+ reversed_int,
104
+ @limit + @pulse_count - @pos,
105
+ @pulse_count))
97
106
  refresh
98
107
  end
99
- sdn.ensure(Message::ILT2::SetMotorPosition.new(addr, :jog_up_pulses, @pulse_count))
108
+ if ilt2?
109
+ sdn.ensure(Message::ILT2::SetMotorPosition.new(addr, :jog_up_pulses, @pulse_count))
110
+ else
111
+ sdn.ensure(Message::MoveOf.new(addr, :jog_up_pulses, @pulse_count))
112
+ end
100
113
  wait_for_stop
101
114
  when Curses::Key::RIGHT
102
115
  if @limit - @pos < @pulse_count
103
116
  sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @pos + @pulse_count, @pos))
104
117
  refresh
105
118
  end
106
- sdn.ensure(Message::ILT2::SetMotorPosition.new(addr, :jog_down_pulses, @pulse_count))
119
+ if ilt2?
120
+ sdn.ensure(Message::ILT2::SetMotorPosition.new(addr, :jog_down_pulses, @pulse_count))
121
+ else
122
+ sdn.ensure(Message::MoveOf.new(addr, :jog_down_pulses, @pulse_count))
123
+ end
107
124
  wait_for_stop
108
- when 'u'
125
+ when "u"
109
126
  if ilt2?
110
127
  sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @limit - @pos, 0))
111
128
  else
112
129
  sdn.ensure(Message::SetMotorLimits.new(addr, :current_position, :up))
113
130
  end
114
131
  refresh
115
- when 'l'
132
+ when "l"
116
133
  if ilt2?
117
134
  sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @pos, @pos))
118
135
  else
119
136
  sdn.ensure(Message::SetMotorLimits.new(addr, :current_position, :down))
120
137
  end
121
138
  refresh
122
- when 'r'
139
+ when "r"
123
140
  @reversed = !@reversed
124
141
  if ilt2?
125
142
  sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @limit, @limit - @pos))
@@ -127,18 +144,24 @@ module SDN
127
144
  sdn.ensure(Message::SetMotorDirection.new(addr, @reversed ? :reversed : :standard))
128
145
  end
129
146
  refresh
130
- when 'R'
147
+ when "R"
131
148
  next unless ilt2?
149
+
132
150
  @reversed = !@reversed
133
151
  sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @limit, @pos))
134
152
  refresh
135
- when '<'
153
+ when "!"
154
+ next if ilt2?
155
+
156
+ sdn.send(Message::SetFactoryDefault.new(addr))
157
+ break
158
+ when "<"
136
159
  @pulse_count /= 2 if @pulse_count > 5
137
160
  print_help
138
- when '>'
161
+ when ">"
139
162
  @pulse_count *= 2
140
163
  print_help
141
- when 'q'
164
+ when "q"
142
165
  break
143
166
  end
144
167
  end
@@ -146,51 +169,62 @@ module SDN
146
169
 
147
170
  def print_help
148
171
  win.setpos(0, 0)
149
- win.addstr(<<-INSTRUCTIONS)
150
- Move the motor. Keys:
151
- Esc stop movement
152
- \u2191 go to upper limit
153
- \u2193 go to lower limit
154
- \u2190 jog up #{@pulse_count} pulses
155
- \u2192 jog down #{@pulse_count} pulses
156
- > increase jog size
157
- < decrease jog size
158
- u set upper limit at current position
159
- l set lower limit at current position
160
- r reverse motor
161
- INSTRUCTIONS
172
+ win.addstr(<<~TEXT)
173
+ Move the motor. Keys:
174
+ Esc stop movement
175
+ \u2191 go to upper limit
176
+ \u2193 go to lower limit
177
+ \u2190 jog up #{@pulse_count} pulses
178
+ \u2192 jog down #{@pulse_count} pulses
179
+ > increase jog size
180
+ < decrease jog size
181
+ u set upper limit at current position
182
+ l set lower limit at current position
183
+ r reverse motor
184
+ TEXT
162
185
 
163
186
  if ilt2?
164
187
  win.addstr("R reverse motor (but leave position alone)\n")
188
+ else
189
+ win.addstr("! factory reset\n")
165
190
  end
166
191
  win.addstr("q quit\n")
167
192
  win.refresh
168
193
  end
169
-
194
+
170
195
  def wait_for_stop
171
196
  win.setpos(13, 0)
172
197
  win.addstr("Moving...\n")
198
+
173
199
  loop do
174
200
  win.nodelay = true
175
201
  stop if win.getch == 27 # Esc
202
+
176
203
  sdn.send(ns::GetMotorPosition.new(addr))
177
- sdn.receive do |message|
178
- next unless message.is_a?(ns::PostMotorPosition)
179
- last_pos = @pos
180
- @pos = message.position_pulses
181
- win.setpos(14, 0)
182
- win.addstr("Position: #{@pos}\n")
183
-
184
- if last_pos == @pos
204
+ unless ilt2?
205
+ sleep 0.1
206
+ sdn.send(ns::GetMotorStatus.new(addr))
207
+ end
208
+
209
+ sdn.receive(0.1) do |message|
210
+ if message.is_a?(ns::PostMotorPosition)
211
+ last_pos = @pos
212
+ @pos = message.position_pulses
213
+ win.setpos(14, 0)
214
+ win.addstr("Position: #{@pos}\n")
215
+ end
216
+
217
+ if (ilt2? && last_pos == @pos) ||
218
+ (message.is_a?(Message::PostMotorStatus) &&
219
+ message.state != :running)
185
220
  win.setpos(13, 0)
186
221
  win.addstr("\n")
187
222
  win.nodelay = false
188
223
  refresh
189
- return
224
+ return # rubocop:disable Lint/NonLocalExitFromIterator
190
225
  end
191
226
  end
192
227
  sleep 0.1
193
-
194
228
  end
195
229
  end
196
230