tp_link_smartplug 0.0.3 → 0.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: f53d7ea800a82c44bc660a56066f5e7ee81d2ff2f786ddcb668c8d3267b8960c
4
- data.tar.gz: af92b7cd428a8a3e1ceafb1868b3651153b5b192d051f0266174b14d4bbfba67
3
+ metadata.gz: 41a05841cdc6ffdd697f903abc1d620debf999f2a9a6c5f248edc9ec93635e30
4
+ data.tar.gz: c3f77299aaeb6b90506f04730467c70889745d85e47148ba738ccc82de484cbd
5
5
  SHA512:
6
- metadata.gz: f406f2bc6b7a7ebb0d0e8434146593a6080f4a2840d4420f96b3fd9454f31645adbfb8e248c5dab75e35c54ed9e72129b0e2355b508949271f1aa75371eb7269
7
- data.tar.gz: 1b21d836bac38def6558452bd6fa6bbb9a4c0fe5023f70abb07c91bdd6921d5f4bb6db7db44d52ecf47d07d7fd417c0a5d8b71e10ccf5832becf864abb90844d
6
+ metadata.gz: eac7e52a043aebdeeb595aba15df19be7923d2da40a6d214bb41955f4baf4da1e2853e935cd7ade3b410981bd8698024f9598a923b20a5c0311ed2398dbced01
7
+ data.tar.gz: 74f0ba6ff80393ee43560a76ec6cab25d94fb1b8b5b3b078884be64b9db274e657053f2c90007df89aa2bde7e849b63a049e4628c91ff4bb90cf1e29b59c7d23
@@ -1,8 +1,11 @@
1
- # frozen_string_literal: true
1
+ require_relative 'tp_link_smartplug/_base'
2
+ require_relative 'tp_link_smartplug/command'
3
+ require_relative 'tp_link_smartplug/device'
4
+ require_relative 'tp_link_smartplug/helpers'
5
+ require_relative 'tp_link_smartplug/message'
6
+ require_relative 'tp_link_smartplug/version'
2
7
 
3
- require 'tp_link_smartplug/command'
4
- require 'tp_link_smartplug/device'
5
- require 'tp_link_smartplug/helpers'
6
- require 'tp_link_smartplug/message_helpers'
7
- require 'tp_link_smartplug/message'
8
- require 'tp_link_smartplug/version'
8
+ # Top level module
9
+ #
10
+ # @author Ben Hughes
11
+ module TpLinkSmartplug; end
@@ -0,0 +1,15 @@
1
+ require_relative 'helpers'
2
+
3
+ module TpLinkSmartplug
4
+ # Base plug class.
5
+ #
6
+ # @author Ben Hughes
7
+ class Base
8
+ include TpLinkSmartplug::Helpers
9
+ end
10
+
11
+ # Base plug error class.
12
+ #
13
+ # @author Ben Hughes
14
+ class BaseError < StandardError; end
15
+ end
@@ -1,18 +1,29 @@
1
- # frozen_string_literal: true
2
-
3
1
  module TpLinkSmartplug
2
+ # Module containing the static commands for the plug
4
3
  module Command
