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 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