somfy_sdn 1.0.9 → 2.1.0

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: 7b76beaca35ebf0ae0847fc75bf0fcd12c45e395768cca0c711557b167488e3e
4
- data.tar.gz: 64dfc392ef651d1afc6e6a94c8c586e1e95e575c0ff9ba12c74f472cbfe88874
3
+ metadata.gz: df44cc648ee7fa0079f6067deee6b8fa488c1cbdc31acc252d9803eb1a32cdf3
4
+ data.tar.gz: 17cda1e8629bc58d88cb202286a7d9b5bb4d88de28d3a9c133c669796b979c60
5
5
  SHA512:
6
- metadata.gz: 87a1138187217fb8bf7fe62e00f69e20b742d2527d4f0059b9a2d4ba42012c887947608fdbfad4123400a0968d99d9c257cb361025ecef668786d34b708a0512
7
- data.tar.gz: e6ddbe689d8d60d252c24a1291893b05040a638ba3b86ac3a7bc1c28479a9ccdf0485e5914035818c50e1005b076d148263916ae1e7a66e05efdb075786d1d95
6
+ metadata.gz: 7915001ae78047ac6e0669d2f362bcb165db06fe6ca29c8a155150c04093044abd9f158b89ee81c836dcece92114a60a39ca139f30317791f08bb99a132b5ad1
7
+ data.tar.gz: 7f97633393e8daeefdf9bafb8e12f4534e1c724939b161357543df808a630815ff34dc42af5fc172dc45d09101bf9bf4bccd24fb53cb181eebb37447287bd32e
data/bin/somfy_sdn ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'somfy_sdn'
4
+ require 'thor'
5
+
6
+ class SomfySDNCLI < Thor
7
+ class_option :verbose, type: :boolean, default: false
8
+
9
+ desc "monitor PORT", "Monitor traffic on the SDN network at PORT"
10
+ def monitor(port)
11
+ handle_global_options
12
+
13
+ sdn = SDN::Client.new(port)
14
+
15
+ loop do
16
+ sdn.receive do |message|
17
+ SDN.logger.info "Received message #{message.inspect}"
18
+ end
19
+ end
20
+ end
21
+
22
+ desc "mqtt PORT MQTT_URI", "Run an MQTT bridge to control the SDN network at PORT"
23
+ option :"device-id", default: "somfy", desc: "The Homie Device ID"
24
+ option :"base-topic", default: "homie", desc: "The base Homie topic"
25
+ option :"auto-discover", type: :boolean, default: true, desc: "Do a discovery at startup"
26
+ def mqtt(port, mqtt_uri)
27
+ handle_global_options
28
+
29
+ require 'sdn/cli/mqtt'
30
+
31
+ SDN::CLI::MQTT.new(port, mqtt_uri,
32
+ device_id: options["device-id"],
33
+ base_topic: options["base-topic"],
34
+ auto_discover: options["auto-discover"])
35
+ end
36
+
37
+ desc "provision PORT [ADDRESS]", "Provision a motor (label and set limits) at PORT"
38
+ def provision(port, address = nil)
39
+ handle_global_options
40
+
41
+ require 'sdn/cli/provisioner'
42
+ SDN::CLI::Provisioner.new(port, address)
43
+ end
44
+
45
+ desc "simulator PORT [ADDRESS]", "Simulate a motor (for debugging purposes) at PORT"
46
+ def simulator(port, address = nil)
47
+ handle_global_options
48
+
49
+ require 'sdn/cli/simulator'
50
+ SDN::CLI::Simulator.new(port, address)
51
+ end
52
+
53
+ private
54
+
55
+ def handle_global_options
56
+ SDN.logger.level = options[:verbose] ? :debug : :info
57
+ end
58
+ end
59
+
60
+ SomfySDNCLI.start(ARGV)
data/lib/sdn.rb CHANGED
@@ -1,6 +1,21 @@
1
+ require 'logger'
2
+
3
+ require 'sdn/client'
1
4
  require 'sdn/message'