5
- INFO = '{"system":{"get_sysinfo":{}}}'
6
- ON = '{"system":{"set_relay_state":{"state":1}}}'
7
- OFF = '{"system":{"set_relay_state":{"state":0}}}'
8
- CLOUDINFO = '{"cnCloud":{"get_info":{}}}'
9
- WLANSCAN = '{"netif":{"get_scaninfo":{"refresh":0}}}'
10
- TIME = '{"time":{"get_time":{}}}'
11
- SCHEDULE = '{"schedule":{"get_rules":{}}}'
12
- COUNTDOWN = '{"count_down":{"get_rules":{}}}'
13
- ANTITHEFT = '{"anti_theft":{"get_rules":{}}}'
14
- REBOOT = '{"system":{"reboot":{"delay":1}}}'
15
- RESET = '{"system":{"reset":{"delay":1}}}'
16
- ENERGY = '{"emeter":{"get_realtime":{}}}'
4
+ # Plug information command
5
+ INFO = '{"system":{"get_sysinfo":{}}}'.freeze
6
+ # Plug output on command
7
+ ON = '{"system":{"set_relay_state":{"state":1}}}'.freeze
8
+ # Plug output off command
9
+ OFF = '{"system":{"set_relay_state":{"state":0}}}'.freeze
10
+ # Plug cloud info command
11
+ CLOUDINFO = '{"cnCloud":{"get_info":{}}}'.freeze
12
+ # Plug WLAN SSID scan command
13
+ WLANSCAN = '{"netif":{"get_scaninfo":{"refresh":0}}}'.freeze
14
+ # Plug time command
15
+ TIME = '{"time":{"get_time":{}}}'.freeze
16
+ # Plug schedule command
17
+ SCHEDULE = '{"schedule":{"get_rules":{}}}'.freeze
18
+ # Plug countdown command
19
+ COUNTDOWN = '{"count_down":{"get_rules":{}}}'.freeze
20
+ # Plug antitheft command
21
+ ANTITHEFT = '{"anti_theft":{"get_rules":{}}}'.freeze
22
+ # Plug reboot command
23
+ REBOOT = '{"system":{"reboot":{"delay":1}}}'.freeze
24
+ # Plug reset command
25
+ RESET = '{"system":{"reset":{"delay":1}}}'.freeze
26
+ # Plug energy command
27
+ ENERGY = '{"emeter":{"get_realtime":{}}}'.freeze
17
28
  end
18
29
  end
@@ -1,27 +1,167 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'socket'
4
2
  require 'ipaddr'
5
3
  require 'json'
6
- require 'tp_link_smartplug/message'
4
+ require_relative './message'
7
5
 
8
6
  module TpLinkSmartplug
9
- # Provides an interface to a plug
10
- class Device
7
+ # Provides an object to represent to a plug
8
+ #
9
+ # @author Ben Hughes
10
+ # @attr [IPAddr] address IP address of the plug
11
+ # @attr [Integer] port Port to connect to on the plug
12
+ # @attr [Integer] timeout Timeout value for connecting and sending commands to the plug
13
+ # @attr [true, false] debug Control debug logging
14
+ class Device < TpLinkSmartplug::Base
15
+ include TpLinkSmartplug::Helpers
11
16
  include TpLinkSmartplug::Message
12
17
 
13
- attr_accessor :address
14
- attr_accessor :timeout
15
- attr_accessor :port
16
- attr_accessor :debug
18
+ attr_accessor :address, :port, :timeout, :poll_auto_close, :debug
17
19
 
18
20
  def initialize(address:, port: 9999)
21
+ super
22
+
19
23
  @address = IPAddr.new(address, Socket::AF_INET)
20
24
  @port = port
21
25
  @timeout = 3
22
26
  @debug = false
