somfy_sdn 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,494 @@
1
+ module SDN
2
+ Motor = Struct.new(:bridge,
3
+ :addr,
4
+ :label,
5
+ :positionpulses,
6
+ :positionpercent,
7
+ :ip,
8
+ :state,
9
+ :last_direction,
10
+ :last_action_source,
11
+ :last_action_cause,
12
+ :uplimit,
13
+ :downlimit,
14
+ :direction,
15
+ :upspeed,
16
+ :downspeed,
17
+ :slowspeed,
18
+ :ip1pulses,
19
+ :ip1percent,
20
+ :ip2pulses,
21
+ :ip2percent,
22
+ :ip3pulses,
23
+ :ip3percent,
24
+ :ip4pulses,
25
+ :ip4percent,
26
+ :ip5pulses,
27
+ :ip5percent,
28
+ :ip6pulses,
29
+ :ip6percent,
30
+ :ip7pulses,
31
+ :ip7percent,
32
+ :ip8pulses,
33
+ :ip8percent,
34
+ :ip9pulses,
35
+ :ip9percent,
36
+ :ip10pulses,
37
+ :ip10percent,
38
+ :ip11pulses,
39
+ :ip11percent,
40
+ :ip12pulses,
41
+ :ip12percent,
42
+ :ip13pulses,
43
+ :ip13percent,
44
+ :ip14pulses,
45
+ :ip14percent,
46
+ :ip15pulses,
47
+ :ip15percent,
48
+ :ip16pulses,
49
+ :ip16percent,
50
+ :groups) do
51
+ def initialize(*)
52
+ members.each { |k| self[k] = :nil }
53
+ @groups = [].fill(nil, 0, 16)
54
+ super
55
+ end
56
+
57
+ def publish(attribute, value)
58
+ if self[attribute] != value
59
+ bridge.publish("#{addr}/#{attribute}", value.to_s)
60
+ self[attribute] = value
61
+ end
62
+ end
63
+
64
+ def add_group(index, address)
65
+ bridge.add_group(SDN::Message.print_address(address)) if address
66
+ @groups[index] = address
67
+ publish(:groups, groups_string)
68
+ end
69
+
70
+ def set_groups(groups)
71
+ return unless groups =~ /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i
72
+ groups = groups.split(',').sort.uniq.map { |g| SDN::Message.parse_address(g) }
73
+ groups.fill(nil, groups.length, 16 - groups.length)
74
+ messages = []
75
+ sdn_addr = SDN::Message.parse_address(addr)
76
+ groups.each_with_index do |g, i|
77
+ if @groups[i] != g
78
+ messages << SDN::Message::SetGroupAddr.new(sdn_addr, i, g)
79
+ messages << SDN::Message::GetGroupAddr.new(sdn_addr, i)
80
+ end
81
+ end
82
+ messages
83
+ end
84
+
85
+ def groups_string
86
+ @groups.compact.map { |g| SDN::Message.print_address(g) }.sort.uniq.join(',')
87
+ end
88
+ end
89
+
90
+ class MQTTBridge
91
+ def initialize(mqtt_uri, serialport, device_id: "somfy", base_topic: "homie")
92
+ @base_topic = "#{base_topic}/#{device_id}"
93
+ @mqtt = MQTT::Client.new(mqtt_uri)
94
+ @mqtt.set_will("#{@base_topic}/$state", "lost", true)
95
+ @mqtt.connect
96
+ @motors = {}
97
+ @groups = Set.new
98
+ @write_queue = Queue.new
99
+
100
+ publish_basic_attributes
101
+
102
+ @sdn = SerialPort.open(serialport, "baud" => 4800, "parity" => SerialPort::ODD)
103
+
104
+ read_thread = Thread.new do
105
+ loop do
106
+ begin
107
+ message = SDN::Message.parse(@sdn)
108
+ next unless message
109
+ src = SDN::Message.print_address(message.src)
110
+ # ignore the UAI Plus and ourselves
111
+ if src != '7F.7F.7F' && !SDN::Message::is_group_address?(message.src) && !(motor = @motors[src])
112
+ motor = publish_motor(src)
113
+ puts "found new motor #{src}"
114
+ end
115
+
116
+ puts "read #{message.inspect}"
117
+ case message
118
+ when SDN::Message::PostNodeLabel
119
+ if (motor.publish(:label, message.label))
120
+ publish("#{motor.addr}/$name", message.label)
121
+ end
122
+ when SDN::Message::PostMotorPosition
123
+ motor.publish(:positionpercent, message.position_percent)
124
+ motor.publish(:positionpulses, message.position_pulses)
125
+ motor.publish(:ip, message.ip)
126
+ when SDN::Message::PostMotorStatus
127
+ if message.state == :running || motor.state == :running
128
+ @write_queue.push(SDN::Message::GetMotorStatus.new(message.src))
129
+ end
130
+ # this will do one more position request after it stopped
131
+ @write_queue.push(SDN::Message::GetMotorPosition.new(message.src))
132
+ motor.publish(:state, message.state)
133
+ motor.publish(:last_direction, message.last_direction)
134
+ motor.publish(:last_action_source, message.last_action_source)
135
+ motor.publish(:last_action_cause, message.last_action_cause)
136
+ when SDN::Message::PostMotorLimits
137
+ motor.publish(:uplimit, message.up_limit)
138
+ motor.publish(:downlimit, message.down_limit)
139
+ when SDN::Message::PostMotorDirection
140
+ motor.publish(:direction, message.direction)
141
+ when SDN::Message::PostMotorRollingSpeed
142
+ motor.publish(:upspeed, message.up_speed)
143
+ motor.publish(:downspeed, message.down_speed)
144
+ motor.publish(:slowspeed, message.slow_speed)
145
+ when SDN::Message::PostMotorIP
146
+ motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
147
+ motor.publish(:"ip#{message.ip}percent", message.position_percent)
148
+ when SDN::Message::PostGroupAddr
149
+ motor.add_group(message.group_index, message.group_address)
150
+ end
151
+
152
+ rescue SDN::MalformedMessage => e
153
+ puts "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
154
+ rescue => e
155
+ puts "got garbage: #{e}; #{e.backtrace}"
156
+ end
157
+ end
158
+ end
159
+
160
+ write_thread = Thread.new do
161
+ loop do
162
+ message = @write_queue.pop
163
+ puts "writing #{message.inspect}"
164
+ @sdn.write(message.serialize)
165
+ # give more response time to a discovery message
166
+ sleep 5 if (message.is_a?(SDN::Message::GetNodeAddr) && message.dest == [0xff, 0xff, 0xff])
167
+ sleep 0.1
168
+ end
169
+ end
170
+
171
+ @mqtt.get do |topic, value|
172
+ puts "got #{value.inspect} at #{topic}"
173
+ if topic == "#{@base_topic}/discovery/discover/set" && value == "true"
174
+ # trigger discovery
175
+ @write_queue.push(SDN::Message::GetNodeAddr.new)
176
+ elsif (match = topic.match(%r{^#{Regexp.escape(@base_topic)}/(?<addr>\h{2}\.\h{2}\.\h{2})/(?<property>label|down|up|stop|positionpulses|positionpercent|ip|wink|reset|(?<speed_type>upspeed|downspeed|slowspeed)|uplimit|downlimit|direction|ip(?<ip>\d+)(?<ip_type>pulses|percent)|groups)/set$}))
177
+ addr = SDN::Message.parse_address(match[:addr])
178
+ property = match[:property]
179
+ # not homie compliant; allows linking the positionpercent property
180
+ # directly to an OpenHAB rollershutter channel
181
+ if property == 'positionpercent' && value =~ /^(?:UP|DOWN|STOP)$/i
182
+ property = value.downcase
183
+ value = "true"
184
+ end
185
+ motor = @motors[SDN::Message.print_address(addr)]
186
+ is_group = SDN::Message.is_group_address?(addr)
187
+ follow_up = SDN::Message::GetMotorStatus.new(addr)
188
+ message = case property
189
+ when 'label'
190
+ follow_up = SDN::Message::GetNodeLabel.new(addr)
191
+ SDN::Message::SetNodeLabel.new(addr, value) unless is_group
192
+ when 'stop'
193
+ SDN::Message::Stop.new(addr) if value == "true"
194
+ when 'up', 'down'
195
+ SDN::Message::MoveTo.new(addr, "#{property}_limit".to_sym) if value == "true"
196
+ when 'wink'
197
+ SDN::Message::Wink.new(addr) if value == "true"
198
+ when 'reset'
199
+ next unless SDN::Message::SetFactoryDefault::RESET.keys.include?(value.to_sym)
200
+ SDN::Message::SetFactoryDefault.new(addr, value.to_sym)
201
+ when 'positionpulses', 'positionpercent', 'ip'
202
+ SDN::Message::MoveTo.new(addr, property.to_sym, value.to_i)
203
+ when 'direction'
204
+ next if is_group
205
+ follow_up = SDN::Message::GetMotorDirection.new(addr)
206
+ next unless %w{standard reversed}.include?(value)
207
+ SDN::Message::SetMotorDirection.new(addr, value.to_sym)
208
+ when 'uplimit', 'downlimit'
209
+ next if is_group
210
+ if %w{delete current_position jog_ms jog_pulses}.include?(value)
211
+ type = value.to_sym
212
+ value = 10
213
+ else
214
+ type = :specified_position
215
+ end
216
+ target = property == 'uplimit' ? :up : :down
217
+ follow_up = SDN::Message::GetMotorLimits.new(addr)
218
+ SDN::Message::SetMotorLimits.new(addr, type, target, value.to_i)
219
+ when /^ip\d(?:pulses|percent)$/
220
+ next if is_group
221
+ ip = match[:ip].to_i
222
+ next unless (1..16).include?(ip)
223
+ follow_up = SDN::Message::GetMotorIP.new(addr, ip)
224
+ type = if value == 'delete'
225
+ :delete
226
+ elsif value == 'current_position'
227
+ :current_position
228
+ elsif match[:ip_type] == 'pulses'
229
+ :position_pulses
230
+ else
231
+ :position_percent
232
+ end
233
+ SDN::Message::SetMotorIP.new(addr, type, ip, value.to_i)
234
+ when 'upspeed', 'downspeed', 'slowspeed'
235
+ next if is_group
236
+ next unless motor
237
+ follow_up = SDN::Message::GetMotorRollingSpeed.new(addr)
238
+ message = SDN::Message::SetMotorRollingSpeed.new(addr,
239
+ up_speed: motor.up_speed,
240
+ down_speed: motor.down_speed,
241
+ slow_speed: motor.slow_speed)
242
+ message.send(:"#{property.sub('speed', '')}_speed=", value.to_i)
243
+ message
244
+ when 'groups'
245
+ next if is_group
246
+ next unless motor
247
+ messages = motor.set_groups(value)
248
+ messages.each { |m| @write_queue.push(m) }
249
+ nil
250
+ end
251
+ if message
252
+ @write_queue.push(message)
253
+ next if follow_up.is_a?(SDN::Message::GetMotorStatus) && motor&.state == :running
254
+ @write_queue.push(follow_up)
255
+ end
256
+ end
257
+ end
258
+ end
259
+
260
+ def publish(topic, value)
261
+ @mqtt.publish("#{@base_topic}/#{topic}", value, true)
262
+ end
263
+
264
+ def subscribe(topic)
265
+ @mqtt.subscribe("#{@base_topic}/#{topic}")
266
+ end
267
+
268
+ def publish_basic_attributes
269
+ publish("$homie", "v4.0.0")
270
+ publish("$name", "Somfy SDN Network")
271
+ publish("$state", "init")
272
+ publish("$nodes", "discovery")
273
+
274
+ publish("discovery/$name", "Discovery Node")
275
+ publish("discovery/$type", "sdn")
276
+ publish("discovery/$properties", "discover")
277
+
278
+ publish("discovery/discover/$name", "Trigger Motor Discovery")
279
+ publish("discovery/discover/$datatype", "boolean")
280
+ publish("discovery/discover/$settable", "true")
281
+ publish("discovery/discover/$retained", "false")
282
+
283
+ subscribe("discovery/discover/set")
284
+ subscribe("+/label/set")
285
+ subscribe("+/down/set")
286
+ subscribe("+/up/set")
287
+ subscribe("+/stop/set")
288
+ subscribe("+/positionpulses/set")
289
+ subscribe("+/positionpercent/set")
290
+ subscribe("+/ip/set")
291
+ subscribe("+/wink/set")
292
+ subscribe("+/reset/set")
293
+ subscribe("+/direction/set")
294
+ subscribe("+/upspeed/set")
295
+ subscribe("+/downspeed/set")
296
+ subscribe("+/slowspeed/set")
297
+ subscribe("+/uplimit/set")
298
+ subscribe("+/downlimit/set")
299
+ subscribe("+/groups/set")
300
+ (1..16).each do |ip|
301
+ subscribe("+/ip#{ip}pulses/set")
302
+ subscribe("+/ip#{ip}percent/set")
303
+ end
304
+
305
+ publish("$state", "ready")
306
+ end
307
+
308
+ def publish_motor(addr)
309
+ publish("#{addr}/$name", addr)
310
+ publish("#{addr}/$type", "Sonesse 30 Motor")
311
+ publish("#{addr}/$properties", "label,down,up,stop,positionpulses,positionpercent,ip,wink,reset,state,last_direction,last_action_source,last_action_cause,uplimit,downlimit,direction,upspeed,downspeed,slowspeed,#{(1..16).map { |ip| "ip#{ip}pulses,ip#{ip}percent" }.join(',')},groups")
312
+
313
+ publish("#{addr}/label/$name", "Node label")
314
+ publish("#{addr}/label/$datatype", "string")
315
+ publish("#{addr}/label/$settable", "true")
316
+
317
+ publish("#{addr}/down/$name", "Move in down direction")
318
+ publish("#{addr}/down/$datatype", "boolean")
319
+ publish("#{addr}/down/$settable", "true")
320
+ publish("#{addr}/down/$retained", "false")
321
+
322
+ publish("#{addr}/up/$name", "Move in up direction")
323
+ publish("#{addr}/up/$datatype", "boolean")
324
+ publish("#{addr}/up/$settable", "true")
325
+ publish("#{addr}/up/$retained", "false")
326
+
327
+ publish("#{addr}/stop/$name", "Cancel adjustments")
328
+ publish("#{addr}/stop/$datatype", "boolean")
329
+ publish("#{addr}/stop/$settable", "true")
330
+ publish("#{addr}/stop/$retained", "false")
331
+
332
+ publish("#{addr}/positionpulses/$name", "Position from up limit (in pulses)")
333
+ publish("#{addr}/positionpulses/$datatype", "integer")
334
+ publish("#{addr}/positionpulses/$format", "0:65535")
335
+ publish("#{addr}/positionpulses/$unit", "pulses")
336
+ publish("#{addr}/positionpulses/$settable", "true")
337
+
338
+ publish("#{addr}/positionpercent/$name", "Position (in %)")
339
+ publish("#{addr}/positionpercent/$datatype", "integer")
340
+ publish("#{addr}/positionpercent/$format", "0:100")
341
+ publish("#{addr}/positionpercent/$unit", "%")
342
+ publish("#{addr}/positionpercent/$settable", "true")
343
+
344
+ publish("#{addr}/ip/$name", "Intermediate Position")
345
+ publish("#{addr}/ip/$datatype", "integer")
346
+ publish("#{addr}/ip/$format", "1:16")
347
+ publish("#{addr}/ip/$settable", "true")
348
+
349
+ publish("#{addr}/wink/$name", "Feedback")
350
+ publish("#{addr}/wink/$datatype", "boolean")
351
+ publish("#{addr}/wink/$settable", "true")
352
+ publish("#{addr}/wink/$retained", "false")
353
+
354
+ publish("#{addr}/reset/$name", "Recall factory settings")
355
+ publish("#{addr}/reset/$datatype", "enum")
356
+ publish("#{addr}/reset/$format", SDN::Message::SetFactoryDefault::RESET.keys.join(','))
357
+ publish("#{addr}/reset/$settable", "true")
358
+ publish("#{addr}/reset/$retained", "false")
359
+
360
+ publish("#{addr}/state/$name", "State of the motor")
361
+ publish("#{addr}/state/$datatype", "enum")
362
+ publish("#{addr}/state/$format", SDN::Message::PostMotorStatus::STATE.keys.join(','))
363
+
364
+ publish("#{addr}/last_direction/$name", "Direction of last motion")
365
+ publish("#{addr}/last_direction/$datatype", "enum")
366
+ publish("#{addr}/last_direction/$format", SDN::Message::PostMotorStatus::DIRECTION.keys.join(','))
367
+
368
+ publish("#{addr}/last_action_source/$name", "Source of last action")
369
+ publish("#{addr}/last_action_source/$datatype", "enum")
370
+ publish("#{addr}/last_action_source/$format", SDN::Message::PostMotorStatus::SOURCE.keys.join(','))
371
+
372
+ publish("#{addr}/last_action_cause/$name", "Cause of last action")
373
+ publish("#{addr}/last_action_cause/$datatype", "enum")
374
+ publish("#{addr}/last_action_cause/$format", SDN::Message::PostMotorStatus::CAUSE.keys.join(','))
375
+
376
+ publish("#{addr}/uplimit/$name", "Up limit (always = 0)")
377
+ publish("#{addr}/uplimit/$datatype", "integer")
378
+ publish("#{addr}/uplimit/$format", "0:65535")
379
+ publish("#{addr}/uplimit/$unit", "pulses")
380
+ publish("#{addr}/uplimit/$settable", "true")
381
+
382
+ publish("#{addr}/downlimit/$name", "Down limit")
383
+ publish("#{addr}/downlimit/$datatype", "integer")
384
+ publish("#{addr}/downlimit/$format", "0:65535")
385
+ publish("#{addr}/downlimit/$unit", "pulses")
386
+ publish("#{addr}/downlimit/$settable", "true")
387
+
388
+ publish("#{addr}/direction/$name", "Motor rotation direction")
389
+ publish("#{addr}/direction/$datatype", "enum")
390
+ publish("#{addr}/direction/$format", "standard,reversed")
391
+ publish("#{addr}/direction/$settable", "true")
392
+
393
+ publish("#{addr}/upspeed/$name", "Up speed")
394
+ publish("#{addr}/upspeed/$datatype", "integer")
395
+ publish("#{addr}/upspeed/$format", "6:28")
396
+ publish("#{addr}/upspeed/$unit", "RPM")
397
+ publish("#{addr}/upspeed/$settable", "true")
398
+
399
+ publish("#{addr}/downspeed/$name", "Down speed, always = Up speed")
400
+ publish("#{addr}/downspeed/$datatype", "integer")
401
+ publish("#{addr}/downspeed/$format", "6:28")
402
+ publish("#{addr}/downspeed/$unit", "RPM")
403
+ publish("#{addr}/downspeed/$settable", "true")
404
+
405
+ publish("#{addr}/slowspeed/$name", "Slow speed")
406
+ publish("#{addr}/slowspeed/$datatype", "integer")
407
+ publish("#{addr}/slowspeed/$format", "6:28")
408
+ publish("#{addr}/slowspeed/$unit", "RPM")
409
+ publish("#{addr}/slowspeed/$settable", "true")
410
+
411
+ publish("#{addr}/groups/$name", "Group Memberships")
412
+ publish("#{addr}/groups/$datatype", "string")
413
+ publish("#{addr}/groups/$settable", "true")
414
+
415
+ (1..16).each do |ip|
416
+ publish("#{addr}/ip#{ip}pulses/$name", "Intermediate Position #{ip}")
417
+ publish("#{addr}/ip#{ip}pulses/$datatype", "integer")
418
+ publish("#{addr}/ip#{ip}pulses/$format", "0:65535")
419
+ publish("#{addr}/ip#{ip}pulses/$unit", "pulses")
420
+ publish("#{addr}/ip#{ip}pulses/$settable", "true")
421
+
422
+ publish("#{addr}/ip#{ip}percent/$name", "Intermediate Position #{ip}")
423
+ publish("#{addr}/ip#{ip}percent/$datatype", "integer")
424
+ publish("#{addr}/ip#{ip}percent/$format", "0:100")
425
+ publish("#{addr}/ip#{ip}percent/$unit", "%")
426
+ publish("#{addr}/ip#{ip}percent/$settable", "true")
427
+ end
428
+
429
+ motor = Motor.new(self, addr)
430
+ @motors[addr] = motor
431
+ publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.to_a).join(","))
432
+
433
+ sdn_addr = SDN::Message.parse_address(addr)
434
+ # these messages are often corrupt; just don't bother for now.
435
+ #@write_queue.push(SDN::Message::GetNodeLabel.new(sdn_addr))
436
+ @write_queue.push(SDN::Message::GetMotorStatus.new(sdn_addr))
437
+ @write_queue.push(SDN::Message::GetMotorLimits.new(sdn_addr))
438
+ @write_queue.push(SDN::Message::GetMotorDirection.new(sdn_addr))
439
+ @write_queue.push(SDN::Message::GetMotorRollingSpeed.new(sdn_addr))
440
+ (1..16).each { |ip| @write_queue.push(SDN::Message::GetMotorIP.new(sdn_addr, ip)) }
441
+ (0...16).each { |g| @write_queue.push(SDN::Message::GetGroupAddr.new(sdn_addr, g)) }
442
+
443
+ motor
444
+ end
445
+
446
+ def add_group(addr)
447
+ return if @groups.include?(addr)
448
+
449
+ publish("#{addr}/$name", addr)
450
+ publish("#{addr}/$type", "Shade Group")
451
+ publish("#{addr}/$properties", "down,up,stop,positionpulses,positionpercent,ip,wink,reset")
452
+
453
+ publish("#{addr}/down/$name", "Move in down direction")
454
+ publish("#{addr}/down/$datatype", "boolean")
455
+ publish("#{addr}/down/$settable", "true")
456
+ publish("#{addr}/down/$retained", "false")
457
+
458
+ publish("#{addr}/up/$name", "Move in up direction")
459
+ publish("#{addr}/up/$datatype", "boolean")
460
+ publish("#{addr}/up/$settable", "true")
461
+ publish("#{addr}/up/$retained", "false")
462
+
463
+ publish("#{addr}/stop/$name", "Cancel adjustments")
464
+ publish("#{addr}/stop/$datatype", "boolean")
465
+ publish("#{addr}/stop/$settable", "true")
466
+ publish("#{addr}/stop/$retained", "false")
467
+
468
+ publish("#{addr}/positionpulses/$name", "Position from up limit (in pulses)")
469
+ publish("#{addr}/positionpulses/$datatype", "integer")
470
+ publish("#{addr}/positionpulses/$format", "0:65535")
471
+ publish("#{addr}/positionpulses/$unit", "pulses")
472
+ publish("#{addr}/positionpulses/$settable", "true")
473
+
474
+ publish("#{addr}/positionpercent/$name", "Position (in %)")
475
+ publish("#{addr}/positionpercent/$datatype", "integer")
476
+ publish("#{addr}/positionpercent/$format", "0:100")
477
+ publish("#{addr}/positionpercent/$unit", "%")
478
+ publish("#{addr}/positionpercent/$settable", "true")
479
+
480
+ publish("#{addr}/ip/$name", "Intermediate Position")
481
+ publish("#{addr}/ip/$datatype", "integer")
482
+ publish("#{addr}/ip/$format", "1:16")
483
+ publish("#{addr}/ip/$settable", "true")
484
+
485
+ publish("#{addr}/wink/$name", "Feedback")
486
+ publish("#{addr}/wink/$datatype", "boolean")
487
+ publish("#{addr}/wink/$settable", "true")
488
+ publish("#{addr}/wink/$retained", "false")
489
+
490
+ @groups << addr
491
+ publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.to_a).join(","))
492
+ end
493
+ end
494
+ end
@@ -0,0 +1,3 @@
1
+ module SDN
2
+ VERSION = '1.0.0'
3
+ end
data/lib/somfy_sdn.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'sdn/message'
2
+ require 'sdn/mqtt_bridge'
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: somfy_sdn
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cody Cutrer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-05-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: serialport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.3.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.3.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: mqtt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.5.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.5.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '9.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '9.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ description:
70
+ email: cody@cutrer.com'
71
+ executables:
72
+ - sdn_mqtt_bridge
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - bin/sdn_mqtt_bridge
77
+ - lib/sdn/message.rb
78
+ - lib/sdn/messages/control.rb
79
+ - lib/sdn/messages/get.rb
80
+ - lib/sdn/messages/helpers.rb
81
+ - lib/sdn/messages/post.rb
82
+ - lib/sdn/messages/set.rb
83
+ - lib/sdn/mqtt_bridge.rb
84
+ - lib/sdn/version.rb
85
+ - lib/somfy_sdn.rb
86
+ homepage: https://github.com/ccutrer/somfy_sdn
87
+ licenses:
88
+ - MIT
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubygems_version: 3.0.3
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: Library for communication with Somfy SDN RS-485 motorized shades
109
+ test_files: []