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