27
+ @sockaddr = Addrinfo.getaddrinfo(@address.to_s, @port, Socket::PF_INET, :STREAM, 6).first.to_sockaddr
28
+ @socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
29
+ @poll_auto_close = true
30
+ end
31
+
32
+ # Open connection to plug
33
+ #
34
+ def connect
35
+ debug_message("Connecting to #{@address} port #{@port}") if @debug
36
+ debug_message("Connecting, socket state: #{@socket.closed? ? 'closed' : 'open'}") if @debug
37
+
38
+ @socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM) if closed?
39
+
40
+ begin
41
+ @socket.connect_nonblock(@sockaddr)
42
+ debug_message('Connected') if @debug
43
+ rescue IO::WaitWritable
44
+ if IO.select(nil, [@socket], nil, timeout)
45
+ begin
46
+ @socket.connect_nonblock(@sockaddr)
47
+ rescue Errno::EISCONN
48
+ debug_message('Connected') if @debug
49
+ rescue Errno::ECONNREFUSED
50
+ @socket.close
51
+ raise TpLinkSmartplug::DeviceError, "Connection refused connecting to address #{@address}, port #{@port}!"
52
+ rescue StandardError => e
53
+ disconnect
54
+ debug_message("Unexpected exception encountered. Error: #{e}") if @debug
55
+ raise
56
+ end
57
+ else
58
+ @socket.close
59
+ raise TpLinkSmartplug::DeviceError, "Connection timeout connecting to address #{@address}, port #{@port}."
60
+ end
61
+ rescue Errno::EISCONN
62
+ debug_message('Connected') if @debug
63
+ rescue Errno::EINPROGRESS
64
+ debug_message('Connection in progress') if @debug
65
+ retry
66
+ rescue Errno::ECONNREFUSED
67
+ raise TpLinkSmartplug::DeviceError, "Connection refused connecting to address #{@address}, port #{@port}."
68
+ end
69
+ end
70
+
71
+ # Close connection to plug
72
+ #
73
+ def disconnect
74
+ @socket.close unless @socket.closed?
75
+ end
76
+
77
+ alias_method :close, :disconnect
78
+
79
+ # Return connection state open
80
+ #
81
+ # @return [True, False]
82
+ def open?
83
+ !@socket.closed?
84
+ end
85
+
86
+ # Return connection state closed
87
+ #
88
+ # @return [True, False]
89
+ def closed?
90
+ @socket.closed?
23
91
  end
24
92
 
93
+ # Polls plug with a command
94
+ #
95
+ # @param command [String] the command to send to the plug
96
+ # @return [Hash] the output from the plug command
97
+ def poll(command:)
98
+ connect
99
+
100
+ begin
101
+ debug_message("Sending: #{decrypt(encrypt(command)[4..(command.length + 4)])}") if @debug
102
+ @socket.write_nonblock(encrypt(command))
103
+ rescue IO::WaitWritable
104
+ IO.select(nil, [@socket])
105
+ retry
106
+ end
107
+
108
+ begin
109
+ data = @socket.recv_nonblock(2048)
110
+ rescue IO::WaitReadable
111
+ IO.select([@socket])
112
+ retry
113
+ end
114
+
115
+ if @poll_auto_close && !closed?
116
+ disconnect
117
+ raise 'Error occured during disconnect' unless closed?
118
+ end
119
+
120
+ raise 'No data received' if nil_or_empty?(data)
121
+
122
+ debug_message("Received Raw: #{data}") if @debug
123
+ data = decrypt(data[4..data.length])
124
+ debug_message("Received Decrypted: #{data}") if @debug
125
+
126
+ data
127
+ end
128
+
129
+ # @!method info
130
+ # Return plug information
131
+ # @return [Hash]
132
+ # @!method on
133
+ # Turn plug output on
134
+ # @return [Hash]
135
+ # @!method off
136
+ # Turn plug output off
137
+ # @return [Hash]
138
+ # @!method cloudinfo
139
+ # Return plug cloud account configuration
140
+ # @return [Hash]
141
+ # @!method wlanscan
142
+ # Perform a scan for wireless SSIDs
143
+ # @return [Hash]
144
+ # @!method time
145
+ # Return system time from the plug
146
+ # @return [Hash]
147
+ # @!method schedule
148
+ # Return schedule configured on the plug
149
+ # @return [Hash]
150
+ # @!method countdown
151
+ # Return countdown configured on the plug
152
+ # @return [Hash]
153
+ # @!method antitheft
154
+ # Unsure
155
+ # @return [Hash]
156
+ # @!method reboot
157
+ # Reboot plug
158
+ # @return [Hash]
159
+ # @!method resry
160
+ # Reset plug
161
+ # @return [Hash]
162
+ # @!method energy
163
+ # Return plug energy data
164
+ # @return [Hash]
25
165
  [
26
166
  :info,
27
167
  :on,
@@ -34,11 +174,13 @@ module TpLinkSmartplug
34
174
  :antitheft,
35
175
  :reboot,
36
176
  :reset,
37
- :energy
177
+ :energy,
38
178
  ].each do |method|
