somfy_sdn 1.0.7 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/sdn.rb +6 -0
- data/lib/sdn/message.rb +23 -6
- data/lib/sdn/messages/control.rb +3 -3
- data/lib/sdn/messages/ilt2/get.rb +9 -0
- data/lib/sdn/messages/ilt2/post.rb +18 -0
- data/lib/sdn/messages/ilt2/set.rb +59 -0
- data/lib/sdn/mqtt_bridge.rb +148 -91
- data/lib/sdn/version.rb +1 -1
- data/lib/somfy_sdn.rb +1 -2
- metadata +16 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b2c2926c376002fa9632188a5d467c64c2987e48f892975a1a725ec71f38d021
|
4
|
+
data.tar.gz: dcafff75ded7c5e5cccc65912179dd6fbf0c33f36655cf29adb6e7e4d796b7a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6fc1a34073e8069bd778575a1e5f8b3531b2bf5a769f3010d2ca93d4c4a2af07af0e26c7e1bb7a7d094d729e41dfb7031a5d569294d285fc82ab5f2b93afeebe
|
7
|
+
data.tar.gz: b61b3e69a01b4b0d709fa381ce0113184f6f5d32227ce3f72eee67c74bcf5e83ff8daa813ee69ef36e9fe76ffa9bc50c9d28eb76c403f1c1db370327ad505155
|
data/lib/sdn.rb
ADDED
data/lib/sdn/message.rb
CHANGED
@@ -3,15 +3,23 @@ require 'sdn/messages/helpers'
|
|
3
3
|
module SDN
|
4
4
|
class MalformedMessage < RuntimeError; end
|
5
5
|
|
6
|
-
class Message
|
6
|
+
class Message
|
7
7
|
class << self
|
8
|
+
def expected_response
|
9
|
+
if name =~ /::Get([A-Za-z]+)/
|
10
|
+
const_get("Post#{$1}", true)
|
11
|
+
else
|
12
|
+
Ack
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
8
16
|
def parse(data)
|
9
17
|
offset = -1
|
10
18
|
msg = length = ack_requested = message_class = nil
|
11
19
|
# we loop here scanning for a valid message
|
12
20
|
loop do
|
13
21
|
offset += 1
|
14
|
-
return nil if data.length - offset < 11
|
22
|
+
return [nil, 0] if data.length - offset < 11
|
15
23
|
msg = to_number(data[offset])
|
16
24
|
length = to_number(data[offset + 1])
|
17
25
|
ack_requested = length & 0x80 == 0x80
|
@@ -39,9 +47,14 @@ module SDN
|
|
39
47
|
reserved = to_number(data[offset + 2])
|
40
48
|
src = transform_param(data.slice(offset + 3, 3))
|
41
49
|
dest = transform_param(data.slice(offset + 6, 3))
|
42
|
-
|
43
|
-
|
44
|
-
|
50
|
+
begin
|
51
|
+
result = message_class.new(reserved: reserved, ack_requested: ack_requested, src: src, dest: dest)
|
52
|
+
result.parse(data.slice(offset + 9, length - 11))
|
53
|
+
result.msg = msg if message_class == UnknownMessage
|
54
|
+
rescue ArgumentError => e
|
55
|
+
puts "discarding illegal message #{e}"
|
56
|
+
result = nil
|
57
|
+
end
|
45
58
|
[result, offset + length]
|
46
59
|
end
|
47
60
|
end
|
@@ -50,6 +63,7 @@ module SDN
|
|
50
63
|
singleton_class.include Helpers
|
51
64
|
|
52
65
|
attr_reader :reserved, :ack_requested, :src, :dest
|
66
|
+
attr_writer :ack_requested
|
53
67
|
|
54
68
|
def initialize(reserved: nil, ack_requested: false, src: nil, dest: nil)
|
55
69
|
@reserved = reserved || 0x02 # message sent to Sonesse 30
|
@@ -72,10 +86,13 @@ module SDN
|
|
72
86
|
length |= 0x80 if ack_requested
|
73
87
|
result = transform_param(self.class.const_get(:MSG)) + transform_param(length) + result
|
74
88
|
result.concat(checksum(result))
|
75
|
-
puts "wrote #{result.map { |b| '%02x' % b }.join(' ')}"
|
76
89
|
result.pack("C*")
|
77
90
|
end
|
78
91
|
|
92
|
+
def ==(other)
|
93
|
+
self.serialize == other.serialize
|
94
|
+
end
|
95
|
+
|
79
96
|
def inspect
|
80
97
|
"#<%s @reserved=%02xh, @ack_requested=%s, @src=%s, @dest=%s%s>" % [self.class.name, reserved, ack_requested, print_address(src), print_address(dest), class_inspect]
|
81
98
|
end
|
data/lib/sdn/messages/control.rb
CHANGED
@@ -27,17 +27,17 @@ module SDN
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def direction=(value)
|
30
|
-
raise ArgumentError, "direction must be one of :down, :up, or :cancel" unless DIRECTION.keys.include?(value)
|
30
|
+
raise ArgumentError, "direction must be one of :down, :up, or :cancel (#{value})" unless DIRECTION.keys.include?(value)
|
31
31
|
@direction = value
|
32
32
|
end
|
33
33
|
|
34
34
|
def duration=(value)
|
35
|
-
raise ArgumentError, "duration must be in range 0x0a to 0xff" unless value
|
35
|
+
raise ArgumentError, "duration must be in range 0x0a to 0xff (#{value})" unless value && value >= 0x0a && value <= 0xff
|
36
36
|
@duration = value
|
37
37
|
end
|
38
38
|
|
39
39
|
def speed=(value)
|
40
|
-
raise ArgumentError, "speed must be one of :up, :down, or :slow" unless SPEED.keys.include?(value)
|
40
|
+
raise ArgumentError, "speed must be one of :up, :down, or :slow (#{value})" unless SPEED.keys.include?(value)
|
41
41
|
@speed = speed
|
42
42
|
end
|
43
43
|
|
@@ -0,0 +1,18 @@
|
|
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
|
@@ -0,0 +1,59 @@
|
|
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
|
data/lib/sdn/mqtt_bridge.rb
CHANGED
@@ -3,7 +3,9 @@ require 'uri'
|
|
3
3
|
require 'set'
|
4
4
|
|
5
5
|
module SDN
|
6
|
-
|
6
|
+
MessageAndRetries = Struct.new(:message, :remaining_retries, :priority)
|
7
|
+
|
8
|
+
Group = Struct.new(:bridge, :addr, :positionpercent, :state, :motors) do
|
7
9
|
def initialize(*)
|
8
10
|
members.each { |k| self[k] = :nil }
|
9
11
|
super
|
@@ -20,9 +22,13 @@ module SDN
|
|
20
22
|
Message.print_address(Message.parse_address(addr))
|
21
23
|
end
|
22
24
|
|
23
|
-
def
|
25
|
+
def motor_objects
|
24
26
|
bridge.motors.select { |addr, motor| motor.groups_string.include?(printed_addr) }.values
|
25
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
|
26
32
|
end
|
27
33
|
|
28
34
|
Motor = Struct.new(:bridge,
|
@@ -73,49 +79,51 @@ module SDN
|
|
73
79
|
:ip15percent,
|
74
80
|
:ip16pulses,
|
75
81
|
:ip16percent,
|
76
|
-
:groups
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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('.', '')] }
|
118
125
|
end
|
126
|
+
end
|
119
127
|
|
120
128
|
class MQTTBridge
|
121
129
|
WAIT_TIME = 0.25
|
@@ -134,8 +142,7 @@ module SDN
|
|
134
142
|
|
135
143
|
@mutex = Mutex.new
|
136
144
|
@cond = ConditionVariable.new
|
137
|
-
@
|
138
|
-
@request_queue = []
|
145
|
+
@queues = [[], [], []]
|
139
146
|
@response_pending = false
|
140
147
|
@broadcast_pending = false
|
141
148
|
|
@@ -152,20 +159,23 @@ module SDN
|
|
152
159
|
'baud' => 4800,
|
153
160
|
'parity' => Net::Telnet::RFC2217::ODD)
|
154
161
|
else
|
155
|
-
require 'serialport'
|
156
|
-
@sdn = SerialPort.
|
162
|
+
require 'ccutrer-serialport'
|
163
|
+
@sdn = CCutrer::SerialPort.new(port, baud: 4800, parity: :odd)
|
157
164
|
end
|
158
165
|
|
159
166
|
read_thread = Thread.new do
|
160
167
|
buffer = ""
|
168
|
+
|
161
169
|
loop do
|
162
170
|
begin
|
163
171
|
message, bytes_read = SDN::Message.parse(buffer.bytes)
|
172
|
+
# discard how much we read
|
173
|
+
buffer = buffer[bytes_read..-1]
|
164
174
|
unless message
|
165
175
|
begin
|
166
176
|
buffer.concat(@sdn.read_nonblock(64 * 1024))
|
167
177
|
next
|
168
|
-
rescue IO::WaitReadable
|
178
|
+
rescue IO::WaitReadable, EOFError
|
169
179
|
wait = buffer.empty? ? nil : WAIT_TIME
|
170
180
|
if @sdn.wait_readable(wait).nil?
|
171
181
|
# timed out; just discard everything
|
@@ -175,8 +185,6 @@ module SDN
|
|
175
185
|
end
|
176
186
|
next
|
177
187
|
end
|
178
|
-
# discard how much we read
|
179
|
-
buffer = buffer[bytes_read..-1]
|
180
188
|
|
181
189
|
src = SDN::Message.print_address(message.src)
|
182
190
|
# ignore the UAI Plus and ourselves
|
@@ -197,7 +205,7 @@ module SDN
|
|
197
205
|
motor.publish(:positionpulses, message.position_pulses)
|
198
206
|
motor.publish(:ip, message.ip)
|
199
207
|
motor.group_objects.each do |group|
|
200
|
-
positions = group.
|
208
|
+
positions = group.motor_objects.map(&:positionpercent)
|
201
209
|
position = nil
|
202
210
|
# calculate an average, but only if we know a position for
|
203
211
|
# every shade
|
@@ -208,7 +216,12 @@ module SDN
|
|
208
216
|
group.publish(:positionpercent, position)
|
209
217
|
end
|
210
218
|
when SDN::Message::PostMotorStatus
|
211
|
-
if message.state == :running || motor.state == :running
|
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?))
|
212
225
|
follow_ups << SDN::Message::GetMotorStatus.new(message.src)
|
213
226
|
end
|
214
227
|
# this will do one more position request after it stopped
|
@@ -218,7 +231,7 @@ module SDN
|
|
218
231
|
motor.publish(:last_action_source, message.last_action_source)
|
219
232
|
motor.publish(:last_action_cause, message.last_action_cause)
|
220
233
|
motor.group_objects.each do |group|
|
221
|
-
states = group.
|
234
|
+
states = group.motor_objects.map(&:state).uniq
|
222
235
|
state = states.length == 1 ? states.first : 'mixed'
|
223
236
|
group.publish(:state, state)
|
224
237
|
end
|
@@ -232,16 +245,19 @@ module SDN
|
|
232
245
|
motor.publish(:downspeed, message.down_speed)
|
233
246
|
motor.publish(:slowspeed, message.slow_speed)
|
234
247
|
when SDN::Message::PostMotorIP
|
235
|
-
motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
|
248
|
+
motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
|
236
249
|
motor.publish(:"ip#{message.ip}percent", message.position_percent)
|
237
250
|
when SDN::Message::PostGroupAddr
|
238
251
|
motor.add_group(message.group_index, message.group_address)
|
239
252
|
end
|
240
253
|
|
241
254
|
@mutex.synchronize do
|
242
|
-
|
243
|
-
|
244
|
-
@
|
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
|
245
261
|
@cond.signal if signal
|
246
262
|
end
|
247
263
|
rescue EOFError
|
@@ -258,7 +274,7 @@ module SDN
|
|
258
274
|
write_thread = Thread.new do
|
259
275
|
begin
|
260
276
|
loop do
|
261
|
-
|
277
|
+
message_and_retries = nil
|
262
278
|
@mutex.synchronize do
|
263
279
|
# got woken up early by another command getting queued; spin
|
264
280
|
if @response_pending
|
@@ -268,33 +284,48 @@ module SDN
|
|
268
284
|
puts "timed out waiting on response"
|
269
285
|
@response_pending = nil
|
270
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
|
271
292
|
else
|
272
293
|
@cond.wait(@mutex, remaining_wait)
|
273
294
|
end
|
274
295
|
end
|
275
296
|
else
|
297
|
+
# minimum time between messages
|
276
298
|
sleep 0.1
|
277
299
|
end
|
278
300
|
|
279
|
-
|
280
|
-
|
281
|
-
message
|
282
|
-
if message
|
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/
|
283
304
|
@response_pending = Time.now.to_f + WAIT_TIME
|
284
|
-
if message.dest ==
|
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)
|
285
306
|
@broadcast_pending = Time.now.to_f + BROADCAST_WAIT
|
286
|
-
end
|
307
|
+
end
|
287
308
|
end
|
288
309
|
end
|
289
310
|
|
290
|
-
#
|
291
|
-
@
|
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
|
292
320
|
end
|
293
|
-
next unless
|
321
|
+
next unless message_and_retries
|
294
322
|
|
323
|
+
message = message_and_retries.message
|
295
324
|
puts "writing #{message.inspect}"
|
296
|
-
|
297
|
-
@sdn.
|
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(' ')}"
|
298
329
|
end
|
299
330
|
rescue => e
|
300
331
|
puts "failure writing: #{e}"
|
@@ -307,10 +338,10 @@ module SDN
|
|
307
338
|
if topic == "#{@base_topic}/discovery/discover/set" && value == "true"
|
308
339
|
# trigger discovery
|
309
340
|
@mutex.synchronize do
|
310
|
-
@
|
341
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeAddr.new, 1, 2))
|
311
342
|
@cond.signal
|
312
343
|
end
|
313
|
-
elsif (match = topic.match(%r{^#{Regexp.escape(@base_topic)}/(?<addr>\h{6})/(?<property>label|down|up|stop|positionpulses|positionpercent|ip|wink|reset|(?<speed_type>upspeed|downspeed|slowspeed)|uplimit|downlimit|direction|ip(?<ip>\d+)(?<ip_type>pulses|percent)|groups)/set$}))
|
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$}))
|
314
345
|
addr = SDN::Message.parse_address(match[:addr])
|
315
346
|
property = match[:property]
|
316
347
|
# not homie compliant; allows linking the positionpercent property
|
@@ -319,10 +350,15 @@ module SDN
|
|
319
350
|
property = value.downcase
|
320
351
|
value = "true"
|
321
352
|
end
|
322
|
-
|
353
|
+
mqtt_addr = SDN::Message.print_address(addr).gsub('.', '')
|
354
|
+
motor = @motors[mqtt_addr]
|
323
355
|
is_group = SDN::Message.is_group_address?(addr)
|
356
|
+
group = @groups[mqtt_addr]
|
324
357
|
follow_up = SDN::Message::GetMotorStatus.new(addr)
|
325
358
|
message = case property
|
359
|
+
when 'discover'
|
360
|
+
follow_up = nil
|
361
|
+
SDN::Message::GetNodeAddr.new(addr) if value == "true"
|
326
362
|
when 'label'
|
327
363
|
follow_up = SDN::Message::GetNodeLabel.new(addr)
|
328
364
|
SDN::Message::SetNodeLabel.new(addr, value) unless is_group
|
@@ -383,15 +419,21 @@ module SDN
|
|
383
419
|
next unless motor
|
384
420
|
messages = motor.set_groups(value)
|
385
421
|
@mutex.synchronize do
|
386
|
-
messages.each { |m| @
|
422
|
+
messages.each { |m| @queues[0].push(MessageAndRetries.new(m, 5, 0)) }
|
387
423
|
@cond.signal
|
388
424
|
end
|
389
425
|
nil
|
390
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
|
+
|
391
432
|
if message
|
433
|
+
message.ack_requested = true if message.class.name !~ /^SDN::Message::Get/
|
392
434
|
@mutex.synchronize do
|
393
|
-
@
|
394
|
-
@
|
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 }
|
395
437
|
@cond.signal
|
396
438
|
end
|
397
439
|
end
|
@@ -432,7 +474,7 @@ module SDN
|
|
432
474
|
publish("discovery/discover/$settable", "true")
|
433
475
|
publish("discovery/discover/$retained", "false")
|
434
476
|
|
435
|
-
subscribe("
|
477
|
+
subscribe("+/discover/set")
|
436
478
|
subscribe("+/label/set")
|
437
479
|
subscribe("+/down/set")
|
438
480
|
subscribe("+/up/set")
|
@@ -460,7 +502,12 @@ module SDN
|
|
460
502
|
def publish_motor(addr)
|
461
503
|
publish("#{addr}/$name", addr)
|
462
504
|
publish("#{addr}/$type", "Sonesse 30 Motor")
|
463
|
-
publish("#{addr}/$properties", "label,down,up,stop,positionpulses,positionpercent,ip,wink,reset,state,last_direction,last_action_source,last_action_cause,uplimit,downlimit,direction,upspeed,downspeed,slowspeed,#{(1..16).map { |ip| "ip#{ip}pulses,ip#{ip}percent" }.join(',')},groups")
|
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")
|
464
511
|
|
465
512
|
publish("#{addr}/label/$name", "Node label")
|
466
513
|
publish("#{addr}/label/$datatype", "string")
|
@@ -580,17 +627,17 @@ module SDN
|
|
580
627
|
|
581
628
|
motor = Motor.new(self, addr)
|
582
629
|
@motors[addr] = motor
|
583
|
-
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort
|
630
|
+
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
|
584
631
|
|
585
632
|
sdn_addr = SDN::Message.parse_address(addr)
|
586
633
|
@mutex.synchronize do
|
587
|
-
@
|
588
|
-
@
|
589
|
-
@
|
590
|
-
@
|
591
|
-
@
|
592
|
-
(1..16).each { |ip| @
|
593
|
-
(0...16).each { |g| @
|
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)) }
|
594
641
|
|
595
642
|
@cond.signal
|
596
643
|
end
|
@@ -600,11 +647,17 @@ module SDN
|
|
600
647
|
|
601
648
|
def add_group(addr)
|
602
649
|
addr = addr.gsub('.', '')
|
603
|
-
|
650
|
+
group = @groups[addr]
|
651
|
+
return group if group
|
604
652
|
|
605
653
|
publish("#{addr}/$name", addr)
|
606
654
|
publish("#{addr}/$type", "Shade Group")
|
607
|
-
publish("#{addr}/$properties", "down,up,stop,positionpulses,positionpercent,ip,wink,reset")
|
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")
|
608
661
|
|
609
662
|
publish("#{addr}/down/$name", "Move in down direction")
|
610
663
|
publish("#{addr}/down/$datatype", "boolean")
|
@@ -619,7 +672,7 @@ module SDN
|
|
619
672
|
publish("#{addr}/stop/$name", "Cancel adjustments")
|
620
673
|
publish("#{addr}/stop/$datatype", "boolean")
|
621
674
|
publish("#{addr}/stop/$settable", "true")
|
622
|
-
publish("#{addr}/stop/$retained", "false")
|
675
|
+
publish("#{addr}/stop/$retained", "false")
|
623
676
|
|
624
677
|
publish("#{addr}/positionpulses/$name", "Position from up limit (in pulses)")
|
625
678
|
publish("#{addr}/positionpulses/$datatype", "integer")
|
@@ -647,8 +700,12 @@ module SDN
|
|
647
700
|
publish("#{addr}/state/$datatype", "enum")
|
648
701
|
publish("#{addr}/state/$format", SDN::Message::PostMotorStatus::STATE.keys.join(','))
|
649
702
|
|
650
|
-
|
651
|
-
publish("
|
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
|
652
709
|
end
|
653
710
|
end
|
654
711
|
end
|
data/lib/sdn/version.rb
CHANGED
data/lib/somfy_sdn.rb
CHANGED
@@ -1,2 +1 @@
|
|
1
|
-
require 'sdn
|
2
|
-
require 'sdn/mqtt_bridge'
|
1
|
+
require 'sdn'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: somfy_sdn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.12
|
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-
|
11
|
+
date: 2020-11-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mqtt
|
@@ -30,28 +30,28 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.0.
|
33
|
+
version: 0.0.3
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 0.0.
|
40
|
+
version: 0.0.3
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name: serialport
|
42
|
+
name: ccutrer-serialport
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 1.
|
47
|
+
version: 1.0.0
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 1.
|
54
|
+
version: 1.0.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: byebug
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,7 +80,7 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '13.0'
|
83
|
-
description:
|
83
|
+
description:
|
84
84
|
email: cody@cutrer.com'
|
85
85
|
executables:
|
86
86
|
- sdn_mqtt_bridge
|
@@ -88,10 +88,14 @@ extensions: []
|
|
88
88
|
extra_rdoc_files: []
|
89
89
|
files:
|
90
90
|
- bin/sdn_mqtt_bridge
|
91
|
+
- lib/sdn.rb
|
91
92
|
- lib/sdn/message.rb
|
92
93
|
- lib/sdn/messages/control.rb
|
93
94
|
- lib/sdn/messages/get.rb
|
94
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
|
95
99
|
- lib/sdn/messages/post.rb
|
96
100
|
- lib/sdn/messages/set.rb
|
97
101
|
- lib/sdn/mqtt_bridge.rb
|
@@ -101,7 +105,7 @@ homepage: https://github.com/ccutrer/somfy_sdn
|
|
101
105
|
licenses:
|
102
106
|
- MIT
|
103
107
|
metadata: {}
|
104
|
-
post_install_message:
|
108
|
+
post_install_message:
|
105
109
|
rdoc_options: []
|
106
110
|
require_paths:
|
107
111
|
- lib
|
@@ -116,8 +120,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
120
|
- !ruby/object:Gem::Version
|
117
121
|
version: '0'
|
118
122
|
requirements: []
|
119
|
-
rubygems_version: 3.
|
120
|
-
signing_key:
|
123
|
+
rubygems_version: 3.1.4
|
124
|
+
signing_key:
|
121
125
|
specification_version: 4
|
122
126
|
summary: Library for communication with Somfy SDN RS-485 motorized shades
|
123
127
|
test_files: []
|