2
- require 'sdn/mqtt_bridge'
3
5
 
4
6
  module SDN
5
7
  BROADCAST_ADDRESS = [0xff, 0xff, 0xff]
8
+
9
+ class << self
10
+ def logger
11
+ @logger ||= begin
12
+ Logger.new(STDOUT, :info).tap do |logger|
13
+ logger.datetime_format = '%Y-%m-%d %H:%M:%S.%L'
14
+ logger.formatter = proc do |severity, datetime, progname, msg|
15
+ "#{datetime.strftime(logger.datetime_format)} [#{Process.pid}/#{Thread.current.object_id}] #{severity}: #{msg}\n"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
6
21
  end
@@ -0,0 +1,387 @@
1
+ require 'mqtt'
2
+ require 'uri'
3
+ require 'set'
4
+
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'
10
+
11
+ module SDN
12
+ module CLI
13
+ class MQTT
14
+ MessageAndRetries = Struct.new(:message, :remaining_retries, :priority)
15
+
16
+ include Read
17
+ include Write
18
+ include Subscriptions
19
+
20
+ WAIT_TIME = 0.25
21
+ BROADCAST_WAIT = 5.0
22
+
23
+ attr_reader :motors, :groups
24
+
25
+ def initialize(port, mqtt_uri, device_id: "somfy", base_topic: "homie", auto_discover: true)
26
+ @base_topic = "#{base_topic}/#{device_id}"
27
+ @mqtt = ::MQTT::Client.new(mqtt_uri)
28
+ @mqtt.set_will("#{@base_topic}/$state", "lost", true)
29
+ @mqtt.connect
30
+
31
+ @motors = {}
32
+ @groups = {}
33
+
34
+ @mutex = Mutex.new
35
+ @cond = ConditionVariable.new
36
+ @queues = [[], [], []]
37
+ @response_pending = false
38
+ @broadcast_pending = false
39
+
40
+ @auto_discover = auto_discover
41
+ @motors_found = true
42
+
43
+ clear_tree(@base_topic)
44
+ publish_basic_attributes
45
+
46
+ @sdn = Client.new(port)
47
+
48
+ read_thread = Thread.new { read }
49
+ write_thread = Thread.new { write }
50
+ @mqtt.get { |topic, value| handle_message(topic, value) }
51
+ end
52
+
53
+ def publish(topic, value)
54
+ @mqtt.publish("#{@base_topic}/#{topic}", value, retain: true, qos: 1)
55
+ end
56
+
57
+ def subscribe(topic)
58
+ @mqtt.subscribe("#{@base_topic}/#{topic}")
59
+ end
60
+
61
+ def enqueue(message, queue = 0)
62
+ @mutex.synchronize do
63
+ queue = @queues[queue]
64
+ unless queue.include?(message)
65
+ queue.push(message)
66
+ @cond.signal
67
+ end
68
+ end
69
+ end
70
+
71
+ def clear_tree(topic)
72
+ @mqtt.subscribe("#{topic}/#")
73
+ @mqtt.unsubscribe("#{topic}/#", wait_for_ack: true)
74
+ while !@mqtt.queue_empty?
75
+ topic, value = @mqtt.get
76
+ @mqtt.publish(topic, nil, retain: true)
77
+ end
78
+ end
79
+
80
+ def publish_basic_attributes
81
+ @mqtt.batch_publish do
82
+ publish("$homie", "v4.0.0")
83
+ publish("$name", "Somfy SDN Network")
84
+ publish("$state", "init")
85
+ publish("$nodes", "FFFFFF")
86
+
87
+ publish("FFFFFF/$name", "Broadcast")
88
+ publish("FFFFFF/$type", "sdn")
89
+ publish("FFFFFF/$properties", "discover")
90
+
91
+ publish("FFFFFF/discover/$name", "Trigger Motor Discovery")
92
+ publish("FFFFFF/discover/$datatype", "enum")
93
+ publish("FFFFFF/discover/$format", "discover")
94
+ publish("FFFFFF/discover/$settable", "true")
95
+ publish("FFFFFF/discover/$retained", "false")
96
+
97
+ subscribe("+/discover/set")
98
+ subscribe("+/label/set")
99
+ subscribe("+/control/set")
100
+ subscribe("+/jog-ms/set")
101
+ subscribe("+/jog-pulses/set")
102
+ subscribe("+/position-pulses/set")
103
+ subscribe("+/position-percent/set")
104
+ subscribe("+/ip/set")
105
+ subscribe("+/reset/set")
106
+ subscribe("+/direction/set")
107
+ subscribe("+/up-speed/set")
108
+ subscribe("+/down-speed/set")
109
+ subscribe("+/slow-speed/set")
110
+ subscribe("+/up-limit/set")
111
+ subscribe("+/down-limit/set")
112
+ subscribe("+/groups/set")
113
+ (1..16).each do |ip|
114
+ subscribe("+/ip#{ip}-pulses/set")
115
+ subscribe("+/ip#{ip}-percent/set")
116
+ end
117
+
118
+ publish("$state", "ready")
119
+ end
120
+ end
121
+
122
+ def publish_motor(addr, node_type)
123
+ motor = nil
124
+
125
+ @mqtt.batch_publish do
126
+ publish("#{addr}/$name", addr)
127
+ publish("#{addr}/$type", node_type.to_s)
128
+ properties = %w{
129
+ discover
130
+ label
131
+ state
132
+ control
133
+ jog-ms
134
+ jog-pulses
135
+ position-pulses
136
+ position-percent
137
+ ip
138
+ down-limit
139
+ groups
140
+ last-direction
141
+ } + (1..16).map { |ip| ["ip#{ip}-pulses", "ip#{ip}-percent"] }.flatten
142
+
143
+ unless node_type == :st50ilt2
144
+ properties.concat %w{
145
+ reset
146
+ last-action-source
147
+ last-action-cause
148
+ up-limit
149
+ direction
150
+ up-speed
151
+ down-speed
152
+ slow-speed
153
+ }
154
+ end
155
+
156
+ publish("#{addr}/$properties", properties.join(","))
157
+
158
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
159
+ publish("#{addr}/discover/$datatype", "enum")
160
+ publish("#{addr}/discover/$format", "discover")
161
+ publish("#{addr}/discover/$settable", "true")
162
+ publish("#{addr}/discover/$retained", "false")
163
+
164
+ publish("#{addr}/label/$name", "Node label")
165
+ publish("#{addr}/label/$datatype", "string")
166
+ publish("#{addr}/label/$settable", "true")
167
+
168
+ publish("#{addr}/state/$name", "Current state of the motor")
169
+ publish("#{addr}/state/$datatype", "enum")
170
+ publish("#{addr}/state/$format", Message::PostMotorStatus::STATE.keys.join(','))
171
+
172
+ publish("#{addr}/control/$name", "Control motor")
173
+ publish("#{addr}/control/$datatype", "enum")
174
+ publish("#{addr}/control/$format", "up,down,stop,wink,next_ip,previous_ip,refresh")
175
+ publish("#{addr}/control/$settable", "true")
176
+ publish("#{addr}/control/$retained", "false")
177
+
178
+ publish("#{addr}/jog-ms/$name", "Jog motor by ms")
179
+ publish("#{addr}/jog-ms/$datatype", "integer")
180
+ publish("#{addr}/jog-ms/$format", "-65535:65535")
181
+ publish("#{addr}/jog-ms/$unit", "ms")
182
+ publish("#{addr}/jog-ms/$settable", "true")
183
+ publish("#{addr}/jog-ms/$retained", "false")
184
+
185
+ publish("#{addr}/jog-pulses/$name", "Jog motor by pulses")
186
+ publish("#{addr}/jog-pulses/$datatype", "integer")
187
+ publish("#{addr}/jog-pulses/$format", "-65535:65535")
188
+ publish("#{addr}/jog-pulses/$unit", "pulses")
189
+ publish("#{addr}/jog-pulses/$settable", "true")
190
+ publish("#{addr}/jog-pulses/$retained", "false")
191
+
192
+ publish("#{addr}/position-percent/$name", "Position (in %)")
193
+ publish("#{addr}/position-percent/$datatype", "integer")
194
+ publish("#{addr}/position-percent/$format", "0:100")
195
+ publish("#{addr}/position-percent/$unit", "%")
196
+ publish("#{addr}/position-percent/$settable", "true")
197
+
198
+ publish("#{addr}/position-pulses/$name", "Position from up limit (in pulses)")
199
+ publish("#{addr}/position-pulses/$datatype", "integer")
200
+ publish("#{addr}/position-pulses/$format", "0:65535")
201
+ publish("#{addr}/position-pulses/$unit", "pulses")
202
+ publish("#{addr}/position-pulses/$settable", "true")
203
+
204
+ publish("#{addr}/ip/$name", "Intermediate Position")
205
+ publish("#{addr}/ip/$datatype", "integer")
206
+ publish("#{addr}/ip/$format", "1:16")
207
+ publish("#{addr}/ip/$settable", "true")
208
+ publish("#{addr}/ip/$retained", "false") if node_type == :st50ilt2
209
+
210
+ publish("#{addr}/down-limit/$name", "Down limit")
211
+ publish("#{addr}/down-limit/$datatype", "integer")
212
+ publish("#{addr}/down-limit/$format", "0:65535")
213
+ publish("#{addr}/down-limit/$unit", "pulses")
214
+ publish("#{addr}/down-limit/$settable", "true")
215
+
216
+ publish("#{addr}/last-direction/$name", "Direction of last motion")
217
+ publish("#{addr}/last-direction/$datatype", "enum")
218
+ publish("#{addr}/last-direction/$format", Message::PostMotorStatus::DIRECTION.keys.join(','))
219
+
220
+ unless node_type == :st50ilt2
221
+ publish("#{addr}/reset/$name", "Recall factory settings")
222
+ publish("#{addr}/reset/$datatype", "enum")
223
+ publish("#{addr}/reset/$format", Message::SetFactoryDefault::RESET.keys.join(','))
224
+ publish("#{addr}/reset/$settable", "true")
225
+ publish("#{addr}/reset/$retained", "false")
226
+
227
+ publish("#{addr}/last-action-source/$name", "Source of last action")
228
+ publish("#{addr}/last-action-source/$datatype", "enum")
229
+ publish("#{addr}/last-action-source/$format", Message::PostMotorStatus::SOURCE.keys.join(','))
230
+
231
+ publish("#{addr}/last-action-cause/$name", "Cause of last action")
232
+ publish("#{addr}/last-action-cause/$datatype", "enum")
233
+ publish("#{addr}/last-action-cause/$format", Message::PostMotorStatus::CAUSE.keys.join(','))
234
+
235
+ publish("#{addr}/up-limit/$name", "Up limit (always = 0)")
236
+ publish("#{addr}/up-limit/$datatype", "integer")
237
+ publish("#{addr}/up-limit/$format", "0:65535")
238
+ publish("#{addr}/up-limit/$unit", "pulses")
239
+ publish("#{addr}/up-limit/$settable", "true")
240
+
241
+ publish("#{addr}/direction/$name", "Motor rotation direction")
242
+ publish("#{addr}/direction/$datatype", "enum")
243
+ publish("#{addr}/direction/$format", "standard,reversed")
244
+ publish("#{addr}/direction/$settable", "true")
245
+
246
+ publish("#{addr}/up-speed/$name", "Up speed")
247
+ publish("#{addr}/up-speed/$datatype", "integer")
248
+ publish("#{addr}/up-speed/$format", "6:28")
249
+ publish("#{addr}/up-speed/$unit", "RPM")
250
+ publish("#{addr}/up-speed/$settable", "true")
251
+
252
+ publish("#{addr}/down-speed/$name", "Down speed, always = Up speed")
253
+ publish("#{addr}/down-speed/$datatype", "integer")
254
+ publish("#{addr}/down-speed/$format", "6:28")
255
+ publish("#{addr}/down-speed/$unit", "RPM")
256
+ publish("#{addr}/down-speed/$settable", "true")
257
+
258
+ publish("#{addr}/slow-speed/$name", "Slow speed")
259
+ publish("#{addr}/slow-speed/$datatype", "integer")
260
+ publish("#{addr}/slow-speed/$format", "6:28")
261
+ publish("#{addr}/slow-speed/$unit", "RPM")
262
+ publish("#{addr}/slow-speed/$settable", "true")
263
+ end
264
+
265
+ publish("#{addr}/groups/$name", "Group Memberships (comma separated, address must start 0101xx)")
266
+ publish("#{addr}/groups/$datatype", "string")
267
+ publish("#{addr}/groups/$settable", "true")
268
+
269
+ (1..16).each do |ip|
270
+ publish("#{addr}/ip#{ip}-pulses/$name", "Intermediate Position #{ip}")
271
+ publish("#{addr}/ip#{ip}-pulses/$datatype", "integer")
272
+ publish("#{addr}/ip#{ip}-pulses/$format", "0:65535")
273
+ publish("#{addr}/ip#{ip}-pulses/$unit", "pulses")
274
+ publish("#{addr}/ip#{ip}-pulses/$settable", "true")
275
+
276
+ publish("#{addr}/ip#{ip}-percent/$name", "Intermediate Position #{ip}")
277
+ publish("#{addr}/ip#{ip}-percent/$datatype", "integer")
278
+ publish("#{addr}/ip#{ip}-percent/$format", "0:100")
279
+ publish("#{addr}/ip#{ip}-percent/$unit", "%")
280
+ publish("#{addr}/ip#{ip}-percent/$settable", "true")
281
+ end
282
+
283
+ motor = Motor.new(self, addr, node_type)
284
+ @motors[addr] = motor
285
+ publish("$nodes", (["FFFFFF"] + @motors.keys.sort + @groups.keys.sort).join(","))
286
+ end
287
+
288
+ sdn_addr = Message.parse_address(addr)
289
+ @mutex.synchronize do
290
+ @queues[2].push(MessageAndRetries.new(Message::GetNodeLabel.new(sdn_addr), 5, 2))
291
+ case node_type
292
+ when :st30
293
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorStatus.new(sdn_addr), 5, 2))
294
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorLimits.new(sdn_addr), 5, 2))
295
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorDirection.new(sdn_addr), 5, 2))
296
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorRollingSpeed.new(sdn_addr), 5, 2))
297
+ (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(Message::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
298
+ when :st50ilt2
299
+ @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorSettings.new(sdn_addr), 5, 2))
300
+ @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorPosition.new(sdn_addr), 5, 2))
301
+ (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
302
+ end
303
+ (1..16).each { |g| @queues[2].push(MessageAndRetries.new(Message::GetGroupAddr.new(sdn_addr, g), 5, 2)) }
304
+
305
+ @cond.signal
306
+ end
307
+
308
+ motor
309
+ end
310
+
311
+ def touch_group(group_addr)
312
+ group = @groups[Message.print_address(group_addr).gsub('.', '')]
313
+ group&.publish(:motors, group.motors_string)
314
+ end
315
+
316
+ def add_group(addr)
317
+ addr = addr.gsub('.', '')
318
+ group = @groups[addr]
319
+ return group if group
320
+
321
+ @mqtt.batch_publish do
322
+ publish("#{addr}/$name", addr)
323
+ publish("#{addr}/$type", "Shade Group")
324
+ publish("#{addr}/$properties", "discover,control,jog-ms,jog-pulses,position-pulses,position-percent,ip,reset,state,last-direction,motors")
325
+
326
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
327
+ publish("#{addr}/discover/$datatype", "enum")
328
+ publish("#{addr}/discover/$format", "discover")
329
+ publish("#{addr}/discover/$settable", "true")
330
+ publish("#{addr}/discover/$retained", "false")
331
+
332
+ publish("#{addr}/control/$name", "Control motors")
333
+ publish("#{addr}/control/$datatype", "enum")
334
+ publish("#{addr}/control/$format", "up,down,stop,wink,next_ip,previous_ip,refresh")
335
+ publish("#{addr}/control/$settable", "true")
336
+ publish("#{addr}/control/$retained", "false")
337
+
338
+ publish("#{addr}/jog-ms/$name", "Jog motors by ms")
339
+ publish("#{addr}/jog-ms/$datatype", "integer")
340
+ publish("#{addr}/jog-ms/$format", "-65535:65535")
341
+ publish("#{addr}/jog-ms/$unit", "ms")
342
+ publish("#{addr}/jog-ms/$settable", "true")
343
+ publish("#{addr}/jog-ms/$retained", "false")
344
+
345
+ publish("#{addr}/jog-pulses/$name", "Jog motors by pulses")
346
+ publish("#{addr}/jog-pulses/$datatype", "integer")
347
+ publish("#{addr}/jog-pulses/$format", "-65535:65535")
348
+ publish("#{addr}/jog-pulses/$unit", "pulses")
349
+ publish("#{addr}/jog-pulses/$settable", "true")
350
+ publish("#{addr}/jog-pulses/$retained", "false")
351
+
352
+ publish("#{addr}/position-pulses/$name", "Position from up limit (in pulses)")
353
+ publish("#{addr}/position-pulses/$datatype", "integer")
354
+ publish("#{addr}/position-pulses/$format", "0:65535")
355
+ publish("#{addr}/position-pulses/$unit", "pulses")
356
+ publish("#{addr}/position-pulses/$settable", "true")
357
+
358
+ publish("#{addr}/position-percent/$name", "Position (in %)")
359
+ publish("#{addr}/position-percent/$datatype", "integer")
360
+ publish("#{addr}/position-percent/$format", "0:100")
361
+ publish("#{addr}/position-percent/$unit", "%")
362
+ publish("#{addr}/position-percent/$settable", "true")
363
+
364
+ publish("#{addr}/ip/$name", "Intermediate Position")
365
+ publish("#{addr}/ip/$datatype", "integer")
366
+ publish("#{addr}/ip/$format", "1:16")
367
+ publish("#{addr}/ip/$settable", "true")
368
+
369
+ publish("#{addr}/state/$name", "State of the motors")
370
+ publish("#{addr}/state/$datatype", "enum")
371
+ publish("#{addr}/state/$format", Message::PostMotorStatus::STATE.keys.join(',') + ",mixed")
372
+
373
+ publish("#{addr}/last-direction/$name", "Direction of last motion")
374
+ publish("#{addr}/last-direction/$datatype", "enum")
375
+ publish("#{addr}/last-direction/$format", Message::PostMotorStatus::DIRECTION.keys.join(',') + ",mixed")
376
+
377
+ publish("#{addr}/motors/$name", "Comma separated motor addresses that are members of this group")
378
+ publish("#{addr}/motors/$datatype", "string")
379
+
380
+ group = @groups[addr] = Group.new(self, addr)
381
+ publish("$nodes", (["FFFFFF"] + @motors.keys.sort + @groups.keys.sort).join(","))
382
+ end
383
+ group
384
+ end
385
+ end
386
+ end
387
+ end