somfy_sdn 1.0.12 → 2.0.0

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: b2c2926c376002fa9632188a5d467c64c2987e48f892975a1a725ec71f38d021
4
- data.tar.gz: dcafff75ded7c5e5cccc65912179dd6fbf0c33f36655cf29adb6e7e4d796b7a9
3
+ metadata.gz: 8b14639678ed3c5a54d4f8d70fd217898238aed5ccd1c81c0f3b78d04592e48b
4
+ data.tar.gz: 26d14eb134eee69523d2bca19f8ec32c63a3fc110ff7da704681569de2b8422b
5
5
  SHA512:
6
- metadata.gz: 6fc1a34073e8069bd778575a1e5f8b3531b2bf5a769f3010d2ca93d4c4a2af07af0e26c7e1bb7a7d094d729e41dfb7031a5d569294d285fc82ab5f2b93afeebe
7
- data.tar.gz: b61b3e69a01b4b0d709fa381ce0113184f6f5d32227ce3f72eee67c74bcf5e83ff8daa813ee69ef36e9fe76ffa9bc50c9d28eb76c403f1c1db370327ad505155
6
+ metadata.gz: 3da036c72689def6022a487078bc1024ff9aa10c1b9ec7d04ce6e19a0b5b1e3956e18dcdd44272e6e7e72175afe335dc7d9f4c29785004b88ad5fda344b7a84d
7
+ data.tar.gz: c12a57173231c7be1b4b7557cb71c4c58fcc312deace5157893850d5301696ca77b1e743d6ed4e2b005c3cc532c267875f234bbe5bf92a564a17d5fa921b3ff9
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,369 @@
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
+ publish_basic_attributes
44
+
45
+ @sdn = Client.new(port)
46
+
47
+ read_thread = Thread.new { read }
48
+ write_thread = Thread.new { write }
49
+ @mqtt.get { |topic, value| handle_message(topic, value) }
50
+ end
51
+
52
+ def publish(topic, value)
53
+ @mqtt.publish("#{@base_topic}/#{topic}", value, true, 0)
54
+ end
55
+
56
+ def subscribe(topic)
57
+ @mqtt.subscribe("#{@base_topic}/#{topic}")
58
+ end
59
+
60
+ def enqueue(message, queue = 0)
61
+ @mutex.synchronize do
62
+ queue = @queues[queue]
63
+ unless queue.include?(message)
64
+ queue.push(message)
65
+ @cond.signal
66
+ end
67
+ end
68
+ end
69
+
70
+ def publish_basic_attributes
71
+ publish("$homie", "v4.0.0")
72
+ publish("$name", "Somfy SDN Network")
73
+ publish("$state", "init")
74
+ publish("$nodes", "FFFFFF")
75
+
76
+ publish("FFFFFF/$name", "Broadcast")
77
+ publish("FFFFFF/$type", "sdn")
78
+ publish("FFFFFF/$properties", "discover")
79
+
80
+ publish("FFFFFF/discover/$name", "Trigger Motor Discovery")
81
+ publish("FFFFFF/discover/$datatype", "enum")
82
+ publish("FFFFFF/discover/$format", "discover")
83
+ publish("FFFFFF/discover/$settable", "true")
84
+ publish("FFFFFF/discover/$retained", "false")
85
+
86
+ subscribe("+/discover/set")
87
+ subscribe("+/label/set")
88
+ subscribe("+/control/set")
89
+ subscribe("+/jog-ms/set")
90
+ subscribe("+/jog-pulses/set")
91
+ subscribe("+/position-pulses/set")
92
+ subscribe("+/position-percent/set")
93
+ subscribe("+/ip/set")
94
+ subscribe("+/reset/set")
95
+ subscribe("+/direction/set")
96
+ subscribe("+/up-speed/set")
97
+ subscribe("+/down-speed/set")
98
+ subscribe("+/slow-speed/set")
99
+ subscribe("+/up-limit/set")
100
+ subscribe("+/down-limit/set")
101
+ subscribe("+/groups/set")
102
+ (1..16).each do |ip|
103
+ subscribe("+/ip#{ip}-pulses/set")
104
+ subscribe("+/ip#{ip}-percent/set")
105
+ end
106
+
107
+ publish("$state", "ready")
108
+ end
109
+
110
+ def publish_motor(addr, node_type)
111
+ publish("#{addr}/$name", addr)
112
+ publish("#{addr}/$type", node_type.to_s)
113
+ properties = %w{
114
+ discover
115
+ label
116
+ state
117
+ control
118
+ jog-ms
119
+ jog-pulses
120
+ position-pulses
121
+ position-percent
122
+ ip
123
+ down-limit
124
+ groups
125
+ last-direction
126
+ } + (1..16).map { |ip| ["ip#{ip}-pulses", "ip#{ip}-percent"] }.flatten
127
+
128
+ unless node_type == :st50ilt2
129
+ properties.concat %w{
130
+ reset
131
+ last-action-source
132
+ last-action-cause
133
+ up-limit
134
+ direction
135
+ up-speed
136
+ down-speed
137
+ slow-speed
138
+ }
139
+ end
140
+
141
+ publish("#{addr}/$properties", properties.join(","))
142
+
143
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
144
+ publish("#{addr}/discover/$datatype", "enum")
145
+ publish("#{addr}/discover/$format", "discover")
146
+ publish("#{addr}/discover/$settable", "true")
147
+ publish("#{addr}/discover/$retained", "false")
148
+
149
+ publish("#{addr}/label/$name", "Node label")
150
+ publish("#{addr}/label/$datatype", "string")
151
+ publish("#{addr}/label/$settable", "true")
152
+
153
+ publish("#{addr}/state/$name", "Current state of the motor")
154
+ publish("#{addr}/state/$datatype", "enum")
155
+ publish("#{addr}/state/$format", Message::PostMotorStatus::STATE.keys.join(','))
156
+
157
+ publish("#{addr}/control/$name", "Control motor")
158
+ publish("#{addr}/control/$datatype", "enum")
159
+ publish("#{addr}/control/$format", "up,down,stop,wink,next_ip,previous_ip,refresh")
160
+ publish("#{addr}/control/$settable", "true")
161
+ publish("#{addr}/control/$retained", "false")
162
+
163
+ publish("#{addr}/jog-ms/$name", "Jog motor by ms")
164
+ publish("#{addr}/jog-ms/$datatype", "integer")
165
+ publish("#{addr}/jog-ms/$format", "-65535:65535")
166
+ publish("#{addr}/jog-ms/$unit", "ms")
167
+ publish("#{addr}/jog-ms/$settable", "true")
168
+ publish("#{addr}/jog-ms/$retained", "false")
169
+
170
+ publish("#{addr}/jog-pulses/$name", "Jog motor by pulses")
171
+ publish("#{addr}/jog-pulses/$datatype", "integer")
172
+ publish("#{addr}/jog-pulses/$format", "-65535:65535")
173
+ publish("#{addr}/jog-pulses/$unit", "pulses")
174
+ publish("#{addr}/jog-pulses/$settable", "true")
175
+ publish("#{addr}/jog-pulses/$retained", "false")
176
+
177
+ publish("#{addr}/position-percent/$name", "Position (in %)")
178
+ publish("#{addr}/position-percent/$datatype", "integer")
179
+ publish("#{addr}/position-percent/$format", "0:100")
180
+ publish("#{addr}/position-percent/$unit", "%")
181
+ publish("#{addr}/position-percent/$settable", "true")
182
+
183
+ publish("#{addr}/position-pulses/$name", "Position from up limit (in pulses)")
184
+ publish("#{addr}/position-pulses/$datatype", "integer")
185
+ publish("#{addr}/position-pulses/$format", "0:65535")
186
+ publish("#{addr}/position-pulses/$unit", "pulses")
187
+ publish("#{addr}/position-pulses/$settable", "true")
188
+
189
+ publish("#{addr}/ip/$name", "Intermediate Position")
190
+ publish("#{addr}/ip/$datatype", "integer")
191
+ publish("#{addr}/ip/$format", "1:16")
192
+ publish("#{addr}/ip/$settable", "true")
193
+ publish("#{addr}/ip/$retained", "false") if node_type == :st50ilt2
194
+
195
+ publish("#{addr}/down-limit/$name", "Down limit")
196
+ publish("#{addr}/down-limit/$datatype", "integer")
197
+ publish("#{addr}/down-limit/$format", "0:65535")
198
+ publish("#{addr}/down-limit/$unit", "pulses")
199
+ publish("#{addr}/down-limit/$settable", "true")
200
+
201
+ publish("#{addr}/last-direction/$name", "Direction of last motion")
202
+ publish("#{addr}/last-direction/$datatype", "enum")
203
+ publish("#{addr}/last-direction/$format", Message::PostMotorStatus::DIRECTION.keys.join(','))
204
+
205
+ unless node_type == :st50ilt2
206
+ publish("#{addr}/reset/$name", "Recall factory settings")
207
+ publish("#{addr}/reset/$datatype", "enum")
208
+ publish("#{addr}/reset/$format", Message::SetFactoryDefault::RESET.keys.join(','))
209
+ publish("#{addr}/reset/$settable", "true")
210
+ publish("#{addr}/reset/$retained", "false")
211
+
212
+ publish("#{addr}/last-action-source/$name", "Source of last action")
213
+ publish("#{addr}/last-action-source/$datatype", "enum")
214
+ publish("#{addr}/last-action-source/$format", Message::PostMotorStatus::SOURCE.keys.join(','))
215
+
216
+ publish("#{addr}/last-action-cause/$name", "Cause of last action")
217
+ publish("#{addr}/last-action-cause/$datatype", "enum")
218
+ publish("#{addr}/last-action-cause/$format", Message::PostMotorStatus::CAUSE.keys.join(','))
219
+
220
+ publish("#{addr}/up-limit/$name", "Up limit (always = 0)")
221
+ publish("#{addr}/up-limit/$datatype", "integer")
222
+ publish("#{addr}/up-limit/$format", "0:65535")
223
+ publish("#{addr}/up-limit/$unit", "pulses")
224
+ publish("#{addr}/up-limit/$settable", "true")
225
+
226
+ publish("#{addr}/direction/$name", "Motor rotation direction")
227
+ publish("#{addr}/direction/$datatype", "enum")
228
+ publish("#{addr}/direction/$format", "standard,reversed")
229
+ publish("#{addr}/direction/$settable", "true")
230
+
231
+ publish("#{addr}/up-speed/$name", "Up speed")
232
+ publish("#{addr}/up-speed/$datatype", "integer")
233
+ publish("#{addr}/up-speed/$format", "6:28")
234
+ publish("#{addr}/up-speed/$unit", "RPM")
235
+ publish("#{addr}/up-speed/$settable", "true")
236
+
237
+ publish("#{addr}/down-speed/$name", "Down speed, always = Up speed")
238
+ publish("#{addr}/down-speed/$datatype", "integer")
239
+ publish("#{addr}/down-speed/$format", "6:28")
240
+ publish("#{addr}/down-speed/$unit", "RPM")
241
+ publish("#{addr}/down-speed/$settable", "true")
242
+
243
+ publish("#{addr}/slow-speed/$name", "Slow speed")
244
+ publish("#{addr}/slow-speed/$datatype", "integer")
245
+ publish("#{addr}/slow-speed/$format", "6:28")
246
+ publish("#{addr}/slow-speed/$unit", "RPM")
247
+ publish("#{addr}/slow-speed/$settable", "true")
248
+ end
249
+
250
+ publish("#{addr}/groups/$name", "Group Memberships (comma separated, address must start 0101xx)")
251
+ publish("#{addr}/groups/$datatype", "string")
252
+ publish("#{addr}/groups/$settable", "true")
253
+
254
+ (1..16).each do |ip|
255
+ publish("#{addr}/ip#{ip}-pulses/$name", "Intermediate Position #{ip}")
256
+ publish("#{addr}/ip#{ip}-pulses/$datatype", "integer")
257
+ publish("#{addr}/ip#{ip}-pulses/$format", "0:65535")
258
+ publish("#{addr}/ip#{ip}-pulses/$unit", "pulses")
259
+ publish("#{addr}/ip#{ip}-pulses/$settable", "true")
260
+
261
+ publish("#{addr}/ip#{ip}-percent/$name", "Intermediate Position #{ip}")
262
+ publish("#{addr}/ip#{ip}-percent/$datatype", "integer")
263
+ publish("#{addr}/ip#{ip}-percent/$format", "0:100")
264
+ publish("#{addr}/ip#{ip}-percent/$unit", "%")
265
+ publish("#{addr}/ip#{ip}-percent/$settable", "true")
266
+ end
267
+
268
+ motor = Motor.new(self, addr, node_type)
269
+ @motors[addr] = motor
270
+ publish("$nodes", (["FFFFFF"] + @motors.keys.sort + @groups.keys.sort).join(","))
271
+
272
+ sdn_addr = Message.parse_address(addr)
273
+ @mutex.synchronize do
274
+ @queues[2].push(MessageAndRetries.new(Message::GetNodeLabel.new(sdn_addr), 5, 2))
275
+ case node_type
276
+ when :st30
277
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorStatus.new(sdn_addr), 5, 2))
278
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorLimits.new(sdn_addr), 5, 2))
279
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorDirection.new(sdn_addr), 5, 2))
280
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorRollingSpeed.new(sdn_addr), 5, 2))
281
+ (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(Message::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
282
+ when :st50ilt2
283
+ @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorSettings.new(sdn_addr), 5, 2))
284
+ @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorPosition.new(sdn_addr), 5, 2))
285
+ (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
286
+ end
287
+ (1..16).each { |g| @queues[2].push(MessageAndRetries.new(Message::GetGroupAddr.new(sdn_addr, g), 5, 2)) }
288
+
289
+ @cond.signal
290
+ end
291
+
292
+ motor
293
+ end
294
+
295
+ def touch_group(group_addr)
296
+ group = @groups[Message.print_address(group_addr).gsub('.', '')]
297
+ group&.publish(:motors, group.motors_string)
298
+ end
299
+
300
+ def add_group(addr)
301
+ addr = addr.gsub('.', '')
302
+ group = @groups[addr]
303
+ return group if group
304
+
305
+ publish("#{addr}/$name", addr)
306
+ publish("#{addr}/$type", "Shade Group")
307
+ publish("#{addr}/$properties", "discover,control,jog-ms,jog-pulses,position-pulses,position-percent,ip,reset,state,last-direction,motors")
308
+
309
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
310
+ publish("#{addr}/discover/$datatype", "enum")
311
+ publish("#{addr}/discover/$format", "discover")
312
+ publish("#{addr}/discover/$settable", "true")
313
+ publish("#{addr}/discover/$retained", "false")
314
+
315
+ publish("#{addr}/control/$name", "Control motors")
316
+ publish("#{addr}/control/$datatype", "enum")
317
+ publish("#{addr}/control/$format", "up,down,stop,wink,next_ip,previous_ip,refresh")
318
+ publish("#{addr}/control/$settable", "true")
319
+ publish("#{addr}/control/$retained", "false")
320
+
321
+ publish("#{addr}/jog-ms/$name", "Jog motors by ms")
322
+ publish("#{addr}/jog-ms/$datatype", "integer")
323
+ publish("#{addr}/jog-ms/$format", "-65535:65535")
324
+ publish("#{addr}/jog-ms/$unit", "ms")
325
+ publish("#{addr}/jog-ms/$settable", "true")
326
+ publish("#{addr}/jog-ms/$retained", "false")
327
+
328
+ publish("#{addr}/jog-pulses/$name", "Jog motors by pulses")
329
+ publish("#{addr}/jog-pulses/$datatype", "integer")
330
+ publish("#{addr}/jog-pulses/$format", "-65535:65535")
331
+ publish("#{addr}/jog-pulses/$unit", "pulses")
332
+ publish("#{addr}/jog-pulses/$settable", "true")
333
+ publish("#{addr}/jog-pulses/$retained", "false")
334
+
335
+ publish("#{addr}/position-pulses/$name", "Position from up limit (in pulses)")
336
+ publish("#{addr}/position-pulses/$datatype", "integer")
337
+ publish("#{addr}/position-pulses/$format", "0:65535")
338
+ publish("#{addr}/position-pulses/$unit", "pulses")
339
+ publish("#{addr}/position-pulses/$settable", "true")
340
+
341
+ publish("#{addr}/position-percent/$name", "Position (in %)")
342
+ publish("#{addr}/position-percent/$datatype", "integer")
343
+ publish("#{addr}/position-percent/$format", "0:100")
344
+ publish("#{addr}/position-percent/$unit", "%")
345
+ publish("#{addr}/position-percent/$settable", "true")
346
+
347
+ publish("#{addr}/ip/$name", "Intermediate Position")
348
+ publish("#{addr}/ip/$datatype", "integer")
349
+ publish("#{addr}/ip/$format", "1:16")
350
+ publish("#{addr}/ip/$settable", "true")
351
+
352
+ publish("#{addr}/state/$name", "State of the motors")
353
+ publish("#{addr}/state/$datatype", "enum")
354
+ publish("#{addr}/state/$format", Message::PostMotorStatus::STATE.keys.join(',') + ",mixed")
355
+
356
+ publish("#{addr}/last-direction/$name", "Direction of last motion")
357
+ publish("#{addr}/last-direction/$datatype", "enum")
358
+ publish("#{addr}/last-direction/$format", Message::PostMotorStatus::DIRECTION.keys.join(',') + ",mixed")
359
+
360
+ publish("#{addr}/motors/$name", "Comma separated motor addresses that are members of this group")
361
+ publish("#{addr}/motors/$datatype", "string")
362
+
363
+ group = @groups[addr] = Group.new(self, addr)
364
+ publish("$nodes", (["FFFFFF"] + @motors.keys.sort + @groups.keys.sort).join(","))
365
+ group
366
+ end
367
+ end
368
+ end
369
+ end