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.
data/lib/sdn/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module SDN
2
- VERSION = '1.0.12'
2
+ VERSION = '2.0.0'
3
3
  end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: somfy_sdn
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.12
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-02 00:00:00.000000000 Z
11
+ date: 2021-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ccutrer-serialport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: curses
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.4'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: mqtt
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -39,19 +67,19 @@ dependencies:
39
67
  - !ruby/object:Gem::Version
40
68
  version: 0.0.3
41
69
  - !ruby/object:Gem::Dependency
42
- name: ccutrer-serialport
70
+ name: thor
43
71
  requirement: !ruby/object:Gem::Requirement
44
72
  requirements:
45
73
  - - "~>"
46
74
  - !ruby/object:Gem::Version
47
- version: 1.0.0
75
+ version: '1.1'
48
76
  type: :runtime
49
77
  prerelease: false
50
78
  version_requirements: !ruby/object:Gem::Requirement
51
79
  requirements:
52
80
  - - "~>"
53
81
  - !ruby/object:Gem::Version
54
- version: 1.0.0
82
+ version: '1.1'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: byebug
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -83,22 +111,31 @@ dependencies:
83
111
  description:
84
112
  email: cody@cutrer.com'
85
113
  executables:
86
- - sdn_mqtt_bridge
114
+ - somfy_sdn
87
115
  extensions: []
88
116
  extra_rdoc_files: []
89
117
  files:
90
- - bin/sdn_mqtt_bridge
118
+ - bin/somfy_sdn
91
119
  - lib/sdn.rb
120
+ - lib/sdn/cli/mqtt.rb
121
+ - lib/sdn/cli/mqtt/group.rb
122
+ - lib/sdn/cli/mqtt/motor.rb
123
+ - lib/sdn/cli/mqtt/read.rb
124
+ - lib/sdn/cli/mqtt/subscriptions.rb
125
+ - lib/sdn/cli/mqtt/write.rb
126
+ - lib/sdn/cli/provisioner.rb
127
+ - lib/sdn/cli/simulator.rb
128
+ - lib/sdn/client.rb
92
129
  - lib/sdn/message.rb
93
- - lib/sdn/messages/control.rb
94
- - lib/sdn/messages/get.rb
95
- - lib/sdn/messages/helpers.rb
96
- - lib/sdn/messages/ilt2/get.rb
97
- - lib/sdn/messages/ilt2/post.rb
98
- - lib/sdn/messages/ilt2/set.rb
99
- - lib/sdn/messages/post.rb
100
- - lib/sdn/messages/set.rb
101
- - lib/sdn/mqtt_bridge.rb
130
+ - lib/sdn/message/control.rb
131
+ - lib/sdn/message/get.rb
132
+ - lib/sdn/message/helpers.rb
133
+ - lib/sdn/message/ilt2/get.rb
134
+ - lib/sdn/message/ilt2/master_control.rb
135
+ - lib/sdn/message/ilt2/post.rb
136
+ - lib/sdn/message/ilt2/set.rb
137
+ - lib/sdn/message/post.rb
138
+ - lib/sdn/message/set.rb
102
139
  - lib/sdn/version.rb
103
140
  - lib/somfy_sdn.rb
104
141
  homepage: https://github.com/ccutrer/somfy_sdn
