From 17ab44714100fedc9d5d087e0e3a202ef9995754 Mon Sep 17 00:00:00 2001 From: Martijn Versluis Date: Wed, 15 Jul 2026 11:40:45 +0200 Subject: [PATCH] Distinguish pair failures by Easee's PairingResultCode title Easee packs a family of pair rejections (IncorrectPIN, TooManyAttempts, AlreadyPairedWithPartner/User, ...) under one shared errorCode 123 and puts the specific reason in the response body's `title` field, following their PairingResultCode enumeration. Before this change the client only looked at `errorCode`, so every one of those failures collapsed into a generic RequestFailed. Consumers of the gem had no way to tell a wrong PIN apart from a 24-hour lockout or a charger that was already paired to someone else. This adds TooManyAttempts and AlreadyPaired error classes and teaches the client to pick the right one based on the title when errorCode is 123. An unknown title still falls through to the generic RequestFailed. https://developer.easee.com/docs/enumerations --- lib/easee/client.rb | 10 +++ lib/easee/errors.rb | 19 ++++++ spec/easee/client_spec.rb | 125 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+) diff --git a/lib/easee/client.rb b/lib/easee/client.rb index 4b9a512..1179200 100644 --- a/lib/easee/client.rb +++ b/lib/easee/client.rb @@ -189,11 +189,21 @@ def raise_credentials_or_request_error(error) raise Errors::InvalidPinCode.new("PIN code rejected", error.response) when Errors::ChargerNotFound::CODE raise Errors::ChargerNotFound.new("Charger not found", error.response) + when Errors::PAIRING_ERROR_CODE + raise_pairing_result_error(error) end raise request_failed(error) end + def raise_pairing_result_error(error) + title = error.response&.dig(:body, "title") + error_class = Errors::PAIRING_RESULT_ERRORS.find { |klass| klass::PAIRING_RESULT_TITLES.include?(title) } + return unless error_class + + raise error_class.new("Pairing rejected: #{title}", error.response) + end + def request_failed(error) Errors::RequestFailed.new("Request returned status #{error.response_status}", error.response) end diff --git a/lib/easee/errors.rb b/lib/easee/errors.rb index e1c6eaa..71001e2 100644 --- a/lib/easee/errors.rb +++ b/lib/easee/errors.rb @@ -21,14 +21,33 @@ class Forbidden < Base; end class InvalidPinCode < RequestFailed CODE = 193 + PAIRING_RESULT_TITLES = %w[IncorrectPIN].freeze end class ChargerNotFound < RequestFailed CODE = 400 end + class TooManyAttempts < RequestFailed + PAIRING_RESULT_TITLES = %w[TooManyAttempts].freeze + end + + class AlreadyPaired < RequestFailed + PAIRING_RESULT_TITLES = %w[AlreadyPairedWithPartner AlreadyPairedWithUser].freeze + end + class RateLimitExceeded < RequestFailed def retryable? = true end + + # https://developer.easee.com/docs/enumerations — pairing result codes (0..7) + # ride on a single HTTP errorCode of 123, with the specific reason in `title`. + PAIRING_ERROR_CODE = 123 + + PAIRING_RESULT_ERRORS = [ + InvalidPinCode, + TooManyAttempts, + AlreadyPaired, + ].freeze end end diff --git a/spec/easee/client_spec.rb b/spec/easee/client_spec.rb index 56fada8..c82e4d5 100644 --- a/spec/easee/client_spec.rb +++ b/spec/easee/client_spec.rb @@ -367,6 +367,131 @@ expect { client.pair(charger_id: "UNKNOWN", pin_code: "1234") } .to raise_error(Easee::Errors::ChargerNotFound) end + + it "raises InvalidPinCode on a PairingError with title IncorrectPIN" do + token_cache = ActiveSupport::Cache::MemoryStore.new + token_cache.write( + Easee::Client::TOKENS_CACHE_KEY, + { "accessToken" => "T123" }.to_json, + ) + + stub_request(:post, "https://api.easee.cloud/api/chargers/123ABC/pair?pinCode=WRONG") + .with(headers: { "Authorization" => "Bearer T123" }) + .to_return( + status: 400, + body: { + errorCode: 123, + errorCodeName: "PairingError", + title: "IncorrectPIN", + }.to_json, + headers: { "Content-Type": "application/json" }, + ) + + client = Easee::Client.new(user_name: "easee", password: "money", token_cache:) + + expect { client.pair(charger_id: "123ABC", pin_code: "WRONG") } + .to raise_error(Easee::Errors::InvalidPinCode) + end + + it "raises TooManyAttempts on a PairingError with title TooManyAttempts" do + token_cache = ActiveSupport::Cache::MemoryStore.new + token_cache.write( + Easee::Client::TOKENS_CACHE_KEY, + { "accessToken" => "T123" }.to_json, + ) + + stub_request(:post, "https://api.easee.cloud/api/chargers/123ABC/pair?pinCode=1234") + .with(headers: { "Authorization" => "Bearer T123" }) + .to_return( + status: 400, + body: { + errorCode: 123, + errorCodeName: "PairingError", + title: "TooManyAttempts", + }.to_json, + headers: { "Content-Type": "application/json" }, + ) + + client = Easee::Client.new(user_name: "easee", password: "money", token_cache:) + + expect { client.pair(charger_id: "123ABC", pin_code: "1234") } + .to raise_error(Easee::Errors::TooManyAttempts) + end + + it "raises AlreadyPaired on a PairingError with title AlreadyPairedWithPartner" do + token_cache = ActiveSupport::Cache::MemoryStore.new + token_cache.write( + Easee::Client::TOKENS_CACHE_KEY, + { "accessToken" => "T123" }.to_json, + ) + + stub_request(:post, "https://api.easee.cloud/api/chargers/123ABC/pair?pinCode=1234") + .with(headers: { "Authorization" => "Bearer T123" }) + .to_return( + status: 400, + body: { + errorCode: 123, + errorCodeName: "PairingError", + title: "AlreadyPairedWithPartner", + }.to_json, + headers: { "Content-Type": "application/json" }, + ) + + client = Easee::Client.new(user_name: "easee", password: "money", token_cache:) + + expect { client.pair(charger_id: "123ABC", pin_code: "1234") } + .to raise_error(Easee::Errors::AlreadyPaired) + end + + it "raises AlreadyPaired on a PairingError with title AlreadyPairedWithUser" do + token_cache = ActiveSupport::Cache::MemoryStore.new + token_cache.write( + Easee::Client::TOKENS_CACHE_KEY, + { "accessToken" => "T123" }.to_json, + ) + + stub_request(:post, "https://api.easee.cloud/api/chargers/123ABC/pair?pinCode=1234") + .with(headers: { "Authorization" => "Bearer T123" }) + .to_return( + status: 400, + body: { + errorCode: 123, + errorCodeName: "PairingError", + title: "AlreadyPairedWithUser", + }.to_json, + headers: { "Content-Type": "application/json" }, + ) + + client = Easee::Client.new(user_name: "easee", password: "money", token_cache:) + + expect { client.pair(charger_id: "123ABC", pin_code: "1234") } + .to raise_error(Easee::Errors::AlreadyPaired) + end + + it "raises the generic RequestFailed on a PairingError with an unknown title" do + token_cache = ActiveSupport::Cache::MemoryStore.new + token_cache.write( + Easee::Client::TOKENS_CACHE_KEY, + { "accessToken" => "T123" }.to_json, + ) + + stub_request(:post, "https://api.easee.cloud/api/chargers/123ABC/pair?pinCode=1234") + .with(headers: { "Authorization" => "Bearer T123" }) + .to_return( + status: 400, + body: { + errorCode: 123, + errorCodeName: "PairingError", + title: "SomethingElse", + }.to_json, + headers: { "Content-Type": "application/json" }, + ) + + client = Easee::Client.new(user_name: "easee", password: "money", token_cache:) + + expect { client.pair(charger_id: "123ABC", pin_code: "1234") } + .to raise_error(Easee::Errors::RequestFailed) + end end describe "#unpair" do