somfy_sdn 2.4.0 → 2.5.0

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.
@@ -4,9 +4,28 @@ require "curses"
4
4
 
5
5
  module SDN
6
6
  module CLI
7
+ # @!visibility private
7
8
  class Provisioner
8
- attr_reader :win, :sdn, :addr, :ns
9
+ # @return [Curses::Window]
10
+ attr_reader :win
9
11
 
12
+ # @return [SDN::Client]
13
+ attr_reader :sdn
14
+
15
+ # Selected motor address as a three-byte array.
16
+ #
17
+ # @return [(Integer, Integer, Integer)]
18
+ attr_reader :addr
19
+
20
+ # Message namespace used for the current motor type (regular or ILT2).
21
+ #
22
+ # @return [Module]
23
+ attr_reader :ns
24
+
25
+ # Builds the interactive provisioning UI for a discovered motor.
26
+ #
27
+ # @param sdn [SDN::Client] SDN client connection
28
+ # @param addr [String, nil] optional motor address
10
29
  def initialize(sdn, addr = nil)
11
30
  @sdn = sdn
12
31
  @reversed = false
@@ -71,6 +90,11 @@ module SDN
71
90
  end
72
91
  end
73
92
 
93
+ private
94
+
95
+ # Runs the key-driven event loop.
96
+ #
97
+ # @return [void]
74
98
  def process
75
99
  win.keypad = true
76
100
  print_help
@@ -188,6 +212,9 @@ module SDN
188
212
  win.refresh
189
213
  end
190
214
 
215
+ # Polls the motor until movement stops, allowing escape to interrupt.
216
+ #
217
+ # @return [void]
191
218
  def wait_for_stop
192
219
  win.setpos(13, 0)
193
220
  win.addstr("Moving...\n")
@@ -224,6 +251,9 @@ module SDN
224
251
  end
225
252
  end
226
253
 
254
+ # Refreshes the displayed motor position, limits, and direction state.
255
+ #
256
+ # @return [void]
227
257
  def refresh
228
258
  pos = sdn.ensure(ns::GetMotorPosition.new(addr))
229
259
  @pos = pos.position_pulses
@@ -256,6 +286,9 @@ module SDN
256
286
  ns == Message::ILT2
257
287
  end
258
288
 
289
+ # Returns the reverse-direction flag as an integer for ILT2 settings writes.
290
+ #
291
+ # @return [Integer]
259
292
  def reversed_int
260
293
  @reversed ? 1 : 0
261
294
  end
@@ -2,26 +2,63 @@
2
2
 
3
3
  module SDN
4
4
  module CLI
5
+ # @!visibility private
5
6
  class Simulator
7
+ # Simulated motor implementation that responds to a subset of SDN requests.
6
8
  class MockMotor
7
- attr_accessor :address,
8
- :node_type,
9
- :label,
10
- :ips,
11
- :position_pulses,
12
- :up_limit,
13
- :down_limit,
14
- :groups,
15
- :network_lock_priority,
16
- :lock_priority,
17
- :ir_channels
9
+ # Simulated motor address.
10
+ #
11
+ # @return [(Integer, Integer, Integer)]
12
+ attr_accessor :address
18
13
 
14
+ # @return [:st30, :lt50]
15
+ attr_accessor :node_type
16
+
17
+ # @return [String]
18
+ attr_accessor :label
19
+
20
+ # Stored intermediate positions keyed by slot index.
21
+ #
22
+ # @return [<Integer, nil>]
23
+ attr_accessor :ips
24
+
25
+ # Current motor position in pulses from the upper limit.
26
+ #
27
+ # @return [Integer, nil]
28
+ attr_accessor :position_pulses
29
+
30
+ # @return [Integer, nil]
31
+ attr_accessor :up_limit
32
+
33
+ # @return [Integer, nil]
34
+ attr_accessor :down_limit
35
+
36
+ # Stored group memberships keyed by slot index.
37
+ #
38
+ # @return [<(Integer, Integer, Integer), nil>]
39
+ attr_accessor :groups
40
+
41
+ # @return [Integer, nil]
42
+ attr_accessor :network_lock_priority
43
+
44
+ # @return [Integer, nil]
45
+ attr_accessor :lock_priority
46
+
47
+ # Simulated ILT2 IR channel bitmask.
48
+ #
49
+ # @return [Integer]
50
+ attr_accessor :ir_channels
51
+
52
+ # Absolute move target types supported by the simulator implementation.
19
53
  ALLOWED_MOVE_TYPES = %i[up_limit
20
54
  down_limit
21
55
  ip
22
56
  position_pulses
23
57
  position_percent].freeze
