somfy_sdn 2.1.5 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83eab2a162518795d82296d471608e8b9a300d34e6b81897630a06e81456ed40
4
- data.tar.gz: d8bfc48d510c2a5b97663326bb893d5abc09fd5a4b1876a70e03a408ad446e15
3
+ metadata.gz: a00033a8ac556f5b578c6d13b678b54393376c1f0439340dbd4afbd4a2376714
4
+ data.tar.gz: 1925c30fa53a68f0eb388b0ff277a00a0ae40bf2798a93ba8e9b955139b2c989
5
5
  SHA512:
6
- metadata.gz: c523da782a69732baf2d114fe412125d0406b96764b9119e97cfea221808c6cff702b6a3f7f8b487afba5f85c919f9a554f707405f15f175682a0072512e9ecd
7
- data.tar.gz: 56a628ec9e87aab0667c67ad379194bb67c2f5bd0a26c9995ac5b164f4173c296566b3b749fc5823ec7d1151a2e6083add55eef4fe7bcc521a2f11c0812d29bb
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)
@@ -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
- if self[attribute] != value
12
- bridge.publish("#{addr}/#{attribute.to_s.gsub('_', '-')}", value.to_s)
13
- self[attribute] = value
14
- end
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 { |addr, motor| motor.groups_string.include?(printed_addr) }.values
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
@@ -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
- if self[attribute] != value
64
- bridge.publish("#{addr}/#{attribute.to_s.gsub('_', '-')}", value.to_s)
65
- self[attribute] = value
66
- end
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 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) }
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(',').map { |addr| bridge.groups[addr.gsub('.', '')] }
103
+ groups_string.split(",").map { |addr| bridge.groups[addr.delete(".")] }
99
104
  end
100
105
  end
101
106
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SDN
4
+ module CLI
5
+ class MQTT
6
+ class PQueue < Array
7
+ def push(obj)
8
+ i = index { |o| o.priority > obj.priority }
9
+ if i
10
+ insert(i, obj)
11
+ else
12
+ super
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -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
- begin
8
- @sdn.receive do |message|
9
- @mqtt.batch_publish do
10
- SDN.logger.info "read #{message.inspect}"
11
-
12
- src = Message.print_address(message.src)
13
- # ignore the UAI Plus and ourselves
14
- if src != '7F.7F.7F' && !Message.is_group_address?(message.src) && !(motor = @motors[src.gsub('.', '')])
15
- SDN.logger.info "found new motor #{src}"
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
- follow_ups = []
21
- case message
22
- when Message::PostNodeLabel
23
- if (motor.publish(:label, message.label))
24
- publish("#{motor.addr}/$name", message.label)
25
- end
26
- when Message::PostMotorPosition,
27
- Message::ILT2::PostMotorPosition
28
- if message.is_a?(Message::ILT2::PostMotorPosition)
29
- # keep polling while it's still moving; check prior two positions
30
- if motor.position_pulses == message.position_pulses &&
31
- motor.last_position_pulses == message.position_pulses
32
- motor.publish(:state, :stopped)
33
- else
34
- motor.publish(:state, :running)
35
- if motor.position_pulses && motor.position_pulses != message.position_pulses
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
- motor.publish(:ip, ip)
36
+ follow_ups << Message::ILT2::GetMotorPosition.new(message.src)
46
37
  end
47
- motor.publish(:position_percent, message.position_percent)
48
- motor.publish(:position_pulses, message.position_pulses)
49
- motor.publish(:ip, message.ip) if message.respond_to?(:ip)
50
- motor.group_objects.each do |group|
51
- positions_percent = group.motor_objects.map(&:position_percent)
52
- positions_pulses = group.motor_objects.map(&:position_pulses)
53
- ips = group.motor_objects.map(&:ip)
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
- position_percent = nil
56
- # calculate an average, but only if we know a position for
57
- # every shade
58
- if !positions_percent.include?(:nil) && !positions_percent.include?(nil)
59
- position_percent = positions_percent.inject(&:+) / positions_percent.length
60
- end
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
- position_pulses = nil
63
- if !positions_pulses.include?(:nil) && !positions_pulses.include?(nil)
64
- position_pulses = positions_pulses.inject(&:+) / positions_pulses.length
65
- end
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
- ip = nil
68
- ip = ips.first if ips.uniq.length == 1
69
- ip = nil if ip == :nil
65
+ ip = nil
66
+ ip = ips.first if ips.uniq.length == 1
67
+ ip = nil if ip == :nil
70
68
 
