somfy_sdn 1.0.3 → 1.0.8

Sign up to get free protection for your applications and to get access to all the features.
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