somfy_sdn 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 43e93c1bd91289025342d8fb230e4fc961a04001eb8a607a0114cf40c92459f3
4
+ data.tar.gz: 1f29a53d8e8f697b0b6933439d42d154e439992567abdcf6f216aa5d3e7dfed3
5
+ SHA512:
6
+ metadata.gz: 13a0bb6280317801ea68d5a87e1fc2105f01fb6f44423cd4e7a9f553c04d2fb8a5d2009ae663b9f46889e7955c3eec6200330ba51abbd0005b4512b00b4041ca
7
+ data.tar.gz: e6bb95d108574b3ef836630721a22bf514a93d17470c6f25ecced7519c3cf646c6129121413e7f263d044b41a275f86f8ebc3050e83866b6b2e278ebc0df491e
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'somfy_sdn'
4
+
5
+ SDN::MQTTBridge.new(ARGV[0], ARGV[1])
@@ -0,0 +1,179 @@
1
+ require 'sdn/messages/helpers'
2
+
3
+ module SDN
4
+ class MalformedMessage < RuntimeError; end
5
+
6
+ class Message
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
19
+ end
20
+ data
21
+ end
22
+
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}"
44
+ 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
81
+ end
82
+ end
83
+
84
+ include Helpers
85
+ singleton_class.include Helpers
86
+
87
+ attr_reader :reserved, :ack_requested, :src, :dest
88
+
89
+ def initialize(reserved: nil, ack_requested: false, src: nil, dest: nil)
90
+ @reserved = reserved || 0x02 # message sent to Sonesse 30
91
+ @ack_requested = ack_requested
92
+ if src.nil? && is_group_address?(dest)
93
+ src = dest
94
+ dest = nil
95
+ end
96
+ @src = src || [0, 0, 1]
97
+ @dest = dest || [0, 0, 0]
98
+ end
99
+
100
+ def parse(params)
101
+ raise MalformedMessage, "unrecognized params for #{self.class.name}: #{params.map { |b| '%02x' % b }}" if self.class.const_defined?(:PARAMS_LENGTH) && params.length != self.class.const_get(:PARAMS_LENGTH)
102
+ end
103
+
104
+ def serialize
105
+ result = transform_param(reserved) + transform_param(src) + transform_param(dest) + params
106
+ length = result.length + 4
107
+ length |= 0x80 if ack_requested
108
+ result = transform_param(self.class.const_get(:MSG)) + transform_param(length) + result
109
+ result.concat(checksum(result))
110
+ puts "wrote #{result.map { |b| '%02x' % b }.join(' ')}"
111
+ result.pack("C*")
112
+ end
113
+
114
+ def inspect
115
+ "#<%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
+ end
117
+
118
+ def class_inspect
119
+ ivars = instance_variables - [:@reserved, :@ack_requested, :@src, :@dest]
120
+ return if ivars.empty?
121
+ ivars.map { |iv| ", #{iv}=#{instance_variable_get(iv).inspect}" }.join
122
+ end
123
+
124
+ protected
125
+
126
+ def params; []; end
127
+
128
+ public
129
+
130
+ class SimpleRequest < Message
131
+ PARAMS_LENGTH = 0
132
+
133
+ def initialize(dest = nil, **kwargs)
134
+ kwargs[:dest] ||= dest
135
+ super(**kwargs)
136
+ end
137
+ end
138
+
139
+ class Nack < Message
140
+ MSG = 0x6f
141
+ PARAMS_LENGTH = 1
142
+ VALUES = { data_error: 0x01, unknown_message: 0x10, node_is_locked: 0x20, wrong_position: 0x21, limits_not_set: 0x22, ip_not_set: 0x23, out_of_range: 0x24, busy: 0xff }
143
+
144
+ # presumed
145
+ attr_accessor :error_code
146
+
147
+ def parse(params)
148
+ super
149
+ error_code = to_number(params[0])
150
+ self.error_code = VALUES[error_code] || error_code
151
+ end
152
+ end
153
+
154
+ class Ack < Message
155
+ MSG = 0x7f
156
+ PARAMS_LENGTH = 0
157
+ end
158
+
159
+ # messages after this point were decoded from UAI+ communication and may be named wrong
160
+ class UnknownMessage < Message
161
+ attr_accessor :msg, :params
162
+
163
+ alias parse params=
164
+
165
+ def class_inspect
166
+ result = ", @msg=%02xh" % msg
167
+ return result if params.empty?
168
+
169
+ result << ", @params=#{params.map { |b| "%02x" % b }.join(' ')}"
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ require 'sdn/messages/control'
177
+ require 'sdn/messages/get'
178
+ require 'sdn/messages/post'
179
+ require 'sdn/messages/set'
@@ -0,0 +1,154 @@
1
+ module SDN
2
+ class Message
3
+ # Move in momentary mode
4
+ class Move < Message
5
+ MSG = 0x01
6
+ PARAMS_LENGTH = 3
7
+ DIRECTION = { down: 0x00, up: 0x01, cancel: 0x02 }.freeze
8
+ SPEED = { up: 0x00, down: 0x01, slow: 0x02 }.freeze
9
+
10
+ attr_reader :direction, :duration, :speed
11
+
12
+ def initialize(dest = nil, direction = :cancel, duration: nil, speed: :up, **kwargs)
13
+ kwargs[:dest] ||= dest
14
+ super(**kwargs)
15
+ self.direction = direction
16
+ self.duration = duration
17
+ self.speed = speed
18
+ end
19
+
20
+ def parse(params)
21
+ super
22
+ self.direction = DIRECTION.invert[to_number(params[0])]
23
+ duration = to_number(params[1])
24
+ duration = nil if duration == 0
25
+ self.duration = duration
26
+ self.speed = SPEED.invert[to_number(params[3])]
27
+ end
28
+
29
+ def direction=(value)
30
+ raise ArgumentError, "direction must be one of :down, :up, or :cancel" unless DIRECTION.keys.include?(value)
31
+ @direction = value
32
+ end
33
+
34
+ def duration=(value)
35
+ raise ArgumentError, "duration must be in range 0x0a to 0xff" unless value || value >= 0x0a && value <= 0xff
36
+ @duration = value
37
+ end
38
+
39
+ def speed=(value)
40
+ raise ArgumentError, "speed must be one of :up, :down, or :slow" unless SPEED.keys.include?(value)
41
+ @speed = speed
42
+ end
43
+
44
+ def params
45
+ transform_param(DIRECTION[direction]) +
46
+ transform_param(duration || 0) +
47
+ transform_param(SPEED[speed])
48
+ end
49
+ end
50
+
51
+ # Stop movement
52
+ class Stop < Message
53
+ MSG = 0x02
54
+ PARAMS_LENGTH = 1
55
+
56
+ def initialize(dest = nil, **kwargs)
57
+ kwargs[:dest] ||= dest
58
+ super(**kwargs)
59
+ end
60
+
61
+ def params
62
+ transform_param(0)
63
+ end
64
+ end
65
+
66
+ # Move to absolute position
67
+ class MoveTo < Message
68
+ MSG = 0x03
69
+ PARAMS_LENGTH = 4
70
+ TARGET_TYPE = { down_limit: 0x00, up_limit: 0x01, ip: 0x02, position_pulses: 0x03, position_percent: 0x04 }.freeze
71
+ SPEED = { up: 0x00, down: 0x01, slow: 0x02 }.freeze
72
+
73
+ attr_reader :target_type, :target, :speed
74
+
75
+ def initialize(dest= nil, target_type = :down_limit, target = nil, speed = :up, **kwargs)
76
+ kwargs[:dest] ||= dest
77
+ super(**kwargs)
78
+ self.target_type = target_type
79
+ self.target = target
80
+ self.speed = speed
81
+ end
82
+
83
+ def parse(params)
84
+ super
85
+ self.target_type = TARGET_TYPE.invert[to_number(params[0])]
86
+ self.target = to_number(params[1..2], nillable: true)
87
+ self.speed = SPEED.invert[to_number(params[3])]
88
+ end
89
+
90
+ def target_type=(value)
91
+ raise ArgumentError, "target_type must be one of :down_limit, :up_limit, :ip, :position_pulses, or :position_percent" unless TARGET_TYPE.keys.include?(value)
92
+ @target_type = value
93
+ end
94
+
95
+ def target=(value)
96
+ value &= 0xffff if value
97
+ @target = value
98
+ end
99
+
100
+ def speed=(value)
101
+ raise ArgumentError, "speed must be one of :up, :down, or :slow" unless SPEED.keys.include?(value)
102
+ @speed = value
103
+ end
104
+
105
+ def params
106
+ transform_param(TARGET_TYPE[target_type]) + from_number(target || 0xffff, 2) + transform_param(SPEED[speed])
107
+ end
108
+ end
109
+
110
+ # Move relative to current position
111
+ class MoveOf < Message
112
+ MSG = 0x04
113
+ PARAMS_LENGTH = 4
114
+ TARGET_TYPE = { next_ip: 0x00, previous_ip: 0x01, jog_down_pulses: 0x02, jog_up_pulses: 0x03, jog_down_ms: 0x04, jog_up_ms: 0x05 }
115
+
116
+ attr_reader :target_type, :target
117
+
118
+ def initialize(dest = nil, target_type = nil, target = nil, **kwargs)
119
+ kwargs[:dest] ||= dest
120
+ super(**kwargs)
121
+ self.target_type = target_type
122
+ self.target = target
123
+ end
124
+
125
+ def parse(params)
126
+ super
127
+ self.target_type = TARGET_TYPE.invert[to_number(params[0])]
128
+ target = to_number(params[1..2], nillable: true)
129
+ target *= 10 if %I{jog_down_ms jog_up_ms}.include?(target_type)
130
+ self.target = target
131
+ end
132
+
133
+ def target_type=(value)
134
+ raise ArgumentError, "target_type must be one of :next_ip, :previous_ip, :jog_down_pulses, :jog_up_pulses, :jog_down_ms, :jog_up_ms" unless value.nil? || TARGET_TYPE.keys.include?(value)
135
+ @target_type = value
136
+ end
137
+
138
+ def target=(value)
139
+ value &= 0xffff if value
140
+ @target = value
141
+ end
142
+
143
+ def params
144
+ param = target || 0xffff
145
+ param /= 10 if %I{jog_down_ms jog_up_ms}.include?(target_type)
146
+ transform_param(TARGET_TYPE[target_type]) + from_number(param, 2) + transform_param(0)
147
+ end
148
+ end
149
+
150
+ class Wink < SimpleRequest
151
+ MSG = 0x05
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,99 @@
1
+ module SDN
2
+ class Message
3
+ class GetMotorPosition < SimpleRequest
4
+ MSG = 0x0c
5
+ end
6
+
7
+ class GetMotorStatus < SimpleRequest
8
+ MSG = 0x0e
9
+ end
10
+
11
+ class GetMotorLimits < SimpleRequest
12
+ MSG = 0x21
13
+ end
14
+
15
+ class GetMotorDirection < SimpleRequest
16
+ MSG = 0x22
17
+ end
18
+
19
+ class GetMotorRollingSpeed < SimpleRequest
20
+ MSG = 0x23
21
+ end
22
+
23
+ class GetMotorIP < Message
24
+ MSG = 0x25
25
+ PARAMS_LENGTH = 1
26
+
27
+ attr_reader :ip
28
+
29
+ def initialize(dest = nil, ip = nil, **kwargs)
30
+ kwargs[:dest] ||= dest
31
+ super(**kwargs)
32
+ self.ip = ip
33
+ end
34
+
35
+ def parse(params)
36
+ super
37
+ self.ip = to_number(params[0], nillable: true)
38
+ end
39
+
40
+ def ip=(value)
41
+ raise ArgumentError, "invalid IP #{ip} (should be 1-16)" unless ip.nil? || (1..16).include?(ip)
42
+ @ip = value
43
+ end
44
+
45
+ def params
46
+ transform_param(@ip || 0xff)
47
+ end
48
+ end
49
+
50
+ class GetGroupAddr < Message
51
+ MSG = 0x41
52
+ PARAMS_LENGTH = 1
53
+
54
+ attr_reader :group_index
55
+
56
+ def initialize(dest = nil, group_index = 0, **kwargs)
57
+ kwargs[:dest] ||= dest
58
+ super(**kwargs)
59
+ self.group_index = group_index
60
+ end
61
+
62
+ def parse(params)
63
+ super
64
+ self.group_index = to_number(params[0])
65
+ end
66
+
67
+ def group_index=(value)
68
+ raise ArgumentError, "group_index is out of range" unless (0...16).include?(value)
69
+ @group_index = value
70
+ end
71
+
72
+ def params
73
+ transform_param(group_index)
74
+ end
75
+ end
76
+
77
+ class GetNodeLabel < SimpleRequest
78
+ MSG = 0x45
79
+ end
80
+
81
+ class GetNodeAddr < Message
82
+ MSG = 0x40
83
+ PARAMS_LENGTH = 0
84
+
85
+ def initialize(dest = [0xff, 0xff, 0xff], **kwargs)
86
+ kwargs[:dest] ||= dest
87
+ super(**kwargs)
88
+ end
89
+ end
90
+
91
+ class GetNodeSerialNumber < SimpleRequest
92
+ MSG = 0x4c
93
+ end
94
+
95
+ class GetNodeStackVersion < SimpleRequest
96
+ MSG = 0x70
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,51 @@
1
+ module SDN
2
+ class Message
3
+ module Helpers
4
+ def parse_address(addr_string)
5
+ addr_string.match(/^(\h{2})[:.]?(\h{2})[:.]?(\h{2})$/).captures.map { |byte| byte.to_i(16) }
6
+ end
7
+
8
+ def print_address(addr_bytes)
9
+ "%02X.%02X.%02X" % addr_bytes
10
+ end
11
+
12
+ def is_group_address?(addr_bytes)
13
+ addr_bytes[0..1] == [1, 1]
14
+ end
15
+
16
+ def transform_param(param)
17
+ Array(param).reverse.map { |byte| 0xff - byte }
18
+ end
19
+
20
+ def to_number(param, nillable: false)
21
+ result = Array(param).reverse.inject(0) { |sum, byte| (sum << 8) + 0xff - byte }
22
+ result = nil if nillable && result == (1 << (8 * Array(param).length)) - 1
23
+ result
24
+ end
25
+
26
+ def from_number(number, bytes)
27
+ bytes.times.inject([]) do |res, _|
28
+ res << (0xff - number & 0xff)
29
+ number >>= 8
30
+ res
31
+ end
32
+ end
33
+
34
+ def to_string(param)
35
+ chars = param.map { |b| 0xff - b }
36
+ chars[0..-1].pack("C*").sub(/\0+$/, '')
37
+ end
38
+
39
+ def from_string(string, bytes)
40
+ chars = string.bytes
41
+ chars = chars[0...16].fill(0, chars.length, bytes - chars.length)
42
+ chars.map { |b| 0xff - b }
43
+ end
44
+
45
+ def checksum(bytes)
46
+ result = bytes.sum
47
+ [result >> 8, result & 0xff]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,162 @@
1
+ module SDN
2
+ class Message
3
+ class PostMotorPosition < Message
4
+ MSG = 0x0d
5
+ PARAMS_LENGTH = 5
6
+
7
+ attr_accessor :position_pulses, :position_percent, :ip
8
+
9
+ def parse(params)
10
+ super
11
+ self.position_pulses = to_number(params[0..1], nillable: true)
12
+ self.position_percent = to_number(params[2], nillable: true)
13
+ self.ip = to_number(params[4], nillable: true)
14
+ end
15
+ end
16
+
17
+ class PostMotorStatus < Message
18
+ MSG = 0x0f
19
+ PARAMS_LENGTH = 4
20
+ STATE = { stopped: 0x00, running: 0x01, blocked: 0x02, locked: 0x03 }.freeze
21
+ DIRECTION = { down: 0x00, up: 0x01 }.freeze
22
+ SOURCE = { internal: 0x00, network: 0x01, dct: 0x02 }.freeze
23
+ CAUSE = { target_reached: 0x00,
24
+ explicit_command: 0x01,
25
+ wink: 0x02,
26
+ limits_not_set: 0x10,
27
+ ip_not_set: 0x11,
28
+ polarity_not_checked: 0x12,
29
+ in_configuration_mode: 0x13,
30
+ obstacle_detection: 0x20,
31
+ over_current_protection: 0x21,
32
+ thermal_protection: 0x22 }.freeze
33
+
34
+ attr_accessor :state, :last_direction, :last_action_source, :last_action_cause
35
+
36
+ def parse(params)
37
+ super
38
+ self.state = STATE.invert[to_number(params[0])]
39
+ self.last_direction = DIRECTION.invert[to_number(params[1])]
40
+ self.last_action_source = SOURCE.invert[to_number(params[2])]
41
+ self.last_action_cause = CAUSE.invert[to_number(params[3])]
42
+ end
43
+ end
44
+
45
+ class PostMotorLimits < Message
46
+ MSG = 0x31
47
+ PARAMS_LENGTH = 4
48
+
49
+ attr_accessor :up_limit, :down_limit
50
+
51
+ def parse(params)
52
+ super
53
+ self.up_limit = to_number(params[0..1], nillable: true)
54
+ self.down_limit = to_number(params[2..3], nillable: true)
55
+ end
56
+ end
57
+
58
+ class PostMotorDirection < Message
59
+ MSG = 0x32
60
+ PARAMS_LENGTH = 1
61
+ DIRECTION = { standard: 0x00, reversed: 0x01 }.freeze
62
+
63
+ attr_accessor :direction
64
+
65
+ def parse(params)
66
+ super
67
+ self.direction = DIRECTION.invert[to_number(params[0])]
68
+ end
69
+ end
70
+
71
+ class PostMotorRollingSpeed < Message
72
+ MSG = 0x33
73
+ PARAMS_LENGTH = 6
74
+
75
+ attr_accessor :up_speed, :down_speed, :slow_speed
76
+
77
+ def parse(params)
78
+ super
79
+ self.up_speed = to_number(params[0])
80
+ self.down_speed = to_number(params[1])
81
+ self.slow_speed = to_number(params[2])
82
+ # 3 ignored params
83
+ end
84
+ end
85
+
86
+ class PostMotorIP < Message
87
+ MSG = 0x35
88
+ PARAMS_LENGTH = 4
89
+
90
+ attr_accessor :ip, :position_pulses, :position_percent
91
+
92
+ def parse(params)
93
+ super
94
+ self.ip = to_number(params[0])
95
+ self.position_pulses = to_number(params[1..2], nillable: true)
96
+ self.position_percent = to_number(params[3], nillable: true)
97
+ end
98
+ end
99
+
100
+ class PostNodeAddr < Message
101
+ MSG = 0x60
102
+ PARAMS_LENGTH = 0
103
+ end
104
+
105
+ class PostGroupAddr < Message
106
+ MSG = 0x61
107
+ PARAMS_LENGTH = 4
108
+
109
+ attr_accessor :group_index, :group_address
110
+
111
+ def parse(params)
112
+ super
113
+ self.group_index = to_number(params[0])
114
+ self.group_address = transform_param(params[1..3])
115
+ self.group_address = nil if group_address == [0, 0, 0] || group_address == [0x01, 0x01, 0xff]
116
+ end
117
+
118
+ def class_inspect
119
+ ", group_index=#{group_index.inspect}, group_address=#{group_address ? print_address(group_address) : 'nil'}"
120
+ end
121
+ end
122
+
123
+ class PostNodeLabel < Message
124
+ MSG = 0x65
125
+
126
+ attr_accessor :label
127
+
128
+ def parse(params)
129
+ @label = to_string(params)
130
+ end
131
+ end
132
+
133
+ class PostNodeSerialNumber < Message
134
+ MSG = 0x6c
135
+
136
+ # format is NNNNNNMMYYWW
137
+ # N = NodeID (address)
138
+ # M = Manufacturer ID
139
+ # Y = Year (last two digits)
140
+ # W = Week
141
+ attr_accessor :serial_number
142
+
143
+ def parse(params)
144
+ @serial_number = to_string(params)
145
+ end
146
+ end
147
+
148
+ class PostNodeStackVersion < UnknownMessage
149
+ MSG = 0x71
150
+
151
+ attr_accessor :params
152
+
153
+ def parse(params)
154
+ # I don't know how to interpret this yet
155
+ # I get b6 bc b2 be fc fe, and UAI+ shows 5063497A3
156
+ super
157
+ end
158
+
159
+ def msg; MSG; end
160
+ end
161
+ end
162
+ end