71
- group.publish(:position_percent, position_percent)
72
- group.publish(:position_pulses, position_pulses)
73
- group.publish(:ip, ip)
74
- end
75
- when Message::PostMotorStatus
76
- if message.state == :running || motor.state == :running ||
77
- # if it's explicitly stopped, but we didn't ask it to, it's probably
78
- # changing directions so keep querying
79
- (message.state == :stopped &&
80
- message.last_action_cause == :explicit_command &&
81
- !(motor.last_action == Message::Stop || motor.last_action.nil?))
82
- follow_ups << Message::GetMotorStatus.new(message.src)
83
- end
84
- # this will do one more position request after it stopped
85
- follow_ups << Message::GetMotorPosition.new(message.src)
86
- motor.publish(:state, message.state)
87
- motor.publish(:last_direction, message.last_direction)
88
- motor.publish(:last_action_source, message.last_action_source)
89
- motor.publish(:last_action_cause, message.last_action_cause)
90
- motor.group_objects.each do |group|
91
- states = group.motor_objects.map(&:state).uniq
92
- state = states.length == 1 ? states.first : 'mixed'
93
- group.publish(:state, state)
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
- directions = group.motor_objects.map(&:last_direction).uniq
96
- direction = directions.length == 1 ? directions.first : 'mixed'
97
- group.publish(:last_direction, direction)
98
- end
99
- when Message::PostMotorLimits
100
- motor.publish(:up_limit, message.up_limit)
101
- motor.publish(:down_limit, message.down_limit)
102
- when Message::ILT2::PostMotorSettings
103
- motor.publish(:down_limit, message.limit)
104
- when Message::PostMotorDirection
105
- motor.publish(:direction, message.direction)
106
- when Message::PostMotorRollingSpeed
107
- motor.publish(:up_speed, message.up_speed)
108
- motor.publish(:down_speed, message.down_speed)
109
- motor.publish(:slow_speed, message.slow_speed)
110
- when Message::PostMotorIP,
111
- Message::ILT2::PostMotorIP
112
- motor.publish(:"ip#{message.ip}_pulses", message.position_pulses)
113
- if message.respond_to?(:position_percent)
114
- motor.publish(:"ip#{message.ip}_percent", message.position_percent)
115
- elsif motor.down_limit
116
- motor.publish(:"ip#{message.ip}_percent", message.position_pulses.to_f / motor.down_limit * 100)
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
- @mutex.synchronize do
123
- prior_message_to_group = Message.is_group_address?(@prior_message&.message&.src) if @prior_message
120
+ @mutex.synchronize do
121
+ prior_message_to_group = Message.group_address?(@prior_message&.message&.src) if @prior_message
124
122
 
125
- correct_response = @response_pending && @prior_message&.message&.class&.expected_response?(message)
126
- correct_response = false if !prior_message_to_group && message.src != @prior_message&.message&.dest
127
- correct_response = false if prior_message_to_group && message.dest != @prior_message&.message&.src
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
- if prior_message_to_group && correct_response
130
- @pending_group_motors.delete(Message.print_address(message.src).gsub('.', ''))
131
- correct_response = false unless @pending_group_motors.empty?
132
- end
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
- signal = correct_response || !follow_ups.empty?
135
- @response_pending = @broadcast_pending if correct_response
136
- follow_ups.each do |follow_up|
137
- @queues[1].push(MessageAndRetries.new(follow_up, 5, 1)) unless @queues[1].any? { |mr| mr.message == follow_up }
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
- 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 =~ /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