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.
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