somfy_sdn 2.4.1 → 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.
- checksums.yaml +4 -4
- data/exe/somfy_sdn +1 -0
- data/lib/sdn/cli/mqtt/group.rb +11 -0
- data/lib/sdn/cli/mqtt/motor.rb +25 -1
- data/lib/sdn/cli/mqtt/p_queue.rb +6 -0
- data/lib/sdn/cli/mqtt/read.rb +46 -47
- data/lib/sdn/cli/mqtt/subscriptions.rb +8 -0
- data/lib/sdn/cli/mqtt/write.rb +67 -27
- data/lib/sdn/cli/mqtt.rb +256 -198
- data/lib/sdn/cli/provisioner.rb +34 -1
- data/lib/sdn/cli/simulator.rb +78 -11
- data/lib/sdn/cli.rb +6 -0
- data/lib/sdn/client.rb +42 -2
- data/lib/sdn/message/control.rb +136 -12
- data/lib/sdn/message/get.rb +57 -0
- data/lib/sdn/message/helpers.rb +64 -0
- data/lib/sdn/message/ilt2/get.rb +24 -0
- data/lib/sdn/message/ilt2/master_control.rb +10 -0
- data/lib/sdn/message/ilt2/post.rb +79 -13
- data/lib/sdn/message/ilt2/set.rb +112 -7
- data/lib/sdn/message/post.rb +181 -20
- data/lib/sdn/message/set.rb +166 -8
- data/lib/sdn/message.rb +121 -14
- data/lib/sdn/version.rb +2 -1
- data/lib/sdn.rb +13 -5
- metadata +5 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0cde44546a65fc3e056bd24f4b2306d87669f219bbb8f759b13d6b5f26dcfdf
|
|
4
|
+
data.tar.gz: 85da8daf249310449f80a7fb1d0bb6579c009efc9f65ab121deb0106be122ddf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4665bc4b50b981ee51f89e1be7a14b943209a639179e59b6175991e7bf94612bf1c9bf603f98603a4234f7c99f4cd81cb25ae1ba96542641555d9d01a152a33
|
|
7
|
+
data.tar.gz: e7fb11e6d77febe8718d732e36f32fce86628fd31169e266b61f0784b063402dba5231494c66b45e04346a7c37191fdaf0eb57c4c7329369ac111f8dca6cf512
|
data/exe/somfy_sdn
CHANGED
|
@@ -5,6 +5,7 @@ require "somfy_sdn"
|
|
|
5
5
|
require "thor"
|
|
6
6
|
require "uri"
|
|
7
7
|
|
|
8
|
+
# @!visibility private
|
|
8
9
|
class SomfySDNCLI < Thor
|
|
9
10
|
class_option :verbose, type: :boolean, default: false, desc: "Log protocol messages"
|
|
10
11
|
class_option :trace, type: :boolean, default: false, desc: "Log protocol bytes"
|
data/lib/sdn/cli/mqtt/group.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module SDN
|
|
4
4
|
module CLI
|
|
5
5
|
class MQTT
|
|
6
|
+
# In-memory state container for a discovered group and its aggregated MQTT attributes.
|
|
6
7
|
Group = Struct.new(:bridge,
|
|
7
8
|
:addr,
|
|
8
9
|
:position_percent,
|
|
@@ -17,6 +18,7 @@ module SDN
|
|
|
17
18
|
super
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
# @!visibility private
|
|
20
22
|
def publish(attribute, value)
|
|
21
23
|
return unless self[attribute] != value
|
|
22
24
|
|
|
@@ -24,14 +26,23 @@ module SDN
|
|
|
24
26
|
self[attribute] = value
|
|
25
27
|
end
|
|
26
28
|
|
|
29
|
+
# @return [String]
|
|
27
30
|
def printed_addr
|
|
28
31
|
Message.print_address(Message.parse_address(addr))
|
|
29
32
|
end
|
|
30
33
|
|
|
34
|
+
# @!attribute [r] motor_objects
|
|
35
|
+
#
|
|
36
|
+
# @return [<SDN::CLI::MQTT::Motor>]
|
|
31
37
|
def motor_objects
|
|
32
38
|
bridge.motors.select { |_addr, motor| motor.groups_string.include?(printed_addr) }.values
|
|
33
39
|
end
|
|
34
40
|
|
|
41
|
+
# @!attribute [r] motors_string
|
|
42
|
+
#
|
|
43
|
+
# Returns the current group membership as a comma-separated motor address list.
|
|
44
|
+
#
|
|
45
|
+
# @return [String]
|
|
35
46
|
def motors_string
|
|
36
47
|
motor_objects.map { |m| Message.print_address(Message.parse_address(m.addr)) }.sort.join(",")
|
|
37
48
|
end
|
data/lib/sdn/cli/mqtt/motor.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module SDN
|
|
4
4
|
module CLI
|
|
5
5
|
class MQTT
|
|
6
|
+
# In-memory state container for a discovered motor and its published MQTT attributes.
|
|
6
7
|
Motor = Struct.new(:bridge,
|
|
7
8
|
:addr,
|
|
8
9
|
:node_type,
|
|
@@ -55,13 +56,17 @@ module SDN
|
|
|
55
56
|
:ip16_percent,
|
|
56
57
|
:groups,
|
|
57
58
|
:last_action,
|
|
58
|
-
:last_position_pulses
|
|
59
|
+
:last_position_pulses,
|
|
60
|
+
:last_action_retries) do
|
|
59
61
|
def initialize(*)
|
|
60
62
|
members.each { |k| self[k] = :nil }
|
|
61
63
|
@groups = [].fill(nil, 0, 16)
|
|
62
64
|
super
|
|
65
|
+
self.last_action = Message::Stop
|
|
66
|
+
self.last_action_retries = 0
|
|
63
67
|
end
|
|
64
68
|
|
|
69
|
+
# @!visibility private
|
|
65
70
|
def publish(attribute, value)
|
|
66
71
|
return unless self[attribute] != value
|
|
67
72
|
|
|
@@ -69,6 +74,12 @@ module SDN
|
|
|
69
74
|
self[attribute] = value
|
|
70
75
|
end
|
|
71
76
|
|
|
77
|
+
# Updates a group slot and refreshes derived group membership state.
|
|
78
|
+
#
|
|
79
|
+
# @param index [Integer] group slot number
|
|
80
|
+
# @param address [(Integer, Integer, Integer), nil] group address
|
|
81
|
+
#
|
|
82
|
+
# @return [void]
|
|
72
83
|
def add_group(index, address)
|
|
73
84
|
group = bridge.add_group(Message.print_address(address)) if address
|
|
74
85
|
old_group = @groups[index - 1]
|
|
@@ -78,6 +89,11 @@ module SDN
|
|
|
78
89
|
bridge.touch_group(old_group) if old_group
|
|
79
90
|
end
|
|
80
91
|
|
|
92
|
+
# Builds the SDN writes needed to reconcile group membership with a comma-separated list.
|
|
93
|
+
#
|
|
94
|
+
# @param groups [String] comma-separated group addresses
|
|
95
|
+
#
|
|
96
|
+
# @return [<SDN::Message>]
|
|
81
97
|
def set_groups(groups) # rubocop:disable Naming/AccessorMethodName
|
|
82
98
|
return unless /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i.match?(groups)
|
|
83
99
|
|
|
@@ -96,10 +112,18 @@ module SDN
|
|
|
96
112
|
messages
|
|
97
113
|
end
|
|
98
114
|
|
|
115
|
+
# @!attribute [r] groups_string
|
|
116
|
+
#
|
|
117
|
+
# Returns the current group membership list as a normalized string.
|
|
118
|
+
#
|
|
119
|
+
# @return [String]
|
|
99
120
|
def groups_string
|
|
100
121
|
@groups.compact.map { |g| Message.print_address(g) }.sort.uniq.join(",")
|
|
101
122
|
end
|
|
102
123
|
|
|
124
|
+
# @!attribute [r] group_objects
|
|
125
|
+
#
|
|
126
|
+
# @return [<SDN::CLI::MQTT::Group>]
|
|
103
127
|
def group_objects
|
|
104
128
|
groups_string.split(",").map { |addr| bridge.groups[addr.delete(".")] }
|
|
105
129
|
end
|
data/lib/sdn/cli/mqtt/p_queue.rb
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
module SDN
|
|
4
4
|
module CLI
|
|
5
5
|
class MQTT
|
|
6
|
+
# Priority queue used to order outbound SDN messages by importance.
|
|
6
7
|
class PQueue < Array
|
|
8
|
+
# Inserts a queued item ahead of lower-priority entries.
|
|
9
|
+
#
|
|
10
|
+
# @param obj [Object] queued item responding to `priority`
|
|
11
|
+
#
|
|
12
|
+
# @return [Array] the queue itself
|
|
7
13
|
def push(obj)
|
|
8
14
|
i = index { |o| o.priority > obj.priority }
|
|
9
15
|
if i
|
data/lib/sdn/cli/mqtt/read.rb
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
module SDN
|
|
4
4
|
module CLI
|
|
5
5
|
class MQTT
|
|
6
|
+
# Reader loop that translates SDN responses into MQTT state updates.
|
|
6
7
|
module Read
|
|
8
|
+
# Continuously consumes SDN messages and republishes state to MQTT.
|
|
9
|
+
#
|
|
10
|
+
# @return [void]
|
|
7
11
|
def read
|
|
8
12
|
loop do
|
|
9
13
|
@sdn.receive do |message|
|
|
@@ -71,53 +75,7 @@ module SDN
|
|
|
71
75
|
group.publish(:ip, ip)
|
|
72
76
|
end
|
|
73
77
|
when Message::PostMotorStatus
|
|
74
|
-
|
|
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
|
-
hass_state = if message.state == :running
|
|
87
|
-
(message.last_direction == :down) ? :closing : :opening
|
|
88
|
-
elsif motor.position_percent&.zero?
|
|
89
|
-
:open
|
|
90
|
-
elsif motor.position_percent == 100
|
|
91
|
-
:closed
|
|
92
|
-
else
|
|
93
|
-
:stopped
|
|
94
|
-
end
|
|
95
|
-
motor.publish(:hass_state, hass_state)
|
|
96
|
-
motor.publish(:last_action_source, message.last_action_source)
|
|
97
|
-
motor.publish(:last_action_cause, message.last_action_cause)
|
|
98
|
-
motor.group_objects.each do |group|
|
|
99
|
-
states = group.motor_objects.map(&:state).uniq
|
|
100
|
-
state = (states.length == 1) ? states.first : "mixed"
|
|
101
|
-
group.publish(:state, state)
|
|
102
|
-
|
|
103
|
-
directions = group.motor_objects.map(&:last_direction).uniq
|
|
104
|
-
direction = (directions.length == 1) ? directions.first : "mixed"
|
|
105
|
-
group.publish(:last_direction, direction)
|
|
106
|
-
|
|
107
|
-
positions = group.motor_objects.map(&:position_percent).uniq
|
|
108
|
-
position = (positions.length == 1) ? positions.first : 50
|
|
109
|
-
|
|
110
|
-
hass_state = if state == :running
|
|
111
|
-
(direction == :down) ? :closing : :opening
|
|
112
|
-
elsif position.zero?
|
|
113
|
-
:open
|
|
114
|
-
elsif position == 100
|
|
115
|
-
:closed
|
|
116
|
-
else
|
|
117
|
-
:stopped
|
|
118
|
-
end
|
|
119
|
-
group.publish(:hass_state, hass_state)
|
|
120
|
-
end
|
|
78
|
+
handle_post_motor_status(message, motor, follow_ups)
|
|
121
79
|
when Message::PostMotorLimits
|
|
122
80
|
motor.publish(:up_limit, message.up_limit)
|
|
123
81
|
motor.publish(:down_limit, message.down_limit)
|
|
@@ -173,6 +131,47 @@ module SDN
|
|
|
173
131
|
end
|
|
174
132
|
end
|
|
175
133
|
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def handle_post_motor_status(message, motor, follow_ups)
|
|
138
|
+
if message.state == :running || motor.state == :running ||
|
|
139
|
+
# if it's explicitly stopped, but we didn't ask it to, it's probably
|
|
140
|
+
# changing directions so keep querying
|
|
141
|
+
(message.state == :stopped &&
|
|
142
|
+
message.last_action_cause == :explicit_command &&
|
|
143
|
+
motor.last_action != Message::Stop && motor.last_action_retries.positive?)
|
|
144
|
+
if message.state == :stopped
|
|
145
|
+
SDN.logger.debug("Re-querying motor status for #{motor.addr} #{motor.last_action_retries} " \
|
|
146
|
+
"more time(s) even though stopped")
|
|
147
|
+
motor.last_action_retries -= 1
|
|
148
|
+
end
|
|
149
|
+
follow_ups << Message::GetMotorStatus.new(message.src)
|
|
150
|
+
end
|
|
151
|
+
# this will do one more position request after it stopped
|
|
152
|
+
follow_ups << Message::GetMotorPosition.new(message.src)
|
|
153
|
+
motor.publish(:state, message.state)
|
|
154
|
+
motor.publish(:last_direction, message.last_direction)
|
|
155
|
+
motor.publish(:hass_state, motor_hass_state(message.state,
|
|
156
|
+
message.last_direction,
|
|
157
|
+
motor.position_percent))
|
|
158
|
+
motor.publish(:last_action_source, message.last_action_source)
|
|
159
|
+
motor.publish(:last_action_cause, message.last_action_cause)
|
|
160
|
+
motor.group_objects.each do |group|
|
|
161
|
+
states = group.motor_objects.map(&:state).uniq
|
|
162
|
+
state = (states.length == 1) ? states.first : "mixed"
|
|
163
|
+
group.publish(:state, state)
|
|
164
|
+
|
|
165
|
+
directions = group.motor_objects.map(&:last_direction).uniq
|
|
166
|
+
direction = (directions.length == 1) ? directions.first : "mixed"
|
|
167
|
+
group.publish(:last_direction, direction)
|
|
168
|
+
|
|
169
|
+
positions = group.motor_objects.map(&:position_percent).uniq
|
|
170
|
+
position = (positions.length == 1) ? positions.first : 50
|
|
171
|
+
|
|
172
|
+
group.publish(:hass_state, motor_hass_state(state, direction, position))
|
|
173
|
+
end
|
|
174
|
+
end
|
|
176
175
|
end
|
|
177
176
|
end
|
|
178
177
|
end
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
module SDN
|
|
4
4
|
module CLI
|
|
5
5
|
class MQTT
|
|
6
|
+
# MQTT subscription handlers that translate inbound command messages into SDN commands.
|
|
6
7
|
module Subscriptions
|
|
8
|
+
# Converts an MQTT command message into one or more queued SDN messages.
|
|
9
|
+
#
|
|
10
|
+
# @param topic [String] absolute MQTT topic
|
|
11
|
+
# @param value [String] MQTT payload
|
|
12
|
+
#
|
|
13
|
+
# @return [void]
|
|
7
14
|
def handle_message(topic, value)
|
|
8
15
|
SDN.logger.info "Got #{value.inspect} at #{topic}"
|
|
9
16
|
unless (match = topic.match(%r{^#{Regexp.escape(@base_topic)}/
|
|
@@ -181,6 +188,7 @@ module SDN
|
|
|
181
188
|
Message::Wink,
|
|
182
189
|
Message::Stop].include?(message.class)
|
|
183
190
|
motor.last_action = message.class
|
|
191
|
+
motor.last_action_retries = 3
|
|
184
192
|
end
|
|
185
193
|
|
|
186
194
|
return unless message
|
data/lib/sdn/cli/mqtt/write.rb
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
module SDN
|
|
4
4
|
module CLI
|
|
5
5
|
class MQTT
|
|
6
|
+
# Writer loop that drains the outbound queue and manages retries and follow-up timing.
|
|
6
7
|
module Write
|
|
8
|
+
# Continuously drains the prioritized queue and transmits SDN messages.
|
|
9
|
+
#
|
|
10
|
+
# @return [void]
|
|
7
11
|
def write
|
|
8
12
|
last_write_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
9
13
|
|
|
@@ -11,34 +15,38 @@ module SDN
|
|
|
11
15
|
message_and_retries = nil
|
|
12
16
|
@mutex.synchronize do
|
|
13
17
|
# got woken up early by another command getting queued; spin
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if
|
|
18
|
-
SDN.logger.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
18
|
+
while @response_pending
|
|
19
|
+
remaining_wait = @response_pending - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
20
|
+
if remaining_wait.negative?
|
|
21
|
+
if @prior_message
|
|
22
|
+
SDN.logger.info "Timed out waiting on response to #{@prior_message.message.inspect}; " \
|
|
23
|
+
"retries left: #{@prior_message.remaining_retries}"
|
|
24
|
+
else
|
|
25
|
+
SDN.logger.info "Timed out waiting on response"
|
|
26
|
+
end
|
|
27
|
+
@response_pending = nil
|
|
28
|
+
@broadcast_pending = nil
|
|
29
|
+
if @prior_message
|
|
30
|
+
if @prior_message.remaining_retries.zero?
|
|
31
|
+
handle_timeout_fallback(@prior_message.message)
|
|
32
|
+
elsif Message.group_address?(@prior_message.message.src) && !@pending_group_motors.empty?
|
|
33
|
+
SDN.logger.debug "Re-targetting group message to individual motors"
|
|
34
|
+
@pending_group_motors.each do |addr|
|
|
35
|
+
new_message = @prior_message.message.dup
|
|
36
|
+
new_message.src = [0, 0, 1]
|
|
37
|
+
new_message.dest = Message.parse_address(addr)
|
|
38
|
+
@queue.push(MessageAndRetries.new(new_message,
|
|
39
|
+
@prior_message.remaining_retries,
|
|
40
|
+
@prior_message.priority))
|
|
36
41
|
end
|
|
37
|
-
@
|
|
42
|
+
@pending_group_motors = []
|
|
43
|
+
else
|
|
44
|
+
@queue.push(@prior_message)
|
|
38
45
|
end
|
|
39
|
-
|
|
40
|
-
@cond.wait(@mutex, remaining_wait)
|
|
46
|
+
@prior_message = nil
|
|
41
47
|
end
|
|
48
|
+
else
|
|
49
|
+
@cond.wait(@mutex, remaining_wait)
|
|
42
50
|
end
|
|
43
51
|
end
|
|
44
52
|
|
|
@@ -46,7 +54,7 @@ module SDN
|
|
|
46
54
|
if message_and_retries && (
|
|
47
55
|
message_and_retries.message.ack_requested ||
|
|
48
56
|
message_and_retries.message.class.name =~ /^SDN::Message::Get/)
|
|
49
|
-
@response_pending =
|
|
57
|
+
@response_pending = Process.clock_gettime(Process::CLOCK_MONOTONIC) + WAIT_TIME
|
|
50
58
|
@pending_group_motors = if Message.group_address?(message_and_retries.message.src)
|
|
51
59
|
group_addr = Message.print_address(message_and_retries.message.src).delete(
|
|
52
60
|
"."
|
|
@@ -59,7 +67,7 @@ module SDN
|
|
|
59
67
|
if message_and_retries.message.dest == BROADCAST_ADDRESS || (
|
|
60
68
|
Message.group_address?(message_and_retries.message.src) &&
|
|
61
69
|
message_and_retries.message.is_a?(Message::GetNodeAddr))
|
|
62
|
-
@broadcast_pending =
|
|
70
|
+
@broadcast_pending = Process.clock_gettime(Process::CLOCK_MONOTONIC) + BROADCAST_WAIT
|
|
63
71
|
end
|
|
64
72
|
end
|
|
65
73
|
|
|
@@ -92,6 +100,38 @@ module SDN
|
|
|
92
100
|
SDN.logger.fatal "Failure writing: #{e}: #{e.backtrace}"
|
|
93
101
|
exit 1
|
|
94
102
|
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def handle_timeout_fallback(message)
|
|
107
|
+
case message
|
|
108
|
+
when Message::GetMotorStatus
|
|
109
|
+
handle_get_motor_status_timeout(message)
|
|
110
|
+
when Message::GetMotorPosition, Message::ILT2::GetMotorPosition
|
|
111
|
+
handle_get_motor_position_timeout(message)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def handle_get_motor_status_timeout(message)
|
|
116
|
+
addr = Message.print_address(message.dest).delete(".")
|
|
117
|
+
motor = @motors[addr]
|
|
118
|
+
return unless motor&.state == :running
|
|
119
|
+
|
|
120
|
+
motor.publish(:state, :stopped)
|
|
121
|
+
motor.publish(:hass_state, motor_hass_state(:stopped, motor.last_direction, motor.position_percent))
|
|
122
|
+
motor.publish(:last_action_source, :bridge)
|
|
123
|
+
motor.publish(:last_action_cause, :timeout)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def handle_get_motor_position_timeout(message)
|
|
127
|
+
addr = Message.print_address(message.dest).delete(".")
|
|
128
|
+
motor = @motors[addr]
|
|
129
|
+
return unless motor
|
|
130
|
+
|
|
131
|
+
motor.publish(:position_pulses, nil)
|
|
132
|
+
motor.publish(:position_percent, nil)
|
|
133
|
+
motor.publish(:ip, nil)
|
|
134
|
+
end
|
|
95
135
|
end
|
|
96
136
|
end
|
|
97
137
|
end
|