somfy_sdn 1.0.12 → 2.0.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: 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