somfy_sdn 2.1.5 → 2.2.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.
- checksums.yaml +4 -4
- data/exe/somfy_sdn +69 -0
- data/lib/sdn/cli/mqtt/group.rb +12 -10
- data/lib/sdn/cli/mqtt/motor.rb +19 -14
- data/lib/sdn/cli/mqtt/p_queue.rb +18 -0
- data/lib/sdn/cli/mqtt/read.rb +125 -126
- data/lib/sdn/cli/mqtt/subscriptions.rb +186 -140
- data/lib/sdn/cli/mqtt/write.rb +39 -34
- data/lib/sdn/cli/mqtt.rb +84 -53
- data/lib/sdn/cli/provisioner.rb +56 -33
- data/lib/sdn/cli/simulator.rb +99 -65
- data/lib/sdn/client.rb +38 -24
- data/lib/sdn/message/control.rb +60 -30
- data/lib/sdn/message/get.rb +6 -2
- data/lib/sdn/message/helpers.rb +23 -22
- data/lib/sdn/message/ilt2/get.rb +6 -3
- data/lib/sdn/message/ilt2/master_control.rb +10 -7
- data/lib/sdn/message/ilt2/post.rb +7 -5
- data/lib/sdn/message/ilt2/set.rb +28 -19
- data/lib/sdn/message/post.rb +3 -5
- data/lib/sdn/message/set.rb +48 -22
- data/lib/sdn/message.rb +50 -34
- data/lib/sdn/version.rb +3 -1
- data/lib/sdn.rb +18 -12
- data/lib/somfy_sdn.rb +3 -1
- metadata +43 -13
- data/bin/somfy_sdn +0 -60
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a00033a8ac556f5b578c6d13b678b54393376c1f0439340dbd4afbd4a2376714
|
4
|
+
data.tar.gz: 1925c30fa53a68f0eb388b0ff277a00a0ae40bf2798a93ba8e9b955139b2c989
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce9dc75cac82bd65e4fd89908f6e6563761c5171c8dc93ab5b0e40fbe0ce0e0bb1dad7d90ef88470d90a050e7893fb1a35edb589ef2881886bf451d5962a6599
|
7
|
+
data.tar.gz: b82d6d1d60509009c7ea7b7501bd9790ae16f295185cdd5c125e10754e3a96ab9ef9d8956da8d4af554f88390eb4faea2eb4bfb7d0bb7e71248c23e043bc0338
|
data/exe/somfy_sdn
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "somfy_sdn"
|
5
|
+
require "thor"
|
6
|
+
|
7
|
+
class SomfySDNCLI < Thor
|
8
|
+
class_option :verbose, type: :boolean, default: false, desc: "Log protocol messages"
|
9
|
+
class_option :trace, type: :boolean, default: false, desc: "Log protocol bytes"
|
10
|
+
class_option :log, type: :string, desc: "Log to a file"
|
11
|
+
|
12
|
+
desc "monitor PORT", "Monitor traffic on the SDN network at PORT"
|
13
|
+
def monitor(port)
|
14
|
+
sdn = handle_global_options(port)
|
15
|
+
SDN.logger.level = :debug
|
16
|
+
|
17
|
+
loop do
|
18
|
+
sdn.receive do |message|
|
19
|
+
# do nothing
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
desc "mqtt PORT MQTT_URI", "Run an MQTT bridge to control the SDN network at PORT"
|
25
|
+
option :"device-id", default: "somfy", desc: "The Homie Device ID"
|
26
|
+
option :"base-topic", default: "homie", desc: "The base Homie topic"
|
27
|
+
option :"auto-discover", type: :boolean, default: true, desc: "Do a discovery at startup"
|
28
|
+
option :address, type: :array, desc: "Specify a known motor address to speed discovery"
|
29
|
+
def mqtt(port, mqtt_uri)
|
30
|
+
sdn = handle_global_options(port)
|
31
|
+
|
32
|
+
require "sdn/cli/mqtt"
|
33
|
+
|
34
|
+
SDN::CLI::MQTT.new(sdn,
|
35
|
+
mqtt_uri,
|
36
|
+
device_id: options["device-id"],
|
37
|
+
base_topic: options["base-topic"],
|
38
|
+
auto_discover: options["auto-discover"],
|
39
|
+
known_motors: options["address"])
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "provision PORT [ADDRESS]", "Provision a motor (label and set limits) at PORT"
|
43
|
+
def provision(port, address = nil)
|
44
|
+
sdn = handle_global_options(port)
|
45
|
+
|
46
|
+
require "sdn/cli/provisioner"
|
47
|
+
SDN::CLI::Provisioner.new(sdn, address)
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "simulator PORT [ADDRESS]", "Simulate a motor (for debugging purposes) at PORT"
|
51
|
+
def simulator(port, address = nil)
|
52
|
+
sdn = handle_global_options(port)
|
53
|
+
SDN.logger.level = :debug
|
54
|
+
|
55
|
+
require "sdn/cli/simulator"
|
56
|
+
SDN::CLI::Simulator.new(sdn, address)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def handle_global_options(port)
|
62
|
+
SDN.logger = Logger.new(options[:log]) if options[:log]
|
63
|
+
SDN.logger.level = options[:verbose] || options[:trace] ? :debug : :info
|
64
|
+
|
65
|
+
SDN::Client.new(port).tap { |sdn| sdn.trace = options[:trace] }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
SomfySDNCLI.start(ARGV)
|
data/lib/sdn/cli/mqtt/group.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SDN
|
2
4
|
module CLI
|
3
5
|
class MQTT
|
@@ -6,24 +8,24 @@ module SDN
|
|
6
8
|
members.each { |k| self[k] = :nil }
|
7
9
|
super
|
8
10
|
end
|
9
|
-
|
11
|
+
|
10
12
|
def publish(attribute, value)
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
return unless self[attribute] != value
|
14
|
+
|
15
|
+
bridge.publish("#{addr}/#{attribute.to_s.tr("_", "-")}", value.to_s)
|
16
|
+
self[attribute] = value
|
15
17
|
end
|
16
|
-
|
18
|
+
|
17
19
|
def printed_addr
|
18
20
|
Message.print_address(Message.parse_address(addr))
|
19
21
|
end
|
20
|
-
|
22
|
+
|
21
23
|
def motor_objects
|
22
|
-
bridge.motors.select { |
|
24
|
+
bridge.motors.select { |_addr, motor| motor.groups_string.include?(printed_addr) }.values
|
23
25
|
end
|
24
|
-
|
26
|
+
|
25
27
|
def motors_string
|
26
|
-
motor_objects.map { |m| Message.print_address(Message.parse_address(m.addr)) }.sort.join(
|
28
|
+
motor_objects.map { |m| Message.print_address(Message.parse_address(m.addr)) }.sort.join(",")
|
27
29
|
end
|
28
30
|
end
|
29
31
|
end
|
data/lib/sdn/cli/mqtt/motor.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SDN
|
2
4
|
module CLI
|
3
5
|
class MQTT
|
@@ -58,14 +60,14 @@ module SDN
|
|
58
60
|
@groups = [].fill(nil, 0, 16)
|
59
61
|
super
|
60
62
|
end
|
61
|
-
|
63
|
+
|
62
64
|
def publish(attribute, value)
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
65
|
+
return unless self[attribute] != value
|
66
|
+
|
67
|
+
bridge.publish("#{addr}/#{attribute.to_s.tr("_", "-")}", value.to_s)
|
68
|
+
self[attribute] = value
|
67
69
|
end
|
68
|
-
|
70
|
+
|
69
71
|
def add_group(index, address)
|
70
72
|
group = bridge.add_group(Message.print_address(address)) if address
|
71
73
|
old_group = @groups[index - 1]
|
@@ -74,10 +76,13 @@ module SDN
|
|
74
76
|
publish(:groups, groups_string)
|
75
77
|
bridge.touch_group(old_group) if old_group
|
76
78
|
end
|
77
|
-
|
78
|
-
def set_groups(groups)
|
79
|
-
return unless
|
80
|
-
|
79
|
+
|
80
|
+
def set_groups(groups) # rubocop:disable Naming/AccessorMethodName
|
81
|
+
return unless /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i.match?(groups)
|
82
|
+
|
83
|
+
groups = groups.split(",").sort.uniq.map do |g|
|
84
|
+
Message.parse_address(g)
|
85
|
+
end.select { |g| Message.group_address?(g) }
|
81
86
|
groups.fill(nil, groups.length, 16 - groups.length)
|
82
87
|
messages = []
|
83
88
|
sdn_addr = Message.parse_address(addr)
|
@@ -89,13 +94,13 @@ module SDN
|
|
89
94
|
end
|
90
95
|
messages
|
91
96
|
end
|
92
|
-
|
97
|
+
|
93
98
|
def groups_string
|
94
|
-
@groups.compact.map { |g| Message.print_address(g) }.sort.uniq.join(
|
99
|
+
@groups.compact.map { |g| Message.print_address(g) }.sort.uniq.join(",")
|
95
100
|
end
|
96
|
-
|
101
|
+
|
97
102
|
def group_objects
|
98
|
-
groups_string.split(
|
103
|
+
groups_string.split(",").map { |addr| bridge.groups[addr.delete(".")] }
|
99
104
|
end
|
100
105
|
end
|
101
106
|
end
|
data/lib/sdn/cli/mqtt/read.rb
CHANGED
@@ -1,151 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SDN
|
2
4
|
module CLI
|
3
5
|
class MQTT
|
4
6
|
module Read
|
5
7
|
def read
|
6
8
|
loop do
|
7
|
-
|
8
|
-
@
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
@motors_found = true
|
17
|
-
motor = publish_motor(src.gsub('.', ''), message.node_type)
|
18
|
-
end
|
9
|
+
@sdn.receive do |message|
|
10
|
+
@mqtt.batch_publish do
|
11
|
+
src = Message.print_address(message.src)
|
12
|
+
# ignore the UAI Plus and ourselves
|
13
|
+
if src != "7F.7F.7F" && !Message.group_address?(message.src) && !(motor = @motors[src.delete(".")])
|
14
|
+
SDN.logger.info "Found new motor #{src}"
|
15
|
+
@motors_found = true
|
16
|
+
motor = publish_motor(src.delete("."), message.node_type)
|
17
|
+
end
|
19
18
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
if message.
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
motor.publish(:
|
35
|
-
|
36
|
-
motor.publish(:last_direction, motor.position_pulses < message.position_pulses ? :down : :up)
|
37
|
-
end
|
38
|
-
follow_ups << Message::ILT2::GetMotorPosition.new(message.src)
|
39
|
-
end
|
40
|
-
motor.last_position_pulses = motor.position_pulses
|
41
|
-
ip = (1..16).find do |i|
|
42
|
-
# divide by 5 for some leniency
|
43
|
-
motor["ip#{i}_pulses"].to_i / 5 == message.position_pulses / 5
|
19
|
+
follow_ups = []
|
20
|
+
case message
|
21
|
+
when Message::PostNodeLabel
|
22
|
+
publish("#{motor.addr}/$name", message.label) if motor.publish(:label, message.label)
|
23
|
+
when Message::PostMotorPosition,
|
24
|
+
Message::ILT2::PostMotorPosition
|
25
|
+
if message.is_a?(Message::ILT2::PostMotorPosition)
|
26
|
+
# keep polling while it's still moving; check prior two positions
|
27
|
+
if motor.position_pulses == message.position_pulses &&
|
28
|
+
motor.last_position_pulses == message.position_pulses
|
29
|
+
motor.publish(:state, :stopped)
|
30
|
+
else
|
31
|
+
motor.publish(:state, :running)
|
32
|
+
if motor.position_pulses && motor.position_pulses != message.position_pulses
|
33
|
+
motor.publish(:last_direction,
|
34
|
+
(motor.position_pulses < message.position_pulses) ? :down : :up)
|
44
35
|
end
|
45
|
-
|
36
|
+
follow_ups << Message::ILT2::GetMotorPosition.new(message.src)
|
46
37
|
end
|
47
|
-
motor.
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
38
|
+
motor.last_position_pulses = motor.position_pulses
|
39
|
+
ip = (1..16).find do |i|
|
40
|
+
# divide by 5 for some leniency
|
41
|
+
motor["ip#{i}_pulses"].to_i / 5 == message.position_pulses / 5
|
42
|
+
end
|
43
|
+
motor.publish(:ip, ip)
|
44
|
+
end
|
45
|
+
motor.publish(:position_percent, message.position_percent)
|
46
|
+
motor.publish(:position_pulses, message.position_pulses)
|
47
|
+
motor.publish(:ip, message.ip) if message.respond_to?(:ip)
|
48
|
+
motor.group_objects.each do |group|
|
49
|
+
positions_percent = group.motor_objects.map(&:position_percent)
|
50
|
+
positions_pulses = group.motor_objects.map(&:position_pulses)
|
51
|
+
ips = group.motor_objects.map(&:ip)
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
53
|
+
position_percent = nil
|
54
|
+
# calculate an average, but only if we know a position for
|
55
|
+
# every shade
|
56
|
+
if !positions_percent.include?(:nil) && !positions_percent.include?(nil)
|
57
|
+
position_percent = positions_percent.sum / positions_percent.length
|
58
|
+
end
|
61
59
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
60
|
+
position_pulses = nil
|
61
|
+
if !positions_pulses.include?(:nil) && !positions_pulses.include?(nil)
|
62
|
+
position_pulses = positions_pulses.sum / positions_pulses.length
|
63
|
+
end
|
66
64
|
|
67
|
-
|
68
|
-
|
69
|
-
|
65
|
+
ip = nil
|
66
|
+
ip = ips.first if ips.uniq.length == 1
|
67
|
+
ip = nil if ip == :nil
|
70
68
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
69
|
+
group.publish(:position_percent, position_percent)
|
70
|
+
group.publish(:position_pulses, position_pulses)
|
71
|
+
group.publish(:ip, ip)
|
72
|
+
end
|
73
|
+
when Message::PostMotorStatus
|
74
|
+
if message.state == :running || motor.state == :running ||
|
75
|
+
# if it's explicitly stopped, but we didn't ask it to, it's probably
|
76
|
+
# changing directions so keep querying
|
77
|
+
(message.state == :stopped &&
|
78
|
+
message.last_action_cause == :explicit_command &&
|
79
|
+
!(motor.last_action == Message::Stop || motor.last_action.nil?))
|
80
|
+
follow_ups << Message::GetMotorStatus.new(message.src)
|
81
|
+
end
|
82
|
+
# this will do one more position request after it stopped
|
83
|
+
follow_ups << Message::GetMotorPosition.new(message.src)
|
84
|
+
motor.publish(:state, message.state)
|
85
|
+
motor.publish(:last_direction, message.last_direction)
|
86
|
+
motor.publish(:last_action_source, message.last_action_source)
|
87
|
+
motor.publish(:last_action_cause, message.last_action_cause)
|
88
|
+
motor.group_objects.each do |group|
|
89
|
+
states = group.motor_objects.map(&:state).uniq
|
90
|
+
state = (states.length == 1) ? states.first : "mixed"
|
91
|
+
group.publish(:state, state)
|
94
92
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
end
|
118
|
-
when Message::PostGroupAddr
|
119
|
-
motor.add_group(message.group_index, message.group_address)
|
93
|
+
directions = group.motor_objects.map(&:last_direction).uniq
|
94
|
+
direction = (directions.length == 1) ? directions.first : "mixed"
|
95
|
+
group.publish(:last_direction, direction)
|
96
|
+
end
|
97
|
+
when Message::PostMotorLimits
|
98
|
+
motor.publish(:up_limit, message.up_limit)
|
99
|
+
motor.publish(:down_limit, message.down_limit)
|
100
|
+
when Message::ILT2::PostMotorSettings
|
101
|
+
motor.publish(:down_limit, message.limit)
|
102
|
+
when Message::PostMotorDirection
|
103
|
+
motor.publish(:direction, message.direction)
|
104
|
+
when Message::PostMotorRollingSpeed
|
105
|
+
motor.publish(:up_speed, message.up_speed)
|
106
|
+
motor.publish(:down_speed, message.down_speed)
|
107
|
+
motor.publish(:slow_speed, message.slow_speed)
|
108
|
+
when Message::PostMotorIP,
|
109
|
+
Message::ILT2::PostMotorIP
|
110
|
+
motor.publish(:"ip#{message.ip}_pulses", message.position_pulses)
|
111
|
+
if message.respond_to?(:position_percent)
|
112
|
+
motor.publish(:"ip#{message.ip}_percent", message.position_percent)
|
113
|
+
elsif motor.down_limit
|
114
|
+
motor.publish(:"ip#{message.ip}_percent", message.position_pulses.to_f / motor.down_limit * 100)
|
120
115
|
end
|
116
|
+
when Message::PostGroupAddr
|
117
|
+
motor.add_group(message.group_index, message.group_address)
|
118
|
+
end
|
121
119
|
|
122
|
-
|
123
|
-
|
120
|
+
@mutex.synchronize do
|
121
|
+
prior_message_to_group = Message.group_address?(@prior_message&.message&.src) if @prior_message
|
124
122
|
|
125
|
-
|
126
|
-
|
127
|
-
|
123
|
+
correct_response = @response_pending && @prior_message&.message&.class&.expected_response?(message)
|
124
|
+
correct_response = false if !prior_message_to_group && message.src != @prior_message&.message&.dest
|
125
|
+
correct_response = false if prior_message_to_group && message.dest != @prior_message&.message&.src
|
128
126
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
127
|
+
if prior_message_to_group && correct_response
|
128
|
+
@pending_group_motors.delete(Message.print_address(message.src).delete("."))
|
129
|
+
correct_response = false unless @pending_group_motors.empty?
|
130
|
+
end
|
133
131
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
132
|
+
signal = correct_response || !follow_ups.empty?
|
133
|
+
@response_pending = @broadcast_pending if correct_response
|
134
|
+
follow_ups.each do |follow_up|
|
135
|
+
unless @queue.any? { |mr| mr.message == follow_up }
|
136
|
+
@queue.push(MessageAndRetries.new(follow_up, 5, 1))
|
138
137
|
end
|
139
|
-
@cond.signal if signal
|
140
138
|
end
|
141
|
-
|
142
|
-
SDN.logger.fatal "EOF reading"
|
143
|
-
exit 2
|
144
|
-
rescue MalformedMessage => e
|
145
|
-
SDN.logger.warn "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
|
146
|
-
rescue => e
|
147
|
-
SDN.logger.error "got garbage: #{e}; #{e.backtrace}"
|
139
|
+
@cond.signal if signal
|
148
140
|
end
|
141
|
+
rescue EOFError
|
142
|
+
SDN.logger.fatal "EOF reading"
|
143
|
+
exit 2
|
144
|
+
rescue MalformedMessage => e
|
145
|
+
SDN.logger.warn "Ignoring malformed message: #{e}" unless e.to_s.include?("issing data")
|
146
|
+
rescue => e
|
147
|
+
SDN.logger.error "Got garbage: #{e}; #{e.backtrace}"
|
149
148
|
end
|
150
149
|
end
|
151
150
|
end
|