somfy_sdn 1.0.12 → 2.0.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.
@@ -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