somfy_sdn 1.0.3 → 1.0.8
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 +45 -72
- data/lib/sdn/messages/control.rb +3 -3
- data/lib/sdn/messages/helpers.rb +1 -1
- 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 +261 -77
- 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: 66516a3bb03695b73075a496a791502683ec12052219a5cf040cfe6e9f82b04f
|
4
|
+
data.tar.gz: bad8e87688534b118e679b4126490f5eab3c6e83ee7288184368e852bf0cc450
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db18252c03d784aeb3461dacffc16dae04aeb176f5b98f3b796d395f95980350576075a434794e16347eef7bfb308eccf7e4307abeceb4675254a3505d74b60d
|
7
|
+
data.tar.gz: e17af388921091abe51a23b6fcef7c3bf0d402ad55446bfc86aa698482a22cca341f7612b73d04e97ad91e060ff13f7573016e4ddb7aca24002c69f1ee9706b9
|
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
|
|
@@ -107,10 +77,13 @@ module SDN
|
|
107
77
|
length |= 0x80 if ack_requested
|
108
78
|
result = transform_param(self.class.const_get(:MSG)) + transform_param(length) + result
|
109
79
|
result.concat(checksum(result))
|
110
|
-
puts "wrote #{result.map { |b| '%02x' % b }.join(' ')}"
|
111
80
|
result.pack("C*")
|
112
81
|
end
|
113
82
|
|
83
|
+
def ==(other)
|
84
|
+
self.serialize == other.serialize
|
85
|
+
end
|
86
|
+
|
114
87
|
def inspect
|
115
88
|
"#<%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
89
|
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
|
|
data/lib/sdn/messages/helpers.rb
CHANGED
@@ -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,34 @@
|
|
1
1
|
require 'mqtt'
|
2
|
-
require '
|
2
|
+
require 'uri'
|
3
3
|
require 'set'
|
4
4
|
|
5
5
|
module SDN
|
6
|
+
Group = Struct.new(:bridge, :addr, :positionpercent, :state, :motors) do
|
7
|
+
def initialize(*)
|
8
|
+
members.each { |k| self[k] = :nil }
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def publish(attribute, value)
|
13
|
+
if self[attribute] != value
|
14
|
+
bridge.publish("#{addr}/#{attribute}", value.to_s)
|
15
|
+
self[attribute] = value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def printed_addr
|
20
|
+
Message.print_address(Message.parse_address(addr))
|
21
|
+
end
|
22
|
+
|
23
|
+
def motor_objects
|
24
|
+
bridge.motors.select { |addr, motor| motor.groups_string.include?(printed_addr) }.values
|
25
|
+
end
|
26
|
+
|
27
|
+
def motors_string
|
28
|
+
motor_objects.map { |m| SDN::Message.print_address(SDN::Message.parse_address(m.addr)) }.sort.join(',')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
6
32
|
Motor = Struct.new(:bridge,
|
7
33
|
:addr,
|
8
34
|
:label,
|
@@ -52,64 +78,111 @@ module SDN
|
|
52
78
|
:ip16pulses,
|
53
79
|
:ip16percent,
|
54
80
|
: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
|
-
|
81
|
+
def initialize(*)
|
82
|
+
members.each { |k| self[k] = :nil }
|
83
|
+
@groups = [].fill(nil, 0, 16)
|
84
|
+
super
|
85
|
+
end
|
86
|
+
|
87
|
+
def publish(attribute, value)
|
88
|
+
if self[attribute] != value
|
89
|
+
bridge.publish("#{addr}/#{attribute}", value.to_s)
|
90
|
+
self[attribute] = value
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_group(index, address)
|
95
|
+
group = bridge.add_group(SDN::Message.print_address(address)) if address
|
96
|
+
@groups[index] = address
|
97
|
+
group&.publish(:motors, group.motors_string)
|
98
|
+
publish(:groups, groups_string)
|
99
|
+
end
|
100
|
+
|
101
|
+
def set_groups(groups)
|
102
|
+
return unless groups =~ /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i
|
103
|
+
groups = groups.split(',').sort.uniq.map { |g| SDN::Message.parse_address(g) }.select { |g| SDN::Message.is_group_address?(g) }
|
104
|
+
groups.fill(nil, groups.length, 16 - groups.length)
|
105
|
+
messages = []
|
106
|
+
sdn_addr = SDN::Message.parse_address(addr)
|
107
|
+
groups.each_with_index do |g, i|
|
108
|
+
if @groups[i] != g
|
109
|
+
messages << SDN::Message::SetGroupAddr.new(sdn_addr, i, g)
|
110
|
+
messages << SDN::Message::GetGroupAddr.new(sdn_addr, i)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
messages
|
114
|
+
end
|
115
|
+
|
116
|
+
def groups_string
|
117
|
+
@groups.compact.map { |g| SDN::Message.print_address(g) }.sort.uniq.join(',')
|
118
|
+
end
|
119
|
+
|
120
|
+
def group_objects
|
121
|
+
groups_string.split(',').map { |addr| bridge.groups[addr.gsub('.', '')] }
|
122
|
+
end
|
92
123
|
end
|
93
124
|
|
94
125
|
class MQTTBridge
|
95
|
-
|
126
|
+
WAIT_TIME = 0.25
|
127
|
+
BROADCAST_WAIT = 5.0
|
128
|
+
|
129
|
+
attr_reader :motors, :groups
|
130
|
+
|
131
|
+
def initialize(mqtt_uri, port, device_id: "somfy", base_topic: "homie")
|
96
132
|
@base_topic = "#{base_topic}/#{device_id}"
|
97
133
|
@mqtt = MQTT::Client.new(mqtt_uri)
|
98
134
|
@mqtt.set_will("#{@base_topic}/$state", "lost", true)
|
99
135
|
@mqtt.connect
|
136
|
+
|
100
137
|
@motors = {}
|
101
|
-
@groups =
|
102
|
-
|
138
|
+
@groups = {}
|
139
|
+
|
140
|
+
@mutex = Mutex.new
|
141
|
+
@cond = ConditionVariable.new
|
142
|
+
@command_queue = []
|
143
|
+
@request_queue = []
|
144
|
+
@response_pending = false
|
145
|
+
@broadcast_pending = false
|
103
146
|
|
104
147
|
publish_basic_attributes
|
105
148
|
|
106
|
-
|
149
|
+
uri = URI.parse(port)
|
150
|
+
if uri.scheme == "tcp"
|
151
|
+
require 'socket'
|
152
|
+
@sdn = TCPSocket.new(uri.host, uri.port)
|
153
|
+
elsif uri.scheme == "telnet" || uri.scheme == "rfc2217"
|
154
|
+
require 'net/telnet/rfc2217'
|
155
|
+
@sdn = Net::Telnet::RFC2217.new('Host' => uri.host,
|
156
|
+
'Port' => uri.port || 23,
|
157
|
+
'baud' => 4800,
|
158
|
+
'parity' => Net::Telnet::RFC2217::ODD)
|
159
|
+
else
|
160
|
+
require 'serialport'
|
161
|
+
@sdn = SerialPort.open(port, "baud" => 4800, "parity" => SerialPort::ODD)
|
162
|
+
end
|
107
163
|
|
108
164
|
read_thread = Thread.new do
|
165
|
+
buffer = ""
|
109
166
|
loop do
|
110
167
|
begin
|
111
|
-
message = SDN::Message.parse(
|
112
|
-
|
168
|
+
message, bytes_read = SDN::Message.parse(buffer.bytes)
|
169
|
+
# discard how much we read
|
170
|
+
buffer = buffer[bytes_read..-1]
|
171
|
+
unless message
|
172
|
+
begin
|
173
|
+
buffer.concat(@sdn.read_nonblock(64 * 1024))
|
174
|
+
next
|
175
|
+
rescue IO::WaitReadable
|
176
|
+
wait = buffer.empty? ? nil : WAIT_TIME
|
177
|
+
if @sdn.wait_readable(wait).nil?
|
178
|
+
# timed out; just discard everything
|
179
|
+
puts "timed out reading; discarding buffer: #{buffer.unpack('H*').first}"
|
180
|
+
buffer = ""
|
181
|
+
end
|
182
|
+
end
|
183
|
+
next
|
184
|
+
end
|
185
|
+
|
113
186
|
src = SDN::Message.print_address(message.src)
|
114
187
|
# ignore the UAI Plus and ourselves
|
115
188
|
if src != '7F.7F.7F' && !SDN::Message::is_group_address?(message.src) && !(motor = @motors[src.gsub('.', '')])
|
@@ -118,6 +191,7 @@ module SDN
|
|
118
191
|
end
|
119
192
|
|
120
193
|
puts "read #{message.inspect}"
|
194
|
+
follow_ups = []
|
121
195
|
case message
|
122
196
|
when SDN::Message::PostNodeLabel
|
123
197
|
if (motor.publish(:label, message.label))
|
@@ -127,16 +201,32 @@ module SDN
|
|
127
201
|
motor.publish(:positionpercent, message.position_percent)
|
128
202
|
motor.publish(:positionpulses, message.position_pulses)
|
129
203
|
motor.publish(:ip, message.ip)
|
204
|
+
motor.group_objects.each do |group|
|
205
|
+
positions = group.motor_objects.map(&:positionpercent)
|
206
|
+
position = nil
|
207
|
+
# calculate an average, but only if we know a position for
|
208
|
+
# every shade
|
209
|
+
if !positions.include?(:nil) && !positions.include?(nil)
|
210
|
+
position = positions.inject(&:+) / positions.length
|
211
|
+
end
|
212
|
+
|
213
|
+
group.publish(:positionpercent, position)
|
214
|
+
end
|
130
215
|
when SDN::Message::PostMotorStatus
|
131
216
|
if message.state == :running || motor.state == :running
|
132
|
-
|
217
|
+
follow_ups << SDN::Message::GetMotorStatus.new(message.src)
|
133
218
|
end
|
134
219
|
# this will do one more position request after it stopped
|
135
|
-
|
220
|
+
follow_ups << SDN::Message::GetMotorPosition.new(message.src)
|
136
221
|
motor.publish(:state, message.state)
|
137
222
|
motor.publish(:last_direction, message.last_direction)
|
138
223
|
motor.publish(:last_action_source, message.last_action_source)
|
139
224
|
motor.publish(:last_action_cause, message.last_action_cause)
|
225
|
+
motor.group_objects.each do |group|
|
226
|
+
states = group.motor_objects.map(&:state).uniq
|
227
|
+
state = states.length == 1 ? states.first : 'mixed'
|
228
|
+
group.publish(:state, state)
|
229
|
+
end
|
140
230
|
when SDN::Message::PostMotorLimits
|
141
231
|
motor.publish(:uplimit, message.up_limit)
|
142
232
|
motor.publish(:downlimit, message.down_limit)
|
@@ -147,12 +237,23 @@ module SDN
|
|
147
237
|
motor.publish(:downspeed, message.down_speed)
|
148
238
|
motor.publish(:slowspeed, message.slow_speed)
|
149
239
|
when SDN::Message::PostMotorIP
|
150
|
-
motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
|
240
|
+
motor.publish(:"ip#{message.ip}pulses", message.position_pulses)
|
151
241
|
motor.publish(:"ip#{message.ip}percent", message.position_percent)
|
152
242
|
when SDN::Message::PostGroupAddr
|
153
243
|
motor.add_group(message.group_index, message.group_address)
|
154
244
|
end
|
155
245
|
|
246
|
+
@mutex.synchronize do
|
247
|
+
signal = @response_pending || !follow_ups.empty?
|
248
|
+
@response_pending = @broadcast_pending
|
249
|
+
follow_ups.each do |follow_up|
|
250
|
+
@request_queue.push(follow_up) unless @request_queue.include?(follow_up)
|
251
|
+
end
|
252
|
+
@cond.signal if signal
|
253
|
+
end
|
254
|
+
rescue EOFError
|
255
|
+
puts "EOF reading"
|
256
|
+
exit 2
|
156
257
|
rescue SDN::MalformedMessage => e
|
157
258
|
puts "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
|
158
259
|
rescue => e
|
@@ -162,13 +263,51 @@ module SDN
|
|
162
263
|
end
|
163
264
|
|
164
265
|
write_thread = Thread.new do
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
266
|
+
begin
|
267
|
+
loop do
|
268
|
+
message = nil
|
269
|
+
@mutex.synchronize do
|
270
|
+
# got woken up early by another command getting queued; spin
|
271
|
+
if @response_pending
|
272
|
+
while @response_pending
|
273
|
+
remaining_wait = @response_pending - Time.now.to_f
|
274
|
+
if remaining_wait < 0
|
275
|
+
puts "timed out waiting on response"
|
276
|
+
@response_pending = nil
|
277
|
+
@broadcast_pending = nil
|
278
|
+
else
|
279
|
+
@cond.wait(@mutex, remaining_wait)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
else
|
283
|
+
sleep 0.1
|
284
|
+
end
|
285
|
+
|
286
|
+
message = @command_queue.shift
|
287
|
+
unless message
|
288
|
+
message = @request_queue.shift
|
289
|
+
if message
|
290
|
+
@response_pending = Time.now.to_f + WAIT_TIME
|
291
|
+
if message.dest == BROADCAST_ADDRESS || SDN::Message::is_group_address?(message.src) && message.is_a?(SDN::Message::GetNodeAddr)
|
292
|
+
@broadcast_pending = Time.now.to_f + BROADCAST_WAIT
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# spin until there is a message
|
298
|
+
@cond.wait(@mutex) unless message
|
299
|
+
end
|
300
|
+
next unless message
|
301
|
+
|
302
|
+
puts "writing #{message.inspect}"
|
303
|
+
serialized = message.serialize
|
304
|
+
@sdn.write(serialized)
|
305
|
+
@sdn.flush
|
306
|
+
puts "wrote #{serialized.unpack("C*").map { |b| '%02x' % b }.join(' ')}"
|
307
|
+
end
|
308
|
+
rescue => e
|
309
|
+
puts "failure writing: #{e}"
|
310
|
+
exit 1
|
172
311
|
end
|
173
312
|
end
|
174
313
|
|
@@ -176,8 +315,11 @@ module SDN
|
|
176
315
|
puts "got #{value.inspect} at #{topic}"
|
177
316
|
if topic == "#{@base_topic}/discovery/discover/set" && value == "true"
|
178
317
|
# trigger discovery
|
179
|
-
@
|
180
|
-
|
318
|
+
@mutex.synchronize do
|
319
|
+
@request_queue.push(SDN::Message::GetNodeAddr.new)
|
320
|
+
@cond.signal
|
321
|
+
end
|
322
|
+
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$}))
|
181
323
|
addr = SDN::Message.parse_address(match[:addr])
|
182
324
|
property = match[:property]
|
183
325
|
# not homie compliant; allows linking the positionpercent property
|
@@ -186,10 +328,15 @@ module SDN
|
|
186
328
|
property = value.downcase
|
187
329
|
value = "true"
|
188
330
|
end
|
189
|
-
|
331
|
+
mqtt_addr = SDN::Message.print_address(addr).gsub('.', '')
|
332
|
+
motor = @motors[mqtt_addr]
|
190
333
|
is_group = SDN::Message.is_group_address?(addr)
|
334
|
+
group = @groups[mqtt_addr]
|
191
335
|
follow_up = SDN::Message::GetMotorStatus.new(addr)
|
192
336
|
message = case property
|
337
|
+
when 'discover'
|
338
|
+
follow_up = nil
|
339
|
+
SDN::Message::GetNodeAddr.new(addr) if value == "true"
|
193
340
|
when 'label'
|
194
341
|
follow_up = SDN::Message::GetNodeLabel.new(addr)
|
195
342
|
SDN::Message::SetNodeLabel.new(addr, value) unless is_group
|
@@ -249,13 +396,18 @@ module SDN
|
|
249
396
|
next if is_group
|
250
397
|
next unless motor
|
251
398
|
messages = motor.set_groups(value)
|
252
|
-
|
399
|
+
@mutex.synchronize do
|
400
|
+
messages.each { |m| @command_queue.push(m) }
|
401
|
+
@cond.signal
|
402
|
+
end
|
253
403
|
nil
|
254
404
|
end
|
255
405
|
if message
|
256
|
-
@
|
257
|
-
|
258
|
-
|
406
|
+
@mutex.synchronize do
|
407
|
+
@command_queue.push(message)
|
408
|
+
@request_queue.push(follow_up) unless @request_queue.include?(follow_up)
|
409
|
+
@cond.signal
|
410
|
+
end
|
259
411
|
end
|
260
412
|
end
|
261
413
|
end
|
@@ -269,6 +421,16 @@ module SDN
|
|
269
421
|
@mqtt.subscribe("#{@base_topic}/#{topic}")
|
270
422
|
end
|
271
423
|
|
424
|
+
def enqueue(message, queue = :command)
|
425
|
+
@mutex.synchronize do
|
426
|
+
queue = instance_variable_get(:"#{@queue}_queue")
|
427
|
+
unless queue.include?(message)
|
428
|
+
queue.push(message)
|
429
|
+
@cond.signal
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
272
434
|
def publish_basic_attributes
|
273
435
|
publish("$homie", "v4.0.0")
|
274
436
|
publish("$name", "Somfy SDN Network")
|
@@ -284,7 +446,7 @@ module SDN
|
|
284
446
|
publish("discovery/discover/$settable", "true")
|
285
447
|
publish("discovery/discover/$retained", "false")
|
286
448
|
|
287
|
-
subscribe("
|
449
|
+
subscribe("+/discover/set")
|
288
450
|
subscribe("+/label/set")
|
289
451
|
subscribe("+/down/set")
|
290
452
|
subscribe("+/up/set")
|
@@ -312,7 +474,12 @@ module SDN
|
|
312
474
|
def publish_motor(addr)
|
313
475
|
publish("#{addr}/$name", addr)
|
314
476
|
publish("#{addr}/$type", "Sonesse 30 Motor")
|
315
|
-
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")
|
477
|
+
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")
|
478
|
+
|
479
|
+
publish("#{addr}/discover/$name", "Trigger Motor Discovery")
|
480
|
+
publish("#{addr}/discover/$datatype", "boolean")
|
481
|
+
publish("#{addr}/discover/$settable", "true")
|
482
|
+
publish("#{addr}/discover/$retained", "false")
|
316
483
|
|
317
484
|
publish("#{addr}/label/$name", "Node label")
|
318
485
|
publish("#{addr}/label/$datatype", "string")
|
@@ -432,28 +599,37 @@ module SDN
|
|
432
599
|
|
433
600
|
motor = Motor.new(self, addr)
|
434
601
|
@motors[addr] = motor
|
435
|
-
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.
|
602
|
+
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
|
436
603
|
|
437
604
|
sdn_addr = SDN::Message.parse_address(addr)
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
605
|
+
@mutex.synchronize do
|
606
|
+
@request_queue.push(SDN::Message::GetNodeLabel.new(sdn_addr))
|
607
|
+
@request_queue.push(SDN::Message::GetMotorStatus.new(sdn_addr))
|
608
|
+
@request_queue.push(SDN::Message::GetMotorLimits.new(sdn_addr))
|
609
|
+
@request_queue.push(SDN::Message::GetMotorDirection.new(sdn_addr))
|
610
|
+
@request_queue.push(SDN::Message::GetMotorRollingSpeed.new(sdn_addr))
|
611
|
+
(1..16).each { |ip| @request_queue.push(SDN::Message::GetMotorIP.new(sdn_addr, ip)) }
|
612
|
+
(0...16).each { |g| @request_queue.push(SDN::Message::GetGroupAddr.new(sdn_addr, g)) }
|
613
|
+
|
614
|
+
@cond.signal
|
615
|
+
end
|
446
616
|
|
447
617
|
motor
|
448
618
|
end
|
449
619
|
|
450
620
|
def add_group(addr)
|
451
621
|
addr = addr.gsub('.', '')
|
452
|
-
|
622
|
+
group = @groups[addr]
|
623
|
+
return group if group
|
453
624
|
|
454
625
|
publish("#{addr}/$name", addr)
|
455
626
|
publish("#{addr}/$type", "Shade Group")
|
456
|
-
publish("#{addr}/$properties", "down,up,stop,positionpulses,positionpercent,ip,wink,reset")
|
627
|
+
publish("#{addr}/$properties", "discover,down,up,stop,positionpulses,positionpercent,ip,wink,reset,state,motors")
|
628
|
+
|
629
|
+
publish("#{addr}/discover/$name", "Trigger Motor Discovery")
|
630
|
+
publish("#{addr}/discover/$datatype", "boolean")
|
631
|
+
publish("#{addr}/discover/$settable", "true")
|
632
|
+
publish("#{addr}/discover/$retained", "false")
|
457
633
|
|
458
634
|
publish("#{addr}/down/$name", "Move in down direction")
|
459
635
|
publish("#{addr}/down/$datatype", "boolean")
|
@@ -468,7 +644,7 @@ module SDN
|
|
468
644
|
publish("#{addr}/stop/$name", "Cancel adjustments")
|
469
645
|
publish("#{addr}/stop/$datatype", "boolean")
|
470
646
|
publish("#{addr}/stop/$settable", "true")
|
471
|
-
publish("#{addr}/stop/$retained", "false")
|
647
|
+
publish("#{addr}/stop/$retained", "false")
|
472
648
|
|
473
649
|
publish("#{addr}/positionpulses/$name", "Position from up limit (in pulses)")
|
474
650
|
publish("#{addr}/positionpulses/$datatype", "integer")
|
@@ -492,8 +668,16 @@ module SDN
|
|
492
668
|
publish("#{addr}/wink/$settable", "true")
|
493
669
|
publish("#{addr}/wink/$retained", "false")
|
494
670
|
|
495
|
-
|
496
|
-
publish("
|
671
|
+
publish("#{addr}/state/$name", "State of the motors; only set if all motors are in the same state")
|
672
|
+
publish("#{addr}/state/$datatype", "enum")
|
673
|
+
publish("#{addr}/state/$format", SDN::Message::PostMotorStatus::STATE.keys.join(','))
|
674
|
+
|
675
|
+
publish("#{addr}/motors/$name", "Motors that are members of this group")
|
676
|
+
publish("#{addr}/motors/$datatype", "string")
|
677
|
+
|
678
|
+
group = @groups[addr] = Group.new(self, addr)
|
679
|
+
publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
|
680
|
+
group
|
497
681
|
end
|
498
682
|
end
|
499
683
|
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.8
|
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-07-07 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
|