From b0870b47424d0c8ca68217daa770f86db67f5129 Mon Sep 17 00:00:00 2001 From: Bob Forma <1178544+bforma@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:13:51 +0200 Subject: [PATCH 1/2] Walk every page of the chargers listing GET /api/chargers is paginated; reading only the first page silently hid every charger beyond it on accounts with more than one page, which left those chargers unresolvable for callers matching on the listing. --- lib/zaptec/client.rb | 21 +++++++++++++++++---- spec/zaptec/client_spec.rb | 30 ++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/lib/zaptec/client.rb b/lib/zaptec/client.rb index d872e6f..2dce06d 100644 --- a/lib/zaptec/client.rb +++ b/lib/zaptec/client.rb @@ -3,6 +3,7 @@ class Client BASE_URL = "https://api.zaptec.com".freeze USER_ROLE = 1 OWNER_ROLE = 2 + CHARGERS_PAGE_SIZE = 100 TOKENS_CACHE_KEY = "zaptec.auth.tokens".freeze DEFAULT_HIERARCHY_MAX_WAIT = 30.seconds @@ -60,10 +61,22 @@ def authorize(username:, password:) # https://api.zaptec.com/help/index.html#/Charger/get_api_chargers def chargers - get("/api/chargers", { Roles: USER_ROLE | OWNER_ROLE }) - .body - .fetch("Data") - .map { |data| Charger.new(data) } + page_index = 0 + chargers = [] + + loop do + body = get( + "/api/chargers", + { Roles: USER_ROLE | OWNER_ROLE, PageIndex: page_index, PageSize: CHARGERS_PAGE_SIZE }, + ).body + + chargers.concat(body.fetch("Data").map { |data| Charger.new(data) }) + page_index += 1 + + break if page_index >= body.fetch("Pages", 1) + end + + chargers end # https://api.zaptec.com/help/index.html#/Installation/get_api_installation__id_ diff --git a/spec/zaptec/client_spec.rb b/spec/zaptec/client_spec.rb index 0a33d17..2ef6c8b 100644 --- a/spec/zaptec/client_spec.rb +++ b/spec/zaptec/client_spec.rb @@ -19,7 +19,7 @@ ) WebMock::API - .stub_request(:get, "https://api.zaptec.com/api/chargers?Roles=3") + .stub_request(:get, "https://api.zaptec.com/api/chargers?Roles=3&PageIndex=0&PageSize=100") .with(headers: { "Authorization" => "Bearer T123" }) .to_return( body: { Data: [] }.to_json, @@ -60,7 +60,7 @@ ) WebMock::API - .stub_request(:get, "https://api.zaptec.com/api/chargers?Roles=3") + .stub_request(:get, "https://api.zaptec.com/api/chargers?Roles=3&PageIndex=0&PageSize=100") .with(headers: { "Authorization" => "Bearer T789" }) .to_return( body: { Data: [] }.to_json, @@ -98,7 +98,7 @@ ) WebMock::API - .stub_request(:get, "https://api.zaptec.com/api/chargers?Roles=3") + .stub_request(:get, "https://api.zaptec.com/api/chargers?Roles=3&PageIndex=0&PageSize=100") .with(headers: { "Authorization" => "Bearer T123" }) .to_return( body: { Data: [] }.to_json, @@ -165,7 +165,7 @@ describe "#chargers" do it "gets the list of chargers" do WebMock::API - .stub_request(:get, "https://api.zaptec.com/api/chargers?Roles=3") + .stub_request(:get, "https://api.zaptec.com/api/chargers?Roles=3&PageIndex=0&PageSize=100") .to_return( body: chargers_example.to_json, headers: { "Content-Type": "application/json" }, @@ -186,6 +186,28 @@ installation_id: "2bbec6f9-c3ce-4edf-a72f-b1b2a663c6ba", ) end + + it "walks every page of the chargers listing" do + first_page = chargers_example.merge(Pages: 2) + second_charger = chargers_example[:Data].first.merge( + Id: "aaa603a7-ff53-4ed8-8dd6-f79c94819458", + DeviceId: "GPN012345", + Name: "Zaptec 2", + ) + second_page = { Pages: 2, Data: [second_charger] } + + WebMock::API + .stub_request(:get, "https://api.zaptec.com/api/chargers?Roles=3&PageIndex=0&PageSize=100") + .to_return(body: first_page.to_json, headers: { "Content-Type": "application/json" }) + WebMock::API + .stub_request(:get, "https://api.zaptec.com/api/chargers?Roles=3&PageIndex=1&PageSize=100") + .to_return(body: second_page.to_json, headers: { "Content-Type": "application/json" }) + + token_cache = build_token_cache("T123") + client = Zaptec::Client.new(username: "zap", password: "tec", token_cache:) + + expect(client.chargers.map(&:device_id)).to eq %w[ZAP049387 GPN012345] + end end describe "#state" do From f64734a7d425f28eb8adcafb9b5bdba44026ab87 Mon Sep 17 00:00:00 2001 From: Bob Forma <1178544+bforma@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:14:53 +0200 Subject: [PATCH 2/2] Look up a single charger through GET /api/chargers/{id} Resolving one charge point's model only needs its own charger data; fetching it directly avoids walking the paginated account-wide listing for every lookup. --- lib/zaptec/client.rb | 6 ++++++ spec/zaptec/client_spec.rb | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/lib/zaptec/client.rb b/lib/zaptec/client.rb index 2dce06d..44f11ca 100644 --- a/lib/zaptec/client.rb +++ b/lib/zaptec/client.rb @@ -79,6 +79,12 @@ def chargers chargers end + # https://docs.zaptec.com/reference/api_chargers_id_get + def charger(charger_id) + get("/api/chargers/#{charger_id}") + .then { |response| Charger.new(response.body) } + end + # https://api.zaptec.com/help/index.html#/Installation/get_api_installation__id_ def get_installation(installation_id) get("/api/installation/#{installation_id}") diff --git a/spec/zaptec/client_spec.rb b/spec/zaptec/client_spec.rb index 2ef6c8b..c9d3bb2 100644 --- a/spec/zaptec/client_spec.rb +++ b/spec/zaptec/client_spec.rb @@ -187,6 +187,25 @@ ) end + it "fetches a single charger by id" do + WebMock::API + .stub_request(:get, "https://api.zaptec.com/api/chargers/93d603a7-ff53-4ed8-8dd6-f79c94819458") + .to_return( + body: chargers_example[:Data].first.to_json, + headers: { "Content-Type": "application/json" }, + ) + + token_cache = build_token_cache("T123") + client = Zaptec::Client.new(username: "zap", password: "tec", token_cache:) + + expect(client.charger("93d603a7-ff53-4ed8-8dd6-f79c94819458")) + .to have_attributes( + id: "93d603a7-ff53-4ed8-8dd6-f79c94819458", + device_id: "ZAP049387", + device_type: 4, + ) + end + it "walks every page of the chargers listing" do first_page = chargers_example.merge(Pages: 2) second_charger = chargers_example[:Data].first.merge(