somfy_sdn 1.0.12 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ module SDN
2
+ module CLI
3
+ class MQTT
4
+ Group = Struct.new(:bridge, :addr, :position_percent, :position_pulses, :ip, :last_direction, :state, :motors) do
5
+ def initialize(*)
6
+ members.each { |k| self[k] = :nil }
7
+ super
8
+ end
9
+
10
+ def publish(attribute, value)
11
+ if self[attribute] != value
12
+ bridge.publish("#{addr}/#{attribute.to_s.gsub('_', '-')}", value.to_s)
13
+ self[attribute] = value
14
+ end
15
+ end
16
+
17
+ def printed_addr
18
+ Message.print_address(Message.parse_address(addr))
19
+ end
20
+
21
+ def motor_objects
22
+ bridge.motors.select { |addr, motor| motor.groups_string.include?(printed_addr) }.values
23
+ end
24
+
25
+ def motors_string
26
+ motor_objects.map { |m| Message.print_address(Message.parse_address(m.addr)) }.sort.join(',')
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,103 @@
1
+ module SDN
2
+ module CLI
3
+ class MQTT
4
+ Motor = Struct.new(:bridge,
5
+ :addr,
6
+ :node_type,
7
+ :label,
8
+ :position_pulses,
9
+ :position_percent,
10
+ :ip,
11
+ :state,
12
+ :last_direction,
13
+ :last_action_source,
14
+ :last_action_cause,
15
+ :up_limit,
16
+ :down_limit,
17
+ :direction,
18
+ :up_speed,
19
+ :down_speed,
20
+ :slow_speed,
21
+ :ip1_pulses,
22
+ :ip1_percent,
23
+ :ip2_pulses,
24
+ :ip2_percent,
25
+ :ip3_pulses,
26
+ :ip3_percent,
27
+ :ip4_pulses,
28
+ :ip4_percent,
29
+ :ip5_pulses,
30
+ :ip5_percent,
31
+ :ip6_pulses,
32
+ :ip6_percent,
33
+ :ip7_pulses,
34
+ :ip7_percent,
35
+ :ip8_pulses,
36
+ :ip8_percent,
37
+ :ip9_pulses,
38
+ :ip9_percent,
39
+ :ip10_pulses,
40
+ :ip10_percent,
41
+ :ip11_pulses,
42
+ :ip11_percent,
43
+ :ip12_pulses,
44
+ :ip12_percent,
45
+ :ip13_pulses,
46
+ :ip13_percent,
47
+ :ip14_pulses,
48
+ :ip14_percent,
49
+ :ip15_pulses,
50
+ :ip15_percent,
51
+ :ip16_pulses,
52
+ :ip16_percent,
53
+ :groups,
54
+ :last_action,
55
+ :last_position_pulses) do
56
+ def initialize(*)
57
+ members.each { |k| self[k] = :nil }
58
+ @groups = [].fill(nil, 0, 16)
59
+ super
60
+ end
61
+
62
+ def publish(attribute, value)
63
+ if self[attribute] != value
64
+ bridge.publish("#{addr}/#{attribute.to_s.gsub('_', '-')}", value.to_s)
65
+ self[attribute] = value
66
+ end
67
+ end
68
+
69
+ def add_group(index, address)
70
+ group = bridge.add_group(Message.print_address(address)) if address
71
+ old_group = @groups[index - 1]
72
+ @groups[index - 1] = address
73
+ group&.publish(:motors, group.motors_string)
74
+ publish(:groups, groups_string)
75
+ bridge.touch_group(old_group) if old_group
76
+ end
77
+
78
+ def set_groups(groups)
79
+ return unless groups =~ /^(?:\h{2}[:.]?\h{2}[:.]?\h{2}(?:,\h{2}[:.]?\h{2}[:.]?\h{2})*)?$/i
80
+ groups = groups.split(',').sort.uniq.map { |g| Message.parse_address(g) }.select { |g| Message.is_group_address?(g) }
81
+ groups.fill(nil, groups.length, 16 - groups.length)
82
+ messages = []
83
+ sdn_addr = Message.parse_address(addr)
84
+ groups.each_with_index do |g, i|
85
+ if @groups[i] != g
86
+ messages << Message::SetGroupAddr.new(sdn_addr, i + 1, g).tap { |m| m.ack_requested = true }
87
+ messages << Message::GetGroupAddr.new(sdn_addr, i + 1)
88
+ end
89
+ end
90
+ messages
91
+ end
92
+
93
+ def groups_string
94
+ @groups.compact.map { |g| Message.print_address(g) }.sort.uniq.join(',')
95
+ end
96
+
97
+ def group_objects
98
+ groups_string.split(',').map { |addr| bridge.groups[addr.gsub('.', '')] }
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,154 @@
1
+ module SDN
2
+ module CLI
3
+ class MQTT
4
+ module Read
5
+ def read
6
+ loop do
7
+ begin
8
+ @sdn.receive do |message|
9
+ SDN.logger.info "read #{message.inspect}"
10
+
11
+ src = Message.print_address(message.src)
12
+ # ignore the UAI Plus and ourselves
13
+ if src != '7F.7F.7F' && !Message.is_group_address?(message.src) && !(motor = @motors[src.gsub('.', '')])
14
+ SDN.logger.info "found new motor #{src}"
15
+ @motors_found = true
16
+ motor = publish_motor(src.gsub('.', ''), message.node_type)
17
+ end
18
+
19
+ follow_ups = []
20
+ case message
21
+ when Message::PostNodeLabel
22
+ if (motor.publish(:label, message.label))
23
+ publish("#{motor.addr}/$name", message.label)
24
+ end
25
+ when Message::PostMotorPosition,
26
+ Message::ILT2::PostMotorPosition
27
+ if message.is_a?(Message::ILT2::PostMotorPosition)
28
+ # keep polling while it's still moving; check prior two positions
29
+ if motor.position_pulses == message.position_pulses &&
30
+ motor.last_position_pulses == message.position_pulses
31
+ motor.publish(:state, :stopped)
32
+ else
33
+ motor.publish(:state, :running)
34
+ if motor.position_pulses && motor.position_pulses != message.position_pulses
35
+ motor.publish(:last_direction, motor.position_pulses < message.position_pulses ? :down : :up)
36
+ end
37
+ follow_ups << Message::ILT2::GetMotorPosition.new(message.src)
38
+ end
39
+ motor.last_position_pulses = motor.position_pulses
40
+ ip = (1..16).find do |i|
41
+ # divide by 5 for some leniency
42
+ motor["ip#{i}_pulses"].to_i / 5 == message.position_pulses / 5
43
+ end
44
+ motor.publish(:ip, ip)
45
+ end
46
+ motor.publish(:position_percent, message.position_percent)
47
+ motor.publish(:position_pulses, message.position_pulses)
48
+ motor.publish(:ip, message.ip) if message.respond_to?(:ip)
49
+ motor.group_objects.each do |group|
50
+ positions_percent = group.motor_objects.map(&:position_percent)
51
+ positions_pulses = group.motor_objects.map(&:position_pulses)
52
+ ips = group.motor_objects.map(&:ip)
53
+
54
+ position_percent = nil
55
+ # calculate an average, but only if we know a position for
56
+ # every shade
57
+ if !positions_percent.include?(:nil) && !positions_percent.include?(nil)
58
+ position_percent = positions_percent.inject(&:+) / positions_percent.length
59
+ end
60
+
61
+ position_pulses = nil
62
+ if !positions_pulses.include?(:nil) && !positions_pulses.include?(nil)
63
+ position_pulses = positions_pulses.inject(&:+) / positions_pulses.length
64
+ end
65
+
66
+ ip = nil
67
+ ip = ips.first if ips.uniq.length == 1
68
+ ip = nil if ip == :nil
69
+
70
+ group.publish(:position_percent, position_percent)
71
+ group.publish(:position_pulses, position_pulses)
72
+ group.publish(:ip, ip)
73
+ end
74
+ when Message::PostMotorStatus
75
+ if message.state == :running || motor.state == :running ||
76
+ # if it's explicitly stopped, but we didn't ask it to, it's probably
77
+ # changing directions so keep querying
78
+ (message.state == :stopped &&
79
+ message.last_action_cause == :explicit_command &&
80
+ !(motor.last_action == Message::Stop || motor.last_action.nil?))
81
+ follow_ups << Message::GetMotorStatus.new(message.src)
82
+ end
83
+ # this will do one more position request after it stopped
84
+ follow_ups << Message::GetMotorPosition.new(message.src)
85
+ motor.publish(:state, message.state)
86
+ motor.publish(:last_direction, message.last_direction)
87
+ motor.publish(:last_action_source, message.last_action_source)
88
+ motor.publish(:last_action_cause, message.last_action_cause)
89
+ motor.group_objects.each do |group|
90
+ states = group.motor_objects.map(&:state).uniq
91
+ state = states.length == 1 ? states.first : 'mixed'
92
+ group.publish(:state, state)
93
+
94
+ directions = group.motor_objects.map(&:last_direction).uniq
95
+ direction = directions.length == 1 ? directions.first : 'mixed'
96
+ group.publish(:last_direction, direction)
97
+ end
98
+ when Message::PostMotorLimits
99
+ motor.publish(:up_limit, message.up_limit)
100
+ motor.publish(:down_limit, message.down_limit)
101
+ when Message::ILT2::PostMotorSettings
102
+ motor.publish(:down_limit, message.limit)
103
+ when Message::PostMotorDirection
104
+ motor.publish(:direction, message.direction)
105
+ when Message::PostMotorRollingSpeed
106
+ motor.publish(:up_speed, message.up_speed)
107
+ motor.publish(:down_speed, message.down_speed)
108
+ motor.publish(:slow_speed, message.slow_speed)
109
+ when Message::PostMotorIP,
110
+ Message::ILT2::PostMotorIP
111
+ motor.publish(:"ip#{message.ip}_pulses", message.position_pulses)
112
+ if message.respond_to?(:position_percent)
113
+ motor.publish(:"ip#{message.ip}_percent", message.position_percent)
114
+ elsif motor.down_limit
115
+ motor.publish(:"ip#{message.ip}_percent", message.position_pulses.to_f / motor.down_limit * 100)
116
+ end
117
+ when Message::PostGroupAddr
118
+ motor.add_group(message.group_index, message.group_address)
119
+ end
120
+
121
+ @mutex.synchronize do
122
+ prior_message_to_group = Message.is_group_address?(@prior_message&.message&.src) if @prior_message
123
+
124
+ correct_response = @response_pending && @prior_message&.message&.class&.expected_response?(message)
125
+ correct_response = false if !prior_message_to_group && message.src != @prior_message&.message&.dest
126
+ correct_response = false if prior_message_to_group && message.dest != @prior_message&.message&.src
127
+
128
+ if prior_message_to_group && correct_response
129
+ @pending_group_motors.delete(Message.print_address(message.src).gsub('.', ''))
130
+ correct_response = false unless @pending_group_motors.empty?
131
+ end
132
+
133
+ signal = correct_response || !follow_ups.empty?
134
+ @response_pending = @broadcast_pending if correct_response
135
+ follow_ups.each do |follow_up|
136
+ @queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
137
+ end
138
+ @cond.signal if signal
139
+ end
140
+ rescue EOFError
141
+ SDN.logger.fatal "EOF reading"
142
+ exit 2
143
+ rescue MalformedMessage => e
144
+ SDN.logger.warn "ignoring malformed message: #{e}" unless e.to_s =~ /issing data/
145
+ rescue => e
146
+ SDN.logger.error "got garbage: #{e}; #{e.backtrace}"
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,156 @@
1
+ module SDN
2
+ module CLI
3
+ class MQTT
4
+ module Subscriptions
5
+ def handle_message(topic, value)
6
+ SDN.logger.info "got #{value.inspect} at #{topic}"
7
+ if (match = topic.match(%r{^#{Regexp.escape(@base_topic)}/(?<addr>\h{6})/(?<property>discover|label|control|jog-(?<jog_type>pulses|ms)|position-pulses|position-percent|ip|reset|(?<speed_type>up-speed|down-speed|slow-speed)|up-limit|down-limit|direction|ip(?<ip>\d+)-(?<ip_type>pulses|percent)|groups)/set$}))
8
+ addr = Message.parse_address(match[:addr])
9
+ property = match[:property]
10
+ # not homie compliant; allows linking the position-percent property
11
+ # directly to an OpenHAB rollershutter channel
12
+ if property == 'position-percent' && value =~ /^(?:UP|DOWN|STOP)$/i
13
+ property = "control"
14
+ value = value.downcase
15
+ end
16
+ mqtt_addr = Message.print_address(addr).gsub('.', '')
17
+ motor = @motors[mqtt_addr]
18
+ is_group = Message.is_group_address?(addr)
19
+ group = @groups[mqtt_addr]
20
+ follow_up = motor&.node_type == :st50ilt2 ? Message::ILT2::GetMotorPosition.new(addr) :
21
+ Message::GetMotorStatus.new(addr)
22
+ ns = motor&.node_type == :st50ilt2 ? Message::ILT2 : Message
23
+
24
+ message = case property
25
+ when 'discover'
26
+ follow_up = nil
27
+ if value == "discover"
28
+ # discovery is low priority, and longer timeout
29
+ enqueue(MessageAndRetries.new(Message::GetNodeAddr.new(addr), 1, 2), 2)
30
+ end
31
+ nil
32
+ when 'label'
33
+ follow_up = Message::GetNodeLabel.new(addr)
34
+ ns::SetNodeLabel.new(addr, value) unless is_group
35
+ when 'control'
36
+ case value
37
+ when 'up', 'down'
38
+ (motor&.node_type == :st50ilt2 ? ns::SetMotorPosition : Message::MoveTo).
39
+ new(addr, "#{value}_limit".to_sym)
40
+ when 'stop'
41
+ motor&.node_type == :st50ilt2 ? ns::SetMotorPosition.new(addr, :stop) : Message::Stop.new(addr)
42
+ when 'next_ip'
43
+ motor&.node_type == :st50ilt2 ? ns::SetMotorPosition.new(addr, :next_ip_down) :
44
+ Message::MoveOf.new(addr, :next_ip)
45
+ when 'previous_ip'
46
+ motor&.node_type == :st50ilt2 ? ns::SetMotorPosition.new(addr, :next_ip_up) :
47
+ Message::MoveOf.new(addr, :previous_ip)
48
+ when 'wink'
49
+ Message::Wink.new(addr)
50
+ when 'refresh'
51
+ follow_up = nil
52
+ (motor&.node_type == :st50ilt2 ? ns::GetMotorPosition : Message::GetMotorStatus).
53
+ new(addr)
54
+ end
55
+ when /jog-(?:pulses|ms)/
56
+ value = value.to_i
57
+ (motor&.node_type == :st50ilt2 ? ns::SetMotorPosition : Message::MoveOf).
58
+ new(addr, "jog_#{value < 0 ? :up : :down }_#{match[:jog_type]}".to_sym, value.abs)
59
+ when 'reset'
60
+ return unless Message::SetFactoryDefault::RESET.keys.include?(value.to_sym)
61
+ Message::SetFactoryDefault.new(addr, value.to_sym)
62
+ when 'position-pulses', 'position-percent', 'ip'
63
+ if value == 'REFRESH'
64
+ follow_up = nil
65
+ (motor&.node_type == :st50ilt2 ? ns::GetMotorPosition : Message::GetMotorStatus).
66
+ new(addr)
67
+ else
68
+ (motor&.node_type == :st50ilt2 ? ns::SetMotorPosition : Message::MoveTo).
69
+ new(addr, property.sub('position-', 'position_').to_sym, value.to_i)
70
+ end
71
+ when 'direction'
72
+ return if is_group
73
+ follow_up = Message::GetMotorDirection.new(addr)
74
+ return unless %w{standard reversed}.include?(value)
75
+ Message::SetMotorDirection.new(addr, value.to_sym)
76
+ when 'up-limit', 'down-limit'
77
+ return if is_group
78
+ if %w{delete current_position jog_ms jog_pulses}.include?(value)
79
+ type = value.to_sym
80
+ value = 10
81
+ else
82
+ type = :specified_position
83
+ end
84
+ target = property == 'up-limit' ? :up : :down
85
+ follow_up = Message::GetMotorLimits.new(addr)
86
+ Message::SetMotorLimits.new(addr, type, target, value.to_i)
87
+ when /^ip\d-(?:pulses|percent)$/
88
+ return if is_group
89
+ ip = match[:ip].to_i
90
+ return unless (1..16).include?(ip)
91
+ follow_up = ns::GetMotorIP.new(addr, ip)
92
+
93
+ if motor&.node_type == :st50ilt2
94
+ value = if value == 'delete'
95
+ nil
96
+ elsif value == 'current_position'
97
+ motor.position_pulses
98
+ elsif match[:ip_type] == 'pulses'
99
+ value.to_i
100
+ else
101
+ value.to_f / motor.down_limit * 100
102
+ end
103
+ ns::SetMotorIP.new(addr, ip, value)
104
+ else
105
+ type = if value == 'delete'
106
+ :delete
107
+ elsif value == 'current_position'
108
+ :current_position
109
+ elsif match[:ip_type] == 'pulses'
110
+ :position_pulses
111
+ else
112
+ :position_percent
113
+ end
114
+ Message::SetMotorIP.new(addr, type, ip, value.to_i)
115
+ end
116
+ when 'up-speed', 'down-speed', 'slow-speed'
117
+ return if is_group
118
+ return unless motor
119
+ follow_up = Message::GetMotorRollingSpeed.new(addr)
120
+ message = Message::SetMotorRollingSpeed.new(addr,
121
+ up_speed: motor.up_speed,
122
+ down_speed: motor.down_speed,
123
+ slow_speed: motor.slow_speed)
124
+ message.send(:"#{property.sub('-', '_')}=", value.to_i)
125
+ message
126
+ when 'groups'
127
+ return if is_group
128
+ return unless motor
129
+ messages = motor.set_groups(value)
130
+ @mutex.synchronize do
131
+ messages.each { |m| @queues[0].push(MessageAndRetries.new(m, 5, 0)) }
132
+ @cond.signal
133
+ end
134
+ nil
135
+ end
136
+
137
+ if motor
138
+ motor.last_action = message.class if [Message::MoveTo, Message::Move, Message::Wink, Message::Stop].include?(message.class)
139
+ end
140
+
141
+ if message
142
+ message.ack_requested = true if message.class.name !~ /^SDN::Message::Get/
143
+ @mutex.synchronize do
144
+ @queues[0].push(MessageAndRetries.new(message, 5, 0))
145
+ if follow_up
146
+ @queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
147
+ end
148
+ @cond.signal
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,93 @@
1
+ module SDN
2
+ module CLI
3
+ class MQTT
4
+ module Write
5
+ def write
6
+ last_write_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
7
+
8
+ loop do
9
+ message_and_retries = nil
10
+ @mutex.synchronize do
11
+ # got woken up early by another command getting queued; spin
12
+ if @response_pending
13
+ while @response_pending
14
+ remaining_wait = @response_pending - Time.now.to_f
15
+ if remaining_wait < 0
16
+ SDN.logger.debug "timed out waiting on response"
17
+ @response_pending = nil
18
+ @broadcast_pending = nil
19
+ if @prior_message && @prior_message&.remaining_retries != 0
20
+ SDN.logger.debug "retrying #{@prior_message.remaining_retries} more times ..."
21
+ if Message.is_group_address?(@prior_message.message.src) && !@pending_group_motors.empty?
22
+ SDN.logger.debug "re-targetting group message to individual motors"
23
+ @pending_group_motors.each do |addr|
24
+ new_message = @prior_message.message.dup
25
+ new_message.src = [0, 0, 1]
26
+ new_message.dest = Message.parse_address(addr)
27
+ @queues[@prior_message.priority].push(MessageAndRetries.new(new_message, @prior_message.remaining_retries, @prior_message.priority))
28
+ end
29
+ @pending_group_motors = []
30
+ else
31
+ @queues[@prior_message.priority].push(@prior_message)
32
+ end
33
+ @prior_message = nil
34
+ end
35
+ else
36
+ @cond.wait(@mutex, remaining_wait)
37
+ end
38
+ end
39
+ end
40
+
41
+ @queues.find { |q| message_and_retries = q.shift }
42
+ if message_and_retries
43
+ if message_and_retries.message.ack_requested || message_and_retries.message.class.name =~ /^SDN::Message::Get/
44
+ @response_pending = Time.now.to_f + WAIT_TIME
45
+ @pending_group_motors = if Message.is_group_address?(message_and_retries.message.src)
46
+ group_addr = Message.print_address(message_and_retries.message.src).gsub('.', '')
47
+ @groups[group_addr]&.motor_objects&.map(&:addr) || []
48
+ else
49
+ []
50
+ end
51
+
52
+ if message_and_retries.message.dest == BROADCAST_ADDRESS || Message.is_group_address?(message_and_retries.message.src) && message_and_retries.message.is_a?(Message::GetNodeAddr)
53
+ @broadcast_pending = Time.now.to_f + BROADCAST_WAIT
54
+ end
55
+ end
56
+ end
57
+
58
+ # wait until there is a message
59
+ if @response_pending
60
+ message_and_retries.remaining_retries -= 1
61
+ @prior_message = message_and_retries
62
+ elsif message_and_retries
63
+ @prior_message = nil
64
+ else
65
+ if @auto_discover && @motors_found
66
+ # nothing pending to write, and motors found on the last iteration;
67
+ # look for more motors
68
+ message_and_retries = MessageAndRetries.new(Message::GetNodeAddr.new, 1, 2)
69
+ @motors_found = false
70
+ else
71
+ @cond.wait(@mutex)
72
+ end
73
+ end
74
+ end
75
+ next unless message_and_retries
76
+
77
+ message = message_and_retries.message
78
+ SDN.logger.info "writing #{message.inspect}"
79
+ # minimum time between messages
80
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
81
+ sleep_time = 0.1 - (now - last_write_at)
82
+ sleep(sleep_time) if sleep_time > 0
83
+ @sdn.send(message)
84
+ last_write_at = now
85
+ end
86
+ rescue => e
87
+ SDN.logger.fatal "failure writing: #{e}: #{e.backtrace}"
88
+ exit 1
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end