stekker_zaptec 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,38 @@
1
+ module Zaptec
2
+ class Charger
3
+ attr_reader :id,
4
+ :name,
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
25
+ end
26
+
27
+ def self.parse(data)
28
+ new(
29
+ id: data.fetch("Id"),
30
+ name: data.fetch("Name"),
31
+ device_id: data.fetch("DeviceId"),
32
+ device_type: data.fetch("DeviceType"),
33
+ installation_name: data.fetch("InstallationName"),
34
+ installation_id: data.fetch("InstallationId")
35
+ )
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,111 @@
1
+ module Zaptec
2
+ class Client
3
+ BASE_URI = "https://api.zaptec.com".freeze
4
+ USER_ROLE = 1
5
+ OWNER_ROLE = 2
6
+
7
+ attr_reader :http_client, :credentials
8
+
9
+ delegate :expired?,
10
+ :access_token,
11
+ :expires_at,
12
+ to: :credentials,
13
+ prefix: true
14
+
15
+ def initialize(credentials: nil)
16
+ @credentials = credentials
17
+
18
+ @http_client = Faraday.new(url: BASE_URI) do |conn|
19
+ conn.request :json
20
+ conn.response :json
21
+ conn.response :raise_error
22
+ end
23
+ end
24
+
25
+ # https://zaptec.com/downloads/ZapChargerPro_Integration.pdf
26
+ def authorize(username:, password:)
27
+ raise Errors::ParameterMissing if username.blank? || password.blank?
28
+
29
+ start = Time.zone.now
30
+
31
+ response = http_client.post(
32
+ "#{BASE_URI}/oauth/token",
33
+ {
34
+ username: username,
35
+ password: password,
36
+ grant_type: "password"
37
+ }.to_query,
38
+ {
39
+ "Content-Type": "application/x-www-form-urlencoded"
40
+ }
41
+ )
42
+
43
+ @credentials = Zaptec::Credentials.new(
44
+ response.body["access_token"],
45
+ start + response.body["expires_in"].to_f
46
+ )
47
+ rescue Faraday::BadRequestError
48
+ raise Errors::AuthorizationFailed
49
+ end
50
+
51
+ # https://api.zaptec.com/help/index.html#/Charger/get_api_chargers
52
+ def chargers
53
+ get("/api/chargers", { Roles: USER_ROLE | OWNER_ROLE })
54
+ .body
55
+ .fetch("Data")
56
+ .map { |data| Charger.parse(data) }
57
+ end
58
+
59
+ # https://api.zaptec.com/help/index.html#/Charger/get_api_chargers__id__state
60
+ def state(charger_id, device_type)
61
+ get("/api/chargers/#{charger_id}/state")
62
+ .body
63
+ .to_h do |state|
64
+ [
65
+ Constants.observation_state_id_to_name(state_id: state.fetch("StateId"), device_type: device_type),
66
+ state.fetch("ValueAsString", nil)
67
+ ]
68
+ end
69
+ .then { |data| State.new(data) }
70
+ end
71
+
72
+ def pause_charging(charger_id) = send_command(charger_id, :StopChargingFinal)
73
+
74
+ def resume_charging(charger_id) = send_command(charger_id, :ResumeCharging)
75
+
76
+ private
77
+
78
+ # https://api.zaptec.com/help/index.html#/Charger/post_api_chargers__id__sendCommand__commandId_
79
+ def send_command(charger_id, command)
80
+ command_id = Constants.command_to_command_id(command)
81
+
82
+ post("/api/chargers/#{charger_id}/sendCommand/#{command_id}")
83
+ end
84
+
85
+ def get(endpoint, query = {})
86
+ require_valid_credentials!
87
+
88
+ http_client.get(
89
+ "#{BASE_URI}#{endpoint}",
90
+ query,
91
+ { Authorization: "Bearer #{credentials.access_token}" }
92
+ )
93
+ end
94
+
95
+ def post(endpoint, body = nil)
96
+ require_valid_credentials!
97
+
98
+ http_client.post(
99
+ "#{BASE_URI}#{endpoint}",
100
+ body,
101
+ { Authorization: "Bearer #{credentials.access_token}" }
102
+ )
103
+ rescue Faraday::Error => e
104
+ raise Errors::RequestFailed, "Request returned status #{e.response_status}"
105
+ end
106
+
107
+ def require_valid_credentials!
108
+ raise Errors::Unauthorized if credentials.blank? || credentials.expired?
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,57 @@
1
+ module Zaptec
2
+ class Constants
3
+ class << self
4
+ def observation_state_id_to_name(state_id:, device_type:)
5
+ device_type_observation_ids(device_type)
6
+ .fetch(state_id)
7
+ end
8
+
9
+ def charger_operation_mode_to_name(operation_mode)
10
+ constants
11
+ .fetch("ChargerOperationModes")
12
+ .detect { |_name, mode| mode == operation_mode }
13
+ .then { |name, _mode| name }
14
+ end
15
+
16
+ def command_to_command_id(command)
17
+ constants
18
+ .fetch("Commands")
19
+ .fetch(command.to_s) { raise "Unknown command '#{command}'" }
20
+ end
21
+
22
+ private
23
+
24
+ def device_type_observation_ids(device_type)
25
+ @device_type_observation_ids ||= {}
26
+
27
+ @device_type_observation_ids[device_type] ||=
28
+ begin
29
+ global_observation_ids = constants.fetch("Observations").invert.transform_values(&:to_sym)
30
+
31
+ device_specific_observations =
32
+ constants
33
+ .fetch("Schema")
34
+ .fetch(device_type_to_name(device_type))
35
+ .fetch("ObservationIds")
36
+ .invert
37
+ .transform_values(&:to_sym)
38
+
39
+ global_observation_ids.merge(device_specific_observations)
40
+ end
41
+ end
42
+
43
+ def device_type_to_name(device_type)
44
+ constants
45
+ .fetch("DeviceTypes")
46
+ .detect { |_name, type| type == device_type }
47
+ .then { |name, _type| name }
48
+ end
49
+
50
+ def constants
51
+ @constants ||= JSON.parse(constants_file.read)
52
+ end
53
+
54
+ def constants_file = Pathname.new(__dir__).join("../../data/constants.json")
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,14 @@
1
+ module Zaptec
2
+ class Credentials
3
+ attr_accessor :access_token, :expires_at
4
+
5
+ def initialize(access_token, expires_at)
6
+ @access_token = access_token
7
+ @expires_at = expires_at
8
+ end
9
+
10
+ def expired?(at = Time.zone.now)
11
+ expires_at.nil? || at >= expires_at
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ module Zaptec
2
+ module Errors
3
+ class Base < StandardError; end
4
+ class ParameterMissing < Base; end
5
+ class Unauthorized < Base; end
6
+ class RequestFailed < Base; end
7
+ class AuthorizationFailed < Base; end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ module Zaptec
2
+ class MeterReading
3
+ attr_reader :reading_kwh, :timestamp
4
+
5
+ def initialize(reading_kwh:, timestamp:)
6
+ @reading_kwh = reading_kwh
7
+ @timestamp = timestamp
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,32 @@
1
+ module Zaptec
2
+ class State
3
+ CHARGING_MODES = %w[Connected_Requesting Connected_Charging].freeze
4
+ DISCONNECTED = "Disconnected".freeze
5
+
6
+ def initialize(data)
7
+ @data = data
8
+ end
9
+
10
+ def total_charge_power = @data.fetch(:TotalChargePower).to_f
11
+
12
+ def max_phases = @data.fetch(:MaxPhases).to_i
13
+
14
+ def total_charge_power_session = @data.fetch(:TotalChargePowerSession).to_f
15
+
16
+ def charging? = charger_operation_mode.in?(CHARGING_MODES)
17
+
18
+ def disconnected? = charger_operation_mode == DISCONNECTED
19
+
20
+ def online? = @data.fetch(:IsOnline).to_i.positive?
21
+
22
+ def meter_reading
23
+ @meter_reading ||= MeterReading.new(reading_kwh: total_charge_power_session, timestamp: Time.zone.now)
24
+ end
25
+
26
+ private
27
+
28
+ def charger_operation_mode
29
+ Constants.charger_operation_mode_to_name(@data.fetch(:ChargerOperationMode).to_i)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ module Zaptec
2
+ VERSION = "1.0.0".freeze
3
+ end
data/lib/zaptec.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "faraday"
2
+ require "faraday_middleware"
3
+ require "faraday/detailed_logger"
4
+ require "faraday-cookie_jar"
5
+ require "active_model"
6
+ require "active_support/all"
7
+
8
+ require "zaptec/charger"
9
+ require "zaptec/client"
10
+ require "zaptec/constants"
11
+ require "zaptec/credentials"
12
+ require "zaptec/errors"
13
+ require "zaptec/meter_reading"
14
+ require "zaptec/state"
15
+ require "zaptec/version"
16
+
17
+ module Zaptec
18
+ end
data/zaptec.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ require_relative "lib/zaptec/version"
2
+ ruby_version = File.read(".ruby-version").strip
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = "stekker_zaptec"
6
+ spec.version = Zaptec::VERSION
7
+ spec.authors = ["Team Stekker"]
8
+ spec.email = ["support@stekker.com"]
9
+
10
+ spec.summary = "Connect to your Zaptec charger"
11
+ spec.description = "Zaptec connector"
12
+ spec.homepage = "https://stekker.com"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= #{ruby_version}")
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = "https://github.com/stekker/zaptec"
17
+ spec.metadata["changelog_uri"] = "https://github.com/stekker/zaptec"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_runtime_dependency "activemodel"
29
+ spec.add_runtime_dependency "activesupport"
30
+ spec.add_runtime_dependency "faraday"
31
+ spec.add_runtime_dependency "faraday-cookie_jar"
32
+ spec.add_runtime_dependency "faraday-detailed_logger"
33
+ spec.add_runtime_dependency "faraday_middleware"
34
+
35
+ spec.metadata["rubygems_mfa_required"] = "true"
36
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stekker_zaptec
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Team Stekker
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-02-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday-cookie_jar
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: faraday-detailed_logger
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: faraday_middleware
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Zaptec connector
98
+ email:
99
+ - support@stekker.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".github/workflows/ruby.yml"
105
+ - ".gitignore"
106
+ - ".rspec"
107
+ - ".rubocop.yml"
108
+ - ".ruby-version"
109
+ - Gemfile
110
+ - Gemfile.lock
111
+ - README.md
112
+ - Rakefile
113
+ - StekkerWeb/gems/zaptec/.gitignore
114
+ - StekkerWeb/gems/zaptec/.rspec
115
+ - StekkerWeb/gems/zaptec/.rubocop.yml
116
+ - StekkerWeb/gems/zaptec/.ruby-version
117
+ - bin/console
118
+ - bin/setup
119
+ - data/constants.json
120
+ - lib/zaptec.rb
121
+ - lib/zaptec/charger.rb
122
+ - lib/zaptec/client.rb
123
+ - lib/zaptec/constants.rb
124
+ - lib/zaptec/credentials.rb
125
+ - lib/zaptec/errors.rb
126
+ - lib/zaptec/meter_reading.rb
127
+ - lib/zaptec/state.rb
128
+ - lib/zaptec/version.rb
129
+ - zaptec.gemspec
130
+ homepage: https://stekker.com
131
+ licenses: []
132
+ metadata:
133
+ homepage_uri: https://stekker.com
134
+ source_code_uri: https://github.com/stekker/zaptec
135
+ changelog_uri: https://github.com/stekker/zaptec
136
+ rubygems_mfa_required: 'true'
137
+ post_install_message:
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: 3.0.2
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ requirements: []
152
+ rubygems_version: 3.2.22
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: Connect to your Zaptec charger
156
+ test_files: []