somfy_sdn 2.1.5 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/exe/somfy_sdn +69 -0
- data/lib/sdn/cli/mqtt/group.rb +12 -10
- data/lib/sdn/cli/mqtt/motor.rb +19 -14
- data/lib/sdn/cli/mqtt/p_queue.rb +18 -0
- data/lib/sdn/cli/mqtt/read.rb +125 -126
- data/lib/sdn/cli/mqtt/subscriptions.rb +186 -140
- data/lib/sdn/cli/mqtt/write.rb +39 -34
- data/lib/sdn/cli/mqtt.rb +84 -53
- data/lib/sdn/cli/provisioner.rb +56 -33
- data/lib/sdn/cli/simulator.rb +99 -65
- data/lib/sdn/client.rb +38 -24
- data/lib/sdn/message/control.rb +60 -30
- data/lib/sdn/message/get.rb +6 -2
- data/lib/sdn/message/helpers.rb +23 -22
- data/lib/sdn/message/ilt2/get.rb +6 -3
- data/lib/sdn/message/ilt2/master_control.rb +10 -7
- data/lib/sdn/message/ilt2/post.rb +7 -5
- data/lib/sdn/message/ilt2/set.rb +28 -19
- data/lib/sdn/message/post.rb +3 -5
- data/lib/sdn/message/set.rb +48 -22
- data/lib/sdn/message.rb +50 -34
- data/lib/sdn/version.rb +3 -1
- data/lib/sdn.rb +18 -12
- data/lib/somfy_sdn.rb +3 -1
- metadata +43 -13
- data/bin/somfy_sdn +0 -60
data/lib/sdn/cli/mqtt.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
-
|
2
|
-
require 'uri'
|
3
|
-
require 'set'
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
|
9
|
-
require
|
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(
|
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
|
-
@
|
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 =
|
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
|
-
|
50
|
-
|
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
|
75
|
+
def enqueue(message)
|
63
76
|
@mutex.synchronize do
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
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
|
-
|
164
|
+
] + (1..16).map { |ip| ["ip#{ip}-pulses", "ip#{ip}-percent"] }.flatten
|
153
165
|
|
154
166
|
unless node_type == :st50ilt2
|
155
|
-
properties.
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
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
|
-
@
|
305
|
-
@
|
306
|
-
@
|
307
|
-
@
|
308
|
-
(1..16).each { |ip|
|
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
|
-
@
|
311
|
-
@
|
312
|
-
(1..16).each
|
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| @
|
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).
|
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.
|
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",
|
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(
|
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(
|
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")
|
data/lib/sdn/cli/provisioner.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
|
-
|
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(
|
9
|
-
@sdn =
|
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}';
|
30
|
-
|
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)
|
@@ -94,33 +99,44 @@ module SDN
|
|
94
99
|
wait_for_stop
|
95
100
|
when Curses::Key::LEFT
|
96
101
|
if @pos < @pulse_count
|
97
|
-
sdn.ensure(Message::ILT2::SetMotorSettings.new(addr,
|
102
|
+
sdn.ensure(Message::ILT2::SetMotorSettings.new(addr,
|
103
|
+
reversed_int,
|
104
|
+
@limit + @pulse_count - @pos,
|
105
|
+
@pulse_count))
|
98
106
|
refresh
|
99
107
|
end
|
100
|
-
|
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
|
101
113
|
wait_for_stop
|
102
114
|
when Curses::Key::RIGHT
|
103
115
|
if @limit - @pos < @pulse_count
|
104
116
|
sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @pos + @pulse_count, @pos))
|
105
117
|
refresh
|
106
118
|
end
|
107
|
-
|
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
|
108
124
|
wait_for_stop
|
109
|
-
when
|
125
|
+
when "u"
|
110
126
|
if ilt2?
|
111
127
|
sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @limit - @pos, 0))
|
112
128
|
else
|
113
129
|
sdn.ensure(Message::SetMotorLimits.new(addr, :current_position, :up))
|
114
130
|
end
|
115
131
|
refresh
|
116
|
-
when
|
132
|
+
when "l"
|
117
133
|
if ilt2?
|
118
134
|
sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @pos, @pos))
|
119
135
|
else
|
120
136
|
sdn.ensure(Message::SetMotorLimits.new(addr, :current_position, :down))
|
121
137
|
end
|
122
138
|
refresh
|
123
|
-
when
|
139
|
+
when "r"
|
124
140
|
@reversed = !@reversed
|
125
141
|
if ilt2?
|
126
142
|
sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @limit, @limit - @pos))
|
@@ -128,18 +144,24 @@ module SDN
|
|
128
144
|
sdn.ensure(Message::SetMotorDirection.new(addr, @reversed ? :reversed : :standard))
|
129
145
|
end
|
130
146
|
refresh
|
131
|
-
when
|
147
|
+
when "R"
|
132
148
|
next unless ilt2?
|
149
|
+
|
133
150
|
@reversed = !@reversed
|
134
151
|
sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @limit, @pos))
|
135
152
|
refresh
|
136
|
-
when
|
153
|
+
when "!"
|
154
|
+
next if ilt2?
|
155
|
+
|
156
|
+
sdn.send(Message::SetFactoryDefault.new(addr))
|
157
|
+
break
|
158
|
+
when "<"
|
137
159
|
@pulse_count /= 2 if @pulse_count > 5
|
138
160
|
print_help
|
139
|
-
when
|
161
|
+
when ">"
|
140
162
|
@pulse_count *= 2
|
141
163
|
print_help
|
142
|
-
when
|
164
|
+
when "q"
|
143
165
|
break
|
144
166
|
end
|
145
167
|
end
|
@@ -147,27 +169,29 @@ module SDN
|
|
147
169
|
|
148
170
|
def print_help
|
149
171
|
win.setpos(0, 0)
|
150
|
-
win.addstr(
|
151
|
-
Move the motor. Keys:
|
152
|
-
Esc stop movement
|
153
|
-
\u2191 go to upper limit
|
154
|
-
\u2193 go to lower limit
|
155
|
-
\u2190 jog up #{@pulse_count} pulses
|
156
|
-
\u2192 jog down #{@pulse_count} pulses
|
157
|
-
> increase jog size
|
158
|
-
< decrease jog size
|
159
|
-
u set upper limit at current position
|
160
|
-
l set lower limit at current position
|
161
|
-
r reverse motor
|
162
|
-
|
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
|
163
185
|
|
164
186
|
if ilt2?
|
165
187
|
win.addstr("R reverse motor (but leave position alone)\n")
|
188
|
+
else
|
189
|
+
win.addstr("! factory reset\n")
|
166
190
|
end
|
167
191
|
win.addstr("q quit\n")
|
168
192
|
win.refresh
|
169
193
|
end
|
170
|
-
|
194
|
+
|
171
195
|
def wait_for_stop
|
172
196
|
win.setpos(13, 0)
|
173
197
|
win.addstr("Moving...\n")
|
@@ -183,7 +207,6 @@ r reverse motor
|
|
183
207
|
end
|
184
208
|
|
185
209
|
sdn.receive(0.1) do |message|
|
186
|
-
|
187
210
|
if message.is_a?(ns::PostMotorPosition)
|
188
211
|
last_pos = @pos
|
189
212
|
@pos = message.position_pulses
|
@@ -198,7 +221,7 @@ r reverse motor
|
|
198
221
|
win.addstr("\n")
|
199
222
|
win.nodelay = false
|
200
223
|
refresh
|
201
|
-
return
|
224
|
+
return # rubocop:disable Lint/NonLocalExitFromIterator
|
202
225
|
end
|
203
226
|
end
|
204
227
|
sleep 0.1
|