Tesla Fleet API: čtení SoC po příjezdu k wallboxu
All checks were successful
CI and deploy / deploy (push) Successful in 58s
CI and deploy / migration-check (push) Successful in 16s

- services/tesla_client.py: access token s cache + ROTACE refresh tokenu do
  ems.tesla_token (env jen seed — Tesla refresh token je jednorázový),
  vehicles → vehicle_data?endpoints=charge_state, 408 (spící auto) = tiché
  přeskočení, výběr vozidla dle VIN / jediného na účtu (VIN se auto-naučí)
- hook _patch_session_from_tesla v _on_ev_arrival: PŘED replanem doplní
  soc_at_connect_pct (+ target z charge_limit_soc) do otevřené session přes
  fn_ev_session_apply_patch (rozšířena o soc_at_connect_pct) — energii si
  odvodí fn_planning_site_context (SQL-first); selhání neblokuje replan
- V086: asset_vehicle.vin, api_type='tesla' pro tesla-my (Model Y, home-01),
  singleton ems.tesla_token; R__095: fn_tesla_token_get/upsert,
  fn_tesla_arrival_context, fn_vehicle_set_vin
- config: TESLA_CLIENT_ID/SECRET/REFRESH_TOKEN (prázdné = vypnuto)
- testy parserů; plná sada beze změny

Aktivace: env do /opt/ems-deploy/.env + recreate backendu (docs/tesla-fleet-api.md §Stav).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dusan Vojacek
2026-06-11 23:29:24 +02:00
parent 21b3d12955
commit 60176fc7b2
8 changed files with 382 additions and 6 deletions

View File

@@ -0,0 +1,25 @@
-- Tesla Fleet API: VIN na vozidle, aktivace api_type pro Model Y (home-01),
-- singleton tabulka tokenů (refresh token Tesla ROTUJE při každém použití —
-- nelze ho držet jen v .env, runtime hodnota žije zde; .env je jen seed).
alter table ems.asset_vehicle
add column if not exists vin text;
comment on column ems.asset_vehicle.vin is
'VIN pro párování s vozidlem v API výrobce (Tesla Fleet). NULL = doplní se automaticky při prvním úspěšném čtení (jediné vozidlo na účtu), jinak nutno vyplnit ručně.';
update ems.asset_vehicle
set api_type = 'tesla'
where code = 'tesla-my'
and site_id = (select id from ems.site where code = 'home-01');
create table if not exists ems.tesla_token (
id int primary key default 1 check (id = 1),
refresh_token text not null,
access_token text,
access_expires_at timestamptz,
updated_at timestamptz not null default now()
);
comment on table ems.tesla_token is
'Singleton: aktuální Tesla Fleet API tokeny. Seed refresh tokenu z env TESLA_REFRESH_TOKEN při prvním použití; rotace ukládá fn_tesla_token_upsert.';

View File

@@ -9,7 +9,9 @@ as $fn$
declare
v_id int;
begin
if not (p_patch ? 'target_soc_pct') and not (p_patch ? 'target_deadline') then
if not (p_patch ? 'target_soc_pct')
and not (p_patch ? 'target_deadline')
and not (p_patch ? 'soc_at_connect_pct') then
return jsonb_build_object('success', false, 'error', 'no_fields');
end if;
@@ -24,6 +26,16 @@ begin
end
else es.target_soc_pct
end,
-- skutečné SoC při připojení (Tesla Fleet API po příjezdu)
soc_at_connect_pct = case
when p_patch ? 'soc_at_connect_pct' then
case
when p_patch->'soc_at_connect_pct' is null
or jsonb_typeof(p_patch->'soc_at_connect_pct') = 'null' then null
else (p_patch->>'soc_at_connect_pct')::double precision
end
else es.soc_at_connect_pct
end,
target_deadline = case
when p_patch ? 'target_deadline' then
case
@@ -46,4 +58,4 @@ end;
$fn$;
comment on function ems.fn_ev_session_apply_patch(int, int, jsonb) is
'PATCH EV session jen klíče přítomné v JSON (ISO string pro deadline).';
'PATCH EV session jen klíče přítomné v JSON (ISO string pro deadline; soc_at_connect_pct z Tesla API).';

View File

@@ -0,0 +1,80 @@
-- Tesla Fleet API DB vrstva (SQL-first): tokeny, kontext příjezdu, učení VIN.
create or replace function ems.fn_tesla_token_get()
returns jsonb
language sql
stable
as $fn$
select coalesce(
(select jsonb_build_object(
'refresh_token', t.refresh_token,
'access_token', t.access_token,
'access_expires_at', t.access_expires_at,
'updated_at', t.updated_at
) from ems.tesla_token t where t.id = 1),
'{}'::jsonb
);
$fn$;
create or replace function ems.fn_tesla_token_upsert(
p_refresh_token text,
p_access_token text,
p_access_expires_at timestamptz
)
returns void
language sql
as $fn$
insert into ems.tesla_token (id, refresh_token, access_token, access_expires_at, updated_at)
values (1, p_refresh_token, p_access_token, p_access_expires_at, now())
on conflict (id) do update set
refresh_token = excluded.refresh_token,
access_token = excluded.access_token,
access_expires_at = excluded.access_expires_at,
updated_at = now();
$fn$;
-- Kontext pro hook po příjezdu EV: vozidlo navázané na charger (default_charger_id)
-- + otevřená session. NULL vehicle => nic nedělat.
create or replace function ems.fn_tesla_arrival_context(
p_site_id int,
p_charger_code text
)
returns jsonb
language sql
stable
as $fn$
select coalesce(jsonb_build_object(
'vehicle_id', v.id,
'api_type', v.api_type,
'vin', v.vin,
'battery_capacity_kwh', v.battery_capacity_kwh,
'session_id', s.id,
'soc_at_connect_pct', s.soc_at_connect_pct
), '{}'::jsonb)
from ems.asset_ev_charger c
join ems.asset_vehicle v
on v.site_id = c.site_id and v.default_charger_id = c.id and v.active
left join lateral (
select es.id, es.soc_at_connect_pct
from ems.ev_session es
where es.charger_id = c.id and es.session_end is null
order by es.id desc
limit 1
) s on true
where c.site_id = p_site_id and c.code = p_charger_code;
$fn$;
create or replace function ems.fn_vehicle_set_vin(
p_vehicle_id int,
p_vin text
)
returns void
language sql
as $fn$
update ems.asset_vehicle
set vin = p_vin
where id = p_vehicle_id and (vin is null or vin = '');
$fn$;
comment on function ems.fn_tesla_arrival_context is
'Vozidlo + otevřená session pro charger (hook po příjezdu EV → Tesla SoC).';