From 664c5bbe60d2666a8137aac4e1bf6bad6e48d929 Mon Sep 17 00:00:00 2001 From: Martijn Versluis Date: Fri, 17 Jul 2026 17:36:37 +0200 Subject: [PATCH] List completed charging sessions for a charger in a time range Backfilling Zaptec's cpo_transaction_identifier on historical sessions worked so well that we want the same for Easee. The Sessions API returns every completed session per charger over an arbitrary date range in a single response, and this exposes it as ArchivedSession value objects so the caller does not need to touch the raw payload. --- lib/easee/archived_session.rb | 32 ++++++++++++ lib/easee/client.rb | 10 ++++ lib/stekker_easee.rb | 1 + spec/easee/archived_session_spec.rb | 48 ++++++++++++++++++ spec/easee/client_spec.rb | 77 +++++++++++++++++++++++++++++ 5 files changed, 168 insertions(+) create mode 100644 lib/easee/archived_session.rb create mode 100644 spec/easee/archived_session_spec.rb diff --git a/lib/easee/archived_session.rb b/lib/easee/archived_session.rb new file mode 100644 index 0000000..1b6a31e --- /dev/null +++ b/lib/easee/archived_session.rb @@ -0,0 +1,32 @@ +module Easee + class ArchivedSession + def initialize(data) + @data = data.deep_symbolize_keys + end + + def id = @data.fetch(:id) + def charger_id = @data.fetch(:chargerId) + + def car_connected = parse_time(:carConnected) + def car_disconnected = parse_time(:carDisconnected) + def first_energy_transfer_period_started = parse_time(:firstEnergyTransferPeriodStarted) + def last_energy_transfer_period_end = parse_time(:lastEnergyTransferPeriodEnd) + + def energy_kwh = @data.fetch(:kiloWattHours).to_f + def duration_seconds = @data[:actualDurationSeconds] + + def complete? = @data.fetch(:isComplete, false) + + def auth_token = @data[:authToken] + def auth_user = @data[:authUser] + + private + + def parse_time(key) + value = @data[key] + return if value.nil? + + Time.zone.parse(value) + end + end +end diff --git a/lib/easee/client.rb b/lib/easee/client.rb index 1179200..71e0513 100644 --- a/lib/easee/client.rb +++ b/lib/easee/client.rb @@ -77,6 +77,12 @@ def ongoing_session(charger_id) .then { |response| Session.new(response.body) } end + # https://developer.easee.com/reference/chargingsessionscharger_getchargersessionsbycharger + def sessions(charger_id, from:, to:) + path = "/api/sessions/charger/#{charger_id}/sessions/#{format_time(from)}/#{format_time(to)}" + get(path).then { |response| response.body.map { |session| ArchivedSession.new(session) } } + end + # https://developer.easee.cloud/reference/get_api-chargers-id-config def configuration(charger_id) get("/api/chargers/#{charger_id}/config") @@ -114,6 +120,10 @@ def authenticated_connection end end + def format_time(time) + time.getutc.iso8601 + end + def get(endpoint, query = {}) with_error_handling do authenticated_connection.get("#{BASE_URL}#{endpoint}", query) diff --git a/lib/stekker_easee.rb b/lib/stekker_easee.rb index e4699bd..11b4e3a 100644 --- a/lib/stekker_easee.rb +++ b/lib/stekker_easee.rb @@ -10,6 +10,7 @@ require_relative "easee/configuration" require_relative "easee/errors" require_relative "easee/meter_reading" +require_relative "easee/archived_session" require_relative "easee/session" require_relative "easee/site" require_relative "easee/state" diff --git a/spec/easee/archived_session_spec.rb b/spec/easee/archived_session_spec.rb new file mode 100644 index 0000000..65bca57 --- /dev/null +++ b/spec/easee/archived_session_spec.rb @@ -0,0 +1,48 @@ +RSpec.describe Easee::ArchivedSession do + it "exposes the session fields" do + session = described_class.new( + "id" => 387, + "chargerId" => "EH98AAGY", + "carConnected" => "2026-01-02T19:23:23Z", + "carDisconnected" => "2026-01-03T11:57:00Z", + "firstEnergyTransferPeriodStarted" => "2026-01-02T20:00:05+00:00", + "lastEnergyTransferPeriodEnd" => "2026-01-03T11:57:00+00:00", + "kiloWattHours" => 29.155331, + "actualDurationSeconds" => 10558, + "isComplete" => true, + "authToken" => "FD2ACA9C", + "authUser" => 709817, + ) + + expect(session).to have_attributes( + id: 387, + charger_id: "EH98AAGY", + car_connected: Time.zone.parse("2026-01-02T19:23:23Z"), + car_disconnected: Time.zone.parse("2026-01-03T11:57:00Z"), + first_energy_transfer_period_started: Time.zone.parse("2026-01-02T20:00:05Z"), + last_energy_transfer_period_end: Time.zone.parse("2026-01-03T11:57:00Z"), + energy_kwh: 29.155331, + duration_seconds: 10558, + complete?: true, + auth_token: "FD2ACA9C", + auth_user: 709817, + ) + end + + it "treats missing car_connected and car_disconnected as nil" do + session = described_class.new( + "id" => 388, + "chargerId" => "EH98AAGY", + "kiloWattHours" => 0.0, + "isComplete" => false, + ) + + expect(session).to have_attributes( + car_connected: nil, + car_disconnected: nil, + first_energy_transfer_period_started: nil, + last_energy_transfer_period_end: nil, + complete?: false, + ) + end +end diff --git a/spec/easee/client_spec.rb b/spec/easee/client_spec.rb index c82e4d5..0b4bf5b 100644 --- a/spec/easee/client_spec.rb +++ b/spec/easee/client_spec.rb @@ -828,6 +828,83 @@ end end + describe "#sessions" do + it "fetches the completed sessions for a charger in the given time range" do + token_cache = ActiveSupport::Cache::MemoryStore.new + token_cache.write( + Easee::Client::TOKENS_CACHE_KEY, + { "accessToken" => "T123" }.to_json, + ) + + from = Time.zone.parse("2026-01-01T00:00:00Z") + to = Time.zone.parse("2026-07-17T00:00:00Z") + + stub_request( + :get, + "https://api.easee.cloud/api/sessions/charger/EH98AAGY/sessions/2026-01-01T00:00:00Z/2026-07-17T00:00:00Z", + ) + .with(headers: { "Authorization" => "Bearer T123" }) + .to_return( + status: 200, + body: [ + { + id: 387, + chargerId: "EH98AAGY", + carConnected: "2026-01-02T19:23:23Z", + carDisconnected: "2026-01-03T11:57:00Z", + kiloWattHours: 29.155331, + isComplete: true, + }, + { + id: 388, + chargerId: "EH98AAGY", + carConnected: "2026-01-08T17:15:48Z", + carDisconnected: nil, + kiloWattHours: 0.0, + isComplete: false, + }, + ].to_json, + headers: { "Content-Type": "application/json" }, + ) + + client = Easee::Client.new(user_name: "easee", password: "money", token_cache:) + + sessions = client.sessions("EH98AAGY", from:, to:) + + expect(sessions.map(&:id)).to eq [387, 388] + expect(sessions.first).to have_attributes( + charger_id: "EH98AAGY", + car_connected: Time.zone.parse("2026-01-02T19:23:23Z"), + car_disconnected: Time.zone.parse("2026-01-03T11:57:00Z"), + energy_kwh: 29.155331, + complete?: true, + ) + expect(sessions.last).to have_attributes(car_disconnected: nil, complete?: false) + end + + it "converts non-UTC timestamps to UTC before sending them to Easee" do + token_cache = ActiveSupport::Cache::MemoryStore.new + token_cache.write( + Easee::Client::TOKENS_CACHE_KEY, + { "accessToken" => "T123" }.to_json, + ) + + from = Time.new(2026, 1, 1, 1, 0, 0, "+01:00") + to = Time.new(2026, 1, 2, 3, 0, 0, "+02:00") + + request = stub_request( + :get, + "https://api.easee.cloud/api/sessions/charger/EH98AAGY/sessions/2026-01-01T00:00:00Z/2026-01-02T01:00:00Z", + ).to_return(status: 200, body: "[]", headers: { "Content-Type": "application/json" }) + + client = Easee::Client.new(user_name: "easee", password: "money", token_cache:) + + client.sessions("EH98AAGY", from:, to:) + + expect(request).to have_been_requested + end + end + describe "#inspect" do it "does not include the user name and password" do token_cache = ActiveSupport::Cache::MemoryStore.new