39
179
  define_method method do
40
- JSON.parse(poll(address: @address, port: @port, command: TpLinkSmartplug::Command.const_get(method.upcase), timeout: @timeout, debug: @debug))
180
+ JSON.parse(poll(command: TpLinkSmartplug::Command.const_get(method.upcase)))
41
181
  end
42
182
  end
43
183
  end
184
+
185
+ class DeviceError < TpLinkSmartplug::BaseError; end
44
186
  end
@@ -1,13 +1,23 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'time'
4
2
 
5
3
  module TpLinkSmartplug
6
4
  # Generic helper methods
7
5
  module Helpers
6
+ # Formats a message for output as a debug log
7
+ #
8
+ # @param string [String] the message to be formatted for debug output
8
9
  def debug_message(string)
9
- caller_method = caller_locations(1..1).label
10
- Time.now.strftime('%Y-%m-%d %H:%M:%S: ').concat("#{caller_method}: ").concat(string)
10
+ caller_method = caller_locations(1..1).first.label
11
+ $stdout.puts(Time.now.strftime('%Y-%m-%d %H:%M:%S: ').concat("#{caller_method}: ").concat(string))
12
+ end
13
+
14
+ # Tests a variable for nil or empty status
15
+ #
16
+ # @param v the variable to be tested
17
+ def nil_or_empty?(value)
18
+ return true if value.nil? || (value.respond_to?(:empty?) && value.empty?)
19
+
20
+ false
11
21
  end
12
22
  end
13
23
  end
@@ -1,68 +1,31 @@
1
- # frozen_string_literal: true
2
-
3
- require 'socket'
4
- require 'tp_link_smartplug/helpers'
5
- require 'tp_link_smartplug/message_helpers'
6
-
7
1
  module TpLinkSmartplug
8
- # Provides methods to send and receive messages from plugs
2
+ # Helper methods for plug communication messages
9
3
  module Message
10
- include TpLinkSmartplug::Helpers
11
- include TpLinkSmartplug::MessageHelpers
12
-
13
- def poll(address:, port:, command:, timeout: 3, debug: false)
14
- socket = connect(address: address, port: port, timeout: timeout, debug: debug)
15
- STDOUT.puts(debug_message("Sending: #{decrypt(encrypt(command)[4..command.length])}")) if debug
16
-
17
- begin
18
- socket.write_nonblock(encrypt(command))
19
- data = socket.recv_nonblock(2048)
20
- rescue IO::WaitReadable, IO::WaitWritable
21
- IO.select([socket], [socket])
22
- retry
23
- ensure
24
- disconnect(socket: socket)
4
+ # Encrypts a message to send to the smart plug
5
+ #
6
+ # @param string [String] the message to be encrypted
7
+ def encrypt(string)
8
+ key = 171
9
+ result = [string.length].pack('N')
10
+ string.each_char do |char|
11
+ key = a = key ^ char.ord
12
+ result.concat(a.chr)
25
13
  end
26
-
27
- raise 'Error occured during disconnect' unless socket.closed?
28
- raise 'No data received' if data.nil? || data.empty?
29
-
30
- STDOUT.puts(debug_message("Received Raw: #{data}")) if debug
31
- data = decrypt(data[4..data.length])
32
- STDOUT.puts(debug_message("Received: #{data}")) if debug
33
-
34
- data
14
+ result
35
15
  end
36
16
 
