somfy_sdn 1.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.
@@ -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: []