sma_api 0.1.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 +7 -0
- data/License.txt +21 -0
- data/README.md +110 -0
- data/lib/sma_api.rb +9 -0
- data/lib/sma_api/client.rb +77 -0
- data/lib/sma_api/http.rb +116 -0
- data/lib/sma_api/version.rb +3 -0
- metadata +173 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 568631fddbba2a58f8967bcc1e794502c2d79fef112429b48b98ce8120ece476
|
4
|
+
data.tar.gz: 1cf5bfdb6f873b24e6364abacde7adea8886415f58e0c5629fcddfd7d8ccef90
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6c3d0d6bd0f85f2cbd16ee090951bf0482cfedd13496d1852f3022ef15ca6d343fc566ca8ac12810c90f208cce3182db6815db74c6fb04f1959b763fee6bf7f7
|
7
|
+
data.tar.gz: 5d3db35181b73f5e64e61108f82aa46839f1ad92cad958ed5e56567c6c7be4a977013ad1fd14cec511b26a29bbbf9dc260728b1305dcf7b06293ae277c58b0e4
|
data/License.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Rutger Wessels
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# SmaApi
|
2
|
+
|
3
|
+
This gem provides an API for the web interface of SMA inverters.
|
4
|
+
|
5
|
+
The gem is in early development and should not be considered stable. Everything might change.
|
6
|
+
|
7
|
+
## Supported inverters
|
8
|
+
|
9
|
+
This gem has been developed using a SMA Sunny Boy 3.0 (SB3.0-1AV-41 902).
|
10
|
+
Firmware version is 3.10.18.R
|
11
|
+
|
12
|
+
It will probably work with other SMA products that have a recent firmware. But
|
13
|
+
it has not been tested.
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'sma_api', git: 'https://github.com/rutgerw/sma_api'
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install sma_api
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
The web interface of the inverter does not allow an unlimited number of sessions.
|
34
|
+
There seems to be a limit of 4 sessions. Another attempt to login will result into
|
35
|
+
an error message from the web interface, which is turned into a `SmaApi::Error`
|
36
|
+
that has the `Creating session failed` message. The software in the inverter will
|
37
|
+
free up a session after a 5 minute inactivity.
|
38
|
+
|
39
|
+
There are different ways of handling the session:
|
40
|
+
- Create the `SmaApi::Client` instance just once and use it multiple times
|
41
|
+
- Store the session id in a cache (file, Redis or another solution)
|
42
|
+
- Use `client.destroy_session` to explicitly remove the session
|
43
|
+
|
44
|
+
### Create client once
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require 'sma_api'
|
48
|
+
|
49
|
+
client = SmaApi::Client.new(host: 'inverter address', password: 'password')
|
50
|
+
|
51
|
+
while true do
|
52
|
+
# Current production
|
53
|
+
puts client.get_values(['6100_40263F00'])
|
54
|
+
sleep 5
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
### Cache the session id
|
59
|
+
|
60
|
+
In case the `sid` is not valid anymore, the client will try to create a new session.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
require 'sma_api'
|
64
|
+
|
65
|
+
# Cache the sid in this file
|
66
|
+
sid_file = '/tmp/sma_sid.txt'
|
67
|
+
|
68
|
+
sid = File.read(sid_file).chop rescue ''
|
69
|
+
|
70
|
+
client = SmaApi::Client.new(host: ENV['SMA_API_HOST'], password: ENV['SMA_API_WEB_PASSWORD'], sid: sid)
|
71
|
+
|
72
|
+
while true do
|
73
|
+
current_yield = client.get_values(['6100_40263F00'])
|
74
|
+
|
75
|
+
# If sid has been changed, save it to the sid file
|
76
|
+
if client.sid != sid
|
77
|
+
File.open(sid_file, 'w') { |f| f.puts sid }
|
78
|
+
end
|
79
|
+
|
80
|
+
puts "#{Time.now}\tCurrent yield: #{current_yield}"
|
81
|
+
|
82
|
+
sleep 2
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
### Use destroy_session
|
87
|
+
|
88
|
+
This is the same as logging out from the web interface.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
require 'sma_api'
|
92
|
+
|
93
|
+
client = SmaApi::Client.new(host: 'inverter address', password: 'password', sid: sid)
|
94
|
+
|
95
|
+
# Current production
|
96
|
+
puts client.get_values(['6100_40263F00'])
|
97
|
+
|
98
|
+
client.destroy_session
|
99
|
+
|
100
|
+
# or:
|
101
|
+
at_exit { client.destroy_session }
|
102
|
+
```
|
103
|
+
|
104
|
+
## Contributing
|
105
|
+
|
106
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rutgerw/sma_api.
|
107
|
+
|
108
|
+
## License
|
109
|
+
|
110
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/sma_api.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SmaApi
|
4
|
+
# Ruby client for communicating with SMA Inverter web interface
|
5
|
+
class Client
|
6
|
+
def initialize(host:, password:, sid: nil)
|
7
|
+
@host = host
|
8
|
+
@password = password
|
9
|
+
@client = Http.new(host: host, password: password, sid: sid)
|
10
|
+
end
|
11
|
+
|
12
|
+
# The current session id. If empty, it will create a new session
|
13
|
+
#
|
14
|
+
# @return [String] the session id that will be used in subsequent requests.
|
15
|
+
def sid
|
16
|
+
@client.sid
|
17
|
+
end
|
18
|
+
|
19
|
+
# Logout and clear the session. This is the same as using Logout from the web interface
|
20
|
+
#
|
21
|
+
# @return [String] An empty session id
|
22
|
+
def destroy_session
|
23
|
+
@client.destroy_session
|
24
|
+
end
|
25
|
+
|
26
|
+
# Retrieve values specified by the keys using getValues.json endpoint.
|
27
|
+
#
|
28
|
+
# @param keys [Array<String>] List of keys
|
29
|
+
# @return [Hash] Key-value pairs
|
30
|
+
def get_values(keys)
|
31
|
+
result = @client.post('/dyn/getValues.json', { destDev: [], keys: keys })
|
32
|
+
return nil unless result['result']
|
33
|
+
|
34
|
+
keys.each_with_object({}) do |k, h|
|
35
|
+
h[k] = scalar_value(result['result'].first[1][k])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Retrieve list of files and directories for the path.
|
40
|
+
#
|
41
|
+
# @return [Array] List of directories and files
|
42
|
+
def get_fs(path)
|
43
|
+
result = @client.post('/dyn/getFS.json', { destDev: [], path: path })
|
44
|
+
|
45
|
+
result['result'].first[1][path].map do |f|
|
46
|
+
type = f.key?('f') ? 'f' : 'd'
|
47
|
+
{
|
48
|
+
name: f['d'] || f['f'],
|
49
|
+
type: type,
|
50
|
+
last_modified: Time.at(f['tm']),
|
51
|
+
size: f['s']
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Download the file specified by url and store it in a file on the local file system.
|
57
|
+
#
|
58
|
+
# @param url [String] URL without /fs prefix. It should start with a '/'
|
59
|
+
# @param path [String] Path of the local file
|
60
|
+
def download(path, target)
|
61
|
+
@client.download(path, target)
|
62
|
+
end
|
63
|
+
|
64
|
+
# ObjectMetadata_Istl endpoint
|
65
|
+
#
|
66
|
+
# @return [Array] List of available object metadata
|
67
|
+
def object_metadata
|
68
|
+
@client.post('/data/ObjectMetadata_Istl.json')
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def scalar_value(value)
|
74
|
+
value['1'].first['val']
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/sma_api/http.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
module SmaApi
|
7
|
+
# Net::HTTP wrapper for the SMA Inverter web interface
|
8
|
+
class Http
|
9
|
+
def initialize(host:, password:, sid: nil)
|
10
|
+
@host = host
|
11
|
+
@password = password
|
12
|
+
@sid = sid || ''
|
13
|
+
end
|
14
|
+
|
15
|
+
# Perform a HTTP POST.
|
16
|
+
#
|
17
|
+
# @param url [String] URL. It should start with a '/'
|
18
|
+
# @param payload [Hash] The payload that will be used in the post
|
19
|
+
# @return [Hash] The response
|
20
|
+
def post(url, payload = {})
|
21
|
+
create_session if @sid.empty?
|
22
|
+
|
23
|
+
response = JSON.parse(http.post(url_with_sid(url), payload.to_json).body)
|
24
|
+
|
25
|
+
return response unless response.key? 'err'
|
26
|
+
|
27
|
+
raise SmaApi::Error, "Error #{response['err']} during request" unless response['err'] == 401
|
28
|
+
|
29
|
+
create_session
|
30
|
+
post(url, payload)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Download the file specified by url and store it in a file on the local file system.
|
34
|
+
#
|
35
|
+
# @param url [String] URL without /fs prefix. It should start with a '/'
|
36
|
+
# @param path [String] Path of the local file
|
37
|
+
def download(url, path)
|
38
|
+
create_session if @sid.empty?
|
39
|
+
|
40
|
+
file = File.open(path, 'wb')
|
41
|
+
|
42
|
+
begin
|
43
|
+
res = retrieve_file(url)
|
44
|
+
|
45
|
+
file.write(res.body)
|
46
|
+
ensure
|
47
|
+
file.close
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Creates a session using the supplied password
|
52
|
+
#
|
53
|
+
# @raise [SmaApi::Error] Creating session failed, for example if the password is wrong
|
54
|
+
# @return [String] the session id that will be used in subsequent requests.
|
55
|
+
def create_session
|
56
|
+
payload = {
|
57
|
+
right: 'usr', pass: @password
|
58
|
+
}
|
59
|
+
result = JSON.parse(http.post('/dyn/login.json', payload.to_json).body).fetch('result', {})
|
60
|
+
|
61
|
+
raise SmaApi::Error, 'Creating session failed' unless result['sid']
|
62
|
+
|
63
|
+
@sid = result['sid']
|
64
|
+
end
|
65
|
+
|
66
|
+
# The current session id. If empty, it will create a new session
|
67
|
+
#
|
68
|
+
# @return [String] the session id that will be used in subsequent requests.
|
69
|
+
def sid
|
70
|
+
create_session if @sid.empty?
|
71
|
+
|
72
|
+
@sid
|
73
|
+
end
|
74
|
+
|
75
|
+
# Logout and clear the session. This is the same as using Logout from the web interface
|
76
|
+
#
|
77
|
+
# @return [String] An empty session id
|
78
|
+
def destroy_session
|
79
|
+
post('/dyn/logout.json', {})
|
80
|
+
|
81
|
+
@sid = ''
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def url_with_sid(url)
|
87
|
+
url + "?sid=#{@sid}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def retrieve_file(url)
|
91
|
+
res = http.get('/fs' + url_with_sid(url))
|
92
|
+
|
93
|
+
unless res.code == '200'
|
94
|
+
# Try again because invalid sid does not result in a 401
|
95
|
+
create_session
|
96
|
+
res = http.get('/fs' + url_with_sid(url))
|
97
|
+
|
98
|
+
raise "Error retrieving file (#{res.code} #{res.message})" unless res.code == '200'
|
99
|
+
end
|
100
|
+
|
101
|
+
res
|
102
|
+
end
|
103
|
+
|
104
|
+
def http
|
105
|
+
@http ||= configure_http_client
|
106
|
+
end
|
107
|
+
|
108
|
+
def configure_http_client
|
109
|
+
client = Net::HTTP.new(@host, 443)
|
110
|
+
client.use_ssl = true
|
111
|
+
client.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
112
|
+
|
113
|
+
client
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
metadata
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sma_api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rutger Wessels
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-07-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.17'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.17'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: byebug
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '11.1'
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 11.1.3
|
37
|
+
type: :development
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '11.1'
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 11.1.3
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '10.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '10.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rspec
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3.9'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3.9'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rubocop
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 0.82.0
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 0.82.0
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rubocop-rspec
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '1.39'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '1.39'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: vcr
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '5.1'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '5.1'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: webmock
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '3.8'
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: 3.8.3
|
127
|
+
type: :development
|
128
|
+
prerelease: false
|
129
|
+
version_requirements: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - "~>"
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '3.8'
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: 3.8.3
|
137
|
+
description: Extract data from a SMA Inverter web interface.
|
138
|
+
email:
|
139
|
+
- rutger@rutgerwessels.nl
|
140
|
+
executables: []
|
141
|
+
extensions: []
|
142
|
+
extra_rdoc_files: []
|
143
|
+
files:
|
144
|
+
- License.txt
|
145
|
+
- README.md
|
146
|
+
- lib/sma_api.rb
|
147
|
+
- lib/sma_api/client.rb
|
148
|
+
- lib/sma_api/http.rb
|
149
|
+
- lib/sma_api/version.rb
|
150
|
+
homepage: https://github.com/rutgerw/sma_api
|
151
|
+
licenses:
|
152
|
+
- MIT
|
153
|
+
metadata: {}
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0'
|
168
|
+
requirements: []
|
169
|
+
rubygems_version: 3.1.2
|
170
|
+
signing_key:
|
171
|
+
specification_version: 4
|
172
|
+
summary: Extract data from a SMA Inverter web interface.
|
173
|
+
test_files: []
|