somfy_sdn 1.0.6 → 1.0.11
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 +208 -87
- data/lib/sdn/version.rb +1 -1
- data/lib/somfy_sdn.rb +1 -2
- metadata +31 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef81905b4b164a5e09d771e07b286f09df6d40cd259b8253c2a0ef1f01f649e4
|
4
|
+
data.tar.gz: 4d6f3ad72d6b2e8f06c530a8657d684f4f0b12c9d54eb13e998841b18a8e37dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8bf05e27c1e7c50e6893198950d7ab91099807c9d8ad139629e89fe3fc97d6f46c87e93400e1083ecba7c64ec3638a302523804fd921125414aa62f3632e5edb
|
7
|
+
data.tar.gz: 3300c346eeb666d82c686c0e5dec4fb830fa96adabdd7dbc45f3d4cd8e609c13f2796d1ae0b6cd2b2186bcf1ae3251d180e17dd33d578ccfa29eba3f86870e4a
|
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
@@ -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,
|
@@ -53,50 +79,58 @@ module SDN
|
|
53
79
|
:ip15percent,
|
54
80
|
:ip16pulses,
|
55
81
|
:ip16percent,
|
56
|
-
:groups
|
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
|
-
|
82
|
+
:groups,
|
83
|
+
:last_action) do
|
84
|
+
def initialize(*)
|
85
|
+
members.each { |k| self[k] = :nil }
|
86
|
+
@groups = [].fill(nil, 0, 16)
|
87
|
+
super
|
88
|
+
end
|
89
|
+
|
90
|
+
def publish(attribute, value)
|
91
|
+
if self[attribute] != value
|
92
|
+
bridge.publish("#{addr}/#{attribute}", value.to_s)
|
93
|
+
self[attribute] = value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def add_group(index, address)
|
98
|
+
group = bridge.add_group(SDN::Message.print_address(address)) if address
|
99
|
+
@groups[index] = address
|
100
|
+
group&.publish(:motors, group.motors_string)
|
101
|
+
publish(:groups, groups_string)
|
102
|
+
end
|
103
|
+
|
104
|
+
def set_groups(groups)
|
105
|
+
return unless groups =~ /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i
|
106
|
+
groups = groups.split(',').sort.uniq.map { |g| SDN::Message.parse_address(g) }.select { |g| SDN::Message.is_group_address?(g) }
|
107
|
+
groups.fill(nil, groups.length, 16 - groups.length)
|
108
|
+
messages = []
|
109
|
+
sdn_addr = SDN::Message.parse_address(addr)
|
110
|
+
groups.each_with_index do |g, i|
|
111
|
+
if @groups[i] != g
|
112
|
+
messages << SDN::Message::SetGroupAddr.new(sdn_addr, i, g).tap { |m| m.ack_requested = true }
|
113
|
+
messages << SDN::Message::GetGroupAddr.new(sdn_addr, i)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
messages
|
117
|
+
end
|
118
|
+
|
119
|
+
def groups_string
|
120
|
+
@groups.compact.map { |g| SDN::Message.print_address(g) }.sort.uniq.join(',')
|
121
|
+
end
|
122
|
+
|
123
|
+
def group_objects
|
124
|
+
groups_string.split(',').map { |addr| bridge.groups[addr.gsub('.', '')] }
|
125
|
+
end
|
94
126
|
end
|
95
127
|
|
96
128
|
class MQTTBridge
|
97
129
|
WAIT_TIME = 0.25
|
98
130
|
BROADCAST_WAIT = 5.0
|
99
131
|
|
132
|
+
attr_reader :motors, :groups
|
133
|
+
|
100
134
|
def initialize(mqtt_uri, port, device_id: "somfy", base_topic: "homie")
|
101
135
|
@base_topic = "#{base_topic}/#{device_id}"
|
102
136
|
@mqtt = MQTT::Client.new(mqtt_uri)
|
@@ -104,12 +138,11 @@ module SDN
|
|
104
138
|
@mqtt.connect
|
105
139
|
|
106
140
|
@motors = {}
|
107
|
-
@groups =
|
141
|
+
@groups = {}
|
108
142
|
|
109
143
|
@mutex = Mutex.new
|
110
144
|
@cond = ConditionVariable.new
|
111
|
-
@
|
112
|
-
@request_queue = []
|
145
|
+
@queues = [[], [], []]
|
113
146
|
@response_pending = false
|
114
147
|
@broadcast_pending = false
|
115
148
|
|
@@ -117,23 +150,34 @@ module SDN
|
|
117
150
|
|
118
151
|
uri = URI.parse(port)
|
119
152
|
if uri.scheme == "tcp"
|
153
|
+
require 'socket'
|
120
154
|
@sdn = TCPSocket.new(uri.host, uri.port)
|
155
|
+
elsif uri.scheme == "telnet" || uri.scheme == "rfc2217"
|
156
|
+
require 'net/telnet/rfc2217'
|
157
|
+
@sdn = Net::Telnet::RFC2217.new('Host' => uri.host,
|
158
|
+
'Port' => uri.port || 23,
|
159
|
+
'baud' => 4800,
|
160
|
+
'parity' => Net::Telnet::RFC2217::ODD)
|
121
161
|
else
|
122
|
-
|
162
|
+
require 'ccutrer-serialport'
|
163
|
+
@sdn = CCutrer::SerialPort.new(port, baud: 4800, parity: :odd)
|
123
164
|
end
|
124
165
|
|
125
166
|
read_thread = Thread.new do
|
126
167
|
buffer = ""
|
168
|
+
|
127
169
|
loop do
|
128
170
|
begin
|
129
171
|
message, bytes_read = SDN::Message.parse(buffer.bytes)
|
172
|
+
# discard how much we read
|
173
|
+
buffer = buffer[bytes_read..-1]
|
130
174
|
unless message
|
131
175
|
begin
|
132
|
-
buffer.concat(@sdn.read_nonblock(
|
176
|
+
buffer.concat(@sdn.read_nonblock(64 * 1024))
|
133
177
|
next
|
134
|
-
rescue IO::WaitReadable
|
178
|
+
rescue IO::WaitReadable, EOFError
|
135
179
|
wait = buffer.empty? ? nil : WAIT_TIME
|
136
|
-
if
|
180
|
+
if @sdn.wait_readable(wait).nil?
|
137
181
|
# timed out; just discard everything
|
138
182
|
puts "timed out reading; discarding buffer: #{buffer.unpack('H*').first}"
|
139
183
|
buffer = ""
|
@@ -141,8 +185,6 @@ module SDN
|
|
141
185
|
end
|
142
186
|
next
|
143
187
|
end
|
144
|
-
# discard how much we read
|
145
|
-
buffer = buffer[bytes_read..-1]
|
146
188
|
|
147
189
|
src = SDN::Message.print_address(message.src)
|
148
190
|
# ignore the UAI Plus and ourselves
|
@@ -162,8 +204,24 @@ module SDN
|
|
162
204
|
motor.publish(:positionpercent, message.position_percent)
|
163
205
|
motor.publish(:positionpulses, message.position_pulses)
|
164
206
|
motor.publish(:ip, message.ip)
|
207
|
+
motor.group_objects.each do |group|
|
208
|
+
positions = group.motor_objects.map(&:positionpercent)
|
209
|
+
position = nil
|
210
|
+
# calculate an average, but only if we know a position for
|
211
|
+
# every shade
|
212
|
+
if !positions.include?(:nil) && !positions.include?(nil)
|
213
|
+
position = positions.inject(&:+) / positions.length
|
214
|
+
end
|
215
|
+
|
216
|
+
group.publish(:positionpercent, position)
|
217
|
+
end
|
165
218
|
when SDN::Message::PostMotorStatus
|
166
|
-
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?))
|
167
225
|
follow_ups << SDN::Message::GetMotorStatus.new(message.src)
|
168
226
|
end
|
169
227
|
# this will do one more position request after it stopped
|
@@ -172,6 +230,11 @@ module SDN
|
|
172
230
|
motor.publish(:last_direction, message.last_direction)
|
173
231
|
motor.publish(:last_action_source, message.last_action_source)
|
174
232
|
motor.publish(:last_action_cause, message.last_action_cause)
|
233
|
+
motor.group_objects.each do |group|
|
234
|
+
states = group.motor_objects.map(&:state).uniq
|
235
|
+
state = states.length == 1 ? states.first : 'mixed'
|
236
|
+
group.publish(:state, state)
|
237
|
+
end
|
175
238
|
when SDN::Message::PostMotorLimits
|
176
239
|
motor.publish(:uplimit, message.up_limit)
|
177
240
|
motor.publish(:downlimit, message.down_limit)
|
@@ -182,18 +245,26 @@ module SDN
|
|
182
245
|
motor.publish(:downspeed, message.down_speed)
|
183
246
|
motor.publish(:slowspeed, message.slow_speed)
|
184
247
|
when SDN::Message::PostMotorIP
|
185
|
-
motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
|
248
|
+
motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
|
186
249
|
motor.publish(:"ip#{message.ip}percent", message.position_percent)
|
187
250
|
when SDN::Message::PostGroupAddr
|
188
251
|
motor.add_group(message.group_index, message.group_address)
|
189
252
|
end
|
190
253
|
|
191
254
|
@mutex.synchronize do
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
+
puts "correct response #{correct_response}"
|
258
|
+
puts "pending: #{@response_pending} #{@broadcast_pending}"
|
259
|
+
@response_pending = @broadcast_pending if correct_response
|
260
|
+
follow_ups.each do |follow_up|
|
261
|
+
@queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
|
262
|
+
end
|
195
263
|
@cond.signal if signal
|
196
264
|
end
|
265
|
+
rescue EOFError
|
266
|
+
puts "EOF reading"
|
267
|
+
exit 2
|
197
268
|
rescue SDN::MalformedMessage => e
|
198
269
|
puts "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
|
199
270
|
rescue => e
|
@@ -205,43 +276,63 @@ module SDN
|
|
205
276
|
write_thread = Thread.new do
|
206
277
|
begin
|
207
278
|
loop do
|
208
|
-
|
279
|
+
message_and_retries = nil
|
209
280
|
@mutex.synchronize do
|
210
281
|
# got woken up early by another command getting queued; spin
|
211
282
|
if @response_pending
|
283
|
+
puts "another message queued, but we're still waiting"
|
212
284
|
while @response_pending
|
213
285
|
remaining_wait = @response_pending - Time.now.to_f
|
214
286
|
if remaining_wait < 0
|
215
287
|
puts "timed out waiting on response"
|
216
288
|
@response_pending = nil
|
217
289
|
@broadcast_pending = nil
|
290
|
+
if @prior_message&.remaining_retries != 0
|
291
|
+
puts "retrying #{@prior_message.remaining_retries} more times ..."
|
292
|
+
@queues[@prior_message.priority].push(@prior_message)
|
293
|
+
@prior_message = nil
|
294
|
+
end
|
218
295
|
else
|
296
|
+
puts "waiting #{remaining_wait} more..."
|
219
297
|
@cond.wait(@mutex, remaining_wait)
|
220
298
|
end
|
221
299
|
end
|
222
300
|
else
|
301
|
+
# minimum time between messages
|
302
|
+
puts "waiting between messages"
|
223
303
|
sleep 0.1
|
224
304
|
end
|
225
305
|
|
226
|
-
message
|
227
|
-
|
228
|
-
|
229
|
-
if message
|
306
|
+
puts "looking for next message to write"
|
307
|
+
@queues.find { |q| message_and_retries = q.shift }
|
308
|
+
if message_and_retries
|
309
|
+
if message_and_retries.message.ack_requested || message_and_retries.message.class.name =~ /^SDN::Message::Get/
|
230
310
|
@response_pending = Time.now.to_f + WAIT_TIME
|
231
|
-
if message.dest ==
|
311
|
+
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
312
|
@broadcast_pending = Time.now.to_f + BROADCAST_WAIT
|
233
|
-
end
|
313
|
+
end
|
234
314
|
end
|
235
315
|
end
|
236
316
|
|
237
|
-
#
|
238
|
-
@
|
317
|
+
# wait until there is a message
|
318
|
+
if @response_pending
|
319
|
+
message_and_retries.remaining_retries -= 1
|
320
|
+
@prior_message = message_and_retries
|
321
|
+
elsif message_and_retries
|
322
|
+
@prior_message = nil
|
323
|
+
else
|
324
|
+
@cond.wait(@mutex)
|
325
|
+
end
|
239
326
|
end
|
240
|
-
next unless
|
327
|
+
next unless message_and_retries
|
241
328
|
|
329
|
+
message = message_and_retries.message
|
242
330
|
puts "writing #{message.inspect}"
|
243
|
-
|
244
|
-
|
331
|
+
puts "(and waiting for response)" if @response_pending
|
332
|
+
serialized = message.serialize
|
333
|
+
@sdn.write(serialized)
|
334
|
+
@sdn.flush if @sdn.respond_to?(:flush)
|
335
|
+
puts "wrote #{serialized.unpack("C*").map { |b| '%02x' % b }.join(' ')}"
|
245
336
|
end
|
246
337
|
rescue => e
|
247
338
|
puts "failure writing: #{e}"
|
@@ -254,10 +345,10 @@ module SDN
|
|
254
345
|
if topic == "#{@base_topic}/discovery/discover/set" && value == "true"
|
255
346
|
# trigger discovery
|
256
347
|
@mutex.synchronize do
|
257
|
-
@
|
348
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeAddr.new, 1, 2))
|
258
349
|
@cond.signal
|
259
350
|
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$}))
|
351
|
+
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
352
|
addr = SDN::Message.parse_address(match[:addr])
|
262
353
|
property = match[:property]
|
263
354
|
# not homie compliant; allows linking the positionpercent property
|
@@ -266,10 +357,15 @@ module SDN
|
|
266
357
|
property = value.downcase
|
267
358
|
value = "true"
|
268
359
|
end
|
269
|
-
|
360
|
+
mqtt_addr = SDN::Message.print_address(addr).gsub('.', '')
|
361
|
+
motor = @motors[mqtt_addr]
|
270
362
|
is_group = SDN::Message.is_group_address?(addr)
|
363
|
+
group = @groups[mqtt_addr]
|
271
364
|
follow_up = SDN::Message::GetMotorStatus.new(addr)
|
272
365
|
message = case property
|
366
|
+
when 'discover'
|
367
|
+
follow_up = nil
|
368
|
+
SDN::Message::GetNodeAddr.new(addr) if value == "true"
|
273
369
|
when 'label'
|
274
370
|
follow_up = SDN::Message::GetNodeLabel.new(addr)
|
275
371
|
SDN::Message::SetNodeLabel.new(addr, value) unless is_group
|
@@ -330,15 +426,21 @@ module SDN
|
|
330
426
|
next unless motor
|
331
427
|
messages = motor.set_groups(value)
|
332
428
|
@mutex.synchronize do
|
333
|
-
messages.each { |m| @
|
429
|
+
messages.each { |m| @queues[0].push(MessageAndRetries.new(m, 5, 0)) }
|
334
430
|
@cond.signal
|
335
431
|
end
|
336
432
|
nil
|
337
433
|
end
|
434
|
+
|
435
|
+
if motor
|
436
|
+
motor.last_action = message.class if [Message::MoveTo, Message::Move, Message::Wink, Message::Stop].include?(message.class)
|
437
|
+
end
|
438
|
+
|
338
439
|
if message
|
440
|
+
message.ack_requested = true if message.class.name !~ /^SDN::Message::Get/
|
339
441
|
@mutex.synchronize do
|
340
|
-
@
|
341
|
-
@
|
442
|
+
@queues[0].push(MessageAndRetries.new(message, 5, 0))
|
443
|
+
@queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
|
342
444
|
@cond.signal
|
343
445
|
end
|
344
446
|
end
|
@@ -379,7 +481,7 @@ module SDN
|
|
379
481
|
publish("discovery/discover/$settable", "true")
|
380
482
|
publish("discovery/discover/$retained", "false")
|
381
483
|
|
382
|
-
subscribe("
|
484
|
+
subscribe("+/discover/set")
|
383
485
|
subscribe("+/label/set")
|
384
486
|
subscribe("+/down/set")
|
385
487
|
subscribe("+/up/set")
|
@@ -407,7 +509,12 @@ module SDN
|
|
407
509
|
def publish_motor(addr)
|
408
510
|
publish("#{addr}/$name", addr)
|
409
511
|
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")
|
512
|
+
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")
|
513
|
+
|
514
|
+
publish("#{addr}/discover/$name", "Trigger Motor Discovery")
|
515
|
+
publish("#{addr}/discover/$datatype", "boolean")
|
516
|
+
publish("#{addr}/discover/$settable", "true")
|
517
|
+
publish("#{addr}/discover/$retained", "false")
|
411
518
|
|
412
519
|
publish("#{addr}/label/$name", "Node label")
|
413
520
|
publish("#{addr}/label/$datatype", "string")
|
@@ -527,17 +634,17 @@ module SDN
|
|
527
634
|
|
528
635
|
motor = Motor.new(self, addr)
|
529
636
|
@motors[addr] = motor
|
530
|
-
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.
|
637
|
+
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
|
531
638
|
|
532
639
|
sdn_addr = SDN::Message.parse_address(addr)
|
533
640
|
@mutex.synchronize do
|
534
|
-
@
|
535
|
-
@
|
536
|
-
@
|
537
|
-
@
|
538
|
-
@
|
539
|
-
(1..16).each { |ip| @
|
540
|
-
(0...16).each { |g| @
|
641
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetNodeLabel.new(sdn_addr), 5, 2))
|
642
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorStatus.new(sdn_addr), 5, 2))
|
643
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorLimits.new(sdn_addr), 5, 2))
|
644
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorDirection.new(sdn_addr), 5, 2))
|
645
|
+
@queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorRollingSpeed.new(sdn_addr), 5, 2))
|
646
|
+
(1..16).each { |ip| @queues[2].push(MessageAndRetries.new(SDN::Message::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
|
647
|
+
(0...16).each { |g| @queues[2].push(MessageAndRetries.new(SDN::Message::GetGroupAddr.new(sdn_addr, g), 5, 2)) }
|
541
648
|
|
542
649
|
@cond.signal
|
543
650
|
end
|
@@ -547,11 +654,17 @@ module SDN
|
|
547
654
|
|
548
655
|
def add_group(addr)
|
549
656
|
addr = addr.gsub('.', '')
|
550
|
-
|
657
|
+
group = @groups[addr]
|
658
|
+
return group if group
|
551
659
|
|
552
660
|
publish("#{addr}/$name", addr)
|
553
661
|
publish("#{addr}/$type", "Shade Group")
|
554
|
-
publish("#{addr}/$properties", "down,up,stop,positionpulses,positionpercent,ip,wink,reset")
|
662
|
+
publish("#{addr}/$properties", "discover,down,up,stop,positionpulses,positionpercent,ip,wink,reset,state,motors")
|
663
|
+
|
664
|
+
publish("#{addr}/discover/$name", "Trigger Motor Discovery")
|
665
|
+
publish("#{addr}/discover/$datatype", "boolean")
|
666
|
+
publish("#{addr}/discover/$settable", "true")
|
667
|
+
publish("#{addr}/discover/$retained", "false")
|
555
668
|
|
556
669
|
publish("#{addr}/down/$name", "Move in down direction")
|
557
670
|
publish("#{addr}/down/$datatype", "boolean")
|
@@ -566,7 +679,7 @@ module SDN
|
|
566
679
|
publish("#{addr}/stop/$name", "Cancel adjustments")
|
567
680
|
publish("#{addr}/stop/$datatype", "boolean")
|
568
681
|
publish("#{addr}/stop/$settable", "true")
|
569
|
-
publish("#{addr}/stop/$retained", "false")
|
682
|
+
publish("#{addr}/stop/$retained", "false")
|
570
683
|
|
571
684
|
publish("#{addr}/positionpulses/$name", "Position from up limit (in pulses)")
|
572
685
|
publish("#{addr}/positionpulses/$datatype", "integer")
|
@@ -590,8 +703,16 @@ module SDN
|
|
590
703
|
publish("#{addr}/wink/$settable", "true")
|
591
704
|
publish("#{addr}/wink/$retained", "false")
|
592
705
|
|
593
|
-
|
594
|
-
publish("
|
706
|
+
publish("#{addr}/state/$name", "State of the motors; only set if all motors are in the same state")
|
707
|
+
publish("#{addr}/state/$datatype", "enum")
|
708
|
+
publish("#{addr}/state/$format", SDN::Message::PostMotorStatus::STATE.keys.join(','))
|
709
|
+
|
710
|
+
publish("#{addr}/motors/$name", "Motors that are members of this group")
|
711
|
+
publish("#{addr}/motors/$datatype", "string")
|
712
|
+
|
713
|
+
group = @groups[addr] = Group.new(self, addr)
|
714
|
+
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
|
715
|
+
group
|
595
716
|
end
|
596
717
|
end
|
597
718
|
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.11
|
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-09-25 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: ccutrer-serialport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.0.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.0.0
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: byebug
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,7 +80,7 @@ dependencies:
|
|
66
80
|
- - "~>"
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '13.0'
|
69
|
-
description:
|
83
|
+
description:
|
70
84
|
email: cody@cutrer.com'
|
71
85
|
executables:
|
72
86
|
- sdn_mqtt_bridge
|
@@ -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
|
@@ -87,7 +105,7 @@ homepage: https://github.com/ccutrer/somfy_sdn
|
|
87
105
|
licenses:
|
88
106
|
- MIT
|
89
107
|
metadata: {}
|
90
|
-
post_install_message:
|
108
|
+
post_install_message:
|
91
109
|
rdoc_options: []
|
92
110
|
require_paths:
|
93
111
|
- lib
|
@@ -102,8 +120,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
120
|
- !ruby/object:Gem::Version
|
103
121
|
version: '0'
|
104
122
|
requirements: []
|
105
|
-
rubygems_version: 3.
|
106
|
-
signing_key:
|
123
|
+
rubygems_version: 3.1.2
|
124
|
+
signing_key:
|
107
125
|
specification_version: 4
|
108
126
|
summary: Library for communication with Somfy SDN RS-485 motorized shades
|
109
127
|
test_files: []
|