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