24
58
 
59
+ # Creates a simulated motor backed by an SDN client.
60
+ #
61
+ # @param client [SDN::Client] SDN client used to receive and send messages
25
62
  def initialize(client)
26
63
  @client = client
27
64
  self.address = Message.parse_address("00.00.00")
@@ -33,6 +70,11 @@ module SDN
33
70
  self.lock_priority = 0
34
71
  end
35
72
 
73
+ private
74
+
75
+ # Starts processing incoming SDN messages for the simulated motor.
76
+ #
77
+ # @return [void]
36
78
  def process
37
79
  loop do
38
80
  @client.receive do |message|
@@ -191,20 +233,41 @@ module SDN
191
233
  end
192
234
  end
193
235
 
236
+ # Converts a pulse position to a percentage using the configured down limit.
237
+ #
238
+ # @param pulses [Integer, nil] position in pulses
239
+ #
240
+ # @return [Numeric, nil]
194
241
  def to_percent(pulses)
195
242
  (pulses && down_limit) ? 100.0 * pulses / down_limit : nil
196
243
  end
197
244
 
245
+ # Converts a percentage to a pulse position using the configured down limit.
246
+ #
247
+ # @param percent [Numeric, nil] position in percent
248
+ #
249
+ # @return [Numeric, nil]
198
250
  def to_pulses(percent)
199
251
  (percent && down_limit) ? down_limit * percent / 100 : nil
200
252
  end
201
253
 
254
+ # Sends an acknowledgement for the supplied message when one was requested.
255
+ #
256
+ # @param message [SDN::Message] request message
257
+ #
258
+ # @return [void]
202
259
  def ack(message)
203
260
  return unless message.ack_requested
204
261
 
205
262
  respond(Message::Ack.new(message.dest))
206
263
  end
207
264
 
265
+ # Sends a negative acknowledgement for the supplied message when one was requested.
266
+ #
267
+ # @param message [SDN::Message] request message
268
+ # @param _error_code [nil] reserved error code argument
269
+ #
270
+ # @return [void]
208
271
  def nack(message, _error_code = nil)
209
272
  return unless message.ack_requested
210
273
 
@@ -219,6 +282,10 @@ module SDN
219
282
  end
220
283
  end
221
284
 
285
+ # Starts a simulator instance for a single mock motor.
286
+ #
287
+ # @param sdn [SDN::Client] SDN client connection
288
+ # @param address [String, nil] optional simulated motor address
222
289
  def initialize(sdn, address = nil)
223
290
  motor = MockMotor.new(sdn)
224
291
  motor.address = Message.parse_address(address) if address
data/lib/sdn/cli.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SDN
4
+ # @!visibility private
5
+ module CLI; end
6
+ end
data/lib/sdn/client.rb CHANGED
@@ -3,15 +3,22 @@
3
3
  require "io/wait"
4
4
 
5
5
  module SDN
6
+ # Transport client for sending and receiving SDN frames over serial, TCP, RFC2217, or PTY.
6
7
  class Client
8
+ # Enables or disables raw byte tracing in the client logs.
9
+ #
10
+ # @return [true, false]
7
11
  attr_writer :trace
8
12
 
13
+ # Opens a connection to an SDN bus over serial, TCP, RFC2217, or PTY.
14
+ #
15
+ # @param port [String] connection target URI or serial device path
9
16
  def initialize(port)
10
17
  uri = URI.parse(port)
11
18
  @io = if uri.scheme == "tcp"
12
19
  require "socket"
13
20
  TCPSocket.new(uri.host, uri.port)
14
- elsif uri.scheme == "telnet" || uri.scheme == "rfc2217"
21
+ elsif %w[telnet rfc2217].include?(uri.scheme)
15
22
  require "net/telnet/rfc2217"
