stekker_zaptec 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/.ruby-version +1 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +2 -0
- data/lib/stekker_zaptec.rb +4 -0
- data/lib/zaptec/charger.rb +8 -32
- data/lib/zaptec/circuit.rb +14 -0
- data/lib/zaptec/client.rb +109 -32
- data/lib/zaptec/constants.rb +16 -0
- data/lib/zaptec/credentials.rb +11 -0
- data/lib/zaptec/errors.rb +8 -1
- data/lib/zaptec/installation.rb +15 -0
- data/lib/zaptec/installation_hierarchy.rb +15 -0
- data/lib/zaptec/null_encryptor.rb +17 -0
- data/lib/zaptec/state.rb +1 -1
- data/lib/zaptec/version.rb +1 -1
- metadata +8 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 033251f5675b4433989cfed337cc22e1271d37e5590b2a375964a3d207a0b5b4
|
4
|
+
data.tar.gz: f486808fc05a7d926cde6cbbb534d76ab5e74c93173f469a065fa5d5bbeedee6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0242033c65f7e6b86e4c24b82692b01697e07a3ac667a7aa551a9a61c1b195cdfc3b6d74e40c7883ea372321119aef753245a1bd63d755f9dbd40806ea3e1205
|
7
|
+
data.tar.gz: d54c276c0f1a24ef8dd8a9a76c9458cf1e3589481f2b627c575a68f238d580b3eda8d12cd51bcf3d9be23c325d8567d38d7d942206b1d1898dfa00417f3f9456
|
data/.github/workflows/ruby.yml
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.
|
1
|
+
3.2.1
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -49,6 +49,7 @@ GEM
|
|
49
49
|
faraday-retry (1.0.3)
|
50
50
|
faraday_middleware (1.2.0)
|
51
51
|
faraday (~> 1.0)
|
52
|
+
gem-release (2.2.2)
|
52
53
|
hashdiff (1.0.1)
|
53
54
|
i18n (1.12.0)
|
54
55
|
concurrent-ruby (~> 1.0)
|
@@ -118,6 +119,7 @@ PLATFORMS
|
|
118
119
|
x86_64-linux
|
119
120
|
|
120
121
|
DEPENDENCIES
|
122
|
+
gem-release (~> 2.2)
|
121
123
|
jwt (~> 2.6)
|
122
124
|
rake (~> 13.0)
|
123
125
|
rspec (~> 3.0)
|
data/lib/stekker_zaptec.rb
CHANGED
@@ -4,11 +4,15 @@ require "active_model"
|
|
4
4
|
require "active_support/all"
|
5
5
|
|
6
6
|
require "zaptec/charger"
|
7
|
+
require "zaptec/circuit"
|
7
8
|
require "zaptec/client"
|
8
9
|
require "zaptec/constants"
|
9
10
|
require "zaptec/credentials"
|
10
11
|
require "zaptec/errors"
|
12
|
+
require "zaptec/installation"
|
13
|
+
require "zaptec/installation_hierarchy"
|
11
14
|
require "zaptec/meter_reading"
|
15
|
+
require "zaptec/null_encryptor"
|
12
16
|
require "zaptec/state"
|
13
17
|
require "zaptec/version"
|
14
18
|
|
data/lib/zaptec/charger.rb
CHANGED
@@ -1,38 +1,14 @@
|
|
1
1
|
module Zaptec
|
2
2
|
class Charger
|
3
|
-
|
4
|
-
|
5
|
-
:device_id,
|
6
|
-
:device_type,
|
7
|
-
:installation_name,
|
8
|
-
:installation_id
|
9
|
-
|
10
|
-
def initialize(
|
11
|
-
id:,
|
12
|
-
name:,
|
13
|
-
device_id:,
|
14
|
-
device_type:,
|
15
|
-
installation_name:,
|
16
|
-
installation_id:
|
17
|
-
)
|
18
|
-
|
19
|
-
@id = id
|
20
|
-
@name = name
|
21
|
-
@device_id = device_id
|
22
|
-
@device_type = device_type
|
23
|
-
@installation_name = installation_name
|
24
|
-
@installation_id = installation_id
|
3
|
+
def initialize(data)
|
4
|
+
@data = data.symbolize_keys
|
25
5
|
end
|
26
6
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
installation_name: data.fetch("InstallationName"),
|
34
|
-
installation_id: data.fetch("InstallationId")
|
35
|
-
)
|
36
|
-
end
|
7
|
+
def id = @data.fetch(:Id)
|
8
|
+
def name = @data.fetch(:Name)
|
9
|
+
def device_id = @data.fetch(:DeviceId)
|
10
|
+
def device_type = @data.fetch(:DeviceType)
|
11
|
+
def installation_name = @data.fetch(:InstallationName)
|
12
|
+
def installation_id = @data.fetch(:InstallationId)
|
37
13
|
end
|
38
14
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Zaptec
|
2
|
+
class Circuit
|
3
|
+
def initialize(data)
|
4
|
+
@data = data.symbolize_keys
|
5
|
+
end
|
6
|
+
|
7
|
+
def id = @data.fetch(:Id)
|
8
|
+
def max_current = @data.fetch(:MaxCurrent)
|
9
|
+
|
10
|
+
def chargers
|
11
|
+
@chargers ||= @data.fetch(:Chargers).map { |data| Charger.new(data) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/zaptec/client.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
module Zaptec
|
2
2
|
class Client
|
3
|
-
|
3
|
+
BASE_URL = "https://api.zaptec.com".freeze
|
4
4
|
USER_ROLE = 1
|
5
5
|
OWNER_ROLE = 2
|
6
|
+
TOKENS_CACHE_KEY = "zaptec.auth.tokens".freeze
|
6
7
|
|
7
|
-
attr_reader :
|
8
|
+
attr_reader :credentials
|
8
9
|
|
9
10
|
delegate :expired?,
|
10
11
|
:access_token,
|
@@ -12,27 +13,35 @@ module Zaptec
|
|
12
13
|
to: :credentials,
|
13
14
|
prefix: true
|
14
15
|
|
15
|
-
def initialize(
|
16
|
-
|
16
|
+
def initialize(
|
17
|
+
username:,
|
18
|
+
password:,
|
19
|
+
token_cache: ActiveSupport::Cache::MemoryStore.new,
|
20
|
+
encryptor: NullEncryptor.new
|
21
|
+
)
|
22
|
+
@username = username
|
23
|
+
@password = password
|
24
|
+
@token_cache = token_cache
|
25
|
+
@encryptor = encryptor
|
26
|
+
end
|
17
27
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
28
|
+
# https://zendesk.zaptec.com/hc/en-001/articles/6062673456657-Access-to-Installations-Authentication-for-Third-Parties#lookup-key-0-0
|
29
|
+
def grant_access_url(lookup_key:, partner_name:, redirect_url: nil, language: "en")
|
30
|
+
query = URI.encode_www_form(partnerName: partner_name, returnUrl: redirect_url, lang: language)
|
31
|
+
"https://portal.zaptec.com/#!/access/request/#{lookup_key}?#{query}"
|
23
32
|
end
|
24
33
|
|
25
34
|
# https://zaptec.com/downloads/ZapChargerPro_Integration.pdf
|
26
35
|
def authorize(username:, password:)
|
27
36
|
raise Errors::ParameterMissing if username.blank? || password.blank?
|
28
37
|
|
29
|
-
start = Time.
|
38
|
+
start = Time.current
|
30
39
|
|
31
|
-
response =
|
32
|
-
"#{
|
40
|
+
response = connection.post(
|
41
|
+
"#{BASE_URL}/oauth/token",
|
33
42
|
{
|
34
|
-
username
|
35
|
-
password
|
43
|
+
username:,
|
44
|
+
password:,
|
36
45
|
grant_type: "password"
|
37
46
|
}.to_query,
|
38
47
|
{
|
@@ -53,7 +62,19 @@ module Zaptec
|
|
53
62
|
get("/api/chargers", { Roles: USER_ROLE | OWNER_ROLE })
|
54
63
|
.body
|
55
64
|
.fetch("Data")
|
56
|
-
.map { |data| Charger.
|
65
|
+
.map { |data| Charger.new(data) }
|
66
|
+
end
|
67
|
+
|
68
|
+
# https://api.zaptec.com/help/index.html#/Installation/get_api_installation__id_
|
69
|
+
def get_installation(installation_id)
|
70
|
+
get("/api/installation/#{installation_id}")
|
71
|
+
.then { |response| Installation.new(response.body) }
|
72
|
+
end
|
73
|
+
|
74
|
+
# https://api.zaptec.com/help/index.html#/Installation/get_api_installation__id__hierarchy
|
75
|
+
def get_installation_hierarchy(installation_id)
|
76
|
+
get("/api/installation/#{installation_id}/hierarchy")
|
77
|
+
.then { |response| InstallationHierarchy.new(response.body) }
|
57
78
|
end
|
58
79
|
|
59
80
|
# https://api.zaptec.com/help/index.html#/Charger/get_api_chargers__id__state
|
@@ -62,7 +83,7 @@ module Zaptec
|
|
62
83
|
.body
|
63
84
|
.to_h do |state|
|
64
85
|
[
|
65
|
-
Constants.observation_state_id_to_name(state_id: state.fetch("StateId"), device_type:
|
86
|
+
Constants.observation_state_id_to_name(state_id: state.fetch("StateId"), device_type:),
|
66
87
|
state.fetch("ValueAsString", nil)
|
67
88
|
]
|
68
89
|
end
|
@@ -75,6 +96,22 @@ module Zaptec
|
|
75
96
|
|
76
97
|
private
|
77
98
|
|
99
|
+
attr_reader :username, :password
|
100
|
+
|
101
|
+
def connection
|
102
|
+
Faraday.new(url: BASE_URL) do |conn|
|
103
|
+
conn.request :json
|
104
|
+
conn.response :json
|
105
|
+
conn.response :raise_error
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def authenticated_connection
|
110
|
+
connection.tap do |conn|
|
111
|
+
conn.request :authorization, "Bearer", access_token
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
78
115
|
# https://api.zaptec.com/help/index.html#/Charger/post_api_chargers__id__sendCommand__commandId_
|
79
116
|
def send_command(charger_id, command)
|
80
117
|
command_id = Constants.command_to_command_id(command)
|
@@ -83,29 +120,69 @@ module Zaptec
|
|
83
120
|
end
|
84
121
|
|
85
122
|
def get(endpoint, query = {})
|
86
|
-
|
123
|
+
with_error_handling do
|
124
|
+
authenticated_connection.get("#{BASE_URL}#{endpoint}", query)
|
125
|
+
end
|
126
|
+
end
|
87
127
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
128
|
+
def post(endpoint, body: nil, query: nil)
|
129
|
+
with_error_handling do
|
130
|
+
authenticated_connection.post("#{BASE_URL}#{endpoint}", body) do |req|
|
131
|
+
req.params = query unless query.nil?
|
132
|
+
end
|
133
|
+
end
|
93
134
|
end
|
94
135
|
|
95
|
-
def
|
96
|
-
|
136
|
+
def with_error_handling
|
137
|
+
token_refreshed ||= false
|
97
138
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
139
|
+
yield
|
140
|
+
rescue Faraday::UnauthorizedError => e
|
141
|
+
if token_refreshed
|
142
|
+
raise Errors::RequestFailed.new("Request returned status #{e.response_status}", e.response)
|
143
|
+
else
|
144
|
+
refresh_access_token!
|
145
|
+
token_refreshed = true
|
146
|
+
|
147
|
+
retry
|
148
|
+
end
|
103
149
|
rescue Faraday::Error => e
|
104
|
-
raise Errors::RequestFailed
|
150
|
+
raise Errors::RequestFailed.new("Request returned status #{e.response_status}", e.response)
|
151
|
+
end
|
152
|
+
|
153
|
+
def access_token
|
154
|
+
current_access_token
|
155
|
+
.then do |current|
|
156
|
+
if current.expired?
|
157
|
+
refresh_access_token!
|
158
|
+
current_access_token
|
159
|
+
else
|
160
|
+
current
|
161
|
+
end
|
162
|
+
end
|
163
|
+
.then(&:access_token)
|
105
164
|
end
|
106
165
|
|
107
|
-
def
|
108
|
-
|
166
|
+
def current_access_token
|
167
|
+
encrypted_tokens = @token_cache.fetch(TOKENS_CACHE_KEY) do
|
168
|
+
@encryptor.encrypt(request_access_token.to_json, cipher_options: { deterministic: true })
|
169
|
+
end
|
170
|
+
|
171
|
+
plain_text_tokens = @encryptor.decrypt(encrypted_tokens)
|
172
|
+
Credentials.parse(JSON.parse(plain_text_tokens))
|
109
173
|
end
|
174
|
+
|
175
|
+
def refresh_access_token!
|
176
|
+
@token_cache.write(
|
177
|
+
TOKENS_CACHE_KEY,
|
178
|
+
@encryptor.encrypt(request_access_token.to_json, cipher_options: { deterministic: true }),
|
179
|
+
expires_in: 1.day
|
180
|
+
)
|
181
|
+
rescue Faraday::Error => e
|
182
|
+
raise Errors::RequestFailed.new("Request returned status #{e.response_status}", e.response)
|
183
|
+
end
|
184
|
+
|
185
|
+
# https://developer.easee.cloud/reference/post_api-accounts-login
|
186
|
+
def request_access_token = authorize(username:, password:)
|
110
187
|
end
|
111
188
|
end
|
data/lib/zaptec/constants.rb
CHANGED
@@ -21,6 +21,22 @@ module Zaptec
|
|
21
21
|
.fetch(command.to_s) { raise "Unknown command '#{command}'" }
|
22
22
|
end
|
23
23
|
|
24
|
+
def country_id_to_country_code(country_id)
|
25
|
+
return if country_id.nil?
|
26
|
+
|
27
|
+
constants
|
28
|
+
.fetch("Countries")
|
29
|
+
.fetch(country_id)
|
30
|
+
.fetch("Code")
|
31
|
+
end
|
32
|
+
|
33
|
+
def network_type_to_name(network_type)
|
34
|
+
constants
|
35
|
+
.fetch("NetworkTypes")
|
36
|
+
.detect { |_name, type| type == network_type }
|
37
|
+
.then { |name, _type| name }
|
38
|
+
end
|
39
|
+
|
24
40
|
private
|
25
41
|
|
26
42
|
def device_type_observation_ids(device_type)
|
data/lib/zaptec/credentials.rb
CHANGED
@@ -10,5 +10,16 @@ module Zaptec
|
|
10
10
|
def expired?(at = Time.zone.now)
|
11
11
|
expires_at.nil? || at >= expires_at
|
12
12
|
end
|
13
|
+
|
14
|
+
def self.parse(data)
|
15
|
+
new(
|
16
|
+
data.fetch("access_token"),
|
17
|
+
Time.zone.at(data.fetch("expires_at"))
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def as_json(*)
|
22
|
+
super.merge("expires_at" => expires_at.to_i)
|
23
|
+
end
|
13
24
|
end
|
14
25
|
end
|
data/lib/zaptec/errors.rb
CHANGED
@@ -3,7 +3,14 @@ module Zaptec
|
|
3
3
|
class Base < StandardError; end
|
4
4
|
class ParameterMissing < Base; end
|
5
5
|
class Unauthorized < Base; end
|
6
|
-
class RequestFailed < Base
|
6
|
+
class RequestFailed < Base
|
7
|
+
attr_reader :response
|
8
|
+
|
9
|
+
def initialize(message, response = nil)
|
10
|
+
@response = response
|
11
|
+
super(message)
|
12
|
+
end
|
13
|
+
end
|
7
14
|
class AuthorizationFailed < Base; end
|
8
15
|
end
|
9
16
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Zaptec
|
2
|
+
class Installation
|
3
|
+
def initialize(data)
|
4
|
+
@data = data.deep_symbolize_keys
|
5
|
+
end
|
6
|
+
|
7
|
+
def id = @data.fetch(:Id)
|
8
|
+
def address = @data[:Address]
|
9
|
+
def zip_code = @data[:ZipCode]
|
10
|
+
def city = @data[:City]
|
11
|
+
def latitude = @data[:Latitude]
|
12
|
+
def country_code = Constants.country_id_to_country_code(@data[:CountryId])
|
13
|
+
def longitude = @data[:Longitude]
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Zaptec
|
2
|
+
class InstallationHierarchy
|
3
|
+
def initialize(data)
|
4
|
+
@data = data.deep_symbolize_keys
|
5
|
+
end
|
6
|
+
|
7
|
+
def id = @data.fetch(:Id)
|
8
|
+
def name = @data.fetch(:Name)
|
9
|
+
def network_type = Constants.network_type_to_name(@data.fetch(:NetworkType))
|
10
|
+
|
11
|
+
def circuits
|
12
|
+
@circuits ||= @data.fetch(:Circuits).map { |data| Circuit.new(data) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Zaptec
|
2
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
3
|
+
class NullEncryptor
|
4
|
+
def encrypt(clear_text, key_provider: nil, cipher_options: {})
|
5
|
+
clear_text
|
6
|
+
end
|
7
|
+
|
8
|
+
def decrypt(encrypted_text, key_provider: nil, cipher_options: {})
|
9
|
+
encrypted_text
|
10
|
+
end
|
11
|
+
|
12
|
+
def encrypted?(_text)
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
17
|
+
end
|
data/lib/zaptec/state.rb
CHANGED
@@ -20,7 +20,7 @@ module Zaptec
|
|
20
20
|
def online? = @data.fetch(:IsOnline).to_i.positive?
|
21
21
|
|
22
22
|
def meter_reading
|
23
|
-
@meter_reading ||= MeterReading.new(reading_kwh:
|
23
|
+
@meter_reading ||= MeterReading.new(reading_kwh: total_charge_power, timestamp: Time.zone.now)
|
24
24
|
end
|
25
25
|
|
26
26
|
private
|
data/lib/zaptec/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stekker_zaptec
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Team Stekker
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-04-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -87,11 +87,15 @@ files:
|
|
87
87
|
- data/constants.json
|
88
88
|
- lib/stekker_zaptec.rb
|
89
89
|
- lib/zaptec/charger.rb
|
90
|
+
- lib/zaptec/circuit.rb
|
90
91
|
- lib/zaptec/client.rb
|
91
92
|
- lib/zaptec/constants.rb
|
92
93
|
- lib/zaptec/credentials.rb
|
93
94
|
- lib/zaptec/errors.rb
|
95
|
+
- lib/zaptec/installation.rb
|
96
|
+
- lib/zaptec/installation_hierarchy.rb
|
94
97
|
- lib/zaptec/meter_reading.rb
|
98
|
+
- lib/zaptec/null_encryptor.rb
|
95
99
|
- lib/zaptec/state.rb
|
96
100
|
- lib/zaptec/version.rb
|
97
101
|
- zaptec.gemspec
|
@@ -110,14 +114,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
110
114
|
requirements:
|
111
115
|
- - ">="
|
112
116
|
- !ruby/object:Gem::Version
|
113
|
-
version: 3.
|
117
|
+
version: 3.2.1
|
114
118
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
119
|
requirements:
|
116
120
|
- - ">="
|
117
121
|
- !ruby/object:Gem::Version
|
118
122
|
version: '0'
|
119
123
|
requirements: []
|
120
|
-
rubygems_version: 3.
|
124
|
+
rubygems_version: 3.4.6
|
121
125
|
signing_key:
|
122
126
|
specification_version: 4
|
123
127
|
summary: Connect to your Zaptec charger
|