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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61d3c1baa4f63612972ab6ba2cfc05aed5173176101a8d19e2e53756f2e4ec2b
4
- data.tar.gz: 6c7999e7755a4d17b304da25d5b70e864a737efde7bb6b7506a309057fae0137
3
+ metadata.gz: 66516a3bb03695b73075a496a791502683ec12052219a5cf040cfe6e9f82b04f
4
+ data.tar.gz: bad8e87688534b118e679b4126490f5eab3c6e83ee7288184368e852bf0cc450
5
5
  SHA512:
6
- metadata.gz: 75d5b1432d83f8713f81738e2ae99127be390465938f38274a026e8fe3c44cd45022cf74814e04a005c2a3b122b6735fb6424b0840cf8c1a82abff8a092a0a41
7
- data.tar.gz: c253d1113a568f14209d2521ace015ccb8f7f0c1665f43dd5b64c29d601a5a163dc97c504c20d4a5c17e6c789cb9c51165f0108ba9e92dd8427677c48c252b51
6
+ metadata.gz: db18252c03d784aeb3461dacffc16dae04aeb176f5b98f3b796d395f95980350576075a434794e16347eef7bfb308eccf7e4307abeceb4675254a3505d74b60d
7
+ data.tar.gz: e17af388921091abe51a23b6fcef7c3bf0d402ad55446bfc86aa698482a22cca341f7612b73d04e97ad91e060ff13f7573016e4ddb7aca24002c69f1ee9706b9
@@ -0,0 +1,6 @@
1
+ require 'sdn/message'
2
+ require 'sdn/mqtt_bridge'
3
+
4
+ module SDN
5
+ BROADCAST_ADDRESS = [0xff, 0xff, 0xff]
6
+ end
@@ -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 readpartial(io, length, allow_empty: true)
9
- data = []
10
- while data.length < length
11
- begin
12
- data.concat(io.read_nonblock(length - data.length).bytes)
13
- rescue EOFError
14
- break
15
- rescue IO::WaitReadable
16
- break if allow_empty
17
- IO.select([io])
18
- end
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
- def parse(io)
24
- io = StringIO.new(io) if io.is_a?(String)
25
- data = readpartial(io, 2, allow_empty: false)
26
- if data.length != 2
27
- # don't have enough data yet; buffer it
28
- io.ungetbyte(data.first) if data.length == 1
29
- raise MalformedMessage, "Could not get message type and length"
30
- end
31
- msg = to_number(data.first)
32
- length = to_number(data.last)
33
- ack_requested = length & 0x80 == 0x80
34
- length &= 0x7f
35
- if length < 11 || length > 32
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
@@ -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 || value >= 0x0a && value <= 0xff
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
 
@@ -43,7 +43,7 @@ module SDN
43
43
  end
44
44
 
45
45
  def checksum(bytes)
46
- result = bytes.sum
46
+ result = bytes.inject(&:+)
47
47
  [result >> 8, result & 0xff]
48
48
  end
49
49
  end
@@ -0,0 +1,9 @@
1
+ module SDN
2
+ class Message
3
+ module ILT2
4
+ class GetMotorPosition < SimpleRequest
5
+ MSG = 0x44
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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
@@ -1,8 +1,34 @@
1
1
  require 'mqtt'
2
- require 'serialport'
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
- def initialize(*)
56
- members.each { |k| self[k] = :nil }
57
- @groups = [].fill(nil, 0, 16)
58
- super
59
- end
60
-
61
- def publish(attribute, value)
62
- if self[attribute] != value
63
- bridge.publish("#{addr}/#{attribute}", value.to_s)
64
- self[attribute] = value
65
- end
66
- end
67
-
68
- def add_group(index, address)
69
- bridge.add_group(SDN::Message.print_address(address)) if address
70
- @groups[index] = address
71
- publish(:groups, groups_string)
72
- end
73
-
74
- def set_groups(groups)
75
- return unless groups =~ /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i
76
- groups = groups.split(',').sort.uniq.map { |g| SDN::Message.parse_address(g) }
77
- groups.fill(nil, groups.length, 16 - groups.length)
78
- messages = []
79
- sdn_addr = SDN::Message.parse_address(addr)
80
- groups.each_with_index do |g, i|
81
- if @groups[i] != g
82
- messages << SDN::Message::SetGroupAddr.new(sdn_addr, i, g)
83
- messages << SDN::Message::GetGroupAddr.new(sdn_addr, i)
84
- end
85
- end
86
- messages
87
- end
88
-
89
- def groups_string
90
- @groups.compact.map { |g| SDN::Message.print_address(g) }.sort.uniq.join(',')
91
- end
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
- def initialize(mqtt_uri, serialport, device_id: "somfy", base_topic: "homie")
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 = Set.new
102
- @write_queue = Queue.new
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
- @sdn = SerialPort.open(serialport, "baud" => 4800, "parity" => SerialPort::ODD)
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(@sdn)
112
- next unless message
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
- @write_queue.push(SDN::Message::GetMotorStatus.new(message.src))
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
- @write_queue.push(SDN::Message::GetMotorPosition.new(message.src))
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
- loop do
166
- message = @write_queue.pop
167
- puts "writing #{message.inspect}"
168
- @sdn.write(message.serialize)
169
- # give more response time to a discovery message
170
- sleep 5 if (message.is_a?(SDN::Message::GetNodeAddr) && message.dest == [0xff, 0xff, 0xff])
171
- sleep 0.1
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
- @write_queue.push(SDN::Message::GetNodeAddr.new)
180
- 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$}))
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
- motor = @motors[SDN::Message.print_address(addr).gsub('.', '')]
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
- messages.each { |m| @write_queue.push(m) }
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
- @write_queue.push(message)
257
- next if follow_up.is_a?(SDN::Message::GetMotorStatus) && motor&.state == :running
258
- @write_queue.push(follow_up)
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("discovery/discover/set")
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.to_a).join(","))
602
+ publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.keys.sort).join(","))
436
603
 
437
604
  sdn_addr = SDN::Message.parse_address(addr)
438
- # these messages are often corrupt; just don't bother for now.
439
- #@write_queue.push(SDN::Message::GetNodeLabel.new(sdn_addr))
440
- @write_queue.push(SDN::Message::GetMotorStatus.new(sdn_addr))
441
- @write_queue.push(SDN::Message::GetMotorLimits.new(sdn_addr))
442
- @write_queue.push(SDN::Message::GetMotorDirection.new(sdn_addr))
443
- @write_queue.push(SDN::Message::GetMotorRollingSpeed.new(sdn_addr))
444
- (1..16).each { |ip| @write_queue.push(SDN::Message::GetMotorIP.new(sdn_addr, ip)) }
445
- (0...16).each { |g| @write_queue.push(SDN::Message::GetGroupAddr.new(sdn_addr, g)) }
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
- return if @groups.include?(addr)
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
- @groups << addr
496
- publish("$nodes", (["discovery"] + @motors.keys.sort + @groups.to_a).join(","))
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
@@ -1,3 +1,3 @@
1
1
  module SDN
2
- VERSION = '1.0.3'
2
+ VERSION = '1.0.8'
3
3
  end
@@ -1,2 +1 @@
1
- require 'sdn/message'
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.3
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-05-12 00:00:00.000000000 Z
11
+ date: 2020-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: serialport
14
+ name: mqtt
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.3.1
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: 1.3.1
26
+ version: 0.5.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: mqtt
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.5.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.5.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