16
23
  Net::Telnet::RFC2217.new(host: uri.host,
17
24
  port: uri.port || 23,
@@ -24,6 +31,10 @@ module SDN
24
31
  io, slave = PTY.open
25
32
  puts "Slave PTY available at #{slave.path}"
26
33
  io
34
+ elsif uri.scheme == "esphome"
35
+ gem "esphome", "~> 1.1"
36
+ require "esphome/serial_proxy"
37
+ ESPHome::SerialProxy.open(uri, baud: 4800, data_bits: 8, parity: :odd, stop_bits: 1)
27
38
  else
28
39
  require "ccutrer-serialport"
29
40
  CCutrer::SerialPort.new(port, baud: 4800, data_bits: 8, parity: :odd, stop_bits: 1)
@@ -31,21 +42,41 @@ module SDN
31
42
  @buffer = +""
32
43
  end
33
44
 
45
+ # Indicates whether raw byte tracing is enabled.
46
+ #
47
+ # @return [true, false]
34
48
  def trace?
35
49
  @trace
36
50
  end
37
51
 
52
+ # Serializes and writes a message to the underlying transport.
53
+ #
54
+ # @param message [SDN::Message]
55
+ #
56
+ # @return [void]
38
57
  def send(message)
39
58
  SDN.logger.debug("Sending #{message.inspect}")
40
59
  @io.write(message.serialize)
41
60
  end
42
61
 
62
+ # Sends a message with acknowledgement requested and waits for an initial response batch.
63
+ #
64
+ # @param message [SDN::Message]
65
+ #
66
+ # @return [<SDN::Message>] received response messages
43
67
  def transact(message)
44
68
  message.ack_requested = true
45
69
  send(message)
46
70
  receive(1)
47
71
  end
48
72
 
73
+ # Re-sends a request until an expected response is received.
74
+ #
75
+ # The expected response message is dependent on the message sent.
76
+ #
77
+ # @param message [SDN::Message]
78
+ #
79
+ # @return [SDN::Message] the message that is of the expected response type
49
80
  def ensure(message)
50
81
  loop do
51
82
  messages = transact(message)
@@ -56,15 +87,24 @@ module SDN
56
87
  end
57
88
  end
58
89
 
90
+ # Default inter-read wait time, in seconds, while assembling a partial frame.
59
91
  WAIT_TIME = 0.25
60
92
 
93
+ # Reads and parses messages from the transport until parsing is exhausted or a timeout occurs.
94
+ #
95
+ # @param timeout [Numeric, nil] optional read timeout in seconds
96
+ #
97
+ # @yield [message] yields each parsed message when a block is given
98
+ # @yieldparam message [SDN::Message] parsed message
99
+ #
100
+ # @return [<SDN::Message>] parsed messages when no block is given
61
101
  def receive(timeout = nil)
62
102
  messages = []
63
103
 
64
104
  loop do
65
105
  message, bytes_read = Message.parse(@buffer.bytes)
66
106
  # discard how much we read
67
- @buffer = @buffer[bytes_read..-1] if bytes_read
107
+ @buffer = @buffer[bytes_read..] if bytes_read
68
108
  unless message
69
109
  break unless messages.empty?
70
110
 
@@ -2,13 +2,37 @@
2
2
 
3
3
  module SDN
4
4
  class Message
5
+ # Command that applies or clears a positional motor lock.
5
6
  class Lock < Message
7
+ # (see Ack::MSG)
6
8
  MSG = 0x06
9
+ # (see Ack::PARAMS_LENGTH)
7
10
  PARAMS_LENGTH = 5
11
+ # Mapping from symbolic lock targets to SDN parameter values.
8
12
  TARGET_TYPE = { current: 0, up_limit: 1, down_limit: 2, ip: 4, unlock: 5, position_percent: 7 }.freeze
9
13
 
10
- attr_reader :target_type, :target, :priority
11
-
14
+ # @!attribute [rw] target_type
15
+ #
16
+ # @return [:current, :up_limit, :down_limit, :ip, :unlock, :position_percent]
17
+ attr_reader :target_type
18
+
19
+ # @!attribute [rw] target
20
+ #
21
+ # @return [Integer, nil]
22
+ attr_reader :target
23
+
24
+ # Requested lock priority.
25
+ #
26
+ # @return [Integer]
27
+ attr_reader :priority
28
+
29
+ # Creates a lock or unlock control message.
30
+ #
31
+ # @param dest [(Integer, Integer, Integer), nil] destination address
32
+ # @param target_type [:current, :up_limit, :down_limit, :ip, :unlock, :position_percent] lock target selector
33
+ # @param target [Integer, nil] lock target value
34
+ # @param priority [Integer] lock priority
35
+ # @param kwargs [Hash] additional message options
12
36
  def initialize(dest = nil, target_type = :unlock, target = nil, priority = 1, **kwargs)
13
37
  kwargs[:dest] ||= dest
14
38
  super(**kwargs)
@@ -34,6 +58,7 @@ module SDN
34
58
  @priority = value & 0xff
35
59
  end
36
60
 
61
+ # (see Message#parse)
37
62
  def parse(params)
38
63
  super
39
64
  self.target_type = TARGET_TYPE.invert[to_number(params[0])]
@@ -42,21 +67,50 @@ module SDN
42
67
  self.priority = to_number(params[3])
43
68
  end
44
69
 
70
+ # (see Message#params)
45
71
  def params
46
72
  transform_param(TARGET_TYPE[target_type]) + from_number(target,
47
73
  2) + transform_param(priority) + transform_param(0)
48
74
  end
49
75
  end
50
76
 
51
- # Move in momentary mode
77
+ # Command that moves a motor in momentary mode.
52
78
  class Move < Message
79
+ # (see Ack::MSG)
53
80
  MSG = 0x01
81
+ # (see Ack::PARAMS_LENGTH)
54
82
  PARAMS_LENGTH = 3
83
+ # Mapping from symbolic directions to SDN parameter values.
55
84
  DIRECTION = { down: 0x00, up: 0x01, cancel: 0x02 }.freeze
85
+ # Mapping from symbolic speed modes to SDN parameter values.
56
86
  SPEED = { up: 0x00, down: 0x01, slow: 0x02 }.freeze
57
87
 
58
- attr_reader :direction, :duration, :speed
59
-
88
+ # @!attribute [rw] direction
89
+ #
90
+ # @return [:down, :up, :cancel]
91
+ attr_reader :direction
92
+
93
+ # @!attribute [rw] duration
94
+ #
95
+ # Requested movement duration in protocol units.
96
+ #
97
+ # @return [Integer, nil]
98
+ attr_reader :duration
99
+
100
+ # @!attribute [rw] speed
101
+ #
102
+ # Requested movement speed mode.
103
+ #
104
+ # @return [:up, :down, :slow]
105
+ attr_reader :speed
106
+
107
+ # Creates a momentary movement command.
108
+ #
109
+ # @param dest [(Integer, Integer, Integer), nil] destination address
110
+ # @param direction [:down, :up, :cancel] movement direction
111
+ # @param duration [Integer, nil] duration in protocol units
112
+ # @param speed [:up, :down, :slow] speed selector
113
+ # @param kwargs [Hash] additional message options
60
114
  def initialize(dest = nil, direction = :cancel, duration: nil, speed: :up, **kwargs)
61
115
  kwargs[:dest] ||= dest
62
116
  super(**kwargs)
@@ -65,6 +119,7 @@ module SDN
65
119
  self.speed = speed
66
120
  end
67
121
 
122
+ # (see Message#parse)
68
123
  def parse(params)
69
124
  super
70
125
  self.direction = DIRECTION.invert[to_number(params[0])]
@@ -98,6 +153,7 @@ module SDN
98
153
  @speed = speed
99
154
  end
100
155
 
156
+ # (see Message#params)
101
157
  def params
102
158
  transform_param(DIRECTION[direction]) +
103
159
  transform_param(duration || 0) +
@@ -105,10 +161,13 @@ module SDN
105
161
  end
106
162
  end
107
163
 
108
- # Move relative to current position
164
+ # Command that moves a motor relative to its current position.
109
165
  class MoveOf < Message
166
+ # (see Ack::MSG)
110
167
  MSG = 0x04
168
+ # (see Ack::PARAMS_LENGTH)
111
169
  PARAMS_LENGTH = 4
170
+ # Mapping from symbolic relative targets to SDN parameter values.
112
171
  TARGET_TYPE = { next_ip: 0x00,
113
172
  previous_ip: 0x01,
114
173
  jog_down_pulses: 0x02,
@@ -116,8 +175,32 @@ module SDN
116
175
  jog_down_ms: 0x04,
117
176
  jog_up_ms: 0x05 }.freeze
118
177
 
119
- attr_reader :target_type, :target
120
-
178
+ # @!attribute [rw] target_type
179
+ #
180
+ # Requested relative target selector.
181
+ #
182
+ # @return [:next_ip, :previous_ip, :jog_down_pulses, :jog_up_pulses, :jog_down_ms, :jog_up_ms, nil]
183
+ attr_reader :target_type
184
+
185
+ # @!attribute [rw] target
186
+ #
187
+ # Requested relative target amount in pulses or milliseconds.
188
+ #
189
+ # @return [Integer, nil]
190
+ attr_reader :target
191
+
192
+ # Creates a relative movement command.
193
+ #
194
+ # @param dest [(Integer, Integer, Integer), nil] destination address
195
+ # @param target_type [:next_ip,
196
+ # :previous_ip,
197
+ # :jog_down_pulses,
198
+ # :jog_up_pulses,
199
+ # :jog_down_ms,
200
+ # :jog_up_ms,
201
+ # nil] relative target selector
202
+ # @param target [Integer, nil] pulses or milliseconds
203
+ # @param kwargs [Hash] additional message options
121
204
  def initialize(dest = nil, target_type = nil, target = nil, **kwargs)
122
205
  kwargs[:dest] ||= dest
123
206
  super(**kwargs)
@@ -125,6 +208,7 @@ module SDN
125
208
  self.target = target
126
209
  end
127
210
 
211
+ # (see Message#parse)
128
212
  def parse(params)
129
213
  super
130
214
  self.target_type = TARGET_TYPE.invert[to_number(params[0])]
@@ -147,6 +231,7 @@ module SDN
147
231
  @target = value
148
232
  end
149
233
 
234
+ # (see Message#params)
150
235
  def params
151
236
  param = target || 0xffff
152
237
  param /= 10 if %I[jog_down_ms jog_up_ms].include?(target_type)
@@ -154,15 +239,43 @@ module SDN
154
239
  end
155
240
  end
156
241
 
157
- # Move to absolute position
242
+ # Command that moves a motor to an absolute target.
158
243
  class MoveTo < Message
244
+ # (see Ack::MSG)
159
245
  MSG = 0x03
246
+ # (see Ack::PARAMS_LENGTH)
160
247
  PARAMS_LENGTH = 4
248
+ # Mapping from symbolic absolute targets to SDN parameter values.
161
249
  TARGET_TYPE = { down_limit: 0x00, up_limit: 0x01, ip: 0x02, position_pulses: 0x03, position_percent: 0x04 }.freeze
250
+ # Mapping from symbolic speed modes to SDN parameter values.
162
251
  SPEED = { up: 0x00, down: 0x01, slow: 0x02 }.freeze
163
252
 
164
- attr_reader :target_type, :target, :speed
165
-
253
+ # @!attribute [rw] target_type
254
+ #
255
+ # Requested absolute target selector.
256
+ #
257
+ # @return [:down_limit, :up_limit, :ip, :position_pulses, :position_percent]
258
+ attr_reader :target_type
259
+
260
+ # @!attribute [rw] target
261
+ #
262
+ # @return [Integer, nil]
263
+ attr_reader :target
264
+
265
+ # @!attribute [rw] speed
266
+ #
267
+ # Requested movement speed mode.
268
+ #
269
+ # @return [:up, :down, :slow]
270
+ attr_reader :speed
271
+
272
+ # Creates an absolute movement command.
273
+ #
274
+ # @param dest [(Integer, Integer, Integer), nil] destination address
275
+ # @param target_type [:down_limit, :up_limit, :ip, :position_pulses, :position_percent] absolute target selector
276
+ # @param target [Integer, nil] target value
277
+ # @param speed [:up, :down, :slow] speed selector
278
+ # @param kwargs [Hash] additional message options
166
279
  def initialize(dest = nil, target_type = :down_limit, target = nil, speed = :up, **kwargs)
167
280
  kwargs[:dest] ||= dest
168
281
  super(**kwargs)
@@ -171,6 +284,7 @@ module SDN
171
284
  self.speed = speed
172
285
  end
173
286
 
287
+ # (see Message#parse)
174
288
  def parse(params)
175
289
  super
176
290
  self.target_type = TARGET_TYPE.invert[to_number(params[0])]
@@ -198,27 +312,37 @@ module SDN
198
312
  @speed = value
199
313
  end
200
314
 
315
+ # (see Message#params)
201
316
  def params
202
317
  transform_param(TARGET_TYPE[target_type]) + from_number(target || 0xffff, 2) + transform_param(SPEED[speed])
203
318
  end
204
319
  end
205
320
 
206
- # Stop movement
321
+ # Command that stops motor movement.
207
322
  class Stop < Message
323
+ # (see Ack::MSG)
208
324
  MSG = 0x02
325
+ # (see Ack::PARAMS_LENGTH)
209
326
  PARAMS_LENGTH = 1
210
327
 
328
+ # Creates a stop command for the specified destination.
329
+ #
330
+ # @param dest [(Integer, Integer, Integer), nil] destination address
331
+ # @param kwargs [Hash] additional message options
211
332
  def initialize(dest = nil, **kwargs)
212
333
  kwargs[:dest] ||= dest
213
334
  super(**kwargs)
214
335
  end
215
336
 
337
+ # (see Message#params)
216
338
  def params
217
339
  transform_param(0)
218
340
  end
219
341
  end
220
342
 
343
+ # Command that triggers the motor's wink or identify behavior.
221
344
  class Wink < SimpleRequest
345
+ # (see Ack::MSG)
222
346
  MSG = 0x05
223
347
  end
224
348
  end