data/bin/sdn_mqtt_bridge DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'somfy_sdn'
4
-
5
- SDN::MQTTBridge.new(ARGV[0], ARGV[1])
@@ -1,9 +0,0 @@
1
- module SDN
2
- class Message
3
- module ILT2
4
- class GetMotorPosition < SimpleRequest
5
- MSG = 0x44
6
- end
7
- end
8
- end
9
- end
@@ -1,18 +0,0 @@
1
- module SDN
2
- class Message
3
- module ILT2
4
- class PostMotorPosition < Message
5
- MSG = 0x64
6
- PARAMS_LENGTH = 3
7
-
8
- attr_accessor :position_pulses, :position_percent
9
-
10
- def parse(params)
11
- super
12
- self.position_pulses = to_number(params[0..1])
13
- self.position_percent = to_number(params[2]).to_f / 255 * 100
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,59 +0,0 @@
1
- module SDN
2
- class Message
3
- module ILT2
4
- class SetMotorPosition < Message
5
- MSG = 0x54
6
- PARAMS_LENGTH = 3
7
- TARGET_TYPE = {
8
- up_limit: 1,
9
- down_limit: 2,
10
- stop: 3,
11
- ip: 4,
12
- next_ip_up: 5,
13
- next_ip_down: 6,
14
- jog_up: 10,
15
- jog_down: 11,
16
- position_percent: 16,
17
- }.freeze
18
-
19
- attr_reader :target_type, :target
20
-
21
- def initialize(dest = nil, target_type = :up_limit, target = 0, **kwargs)
22
- kwargs[:dest] ||= dest
23
- super(**kwargs)
24
- self.target_type = target_type
25
- self.target = target
26
- end
27
-
28
- def parse(params)
29
- super
30
- self.target_type = TARGET_TYPE.invert[to_number(params[0])]
31
- target = to_number(params[1..2])
32
- if target_type == :position_percent
33
- target = target.to_f / 255 * 100
34
- end
35
- self.target = target
36
- end
37
-
38
- def target_type=(value)
39
- raise ArgumentError, "target_type must be one of :up_limit, :down_limit, :stop, :ip, :next_ip_up, :next_ip_down, :jog_up, :jog_down, or :position_percent" unless TARGET_TYPE.keys.include?(value)
40
- @target_type = value
41
- end
42
-
43
- def target=(value)
44
- if target_type == :position_percent && value
45
- @target = [[0, value].max, 100].min
46
- else
47
- @target = value&. & 0xffff
48
- end
49
- end
50
-
51
- def params
52
- param = target
53
- param = (param * 255 / 100).to_i if target_type == :position_percent
54
- transform_param(TARGET_TYPE[target_type]) + from_number(param, 2)
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,711 +0,0 @@
1
- require 'mqtt'
2
- require 'uri'
3
- require 'set'
4
-
5
- module SDN
6
- MessageAndRetries = Struct.new(:message, :remaining_retries, :priority)
7
-
8
- Group = Struct.new(:bridge, :addr, :positionpercent, :state, :motors) do
9
- def initialize(*)
10
- members.each { |k| self[k] = :nil }
11
- super
12
- end
13
-
14
- def publish(attribute, value)
15
- if self[attribute] != value
16
- bridge.publish("#{addr}/#{attribute}", value.to_s)
17
- self[attribute] = value
18
- end
19
- end
20
-
21
- def printed_addr
22
- Message.print_address(Message.parse_address(addr))
23
- end
24
-
25
- def motor_objects
26
- bridge.motors.select { |addr, motor| motor.groups_string.include?(printed_addr) }.values
27
- end
28
-
29
- def motors_string
30
- motor_objects.map { |m| SDN::Message.print_address(SDN::Message.parse_address(m.addr)) }.sort.join(',')
31
- end
32
- end
33
-
34
- Motor = Struct.new(:bridge,
35
- :addr,
36
- :label,
37
- :positionpulses,
38
- :positionpercent,
39
- :ip,
40
- :state,
41
- :last_direction,
42
- :last_action_source,
43
- :last_action_cause,
44
- :uplimit,
45
- :downlimit,
46
- :direction,
47
- :upspeed,
48
- :downspeed,
49
- :slowspeed,
50
- :ip1pulses,
51
- :ip1percent,
52
- :ip2pulses,
53
- :ip2percent,
54
- :ip3pulses,
55
- :ip3percent,
56
- :ip4pulses,
57
- :ip4percent,
58
- :ip5pulses,
59
- :ip5percent,
60
- :ip6pulses,
61
- :ip6percent,
62
- :ip7pulses,
63
- :ip7percent,
64
- :ip8pulses,
65
- :ip8percent,
66
- :ip9pulses,
67
- :ip9percent,
68
- :ip10pulses,
69
- :ip10percent,
70
- :ip11pulses,
71
- :ip11percent,
72
- :ip12pulses,
73
- :ip12percent,
74
- :ip13pulses,
75
- :ip13percent,
76
- :ip14pulses,
77
- :ip14percent,
78
- :ip15pulses,
79
- :ip15percent,
80
- :ip16pulses,
81
- :ip16percent,
82
- :groups,
83
- :last_action) do
84
- def initialize(*)
85
- members.each { |k| self[k] = :nil }
86
- @groups = [].fill(nil, 0, 16)
87
- super
88
- end
89
-
90
- def publish(attribute, value)
91
- if self[attribute] != value
92
- bridge.publish("#{addr}/#{attribute}", value.to_s)
93
- self[attribute] = value
94
- end
95
- end
96
-
97
- def add_group(index, address)
98
- group = bridge.add_group(SDN::Message.print_address(address)) if address
99
- @groups[index] = address
100
- group&.publish(:motors, group.motors_string)
101
- publish(:groups, groups_string)
102
- end
103
-
104
- def set_groups(groups)
105
- return unless groups =~ /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i
106
- groups = groups.split(',').sort.uniq.map { |g| SDN::Message.parse_address(g) }.select { |g| SDN::Message.is_group_address?(g) }
107
- groups.fill(nil, groups.length, 16 - groups.length)
108
- messages = []
109
- sdn_addr = SDN::Message.parse_address(addr)
110
- groups.each_with_index do |g, i|
111
- if @groups[i] != g
112
- messages << SDN::Message::SetGroupAddr.new(sdn_addr, i, g).tap { |m| m.ack_requested = true }
113
- messages << SDN::Message::GetGroupAddr.new(sdn_addr, i)
114
- end
115
- end
116
- messages
117
- end
118
-
119
- def groups_string
120
- @groups.compact.map { |g| SDN::Message.print_address(g) }.sort.uniq.join(',')
121
- end
122
-
123
- def group_objects
124
- groups_string.split(',').map { |addr| bridge.groups[addr.gsub('.', '')] }
125
- end
126
- end
127
-
128
- class MQTTBridge
129
- WAIT_TIME = 0.25
130
- BROADCAST_WAIT = 5.0
131
-
132
- attr_reader :motors, :groups
133
-
134
- def initialize(mqtt_uri, port, device_id: "somfy", base_topic: "homie")
135
- @base_topic = "#{base_topic}/#{device_id}"
136
- @mqtt = MQTT::Client.new(mqtt_uri)
137
- @mqtt.set_will("#{@base_topic}/$state", "lost", true)
138
- @mqtt.connect
139
-
140
- @motors = {}
141
- @groups = {}
142
-
143
- @mutex = Mutex.new
144
- @cond = ConditionVariable.new
145
- @queues = [[], [], []]
146
- @response_pending = false
147
- @broadcast_pending = false
148
-
149
- publish_basic_attributes
150
-
151
- uri = URI.parse(port)
152
- if uri.scheme == "tcp"
153
- require 'socket'
154
- @sdn = TCPSocket.new(uri.host, uri.port)
155
- elsif uri.scheme == "telnet" || uri.scheme == "rfc2217"
156
- require 'net/telnet/rfc2217'
157
- @sdn = Net::Telnet::RFC2217.new('Host' => uri.host,
158
- 'Port' => uri.port || 23,
159
- 'baud' => 4800,
160
- 'parity' => Net::Telnet::RFC2217::ODD)
161
- else
162
- require 'ccutrer-serialport'
163
- @sdn = CCutrer::SerialPort.new(port, baud: 4800, parity: :odd)
164
- end
165
-
166
- read_thread = Thread.new do
167
- buffer = ""
168
-
169
- loop do
170
- begin
171
- message, bytes_read = SDN::Message.parse(buffer.bytes)
172
- # discard how much we read
173
- buffer = buffer[bytes_read..-1]
174
- unless message
175
- begin
176
- buffer.concat(@sdn.read_nonblock(64 * 1024))
177
- next
178
- rescue IO::WaitReadable, EOFError
179
- wait = buffer.empty? ? nil : WAIT_TIME
180
- if @sdn.wait_readable(wait).nil?
181
- # timed out; just discard everything
182
- puts "timed out reading; discarding buffer: #{buffer.unpack('H*').first}"
183
- buffer = ""
184
- end
185
- end
186
- next
187
- end
188
-
189
- src = SDN::Message.print_address(message.src)
190
- # ignore the UAI Plus and ourselves
191
- if src != '7F.7F.7F' && !SDN::Message::is_group_address?(message.src) && !(motor = @motors[src.gsub('.', '')])
192
- motor = publish_motor(src.gsub('.', ''))
193
- puts "found new motor #{src}"
194
- end
195
-
196
- puts "read #{message.inspect}"
197
- follow_ups = []
198
- case message
199
- when SDN::Message::PostNodeLabel
200
- if (motor.publish(:label, message.label))
201
- publish("#{motor.addr}/$name", message.label)
202
- end
203
- when SDN::Message::PostMotorPosition
204
- motor.publish(:positionpercent, message.position_percent)
205
- motor.publish(:positionpulses, message.position_pulses)
206
- motor.publish(:ip, message.ip)
207
- motor.group_objects.each do |group|
208
- positions = group.motor_objects.map(&:positionpercent)
209
- position = nil
210
- # calculate an average, but only if we know a position for
211
- # every shade
212
- if !positions.include?(:nil) && !positions.include?(nil)
213
- position = positions.inject(&:+) / positions.length
214
- end
215
-
216
- group.publish(:positionpercent, position)
217
- end
218
- when SDN::Message::PostMotorStatus
219
- if message.state == :running || motor.state == :running ||
220
- # if it's explicitly stopped, but we didn't ask it to, it's probably
221
- # changing directions so keep querying
222
- (message.state == :stopped &&
223
- message.last_action_cause == :explicit_command &&
224
- !(motor.last_action == SDN::Message::Stop || motor.last_action.nil?))
225
- follow_ups << SDN::Message::GetMotorStatus.new(message.src)
226
- end
227
- # this will do one more position request after it stopped
228
- follow_ups << SDN::Message::GetMotorPosition.new(message.src)
229
- motor.publish(:state, message.state)
230
- motor.publish(:last_direction, message.last_direction)
231
- motor.publish(:last_action_source, message.last_action_source)
232
- motor.publish(:last_action_cause, message.last_action_cause)
233
- motor.group_objects.each do |group|
234
- states = group.motor_objects.map(&:state).uniq
235
- state = states.length == 1 ? states.first : 'mixed'
236
- group.publish(:state, state)
237
- end
238
- when SDN::Message::PostMotorLimits
239
- motor.publish(:uplimit, message.up_limit)
240
- motor.publish(:downlimit, message.down_limit)
241
- when SDN::Message::PostMotorDirection
242
- motor.publish(:direction, message.direction)
243
- when SDN::Message::PostMotorRollingSpeed
244
- motor.publish(:upspeed, message.up_speed)
245
- motor.publish(:downspeed, message.down_speed)
246
- motor.publish(:slowspeed, message.slow_speed)
247
- when SDN::Message::PostMotorIP
248
- motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
249
- motor.publish(:"ip#{message.ip}percent", message.position_percent)
250
- when SDN::Message::PostGroupAddr
251
- motor.add_group(message.group_index, message.group_address)
252
- end
253
-
254
- @mutex.synchronize do
255
- correct_response = @response_pending && message.src == @prior_message&.message&.dest && message.is_a?(@prior_message&.message&.class&.expected_response)
256
- signal = correct_response || !follow_ups.empty?
257
- @response_pending = @broadcast_pending if correct_response
258
- follow_ups.each do |follow_up|
259
- @queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
260
- end
261
- @cond.signal if signal
262
- end
263
- rescue EOFError
264
- puts "EOF reading"
265
- exit 2
266
- rescue SDN::MalformedMessage => e
267
- puts "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
268
- rescue => e
269
- puts "got garbage: #{e}; #{e.backtrace}"
270
- end
271
- end
272
- end
273
-
274
- write_thread = Thread.new do
275
- begin
276
- loop do
277
- message_and_retries = nil
278
- @mutex.synchronize do
279
- # got woken up early by another command getting queued; spin
280
- if @response_pending
281
- while @response_pending
282
- remaining_wait = @response_pending - Time.now.to_f
283
- if remaining_wait < 0
284
- puts "timed out waiting on response"
285
- @response_pending = nil
286
- @broadcast_pending = nil
287
- if @prior_message&.remaining_retries != 0
288
- puts "retrying #{@prior_message.remaining_retries} more times ..."
289
- @queues[@prior_message.priority].push(@prior_message)
290
- @prior_message = nil
291
- end
292
- else
293
- @cond.wait(@mutex, remaining_wait)
294
- end
295
- end
296
- else
297
- # minimum time between messages
298
- sleep 0.1
299
- end
300
-
301
- @queues.find { |q| message_and_retries = q.shift }
302
- if message_and_retries
303
- if message_and_retries.message.ack_requested || message_and_retries.message.class.name =~ /^SDN::Message::Get/
304
- @response_pending = Time.now.to_f + WAIT_TIME
305
- if message_and_retries.message.dest == BROADCAST_ADDRESS || SDN::Message::is_group_address?(message_and_retries.message.src) && message_and_retries.message.is_a?(SDN::Message::GetNodeAddr)
306
- @broadcast_pending = Time.now.to_f + BROADCAST_WAIT
307
- end
308
- end
309
- end
310
-
311
- # wait until there is a message
312
- if @response_pending
313
- message_and_retries.remaining_retries -= 1
314
- @prior_message = message_and_retries
315
- elsif message_and_retries
316
- @prior_message = nil
317
- else
318
- @cond.wait(@mutex)
319
- end
320
- end
321
- next unless message_and_retries
322
-
323
- message = message_and_retries.message
324
- puts "writing #{message.inspect}"
325
- serialized = message.serialize
326
- @sdn.write(serialized)
327
- @sdn.flush if @sdn.respond_to?(:flush)
328
- puts "wrote #{serialized.unpack("C*").map { |b| '%02x' % b }.join(' ')}"
329
- end
330
- rescue => e
331
- puts "failure writing: #{e}"
332
- exit 1
333
- end
334
- end
335
-
336
- @mqtt.get do |topic, value|
337
- puts "got #{value.inspect} at #{topic}"
338
- if topic == "#{@base_topic}/discovery/discover/set" && value == "true"
339
- # trigger discovery
340
- @mutex.synchronize do
341
- @queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeAddr.new, 1, 2))
342
- @cond.signal
343
- end
344
- elsif (match = topic.match(%r{^#{Regexp.escape(@base_topic)}/(?<addr>\h{6})/(?<property>discover|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$}))
345
- addr = SDN::Message.parse_address(match[:addr])
346
- property = match[:property]
347
- # not homie compliant; allows linking the positionpercent property
348
- # directly to an OpenHAB rollershutter channel
349
- if property == 'positionpercent' && value =~ /^(?:UP|DOWN|STOP)$/i
350
- property = value.downcase
351
- value = "true"
352
- end
353
- mqtt_addr = SDN::Message.print_address(addr).gsub('.', '')
354
- motor = @motors[mqtt_addr]
355
- is_group = SDN::Message.is_group_address?(addr)
356
- group = @groups[mqtt_addr]
357
- follow_up = SDN::Message::GetMotorStatus.new(addr)
358
- message = case property
359
- when 'discover'
360
- follow_up = nil
361
- SDN::Message::GetNodeAddr.new(addr) if value == "true"
362
- when 'label'
363
- follow_up = SDN::Message::GetNodeLabel.new(addr)
364
- SDN::Message::SetNodeLabel.new(addr, value) unless is_group
365
- when 'stop'
366
- SDN::Message::Stop.new(addr) if value == "true"
367
- when 'up', 'down'
368
- SDN::Message::MoveTo.new(addr, "#{property}_limit".to_sym) if value == "true"
369
- when 'wink'
370
- SDN::Message::Wink.new(addr) if value == "true"
371
- when 'reset'
372
- next unless SDN::Message::SetFactoryDefault::RESET.keys.include?(value.to_sym)
373
- SDN::Message::SetFactoryDefault.new(addr, value.to_sym)
374
- when 'positionpulses', 'positionpercent', 'ip'
375
- SDN::Message::MoveTo.new(addr, property.sub('position', 'position_').to_sym, value.to_i)
376
- when 'direction'
377
- next if is_group
378
- follow_up = SDN::Message::GetMotorDirection.new(addr)
379
- next unless %w{standard reversed}.include?(value)
380
- SDN::Message::SetMotorDirection.new(addr, value.to_sym)
381
- when 'uplimit', 'downlimit'
382
- next if is_group
383
- if %w{delete current_position jog_ms jog_pulses}.include?(value)
384
- type = value.to_sym
385
- value = 10
386
- else
387
- type = :specified_position
388
- end
389
- target = property == 'uplimit' ? :up : :down
390
- follow_up = SDN::Message::GetMotorLimits.new(addr)
391
- SDN::Message::SetMotorLimits.new(addr, type, target, value.to_i)
392
- when /^ip\d(?:pulses|percent)$/
393
- next if is_group
394
- ip = match[:ip].to_i
395
- next unless (1..16).include?(ip)
396
- follow_up = SDN::Message::GetMotorIP.new(addr, ip)
397
- type = if value == 'delete'
398
- :delete
399
- elsif value == 'current_position'
400
- :current_position
401
- elsif match[:ip_type] == 'pulses'
402
- :position_pulses
403
- else
404
- :position_percent
405
- end
406
- SDN::Message::SetMotorIP.new(addr, type, ip, value.to_i)
407
- when 'upspeed', 'downspeed', 'slowspeed'
408
- next if is_group
409
- next unless motor
410
- follow_up = SDN::Message::GetMotorRollingSpeed.new(addr)
411
- message = SDN::Message::SetMotorRollingSpeed.new(addr,
412
- up_speed: motor.up_speed,
413
- down_speed: motor.down_speed,
414
- slow_speed: motor.slow_speed)
415
- message.send(:"#{property.sub('speed', '')}_speed=", value.to_i)
416
- message
417
- when 'groups'
418
- next if is_group
419
- next unless motor
420
- messages = motor.set_groups(value)
421
- @mutex.synchronize do
422
- messages.each { |m| @queues[0].push(MessageAndRetries.new(m, 5, 0)) }
423
- @cond.signal
424
- end
425
- nil
426
- end
427
-
428
- if motor
429
- motor.last_action = message.class if [Message::MoveTo, Message::Move, Message::Wink, Message::Stop].include?(message.class)
430
- end
431
-
432
- if message
433
- message.ack_requested = true if message.class.name !~ /^SDN::Message::Get/
434
- @mutex.synchronize do
435
- @queues[0].push(MessageAndRetries.new(message, 5, 0))
436
- @queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
437
- @cond.signal
438
- end
439
- end
440
- end
441
- end
442
- end
443
-
444
- def publish(topic, value)
445
- @mqtt.publish("#{@base_topic}/#{topic}", value, true)
446
- end
447
-
448
- def subscribe(topic)
449
- @mqtt.subscribe("#{@base_topic}/#{topic}")
450
- end
451
-
452
- def enqueue(message, queue = :command)
453
- @mutex.synchronize do
454
- queue = instance_variable_get(:"#{@queue}_queue")
455
- unless queue.include?(message)
456
- queue.push(message)
457
- @cond.signal
458
- end
459
- end
460
- end
461
-
462
- def publish_basic_attributes
463
- publish("$homie", "v4.0.0")
464
- publish("$name", "Somfy SDN Network")
465
- publish("$state", "init")
466
- publish("$nodes", "discovery")
467
-
468
- publish("discovery/$name", "Discovery Node")
469
- publish("discovery/$type", "sdn")
470
- publish("discovery/$properties", "discover")
471
-
472
- publish("discovery/discover/$name", "Trigger Motor Discovery")
473
- publish("discovery/discover/$datatype", "boolean")
474
- publish("discovery/discover/$settable", "true")
475
- publish("discovery/discover/$retained", "false")
476
-
477
- subscribe("+/discover/set")
478
- subscribe("+/label/set")
479
- subscribe("+/down/set")
480
- subscribe("+/up/set")
481
- subscribe("+/stop/set")
482
- subscribe("+/positionpulses/set")
483
- subscribe("+/positionpercent/set")
484
- subscribe("+/ip/set")
485
- subscribe("+/wink/set")
486
- subscribe("+/reset/set")
487
- subscribe("+/direction/set")
488
- subscribe("+/upspeed/set")
489
- subscribe("+/downspeed/set")
490
- subscribe("+/slowspeed/set")
491
- subscribe("+/uplimit/set")
492
- subscribe("+/downlimit/set")
493
- subscribe("+/groups/set")
494
- (1..16).each do |ip|
495
- subscribe("+/ip#{ip}pulses/set")
496
- subscribe("+/ip#{ip}percent/set")
497
- end
498
-
499
- publish("$state", "ready")
500
- end
501
-
502
- def publish_motor(addr)
503
- publish("#{addr}/$name", addr)
504
- publish("#{addr}/$type", "Sonesse 30 Motor")
505
- publish("#{addr}/$properties", "discover,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")
506
-
507
- publish("#{addr}/discover/$name", "Trigger Motor Discovery")
508
- publish("#{addr}/discover/$datatype", "boolean")
509
- publish("#{addr}/discover/$settable", "true")
510
- publish("#{addr}/discover/$retained", "false")
511
-
512
- publish("#{addr}/label/$name", "Node label")
513
- publish("#{addr}/label/$datatype", "string")
514
- publish("#{addr}/label/$settable", "true")
515
-
516
- publish("#{addr}/down/$name", "Move in down direction")
517
- publish("#{addr}/down/$datatype", "boolean")
518
- publish("#{addr}/down/$settable", "true")
519
- publish("#{addr}/down/$retained", "false")
520
-
521
- publish("#{addr}/up/$name", "Move in up direction")
522
- publish("#{addr}/up/$datatype", "boolean")
523
- publish("#{addr}/up/$settable", "true")
524
- publish("#{addr}/up/$retained", "false")
525
-
526
- publish("#{addr}/stop/$name", "Cancel adjustments")
527
- publish("#{addr}/stop/$datatype", "boolean")
528
- publish("#{addr}/stop/$settable", "true")
529
- publish("#{addr}/stop/$retained", "false")
530
-
531
- publish("#{addr}/positionpulses/$name", "Position from up limit (in pulses)")
532
- publish("#{addr}/positionpulses/$datatype", "integer")
533
- publish("#{addr}/positionpulses/$format", "0:65535")
534
- publish("#{addr}/positionpulses/$unit", "pulses")
535
- publish("#{addr}/positionpulses/$settable", "true")
536
-
537
- publish("#{addr}/positionpercent/$name", "Position (in %)")
538
- publish("#{addr}/positionpercent/$datatype", "integer")
539
- publish("#{addr}/positionpercent/$format", "0:100")
540
- publish("#{addr}/positionpercent/$unit", "%")
541
- publish("#{addr}/positionpercent/$settable", "true")
542
-
543
- publish("#{addr}/ip/$name", "Intermediate Position")
544
- publish("#{addr}/ip/$datatype", "integer")
545
- publish("#{addr}/ip/$format", "1:16")
546
- publish("#{addr}/ip/$settable", "true")
547
-
548
- publish("#{addr}/wink/$name", "Feedback")
549
- publish("#{addr}/wink/$datatype", "boolean")
550
- publish("#{addr}/wink/$settable", "true")
551
- publish("#{addr}/wink/$retained", "false")
552
-
553
- publish("#{addr}/reset/$name", "Recall factory settings")
554
- publish("#{addr}/reset/$datatype", "enum")
555
- publish("#{addr}/reset/$format", SDN::Message::SetFactoryDefault::RESET.keys.join(','))
556
- publish("#{addr}/reset/$settable", "true")
557
- publish("#{addr}/reset/$retained", "false")
558
-
559
- publish("#{addr}/state/$name", "State of the motor")
560
- publish("#{addr}/state/$datatype", "enum")
561
- publish("#{addr}/state/$format", SDN::Message::PostMotorStatus::STATE.keys.join(','))
562
-
563
- publish("#{addr}/last_direction/$name", "Direction of last motion")
564
- publish("#{addr}/last_direction/$datatype", "enum")
565
- publish("#{addr}/last_direction/$format", SDN::Message::PostMotorStatus::DIRECTION.keys.join(','))
566
-
567
- publish("#{addr}/last_action_source/$name", "Source of last action")
568
- publish("#{addr}/last_action_source/$datatype", "enum")
569
- publish("#{addr}/last_action_source/$format", SDN::Message::PostMotorStatus::SOURCE.keys.join(','))
570
-
571
- publish("#{addr}/last_action_cause/$name", "Cause of last action")
572
- publish("#{addr}/last_action_cause/$datatype", "enum")
573
- publish("#{addr}/last_action_cause/$format", SDN::Message::PostMotorStatus::CAUSE.keys.join(','))
574
-
575
- publish("#{addr}/uplimit/$name", "Up limit (always = 0)")
576
- publish("#{addr}/uplimit/$datatype", "integer")
577
- publish("#{addr}/uplimit/$format", "0:65535")
578
- publish("#{addr}/uplimit/$unit", "pulses")
579
- publish("#{addr}/uplimit/$settable", "true")
580
-
581
- publish("#{addr}/downlimit/$name", "Down limit")
582
- publish("#{addr}/downlimit/$datatype", "integer")
583
- publish("#{addr}/downlimit/$format", "0:65535")
584
- publish("#{addr}/downlimit/$unit", "pulses")
585
- publish("#{addr}/downlimit/$settable", "true")
586
-
587
- publish("#{addr}/direction/$name", "Motor rotation direction")
588
- publish("#{addr}/direction/$datatype", "enum")
589
- publish("#{addr}/direction/$format", "standard,reversed")
590
- publish("#{addr}/direction/$settable", "true")
591
-
592
- publish("#{addr}/upspeed/$name", "Up speed")
593
- publish("#{addr}/upspeed/$datatype", "integer")
594
- publish("#{addr}/upspeed/$format", "6:28")
595
- publish("#{addr}/upspeed/$unit", "RPM")
596
- publish("#{addr}/upspeed/$settable", "true")
597
-
598
- publish("#{addr}/downspeed/$name", "Down speed, always = Up speed")
599
- publish("#{addr}/downspeed/$datatype", "integer")
600
- publish("#{addr}/downspeed/$format", "6:28")
601
- publish("#{addr}/downspeed/$unit", "RPM")
602
- publish("#{addr}/downspeed/$settable", "true")
603
-
604
- publish("#{addr}/slowspeed/$name", "Slow speed")
605
- publish("#{addr}/slowspeed/$datatype", "integer")
606
- publish("#{addr}/slowspeed/$format", "6:28")
607
- publish("#{addr}/slowspeed/$unit", "RPM")
608
- publish("#{addr}/slowspeed/$settable", "true")
609
-
610
- publish("#{addr}/groups/$name", "Group Memberships")
611
- publish("#{addr}/groups/$datatype", "string")
612
- publish("#{addr}/groups/$settable", "true")
613
-
614
- (1..16).each do |ip|
615
- publish("#{addr}/ip#{ip}pulses/$name", "Intermediate Position #{ip}")
616
- publish("#{addr}/ip#{ip}pulses/$datatype", "integer")
617
- publish("#{addr}/ip#{ip}pulses/$format", "0:65535")
618
- publish("#{addr}/ip#{ip}pulses/$unit", "pulses")
619
- publish("#{addr}/ip#{ip}pulses/$settable", "true")
620
-
621
- publish("#{addr}/ip#{ip}percent/$name", "Intermediate Position #{ip}")
622
- publish("#{addr}/ip#{ip}percent/$datatype", "integer")
623
- publish("#{addr}/ip#{ip}percent/$format", "0:100")
624
- publish("#{addr}/ip#{ip}percent/$unit", "%")
625
- publish("#{addr}/ip#{ip}percent/$settable", "true")
626
- end
627
-
628
- motor = Motor.new(self, addr)
629
- @motors[addr] = motor
630
- publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
631
-
632
- sdn_addr = SDN::Message.parse_address(addr)
633
- @mutex.synchronize do
634
- @queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeLabel.new(sdn_addr), 5, 2))
635
- @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorStatus.new(sdn_addr), 5, 2))
636
- @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorLimits.new(sdn_addr), 5, 2))
637
- @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorDirection.new(sdn_addr), 5, 2))
638
- @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorRollingSpeed.new(sdn_addr), 5, 2))
639
- (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
640
- (0...16).each { |g| @queues[2].push(MessageAndRetries.new(SDN::Message::GetGroupAddr.new(sdn_addr, g), 5, 2)) }
641
-
642
- @cond.signal
643
- end
644
-
645
- motor
646
- end
647
-
648
- def add_group(addr)
649
- addr = addr.gsub('.', '')
650
- group = @groups[addr]
651
- return group if group
652
-
653
- publish("#{addr}/$name", addr)
654
- publish("#{addr}/$type", "Shade Group")
655
- publish("#{addr}/$properties", "discover,down,up,stop,positionpulses,positionpercent,ip,wink,reset,state,motors")
656
-
657
- publish("#{addr}/discover/$name", "Trigger Motor Discovery")
658
- publish("#{addr}/discover/$datatype", "boolean")
659
- publish("#{addr}/discover/$settable", "true")
660
- publish("#{addr}/discover/$retained", "false")
661
-
662
- publish("#{addr}/down/$name", "Move in down direction")
663
- publish("#{addr}/down/$datatype", "boolean")
664
- publish("#{addr}/down/$settable", "true")
665
- publish("#{addr}/down/$retained", "false")
666
-
667
- publish("#{addr}/up/$name", "Move in up direction")
668
- publish("#{addr}/up/$datatype", "boolean")
669
- publish("#{addr}/up/$settable", "true")
670
- publish("#{addr}/up/$retained", "false")
671
-
672
- publish("#{addr}/stop/$name", "Cancel adjustments")
673
- publish("#{addr}/stop/$datatype", "boolean")
674
- publish("#{addr}/stop/$settable", "true")
675
- publish("#{addr}/stop/$retained", "false")
676
-
677
- publish("#{addr}/positionpulses/$name", "Position from up limit (in pulses)")
678
- publish("#{addr}/positionpulses/$datatype", "integer")
679
- publish("#{addr}/positionpulses/$format", "0:65535")
680
- publish("#{addr}/positionpulses/$unit", "pulses")
681
- publish("#{addr}/positionpulses/$settable", "true")
682
-
683
- publish("#{addr}/positionpercent/$name", "Position (in %)")
684
- publish("#{addr}/positionpercent/$datatype", "integer")
685
- publish("#{addr}/positionpercent/$format", "0:100")
686
- publish("#{addr}/positionpercent/$unit", "%")
687
- publish("#{addr}/positionpercent/$settable", "true")
688
-
689
- publish("#{addr}/ip/$name", "Intermediate Position")
690
- publish("#{addr}/ip/$datatype", "integer")
691
- publish("#{addr}/ip/$format", "1:16")
692
- publish("#{addr}/ip/$settable", "true")
693
-
694
- publish("#{addr}/wink/$name", "Feedback")
695
- publish("#{addr}/wink/$datatype", "boolean")
696
- publish("#{addr}/wink/$settable", "true")
697
- publish("#{addr}/wink/$retained", "false")
698
-
699
- publish("#{addr}/state/$name", "State of the motors; only set if all motors are in the same state")
700
- publish("#{addr}/state/$datatype", "enum")
701
- publish("#{addr}/state/$format", SDN::Message::PostMotorStatus::STATE.keys.join(','))
702
-
703
- publish("#{addr}/motors/$name", "Motors that are members of this group")
704
- publish("#{addr}/motors/$datatype", "string")
705
-
706
- group = @groups[addr] = Group.new(self, addr)
707
- publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
708
- group
709
- end
710
- end
711
- end