stekker_zaptec 1.1.1 → 1.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 +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
|