somfy_sdn 1.0.5 → 1.0.10
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 +15 -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 +187 -81
- data/lib/sdn/version.rb +1 -1
- data/lib/somfy_sdn.rb +1 -2
- metadata +26 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 57ade06be997d5f36fa76aa13c87aa00e26b1bfe07de7a7fd29b9015f249a1a3
|
4
|
+
data.tar.gz: '03462586cabf999666ac43e83036811b8a10df15515ea17d7bfd7b4d2e421e3a'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '00080b23f47bd5c11c13406d818feec587c744b132cb1b1f4cbd57f3a64cd002c0747e4bccb575315e996b7ec7b92d736a5892116c31b277f85fcbf83fd36c6f'
|
7
|
+
data.tar.gz: '01700481ff1ed2966a00287c5c9f1193b600b56518ce36da8a06dae4768daa51fac0da4635993b9eb3daf6ea8a0f068f3e8fd7aee5bd9977e3350eb46aa58d6a'
|
data/lib/sdn.rb
ADDED
data/lib/sdn/message.rb
CHANGED
@@ -3,7 +3,7 @@ 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
8
|
def parse(data)
|
9
9
|
offset = -1
|
@@ -11,7 +11,7 @@ module SDN
|
|
11
11
|
# we loop here scanning for a valid message
|
12
12
|
loop do
|
13
13
|
offset += 1
|
14
|
-
return nil if data.length - offset < 11
|
14
|
+
return [nil, 0] if data.length - offset < 11
|
15
15
|
msg = to_number(data[offset])
|
16
16
|
length = to_number(data[offset + 1])
|
17
17
|
ack_requested = length & 0x80 == 0x80
|
@@ -39,9 +39,14 @@ module SDN
|
|
39
39
|
reserved = to_number(data[offset + 2])
|
40
40
|
src = transform_param(data.slice(offset + 3, 3))
|
41
41
|
dest = transform_param(data.slice(offset + 6, 3))
|
42
|
-
|
43
|
-
|
44
|
-
|
42
|
+
begin
|
43
|
+
result = message_class.new(reserved: reserved, ack_requested: ack_requested, src: src, dest: dest)
|
44
|
+
result.parse(data.slice(offset + 9, length - 11))
|
45
|
+
result.msg = msg if message_class == UnknownMessage
|
46
|
+
rescue ArgumentError => e
|
47
|
+
puts "discarding illegal message #{e}"
|
48
|
+
result = nil
|
49
|
+
end
|
45
50
|
[result, offset + length]
|
46
51
|
end
|
47
52
|
end
|
@@ -50,6 +55,7 @@ module SDN
|
|
50
55
|
singleton_class.include Helpers
|
51
56
|
|
52
57
|
attr_reader :reserved, :ack_requested, :src, :dest
|
58
|
+
attr_writer :ack_requested
|
53
59
|
|
54
60
|
def initialize(reserved: nil, ack_requested: false, src: nil, dest: nil)
|
55
61
|
@reserved = reserved || 0x02 # message sent to Sonesse 30
|
@@ -72,10 +78,13 @@ module SDN
|
|
72
78
|
length |= 0x80 if ack_requested
|
73
79
|
result = transform_param(self.class.const_get(:MSG)) + transform_param(length) + result
|
74
80
|
result.concat(checksum(result))
|
75
|
-
puts "wrote #{result.map { |b| '%02x' % b }.join(' ')}"
|
76
81
|
result.pack("C*")
|
77
82
|
end
|
78
83
|
|
84
|
+
def ==(other)
|
85
|
+
self.serialize == other.serialize
|
86
|
+
end
|
87
|
+
|
79
88
|
def inspect
|
80
89
|
"#<%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
90
|
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
@@ -1,10 +1,36 @@
|
|
1
1
|
require 'mqtt'
|
2
|
-
require 'serialport'
|
3
|
-
require 'socket'
|
4
2
|
require 'uri'
|
5
3
|
require 'set'
|
6
4
|
|
7
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
|
+
|
8
34
|
Motor = Struct.new(:bridge,
|
9
35
|
:addr,
|
10
36
|
:label,
|
@@ -54,49 +80,56 @@ module SDN
|
|
54
80
|
:ip16pulses,
|
55
81
|
:ip16percent,
|
56
82
|
:groups) do
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
94
125
|
end
|
95
126
|
|
96
127
|
class MQTTBridge
|
97
128
|
WAIT_TIME = 0.25
|
98
129
|
BROADCAST_WAIT = 5.0
|
99
130
|
|
131
|
+
attr_reader :motors, :groups
|
132
|
+
|
100
133
|
def initialize(mqtt_uri, port, device_id: "somfy", base_topic: "homie")
|
101
134
|
@base_topic = "#{base_topic}/#{device_id}"
|
102
135
|
@mqtt = MQTT::Client.new(mqtt_uri)
|
@@ -104,12 +137,11 @@ module SDN
|
|
104
137
|
@mqtt.connect
|
105
138
|
|
106
139
|
@motors = {}
|
107
|
-
@groups =
|
140
|
+
@groups = {}
|
108
141
|
|
109
142
|
@mutex = Mutex.new
|
110
143
|
@cond = ConditionVariable.new
|
111
|
-
@
|
112
|
-
@request_queue = []
|
144
|
+
@queues = [[], [], []]
|
113
145
|
@response_pending = false
|
114
146
|
@broadcast_pending = false
|
115
147
|
|
@@ -117,9 +149,17 @@ module SDN
|
|
117
149
|
|
118
150
|
uri = URI.parse(port)
|
119
151
|
if uri.scheme == "tcp"
|
152
|
+
require 'socket'
|
120
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)
|
121
160
|
else
|
122
|
-
|
161
|
+
require 'serialport'
|
162
|
+
@sdn = SerialPort.open(port, "baud" => 4800, "parity" => SerialPort::ODD)
|
123
163
|
end
|
124
164
|
|
125
165
|
read_thread = Thread.new do
|
@@ -127,13 +167,15 @@ module SDN
|
|
127
167
|
loop do
|
128
168
|
begin
|
129
169
|
message, bytes_read = SDN::Message.parse(buffer.bytes)
|
170
|
+
# discard how much we read
|
171
|
+
buffer = buffer[bytes_read..-1]
|
130
172
|
unless message
|
131
173
|
begin
|
132
|
-
buffer.concat(@sdn.read_nonblock(
|
174
|
+
buffer.concat(@sdn.read_nonblock(64 * 1024))
|
133
175
|
next
|
134
176
|
rescue IO::WaitReadable
|
135
177
|
wait = buffer.empty? ? nil : WAIT_TIME
|
136
|
-
if
|
178
|
+
if @sdn.wait_readable(wait).nil?
|
137
179
|
# timed out; just discard everything
|
138
180
|
puts "timed out reading; discarding buffer: #{buffer.unpack('H*').first}"
|
139
181
|
buffer = ""
|
@@ -141,8 +183,6 @@ module SDN
|
|
141
183
|
end
|
142
184
|
next
|
143
185
|
end
|
144
|
-
# discard how much we read
|
145
|
-
buffer = buffer[bytes_read..-1]
|
146
186
|
|
147
187
|
src = SDN::Message.print_address(message.src)
|
148
188
|
# ignore the UAI Plus and ourselves
|
@@ -162,6 +202,17 @@ module SDN
|
|
162
202
|
motor.publish(:positionpercent, message.position_percent)
|
163
203
|
motor.publish(:positionpulses, message.position_pulses)
|
164
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
|
165
216
|
when SDN::Message::PostMotorStatus
|
166
217
|
if message.state == :running || motor.state == :running
|
167
218
|
follow_ups << SDN::Message::GetMotorStatus.new(message.src)
|
@@ -172,6 +223,11 @@ module SDN
|
|
172
223
|
motor.publish(:last_direction, message.last_direction)
|
173
224
|
motor.publish(:last_action_source, message.last_action_source)
|
174
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
|
175
231
|
when SDN::Message::PostMotorLimits
|
176
232
|
motor.publish(:uplimit, message.up_limit)
|
177
233
|
motor.publish(:downlimit, message.down_limit)
|
@@ -182,7 +238,7 @@ module SDN
|
|
182
238
|
motor.publish(:downspeed, message.down_speed)
|
183
239
|
motor.publish(:slowspeed, message.slow_speed)
|
184
240
|
when SDN::Message::PostMotorIP
|
185
|
-
motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
|
241
|
+
motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
|
186
242
|
motor.publish(:"ip#{message.ip}percent", message.position_percent)
|
187
243
|
when SDN::Message::PostGroupAddr
|
188
244
|
motor.add_group(message.group_index, message.group_address)
|
@@ -191,9 +247,14 @@ module SDN
|
|
191
247
|
@mutex.synchronize do
|
192
248
|
signal = @response_pending || !follow_ups.empty?
|
193
249
|
@response_pending = @broadcast_pending
|
194
|
-
|
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
|
195
253
|
@cond.signal if signal
|
196
254
|
end
|
255
|
+
rescue EOFError
|
256
|
+
puts "EOF reading"
|
257
|
+
exit 2
|
197
258
|
rescue SDN::MalformedMessage => e
|
198
259
|
puts "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
|
199
260
|
rescue => e
|
@@ -205,43 +266,63 @@ module SDN
|
|
205
266
|
write_thread = Thread.new do
|
206
267
|
begin
|
207
268
|
loop do
|
208
|
-
|
269
|
+
message_and_retries = nil
|
209
270
|
@mutex.synchronize do
|
210
271
|
# got woken up early by another command getting queued; spin
|
211
272
|
if @response_pending
|
273
|
+
puts "another message queued, but we're still waiting"
|
212
274
|
while @response_pending
|
213
275
|
remaining_wait = @response_pending - Time.now.to_f
|
214
276
|
if remaining_wait < 0
|
215
277
|
puts "timed out waiting on response"
|
216
278
|
@response_pending = nil
|
217
279
|
@broadcast_pending = nil
|
280
|
+
if @prior_message&.remaining_retries != 0
|
281
|
+
puts "retrying #{@prior_message.remaining_retries} more times ..."
|
282
|
+
@queues[@prior_message.priority].push(@prior_message)
|
283
|
+
@prior_message = nil
|
284
|
+
end
|
218
285
|
else
|
286
|
+
puts "waiting #{remaining_wait} more..."
|
219
287
|
@cond.wait(@mutex, remaining_wait)
|
220
288
|
end
|
221
289
|
end
|
222
290
|
else
|
291
|
+
# minimum time between messages
|
292
|
+
puts "waiting between messages"
|
223
293
|
sleep 0.1
|
224
294
|
end
|
225
295
|
|
226
|
-
message
|
227
|
-
|
228
|
-
|
229
|
-
if message
|
296
|
+
puts "looking for next message to write"
|
297
|
+
@queues.find { |q| message_and_retries = q.shift }
|
298
|
+
if message_and_retries
|
299
|
+
if message_and_retries.message.ack_requested || message_and_retries.message.class.name =~ /^SDN::Message::Get/
|
230
300
|
@response_pending = Time.now.to_f + WAIT_TIME
|
231
|
-
if message.dest ==
|
301
|
+
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)
|
232
302
|
@broadcast_pending = Time.now.to_f + BROADCAST_WAIT
|
233
|
-
end
|
303
|
+
end
|
234
304
|
end
|
235
305
|
end
|
236
306
|
|
237
|
-
#
|
238
|
-
@
|
307
|
+
# wait until there is a message
|
308
|
+
if @response_pending
|
309
|
+
message_and_retries.remaining_retries -= 1
|
310
|
+
@prior_message = message_and_retries
|
311
|
+
elsif message_and_retries
|
312
|
+
@prior_message = nil
|
313
|
+
else
|
314
|
+
@cond.wait(@mutex)
|
315
|
+
end
|
239
316
|
end
|
240
|
-
next unless
|
317
|
+
next unless message_and_retries
|
241
318
|
|
319
|
+
message = message_and_retries.message
|
242
320
|
puts "writing #{message.inspect}"
|
243
|
-
|
321
|
+
puts "(and waiting for response)" if @response_pending
|
322
|
+
serialized = message.serialize
|
323
|
+
@sdn.write(serialized)
|
244
324
|
@sdn.flush
|
325
|
+
puts "wrote #{serialized.unpack("C*").map { |b| '%02x' % b }.join(' ')}"
|
245
326
|
end
|
246
327
|
rescue => e
|
247
328
|
puts "failure writing: #{e}"
|
@@ -254,10 +335,10 @@ module SDN
|
|
254
335
|
if topic == "#{@base_topic}/discovery/discover/set" && value == "true"
|
255
336
|
# trigger discovery
|
256
337
|
@mutex.synchronize do
|
257
|
-
@
|
338
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeAddr.new, 1, 2))
|
258
339
|
@cond.signal
|
259
340
|
end
|
260
|
-
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$}))
|
341
|
+
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$}))
|
261
342
|
addr = SDN::Message.parse_address(match[:addr])
|
262
343
|
property = match[:property]
|
263
344
|
# not homie compliant; allows linking the positionpercent property
|
@@ -266,10 +347,15 @@ module SDN
|
|
266
347
|
property = value.downcase
|
267
348
|
value = "true"
|
268
349
|
end
|
269
|
-
|
350
|
+
mqtt_addr = SDN::Message.print_address(addr).gsub('.', '')
|
351
|
+
motor = @motors[mqtt_addr]
|
270
352
|
is_group = SDN::Message.is_group_address?(addr)
|
353
|
+
group = @groups[mqtt_addr]
|
271
354
|
follow_up = SDN::Message::GetMotorStatus.new(addr)
|
272
355
|
message = case property
|
356
|
+
when 'discover'
|
357
|
+
follow_up = nil
|
358
|
+
SDN::Message::GetNodeAddr.new(addr) if value == "true"
|
273
359
|
when 'label'
|
274
360
|
follow_up = SDN::Message::GetNodeLabel.new(addr)
|
275
361
|
SDN::Message::SetNodeLabel.new(addr, value) unless is_group
|
@@ -330,15 +416,16 @@ module SDN
|
|
330
416
|
next unless motor
|
331
417
|
messages = motor.set_groups(value)
|
332
418
|
@mutex.synchronize do
|
333
|
-
messages.each { |m| @
|
419
|
+
messages.each { |m| @queues[0].push(MessageAndRetries.new(m, 5, 0)) }
|
334
420
|
@cond.signal
|
335
421
|
end
|
336
422
|
nil
|
337
423
|
end
|
338
424
|
if message
|
425
|
+
message.ack_requested = true if message.class.name !~ /^SDN::Message::Get/
|
339
426
|
@mutex.synchronize do
|
340
|
-
@
|
341
|
-
@
|
427
|
+
@queues[0].push(MessageAndRetries.new(message, 5, 0))
|
428
|
+
@queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
|
342
429
|
@cond.signal
|
343
430
|
end
|
344
431
|
end
|
@@ -379,7 +466,7 @@ module SDN
|
|
379
466
|
publish("discovery/discover/$settable", "true")
|
380
467
|
publish("discovery/discover/$retained", "false")
|
381
468
|
|
382
|
-
subscribe("
|
469
|
+
subscribe("+/discover/set")
|
383
470
|
subscribe("+/label/set")
|
384
471
|
subscribe("+/down/set")
|
385
472
|
subscribe("+/up/set")
|
@@ -407,7 +494,12 @@ module SDN
|
|
407
494
|
def publish_motor(addr)
|
408
495
|
publish("#{addr}/$name", addr)
|
409
496
|
publish("#{addr}/$type", "Sonesse 30 Motor")
|
410
|
-
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")
|
497
|
+
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")
|
498
|
+
|
499
|
+
publish("#{addr}/discover/$name", "Trigger Motor Discovery")
|
500
|
+
publish("#{addr}/discover/$datatype", "boolean")
|
501
|
+
publish("#{addr}/discover/$settable", "true")
|
502
|
+
publish("#{addr}/discover/$retained", "false")
|
411
503
|
|
412
504
|
publish("#{addr}/label/$name", "Node label")
|
413
505
|
publish("#{addr}/label/$datatype", "string")
|
@@ -527,17 +619,17 @@ module SDN
|
|
527
619
|
|
528
620
|
motor = Motor.new(self, addr)
|
529
621
|
@motors[addr] = motor
|
530
|
-
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.
|
622
|
+
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
|
531
623
|
|
532
624
|
sdn_addr = SDN::Message.parse_address(addr)
|
533
625
|
@mutex.synchronize do
|
534
|
-
@
|
535
|
-
@
|
536
|
-
@
|
537
|
-
@
|
538
|
-
@
|
539
|
-
(1..16).each { |ip| @
|
540
|
-
(0...16).each { |g| @
|
626
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeLabel.new(sdn_addr), 5, 2))
|
627
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorStatus.new(sdn_addr), 5, 2))
|
628
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorLimits.new(sdn_addr), 5, 2))
|
629
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorDirection.new(sdn_addr), 5, 2))
|
630
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorRollingSpeed.new(sdn_addr), 5, 2))
|
631
|
+
(1..16).each { |ip| @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
|
632
|
+
(0...16).each { |g| @queues[2].push(MessageAndRetries.new(SDN::Message::GetGroupAddr.new(sdn_addr, g), 5, 2)) }
|
541
633
|
|
542
634
|
@cond.signal
|
543
635
|
end
|
@@ -547,11 +639,17 @@ module SDN
|
|
547
639
|
|
548
640
|
def add_group(addr)
|
549
641
|
addr = addr.gsub('.', '')
|
550
|
-
|
642
|
+
group = @groups[addr]
|
643
|
+
return group if group
|
551
644
|
|
552
645
|
publish("#{addr}/$name", addr)
|
553
646
|
publish("#{addr}/$type", "Shade Group")
|
554
|
-
publish("#{addr}/$properties", "down,up,stop,positionpulses,positionpercent,ip,wink,reset")
|
647
|
+
publish("#{addr}/$properties", "discover,down,up,stop,positionpulses,positionpercent,ip,wink,reset,state,motors")
|
648
|
+
|
649
|
+
publish("#{addr}/discover/$name", "Trigger Motor Discovery")
|
650
|
+
publish("#{addr}/discover/$datatype", "boolean")
|
651
|
+
publish("#{addr}/discover/$settable", "true")
|
652
|
+
publish("#{addr}/discover/$retained", "false")
|
555
653
|
|
556
654
|
publish("#{addr}/down/$name", "Move in down direction")
|
557
655
|
publish("#{addr}/down/$datatype", "boolean")
|
@@ -566,7 +664,7 @@ module SDN
|
|
566
664
|
publish("#{addr}/stop/$name", "Cancel adjustments")
|
567
665
|
publish("#{addr}/stop/$datatype", "boolean")
|
568
666
|
publish("#{addr}/stop/$settable", "true")
|
569
|
-
publish("#{addr}/stop/$retained", "false")
|
667
|
+
publish("#{addr}/stop/$retained", "false")
|
570
668
|
|
571
669
|
publish("#{addr}/positionpulses/$name", "Position from up limit (in pulses)")
|
572
670
|
publish("#{addr}/positionpulses/$datatype", "integer")
|
@@ -590,8 +688,16 @@ module SDN
|
|
590
688
|
publish("#{addr}/wink/$settable", "true")
|
591
689
|
publish("#{addr}/wink/$retained", "false")
|
592
690
|
|
593
|
-
|
594
|
-
publish("
|
691
|
+
publish("#{addr}/state/$name", "State of the motors; only set if all motors are in the same state")
|
692
|
+
publish("#{addr}/state/$datatype", "enum")
|
693
|
+
publish("#{addr}/state/$format", SDN::Message::PostMotorStatus::STATE.keys.join(','))
|
694
|
+
|
695
|
+
publish("#{addr}/motors/$name", "Motors that are members of this group")
|
696
|
+
publish("#{addr}/motors/$datatype", "string")
|
697
|
+
|
698
|
+
group = @groups[addr] = Group.new(self, addr)
|
699
|
+
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
|
700
|
+
group
|
595
701
|
end
|
596
702
|
end
|
597
703
|
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,43 +1,57 @@
|
|
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.10
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cody Cutrer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: mqtt
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 0.5.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:
|
26
|
+
version: 0.5.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: net-telnet-rfc2217
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 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.
|
40
|
+
version: 0.0.3
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: serialport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.3.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.3.1
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: byebug
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -74,10 +88,14 @@ extensions: []
|
|
74
88
|
extra_rdoc_files: []
|
75
89
|
files:
|
76
90
|
- bin/sdn_mqtt_bridge
|
91
|
+
- lib/sdn.rb
|
77
92
|
- lib/sdn/message.rb
|
78
93
|
- lib/sdn/messages/control.rb
|
79
94
|
- lib/sdn/messages/get.rb
|
80
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
|
81
99
|
- lib/sdn/messages/post.rb
|
82
100
|
- lib/sdn/messages/set.rb
|
83
101
|
- lib/sdn/mqtt_bridge.rb
|