somfy_sdn 1.0.4 → 1.0.9
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 +46 -72
- 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 +267 -75
- 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: 7b76beaca35ebf0ae0847fc75bf0fcd12c45e395768cca0c711557b167488e3e
|
4
|
+
data.tar.gz: 64dfc392ef651d1afc6e6a94c8c586e1e95e575c0ff9ba12c74f472cbfe88874
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 87a1138187217fb8bf7fe62e00f69e20b742d2527d4f0059b9a2d4ba42012c887947608fdbfad4123400a0968d99d9c257cb361025ecef668786d34b708a0512
|
7
|
+
data.tar.gz: e6ddbe689d8d60d252c24a1291893b05040a638ba3b86ac3a7bc1c28479a9ccdf0485e5914035818c50e1005b076d148263916ae1e7a66e05efdb075786d1d95
|
data/lib/sdn.rb
ADDED
data/lib/sdn/message.rb
CHANGED
@@ -3,81 +3,51 @@ 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
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
8
|
+
def parse(data)
|
9
|
+
offset = -1
|
10
|
+
msg = length = ack_requested = message_class = nil
|
11
|
+
# we loop here scanning for a valid message
|
12
|
+
loop do
|
13
|
+
offset += 1
|
14
|
+
return [nil, 0] if data.length - offset < 11
|
15
|
+
msg = to_number(data[offset])
|
16
|
+
length = to_number(data[offset + 1])
|
17
|
+
ack_requested = length & 0x80 == 0x80
|
18
|
+
length &= 0x7f
|
19
|
+
# impossible message
|
20
|
+
next if length < 11 || length > 32
|
21
|
+
# don't have enough data for what this message wants;
|
22
|
+
# it could be garbage on the line so keep scanning
|
23
|
+
next if length > data.length - offset
|
24
|
+
|
25
|
+
message_class = constants.find { |c| (const_get(c, false).const_get(:MSG, false) rescue nil) == msg }
|
26
|
+
message_class = const_get(message_class, false) if message_class
|
27
|
+
message_class ||= UnknownMessage
|
28
|
+
|
29
|
+
calculated_sum = checksum(data.slice(offset, length - 2))
|
30
|
+
read_sum = data.slice(offset + length - 2, 2)
|
31
|
+
next unless read_sum == calculated_sum
|
32
|
+
|
33
|
+
break
|
19
34
|
end
|
20
|
-
data
|
21
|
-
end
|
22
35
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
# only skip over one byte to try and re-sync
|
37
|
-
io.ungetbyte(data.last)
|
38
|
-
raise MalformedMessage, "Message has bogus length: #{length}"
|
39
|
-
end
|
40
|
-
data.concat(readpartial(io, length - 4))
|
41
|
-
unless data.length == length - 2
|
42
|
-
data.reverse.each { |byte| io.ungetbyte(byte) }
|
43
|
-
raise MalformedMessage, "Missing data: got #{data.length} expected #{length}"
|
36
|
+
puts "discarding invalid data prior to message #{data[0...offset].map { |b| '%02x' % b }.join(' ')}" unless offset == 0
|
37
|
+
puts "read #{data.slice(offset, length).map { |b| '%02x' % b }.join(' ')}"
|
38
|
+
|
39
|
+
reserved = to_number(data[offset + 2])
|
40
|
+
src = transform_param(data.slice(offset + 3, 3))
|
41
|
+
dest = transform_param(data.slice(offset + 6, 3))
|
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
|
44
49
|
end
|
45
|
-
|
46
|
-
message_class = constants.find { |c| (const_get(c, false).const_get(:MSG, false) rescue nil) == msg }
|
47
|
-
message_class = const_get(message_class, false) if message_class
|
48
|
-
message_class ||= UnknownMessage
|
49
|
-
|
50
|
-
bogus_checksums = [SetNodeLabel::MSG, PostNodeLabel::MSG].include?(msg)
|
51
|
-
|
52
|
-
calculated_sum = checksum(data)
|
53
|
-
read_sum = readpartial(io, 2)
|
54
|
-
if read_sum.length == 0 || (!bogus_checksums && read_sum.length == 1)
|
55
|
-
read_sum.each { |byte| io.ungetbyte(byte) }
|
56
|
-
data.reverse.each { |byte| io.ungetbyte(byte) }
|
57
|
-
raise MalformedMessage, "Missing data: got #{data.length} expected #{length}"
|
58
|
-
end
|
59
|
-
|
60
|
-
# check both the proper checksum, and a truncated checksum
|
61
|
-
unless calculated_sum == read_sum || (bogus_checksums && calculated_sum.last == read_sum.first)
|
62
|
-
raw_message = (data + read_sum).map { |b| '%02x' % b }.join(' ')
|
63
|
-
# skip over single byte to try and re-sync
|
64
|
-
data.shift
|
65
|
-
read_sum.reverse.each { |byte| io.ungetbyte(byte) }
|
66
|
-
data.reverse.each { |byte| io.ungetbyte(byte) }
|
67
|
-
raise MalformedMessage, "Checksum mismatch for #{message_class.name}: #{raw_message}"
|
68
|
-
end
|
69
|
-
# the checksum was truncated; put back the unused byte
|
70
|
-
io.ungetbyte(read_sum.last) if calculated_sum != read_sum && read_sum.length == 2
|
71
|
-
|
72
|
-
puts "read #{(data + read_sum).map { |b| '%02x' % b }.join(' ')}"
|
73
|
-
|
74
|
-
reserved = to_number(data[2])
|
75
|
-
src = transform_param(data[3..5])
|
76
|
-
dest = transform_param(data[6..8])
|
77
|
-
result = message_class.new(reserved: reserved, ack_requested: ack_requested, src: src, dest: dest)
|
78
|
-
result.parse(data[9..-1])
|
79
|
-
result.msg = msg if message_class == UnknownMessage
|
80
|
-
result
|
50
|
+
[result, offset + length]
|
81
51
|
end
|
82
52
|
end
|
83
53
|
|
@@ -85,6 +55,7 @@ module SDN
|
|
85
55
|
singleton_class.include Helpers
|
86
56
|
|
87
57
|
attr_reader :reserved, :ack_requested, :src, :dest
|
58
|
+
attr_writer :ack_requested
|
88
59
|
|
89
60
|
def initialize(reserved: nil, ack_requested: false, src: nil, dest: nil)
|
90
61
|
@reserved = reserved || 0x02 # message sent to Sonesse 30
|
@@ -107,10 +78,13 @@ module SDN
|
|
107
78
|
length |= 0x80 if ack_requested
|
108
79
|
result = transform_param(self.class.const_get(:MSG)) + transform_param(length) + result
|
109
80
|
result.concat(checksum(result))
|
110
|
-
puts "wrote #{result.map { |b| '%02x' % b }.join(' ')}"
|
111
81
|
result.pack("C*")
|
112
82
|
end
|
113
83
|
|
84
|
+
def ==(other)
|
85
|
+
self.serialize == other.serialize
|
86
|
+
end
|
87
|
+
|
114
88
|
def inspect
|
115
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]
|
116
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,8 +1,36 @@
|
|
1
1
|
require 'mqtt'
|
2
|
-
require '
|
2
|
+
require 'uri'
|
3
3
|
require 'set'
|
4
4
|
|
5
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
|
+
|
6
34
|
Motor = Struct.new(:bridge,
|
7
35
|
:addr,
|
8
36
|
:label,
|
@@ -52,64 +80,110 @@ module SDN
|
|
52
80
|
:ip16pulses,
|
53
81
|
:ip16percent,
|
54
82
|
:groups) do
|
55
|
-
|
56
|
-
|
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
|
-
|
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
|
92
125
|
end
|
93
126
|
|
94
127
|
class MQTTBridge
|
95
|
-
|
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")
|
96
134
|
@base_topic = "#{base_topic}/#{device_id}"
|
97
135
|
@mqtt = MQTT::Client.new(mqtt_uri)
|
98
136
|
@mqtt.set_will("#{@base_topic}/$state", "lost", true)
|
99
137
|
@mqtt.connect
|
138
|
+
|
100
139
|
@motors = {}
|
101
|
-
@groups =
|
102
|
-
|
140
|
+
@groups = {}
|
141
|
+
|
142
|
+
@mutex = Mutex.new
|
143
|
+
@cond = ConditionVariable.new
|
144
|
+
@queues = [[], [], []]
|
145
|
+
@response_pending = false
|
146
|
+
@broadcast_pending = false
|
103
147
|
|
104
148
|
publish_basic_attributes
|
105
149
|
|
106
|
-
|
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
|
107
164
|
|
108
165
|
read_thread = Thread.new do
|
166
|
+
buffer = ""
|
109
167
|
loop do
|
110
168
|
begin
|
111
|
-
message = SDN::Message.parse(
|
112
|
-
|
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
|
+
|
113
187
|
src = SDN::Message.print_address(message.src)
|
114
188
|
# ignore the UAI Plus and ourselves
|
115
189
|
if src != '7F.7F.7F' && !SDN::Message::is_group_address?(message.src) && !(motor = @motors[src.gsub('.', '')])
|
@@ -118,6 +192,7 @@ module SDN
|
|
118
192
|
end
|
119
193
|
|
120
194
|
puts "read #{message.inspect}"
|
195
|
+
follow_ups = []
|
121
196
|
case message
|
122
197
|
when SDN::Message::PostNodeLabel
|
123
198
|
if (motor.publish(:label, message.label))
|
@@ -127,16 +202,32 @@ module SDN
|
|
127
202
|
motor.publish(:positionpercent, message.position_percent)
|
128
203
|
motor.publish(:positionpulses, message.position_pulses)
|
129
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
|
130
216
|
when SDN::Message::PostMotorStatus
|
131
217
|
if message.state == :running || motor.state == :running
|
132
|
-
|
218
|
+
follow_ups << SDN::Message::GetMotorStatus.new(message.src)
|
133
219
|
end
|
134
220
|
# this will do one more position request after it stopped
|
135
|
-
|
221
|
+
follow_ups << SDN::Message::GetMotorPosition.new(message.src)
|
136
222
|
motor.publish(:state, message.state)
|
137
223
|
motor.publish(:last_direction, message.last_direction)
|
138
224
|
motor.publish(:last_action_source, message.last_action_source)
|
139
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
|
140
231
|
when SDN::Message::PostMotorLimits
|
141
232
|
motor.publish(:uplimit, message.up_limit)
|
142
233
|
motor.publish(:downlimit, message.down_limit)
|
@@ -147,12 +238,23 @@ module SDN
|
|
147
238
|
motor.publish(:downspeed, message.down_speed)
|
148
239
|
motor.publish(:slowspeed, message.slow_speed)
|
149
240
|
when SDN::Message::PostMotorIP
|
150
|
-
motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
|
241
|
+
motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
|
151
242
|
motor.publish(:"ip#{message.ip}percent", message.position_percent)
|
152
243
|
when SDN::Message::PostGroupAddr
|
153
244
|
motor.add_group(message.group_index, message.group_address)
|
154
245
|
end
|
155
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
|
156
258
|
rescue SDN::MalformedMessage => e
|
157
259
|
puts "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
|
158
260
|
rescue => e
|
@@ -164,13 +266,57 @@ module SDN
|
|
164
266
|
write_thread = Thread.new do
|
165
267
|
begin
|
166
268
|
loop do
|
167
|
-
|
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
|
168
309
|
puts "writing #{message.inspect}"
|
169
|
-
|
310
|
+
serialized = message.serialize
|
311
|
+
@sdn.write(serialized)
|
170
312
|
@sdn.flush
|
171
|
-
#
|
172
|
-
|
173
|
-
|
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
|
174
320
|
end
|
175
321
|
rescue => e
|
176
322
|
puts "failure writing: #{e}"
|
@@ -182,8 +328,11 @@ module SDN
|
|
182
328
|
puts "got #{value.inspect} at #{topic}"
|
183
329
|
if topic == "#{@base_topic}/discovery/discover/set" && value == "true"
|
184
330
|
# trigger discovery
|
185
|
-
@
|
186
|
-
|
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$}))
|
187
336
|
addr = SDN::Message.parse_address(match[:addr])
|
188
337
|
property = match[:property]
|
189
338
|
# not homie compliant; allows linking the positionpercent property
|
@@ -192,10 +341,15 @@ module SDN
|
|
192
341
|
property = value.downcase
|
193
342
|
value = "true"
|
194
343
|
end
|
195
|
-
|
344
|
+
mqtt_addr = SDN::Message.print_address(addr).gsub('.', '')
|
345
|
+
motor = @motors[mqtt_addr]
|
196
346
|
is_group = SDN::Message.is_group_address?(addr)
|
347
|
+
group = @groups[mqtt_addr]
|
197
348
|
follow_up = SDN::Message::GetMotorStatus.new(addr)
|
198
349
|
message = case property
|
350
|
+
when 'discover'
|
351
|
+
follow_up = nil
|
352
|
+
SDN::Message::GetNodeAddr.new(addr) if value == "true"
|
199
353
|
when 'label'
|
200
354
|
follow_up = SDN::Message::GetNodeLabel.new(addr)
|
201
355
|
SDN::Message::SetNodeLabel.new(addr, value) unless is_group
|
@@ -255,13 +409,19 @@ module SDN
|
|
255
409
|
next if is_group
|
256
410
|
next unless motor
|
257
411
|
messages = motor.set_groups(value)
|
258
|
-
|
412
|
+
@mutex.synchronize do
|
413
|
+
messages.each { |m| @queues[0].push(MessageAndRetries.new(m, 5, 0)) }
|
414
|
+
@cond.signal
|
415
|
+
end
|
259
416
|
nil
|
260
417
|
end
|
261
418
|
if message
|
262
|
-
|
263
|
-
|
264
|
-
|
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
|
265
425
|
end
|
266
426
|
end
|
267
427
|
end
|
@@ -275,6 +435,16 @@ module SDN
|
|
275
435
|
@mqtt.subscribe("#{@base_topic}/#{topic}")
|
276
436
|
end
|
277
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
|
+
|
278
448
|
def publish_basic_attributes
|
279
449
|
publish("$homie", "v4.0.0")
|
280
450
|
publish("$name", "Somfy SDN Network")
|
@@ -290,7 +460,7 @@ module SDN
|
|
290
460
|
publish("discovery/discover/$settable", "true")
|
291
461
|
publish("discovery/discover/$retained", "false")
|
292
462
|
|
293
|
-
subscribe("
|
463
|
+
subscribe("+/discover/set")
|
294
464
|
subscribe("+/label/set")
|
295
465
|
subscribe("+/down/set")
|
296
466
|
subscribe("+/up/set")
|
@@ -318,7 +488,12 @@ module SDN
|
|
318
488
|
def publish_motor(addr)
|
319
489
|
publish("#{addr}/$name", addr)
|
320
490
|
publish("#{addr}/$type", "Sonesse 30 Motor")
|
321
|
-
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")
|
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")
|
322
497
|
|
323
498
|
publish("#{addr}/label/$name", "Node label")
|
324
499
|
publish("#{addr}/label/$datatype", "string")
|
@@ -438,28 +613,37 @@ module SDN
|
|
438
613
|
|
439
614
|
motor = Motor.new(self, addr)
|
440
615
|
@motors[addr] = motor
|
441
|
-
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.
|
616
|
+
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
|
442
617
|
|
443
618
|
sdn_addr = SDN::Message.parse_address(addr)
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
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
|
452
630
|
|
453
631
|
motor
|
454
632
|
end
|
455
633
|
|
456
634
|
def add_group(addr)
|
457
635
|
addr = addr.gsub('.', '')
|
458
|
-
|
636
|
+
group = @groups[addr]
|
637
|
+
return group if group
|
459
638
|
|
460
639
|
publish("#{addr}/$name", addr)
|
461
640
|
publish("#{addr}/$type", "Shade Group")
|
462
|
-
publish("#{addr}/$properties", "down,up,stop,positionpulses,positionpercent,ip,wink,reset")
|
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")
|
463
647
|
|
464
648
|
publish("#{addr}/down/$name", "Move in down direction")
|
465
649
|
publish("#{addr}/down/$datatype", "boolean")
|
@@ -474,7 +658,7 @@ module SDN
|
|
474
658
|
publish("#{addr}/stop/$name", "Cancel adjustments")
|
475
659
|
publish("#{addr}/stop/$datatype", "boolean")
|
476
660
|
publish("#{addr}/stop/$settable", "true")
|
477
|
-
publish("#{addr}/stop/$retained", "false")
|
661
|
+
publish("#{addr}/stop/$retained", "false")
|
478
662
|
|
479
663
|
publish("#{addr}/positionpulses/$name", "Position from up limit (in pulses)")
|
480
664
|
publish("#{addr}/positionpulses/$datatype", "integer")
|
@@ -498,8 +682,16 @@ module SDN
|
|
498
682
|
publish("#{addr}/wink/$settable", "true")
|
499
683
|
publish("#{addr}/wink/$retained", "false")
|
500
684
|
|
501
|
-
|
502
|
-
publish("
|
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
|
503
695
|
end
|
504
696
|
end
|
505
697
|
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.9
|
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
|