37
- private
38
-
39
- def connect(address:, port:, timeout: 3, debug: false)
40
- STDOUT.puts(debug_message("Connecting to #{address} port #{port}")) if debug
41
-
42
- Socket.new(Socket::AF_INET, Socket::SOCK_STREAM).tap do |socket|
43
- sockaddr = Addrinfo.getaddrinfo(address.to_s, port, Socket::PF_INET, :STREAM, 6).first.to_sockaddr
44
- STDOUT.puts(debug_message("Connecting, socket closed: #{socket.closed?}")) if debug
45
- socket.connect_nonblock(sockaddr)
46
- rescue IO::WaitWritable
47
- if IO.select(nil, [socket], nil, timeout)
48
- begin
49
- socket.connect_nonblock(sockaddr)
50
- rescue Errno::EISCONN
51
- STDOUT.puts(debug_message('Connected')) if debug
52
- rescue StandardError => e
53
- socket.close
54
- STDOUT.puts(debug_message('Unexpected exception encountered.')) if debug
55
- raise e
56
- end
57
- else
58
- socket.close
59
- raise "Connection timeout connecting to address #{address}, port #{port}."
60
- end
17
+ # Decrypts a message received from the smart plug
18
+ #
19
+ # @param string [String] the message to be decrypted
20
+ def decrypt(string)
21
+ key = 171
22
+ result = ''
23
+ string.each_char do |char|
24
+ a = key ^ char.ord
25
+ key = char.ord
26
+ result.concat(a.chr)
61
27
  end
62
- end
63
-
64
- def disconnect(socket:)
65
- socket.close unless socket.closed? || socket.nil?
28
+ result
66
29
  end
67
30
  end
68
31
  end
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
-
1
+ # @author Ben Hughes
3
2
  module TpLinkSmartplug
4
- VERSION = '0.0.1'
3
+ # Gem version
4
+ VERSION = '0.2.0'.freeze
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tp_link_smartplug
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Hughes
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-06-07 00:00:00.000000000 Z
11
+ date: 2021-01-24 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Control and retrieve data from a TP-Link HS100/110 (Metered) Smartplug
14
14
  email: bmhughes@bmhughes.co.uk
@@ -17,11 +17,11 @@ extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
19
  - "./lib/tp_link_smartplug.rb"
20
+ - "./lib/tp_link_smartplug/_base.rb"
20
21
  - "./lib/tp_link_smartplug/command.rb"
21
22
  - "./lib/tp_link_smartplug/device.rb"
22
23
  - "./lib/tp_link_smartplug/helpers.rb"
23
24
  - "./lib/tp_link_smartplug/message.rb"
24
- - "./lib/tp_link_smartplug/message_helpers.rb"
25
25
  - "./lib/tp_link_smartplug/version.rb"
26
26
  homepage: https://github.com/bmhughes/tp_link_smartplug
27
27
  licenses:
@@ -42,8 +42,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
42
42
  - !ruby/object:Gem::Version
43
43
  version: '0'
44
44
  requirements: []
45
- rubyforge_project:
46
- rubygems_version: 2.7.7
45
+ rubygems_version: 3.1.4
47
46
  signing_key:
48
47
  specification_version: 4
49
48
  summary: TP-Link HS100/110 Smart Plug interaction library
@@ -1,31 +0,0 @@
1
- module TpLinkSmartplug
2
- # Helper methods for plug communication messages
3
- module MessageHelpers
4
- # Encrypts a message to send to the smart plug
5
- #
6
- # @string format [String] the message to be encrypted
7
- def encrypt(string)
8
- key = 171
9
- result = [string.length].pack('N')
10
- string.each_char do |char|
11
- key = a = key ^ char.ord
12
- result.concat(a.chr)
13
- end
14
- result
15
- end
16
-
17
- # Decrypts a message received from the smart plug
18
- #
19
- # @string format [String] the message to be decrypted
20
- def decrypt(string)
21
- key = 171
22
- result = ''
23
- string.each_char do |char|
24
- a = key ^ char.ord
25
- key = char.ord
26
- result.concat(a.chr)
27
- end
28
- result
29
- end
30
- end
31
- end