Skip to content

Reused one of your capabilities and added more translations, remove ps2 - #6

Open
kaohlive wants to merge 93 commits into
Coderaxx:masterfrom
kaohlive:master
Open

Reused one of your capabilities and added more translations, remove ps2#6
kaohlive wants to merge 93 commits into
Coderaxx:masterfrom
kaohlive:master

Conversation

@kaohlive

Copy link
Copy Markdown
Contributor

No description provided.

@kaohlive

Copy link
Copy Markdown
Contributor Author

Here is a new pull request, it cleans up the vehicle driver and adds translations for NL in your driver.
I also switched to the polestarbattery measurement instead of the measure_battery. For a car the number view is more usefull than the bar icon and the battery warning also is not usefull IMO.

@kaohlive

Copy link
Copy Markdown
Contributor Author

@Coderaxx can you merge this request please?
This version should be ready for a release IMO and would like to see it released.

@kaohlive

Copy link
Copy Markdown
Contributor Author

@Coderaxx hey, I added support for the new feature of showing the charing KW and amps to the vehicle driver. Very nice that it is now available.
Can you please merge this pull request?

kaohlive added 19 commits May 6, 2024 23:09
Changelog: Added support for charge speed details
Changelog: Changed polestar vehicle to new device class car
Changelog: Fix for trigger events
Changelog: hotfix on the csv driver
Changelog: Improved insights on power measurements
Changelog: update to fix api changes
Changelog: Update npm modules
Changelog: Widget, service warning and login bug fix
Changelog: preview images
Changelog: fix widget update on device state change
kaohlive and others added 30 commits April 17, 2026 22:20
Wire the C3 chronos services (ChargeNow, TargetSoc, AmpLimit) through the
JS client so users can actually control their car from Homey, not just
read status. Adds flow action + condition cards and in-tile sliders for
charge limit and amp limit plus momentary Start/Stop charging buttons.

Device setting "Allow remote commands" is a master-switch kill-flag —
writes fail instantly when turned off. Replaces an earlier PIN prototype
that produced ugly titleFormatted strings.

Device setting "Charge limit slot" (Daily/LongTrip/Custom/Unspecified)
is exposed because Polestar 4 silently rejects writes to the Daily slot
but commits to Custom — and we don't yet know which slot other models
prefer. Default stays on Daily to match the Python reference; Polestar 4
users switch once. Writes that don't land surface a log warning pointing
at this setting.

gRPC reliability: HTTP/2 keep-alive ping every 30 s, goaway-event handler
that nulls the session eagerly, and one automatic retry on transient
errors (INTERNAL/UNAVAILABLE/GOAWAY/ECONNRESET/ETIMEDOUT) with a fresh
session. Resolves the cold-session GOAWAY hit we saw on the first write
after idle.

UNIMPLEMENTED responses are now captured in a persistent device-store
flag so unsupported features (e.g. Polestar 4 returns "Functionality not
supported: AMP_LIMIT") hide the slider tile and skip further calls on
subsequent runs instead of spamming the log forever.

Charge limit polling moved to the fast 60 s cycle (down from 15 min) so
changes made in the Polestar app or in-car menu show up within a minute.
Amp limit stays on the slow 15 min cycle — it rarely changes and is
skipped entirely on vehicles that don't support it.

Set-response parsing no longer trusts the echoed level: the P4 chronos
envelope returns a stale value, so we rely on a delayed Get 3 s after
the write to reflect the committed state. The slider stays on the
user-chosen value until that re-read either confirms or corrects it,
avoiding the disorienting snap-back-then-forward UX.

The custom measure_polestarTyrePressure{FL,FR,RL,RR} capabilities are
also migrated to measure_pressure.{front_left,front_right,rear_left,
rear_right} with a kPa unit override — the standard capability gets
better UI + Insights integration. Legacy JSON files remain with a
_deprecated marker so existing devices can migrate via removeCapability;
they will be deleted in a future release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire two more C3 services so the car's external closures and parking
climatization show up as Homey tiles:

  ExteriorService/GetLatestExterior  — locks, per-door/window open state,
  hood, tailgate, sunroof, charging-port cover
  ParkingClimatizationService/GetLatestParkingClimatization — running
  state, target temperature, interior temperature, minutes remaining

Both are unary calls on the 60 s fast cycle so a user walking up to the
car sees door-opened alarms within the minute.

Homey capabilities chosen so they can be switched to setable later
without a rename, once lock/unlock + climate start/stop writes land:

  locked                     — central lock state (setable:false for now)
  onoff.climate              — climatization running/not (setable:false)
  target_temperature         — last requested temp (setable:false)
  measure_temperature        — interior temperature (inherently read-only)
  measure_polestarClimateRemaining — minutes left on the 30-min parking
                               climatization session; blank when idle
  alarm_contact.door_{fl,fr,rl,rr} — per-door open alarms
  alarm_contact.window_any   — aggregate window-open alarm
  alarm_contact.{tailgate,hood,sunroof,tank_lid} — body + charge port
                               open alarms

The capabilitiesOptions setable:false overrides keep the tiles visible
as read-only indicators. When writes arrive they just drop the override
and register listeners.

DigitalTwin flat-field format was reverse-engineered against a live
Polestar 4 session: exterior fields 2-16 hold ints (LockStatus enum on
2/16, OpenStatus 1/2/3 on all others), climate fields 2,3,7,8 are the
ones we trust (running_status, time_remaining in minutes, current and
requested temperature as float32). ventilation_only and request_type
are still speculative per-model so they stay in debug logs only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Complete the invocation surface so every write Polestar's mobile app
offers is available from Homey flows and device tiles, plus a couple
of reads that round out the picture.

Writes (via /invocation.InvocationService):
  lock / unlock                  — wired to the setable `locked` tile
  unlock_trunk                   — tile button + flow action
  honk_flash                     — tile button (flash-only default) +
                                   flow action with HONK/FLASH/BOTH
  climate_start / climate_stop   — onoff.climate toggle reads seat and
                                   steering-wheel heating defaults from
                                   new device settings; a separate flow
                                   action exposes all 6 params per-call
                                   for one-off custom sessions
  windows_open / windows_close   — flow actions (OPEN_ALL / CLOSE_ALL —
                                   the API doesn't support per-window)

Reads:
  Location (/dtlinternet.DtlInternetService/GetLastKnownLocation) —
    parser handles three documented response layouts, populates a
    measure_polestarLocation string tile on the 15-min slow cycle.
    A get_location flow action refreshes on-demand and exposes
    latitude, longitude, and a formatted "lat, lng" string as tokens
    for downstream cards (push notifications with a map link).
  OTA (/ota_mobcache.OtaDiscoveryService/GetSoftwareInfo) —
    alarm_polestarOtaAvailable fires on DOWNLOAD_READY /
    DOWNLOAD_COMPLETED / INSTALLATION_DEFERRED / INSTALLATION_SCHEDULED.
    State text falls back to a friendly "No pending update" when the
    car returns an empty CarSoftwareInfo (no update queued).
    Write side (schedule/install/cancel) deliberately omitted — the
    risk of a misconfigured flow triggering an install mid-drive
    outweighs the convenience over the Polestar app.

Flow cards:
  11 action cards (charge ±, limits, lock ±, trunk, honk, climate ±,
  windows ±, get_location)
  3 conditions (is_locked, target_soc_is, amp_limit_is)
  1 condition trigger (is_locked) + standard triggers from button.*,
  locked, onoff.climate capabilities.

Housekeeping:
  The four legacy measure_polestarTyrePressure{FL,FR,RL,RR} JSONs are
  removed — they never shipped in a release and the migration loop
  that would auto-remove them from existing devices is gone too, since
  no one has those capabilities in their device state. All current and
  future users take the standard measure_pressure.* sub-IDs directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps to 3.0.0 and wraps up the C3-migration work.

- Mark the legacy polestar-2-csv driver as deprecated via Homey's
  official driver flag so new pairings are rejected but existing
  devices keep working. The Car Stats Viewer webhook still supplies
  live telemetry the C3 backend doesn't expose (speed, gear,
  ignition, battery temperature, trip summaries) so it stays around
  for P2 owners who rely on it.

- Rewrite README.md with real feature list and deprecation note,
  rewrite README.txt (app-store listing) with concrete read/write
  capabilities and a privacy section, and refresh the
  .homeycompose/app.json description + tags to reflect what the
  app actually does now instead of generic marketing copy.

- Drop the `measure_power: { approximated: true }` override. The
  new C3 client returns real power_watts from the car, not a
  calculated estimate, so Homey Energy can treat it as a real
  sensor reading.

- Add a `windows_supported` device setting and wire it into the
  same OPTIONAL_FEATURES machinery that already auto-hides
  amp_limit on Polestar 4. Unchecking hides the two window
  buttons; the backend auto-disables the feature when it returns
  UNIMPLEMENTED.

- Make `target_temperature` fall back to the configured climate
  default when the C3 climatization response has no
  requested_temp (happens whenever climate isn't running). Tile
  no longer stays empty on a fresh device.

- After every state-changing write (lock, climate, windows,
  charging, trunk) schedule two follow-up reads at +3 s and +10 s
  so the UI reflects the actual state faster than the 60 s regular
  poll cycle. Window opens/closes use a wider +15 s second refresh
  because physical movement takes longer to complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Point every custom capability used by the active vehicle driver at an
SVG in drivers/vehicle/assets/ so the driver stops depending on files
that live in the deprecated polestar-2-csv driver's folder. Copies
battery-75, battery-100, powerdc, hvbattery, location and update from
the CSV driver into the vehicle driver's own asset folder.

Add tire-pressure.svg and wire it to alarm_polestarTyrePressure —
Homey threw a render error on the iconless alarm tile.

Fill in the two remaining missing icons (alarm_polestarOtaAvailable,
measure_polestarOtaVersion) with update.svg so all three OTA
capabilities share the same glyph.

CSV-only capabilities (speed, gear, ignition, temperature, etc.)
keep pointing at their own driver's assets — they live together and
will move as a set when that driver is eventually removed.

Adds a 3.0.0 changelog entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Changelog: Polish on icons etc
Polls kept firing after a device was removed or while the app was
restarting, so each in-flight updateVehicleState() eventually hit
setCapabilityValue on a device Homey had already forgotten about —
surfacing as 'Not Found: Device with ID …' in the app log.

Adds a _cleanup() that clearIntervals both poll timers and closes the
gRPC session, invoked from onDeleted() and onUninit(). Every update
method (vehicle, health, exterior, climate, location, ota) and the
post-write _scheduleStateRefresh closures now early-return when
_destroyed is set, so in-flight promises can't race past the cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CHARGING_STATUS_DONE now surfaces as 'plugged_in' instead of
  'plugged_in_paused'. A completed session is not a pause — the
  Polestar app shows "Finished" for this state, and users were
  getting "Paused" on their Homey tile.

- Honk/flash flow card retitled to "Find my car — [action]". The
  previous titleFormatted was just the action value, making the
  card hard to identify in a flow list.

- Added a 'Start climate (defaults)' flow action that takes no
  arguments and reads temperature + seat/steering heating from
  device settings. Mirrors the one-tap climate behaviour of the
  Polestar mobile app; complements the existing argument-driven
  climate_start action for custom sessions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Car Stats Viewer Android app sometimes pushes driving points with
no altitude or state-of-charge field (GPS dropout, sensor delay).
trip-ended trigger then crashed on undefined.toFixed(0).

Replaces the bare .toFixed() calls with a fmt() helper that checks
Number.isFinite first and falls back to the existing "Unavailable"
localization. energy_delta accumulator also now defaults a missing
field to 0 so a single bad point can't NaN the whole sum.

Touches only the deprecated CSV driver; the new vehicle driver is
unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues spotted in a tester's pair-flow log.

- The settings-changed listener was logging user_email and
  user_password values verbatim. Even though the password is
  encrypted-at-rest, dropping the ciphertext in a debug log is the
  kind of thing that ends up in a screenshot. Now we log only
  '[N chars]' or '[empty]' for user_email, user_password, and
  polestar_token.

- testlogin returned a plain boolean, so the pair UI couldn't tell
  the difference between 'login failed' and 'login OK but the
  account has no vehicles'. Both paths showed the same red
  'Login validation failed' alert, which confused at least one
  tester whose login was actually fine. testlogin now returns
  { ok, reason } so the HTML can show a specific 'no vehicles'
  message that points at account selection or a transient backend
  hiccup as likely causes. Old boolean return is still accepted
  by the pair view as a fallback.

While in the area, fixes a small bug where a 'Credential test ok'
log line referenced `vehicles` before its var was declared.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark_Beech (self-hosted Homey) hit an unhandledRejection during
pairing because his network interfaces are not named wlan0/eth0/eth1
— the salt derivation in lib/homeycrypt.js fell off the if-else chain
with mac=null and crashed on null.join('').

Adds a fallback that iterates os.networkInterfaces() and picks the
first non-internal interface with a non-zero MAC, sorted by name so
the same interface keeps being chosen across reboots. Preserves the
preferred wlan0/eth0/eth1 order so existing users' encrypted
passwords still decrypt with the same MAC they were salted with.

If no usable MAC can be found at all (extremely rare, would only
happen in a stripped-down container), throws an explicit error
instead of the cryptic null.join crash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Translations: added de, fr, it, sv, es, da, ru, pl, ko, ja to every
capability title/unit, driver name, driver capabilitiesOptions, flow
cards (titles, hints, titleFormatted, dropdown labels, tokens), and
device settings (labels, hints, dropdown values). Source en/no/nl
strings preserved verbatim. AI-generated — non-elegant phrasing can
be reported and fixed in follow-ups.

Widget: the charging block now mirrors the orange-label / white-value
pattern of charge and range. Power (kW) is the primary number; current
and time-remaining form a subtitle. Numbers gracefully fall back to
"— kW" / omitted parts when capability values are missing, instead of
showing "0 A | 0 kW · 0h 0m".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The legacy Car Stats Viewer driver stored a webhook_slug only when the
deprecated trip-image API was used. Newer pairings leave it null, but
the trip-ended handler still ran `base64url.encode(this.slug)` as the
first step. That throws ERR_INVALID_ARG_TYPE on null and aborts the
trigger before any flow ever fires.

The encoded slug fed only image URLs that are now fully commented out,
so the line is removed entirely along with its commented-out block.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3.2.2 left the lastTrip/tripInfo/tripScore image tokens in the flow
definition (removing them would break user flows referencing those
tokens) but the trigger payload no longer supplied them, so every
trip-ended event threw "Invalid value for token lastTrip. Expected
object but got undefined".

Create three Homey images on device init, all pointing at the driver
brand asset. The token payload is schema-valid again; existing flows
keep firing. Per-trip renders are gone with the deprecated backend, so
notifications now show the Polestar driver image instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The auto-generated alarm_contact flow cards render as "{title} is open" /
"{title} is not open". With the previous titles ending in "open" ("Door
FL open", "Raam open", "Laadklep open") the card text read awkwardly
("Door FL open is open") or the UI dropped the redundant suffix, leaving
several "is open" cards in a flow indistinguishable from each other.

Renamed all nine alarm_contact sub-capabilities to a sensor noun only —
e.g. "Front-left door", "Side window", "Charge port lid" — so the auto-
generated cards read naturally and identify the sensor in the flow
editor. Applied across all 13 languages. Doors switched from FL/FR/RL/RR
abbreviations to fully-spelled positions for legibility in the flow
card library.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Homey's auto-generated alarm_contact triggers all show "Contact alarm
turned on for [Duration]" in the card library without any sub-cap
discriminator, making the nine alarm_contact triggers visually identical
in the picker. Renaming the capability titles in 3.2.4 fixed the device
tile but did not help here — the auto-card title comes from the base
capability text.

Add three explicit Polestar-owned flow cards instead:
- contact_opened (trigger)
- contact_closed (trigger)
- contact_is_open (condition)

Each carries a dropdown that lists the nine alarm_contact sub-caps with
their localized sensor labels (all 13 languages). titleFormatted renders
e.g. "Front-left door opened" once the user picks the sensor.

Wiring: _setContact() in device.js wraps setCapabilityValue, detects
false→true / true→false transitions, and fires the matching trigger
with state.sensor = sub-cap id. The driver registers run-listeners that
gate by args.sensor === state.sensor.

The auto-generated Contact alarm cards still exist (we can't hide them
without breaking existing flows that already use them) but users are
now nudged toward the unambiguous explicit cards.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A 2021 Polestar 2 owner reported "no car found" during pairing. Logs
showed C3 login succeeded but getVehicles() returned an empty list —
the new C3 backend doesn't list every older P2. The c3_backend_disabled
escape hatch exists but the user has no way to discover it from the
pair dialog, only by spelunking through app settings.

testlogin (pair + repair) now silently retries with the legacy backend
when C3 returns 0 vehicles. If legacy finds vehicles, c3_backend_disabled
is persisted so all later discovery / refresh / write calls go through
the legacy client too. Only when both backends return empty does the
"no_vehicles" friendly error reach the UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A user reporting "no vehicles found" sent a log with the line:
  "Credential test ok, vehicle count:"
ending mid-sentence. Cause: app.log uses `if (data)` to decide whether
to append the data argument, so 0 / false / '' silently fall through to
the no-data branch. We logged the right call but the count never made
it to disk.

Switched to a nullish check (data !== null && data !== undefined). Also
guarded the `data.message` access so a non-object/non-error data value
no longer throws. Next time we ask a user for a log, "vehicle count: 0"
will actually say 0 instead of leaving us guessing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace GraphQL GetVDMSCars vehicle discovery with the C3 gRPC
GetMyCars service. Discovery now uses the same session / auth / retry
machinery as every other C3 call and drops the pc-api.polestar.com
app-backend dependency (with its versioned user-agent and apollo
headers). New GetMyCars response fields userIsLinked and userIsOwner
are surfaced on each vehicle for future diagnostics.

Includes the first fixture-based test under test/ — hex-encoded
GetMyCars proto response for a linked non-owner Polestar 3, verified
via node:test.

Tested by olemarkus on a non-owner Polestar 3.
Follow-up to olemarkus' PR #3. Two small additions on top of the merge:

1. package.json: "test" script runs the new fixture suite via
   `node --test test/*.test.js`. Future PRs can drop a hex proto
   response in test/ and get regression coverage for free.

2. driver.js discover_vehicles: log `linked:<y/n> owner:<y/n>` per
   vehicle at pair time. Non-owner accounts (lease, secondhand
   without ownership transfer) can now pair thanks to the C3
   GetMyCars endpoint, but we don't yet know which features — if
   any — require owner rights. Logging the state so we can correlate
   any feature-availability reports later. No user-facing message
   for now; only add one once we see actual missing capabilities.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
--- 'Car at home' feature ---

New boolean capability measure_polestarAtHome that flips true whenever
the vehicle's last-known location is within a configurable radius of
Homey's own geolocation. Default radius 150 m; adjustable 25–2000 m
via a new device setting home_radius_m.

Three explicit flow cards (custom capabilities don't get auto-generated
ones — corrected from my earlier assumption in the interim commit):
- Trigger "Car came home"   — fires on false→true transition
- Trigger "Car left home"   — fires on true→false transition
- Condition "Car is / is not at home" — invertable via the !{{is|is not}}
  tokens so the same card serves both semantics

Wiring: _evaluateAtHome() runs at the end of every successful location
refresh, reads Homey's lat/lng via the geolocation manager (new
`homey:manager:geolocation` permission), Haversines the distance, and
fires the appropriate trigger card on transition. Only fires if the
previous capability value was a boolean, so the first-ever set at boot
does not spuriously trigger "Car came home". If Homey has no usable
geolocation (permission missing / manual mode with no fix) the eval
skips silently rather than pinning the tile to false.

Titles + hints + settings label/hint translated across all 13 languages.
fixCapabilities() adds the new cap to existing paired devices.

--- Location refresh diagnostics ---

A user reported their location tile stopped updating after pair. Their
log covered 30 minutes / two slow-cycles but contained zero location-
related lines, because updateLocationState was silent on every non-
throw path. Added a "Retrieve vehicle location" start marker (mirrors
updateHealthState), distinct DEBUG lines for null-result vs. non-
finite-coords early returns (with raw payload for the latter),
UNIMPLEMENTED handling, and a success line. Next log we get will tell
us which of the four cases applies.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Audited every boolean capability in the app against the flow cards in
driver.flow.compose.json. Five caps had gaps that made obvious "when X
happens" / "if X is true" flows impossible to build. This commit fills
them all in — 6 new triggers + 5 new conditions, 13 languages each.

Filled gaps:
- alarm_polestarOtaAvailable: no trigger, no condition
  → 'New software update available' / 'Software update no longer
    pending' triggers + 'Software update is/is not available' condition.
- alarm_polestarTyrePressure: no trigger, no condition
  → 'Tyre pressure warning raised' / 'cleared' triggers + condition.
- onoff.climate (sub-cap, no auto-gen per SDK): no trigger, no condition
  → 'Climate started' / 'Climate stopped' triggers + condition.
- measure_vehicleChargeState: had triggers, no condition
  → 'Car is/is not charging' condition.
- measure_vehicleConnected: had triggers, no condition
  → 'Car is/is not connected to a charger' condition.

Wiring: new _setBoolAndTrigger(capId, val, trueKey, falseKey) helper in
device.js wraps setCapabilityValue and fires the matching device-trigger
card on real boolean transitions (skips when the previous value was not
yet a boolean, so device boot doesn't spuriously fire "climate started").
updateOtaState, updateHealthState (tyre pressure), and updateClimateState
now call this helper instead of raw setCapabilityValue. driver.js holds
refs to the six new trigger cards + registers five bool-reading condition
run-listeners with a small shared closure.

All condition titles use !{{is|is not}} tokens so users can invert them
in the flow editor without extra cards.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two housekeeping changes prompted by PR #4 review.

1. Removed the @andysmithfal/polestar.js entry from dependencies. The
   legacy client has always loaded from the local kaohlive/polestar.js
   fork under clone_modules/polestar.js — the npm package sat in the
   tree unused. Its presence made two identically-named, differently-
   patched copies look interchangeable, which triggered PR #4. Added
   LEGACY_CLIENT_FORK_NOTES.md at the repo root documenting exactly
   what our fork changes vs upstream 1.8.0 (CarTelematicsV2 migration,
   GraphQL error surfacing, getAccessToken/getVehicleVin helpers) so
   the next contributor doesn't need to diff to find out.

2. Security bumps. axios floor raised to ^1.7.4 (installed 1.18.1) to
   cover CVE-2024-39338 (SSRF). qs and ws direct deps refreshed via
   npm audit fix to patched versions (qs 6.15.3, ws 8.21.1). Four
   moderate CVEs remain in transitive deps of homey-api (parseuri,
   engine.io-client, socket.io-client) — all in the AthomCloudAPI
   OAuth path used only by the deprecated Car Stats Viewer driver
   and, within that, only the REST portion, not the socket.io
   real-time transport where the vulnerabilities live. Will clear
   when Athom bumps their transport stack.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ev_charging_state (cars) and evcharger_charging_state (chargers) are the
same five values in homey-lib, so a car and the charger it is plugged into
should report the same value for the same physical moment. Checked against
the Tesla car app, the Tesla Wall Connector, Easee and SolarEdge; three
mappings disagreed with all of them:

- SMART_CHARGING reported plugged_in_paused while power was flowing.
- DISCHARGING reported plugged_in, discarding plugged_in_discharging even
  though C3 reports the state explicitly.
- UNSPECIFIED and any unrecognised status reported plugged_out with the
  connector still in — the one value consumers act on as "nothing here".

Two coupled defects shared a root cause: "is it charging?" was derived
twice. The energy accrual gate compared the raw status against
CHARGING_STATUS_CHARGING, so smart-charging sessions accrued no
meter_power or session kWh at all, and the switch set
measure_vehicleChargeState per-branch, letting it contradict
ev_charging_state within a poll.

Resolve the plug state once per poll in a pure lib module and let the
accrual gate, measure_vehicleChargeState, measure_vehicleConnected and
ev_charging_state all read it. Connection status now wins outright, so
the device can no longer report plugged_in_charging alongside
measure_vehicleConnected=false.

Also drops dead code: CHARGING_STATUS_FAULT is a ChargerConnectionStatus
value, never a ChargingStatus, so its case and set entry were unreachable;
and the unconditional plugged_out pre-set was always overwritten by the
switch, firing ev_charging_state_changed twice per poll where the two
disagreed.

ERROR stays at plugged_in: chargers report plugged_out on a fault, but
both car apps keep it at plugged_in because the car knows its own cable
is still in.

Charging and smart charging remain distinct statuses that happen to share
a plug state — isSmartCharging() keeps that axis addressable.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Merges PRs #6, #8 and #9 from olemarkus, plus the parts of #1 (kvolden)
and #5 (olemarkus) worth keeping.

ev_charging_state (#9) — "is it charging?" was derived twice, and the
energy accrual gate only recognised CHARGING_STATUS_CHARGING, so smart
charging sessions accrued no meter_power or session kWh at all. Plug
state now resolves once per poll in lib/evChargingState.js and every
consumer reads it. Also maps V2L/V2G to plugged_in_discharging and
fails toward plugged_in on an unrecognised status.

Pairing (#8) — vehicles without a registrationNo paired as
"Polestar 3 (null)"; they now use the bare model name.

Submodule (#6) — adds the .gitmodules entry for clone_modules/polestar.js
pointing at our own fork (kaohlive/polestar.js @ 75dcf9a, verified to
match the committed gitlink), so fresh clones can initialise it.

measure_polestarBattery — declared deprecated rather than removed, which
is what #5 proposed. No longer added to newly paired vehicles, still fed
on existing ones: removeCapability would drop their Insights history and
break tiles and widgets already pointing at it. The capability definition
stays regardless — polestar-2-csv still uses it. Took #5's widget fix, so
the dashboard reads measure_battery and works on new devices too.

Norwegian (#1) — cherry-picked the native-speaker wording over our
current strings; the rest of that PR had been overtaken by the locale
expansion since May 2025.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dashboard widget keyed on the registration number, which the Polestar
API treats as optional — cars that report none (notably some Polestar 3
accounts) were unaddressable, and getVehicleStatus threw Vehicle Not Found
for them. The registration is also mutable in a way a VIN is not: import,
re-registration or new plates would orphan a configured widget.

The VIN was already the device identity (pairing sets id and data.vin);
the widget simply never used it.

The identity is decided in app.js, not in the widget API: the settings
picker is served by the autocomplete listener registered there, and Homey
stores the entry it returns verbatim. That listener returned
{name, registration}, so a widget's settings could never contain a VIN no
matter what the widget API offered. It now returns the VIN as well.

Resolution order is VIN first, registration second. The fallback is
permanent — a widget cannot rewrite its own settings, so widgets already
configured out there hold only a registration and would otherwise have to
be set up again by hand.

Both keys are validated as non-empty strings before matching. Without
that, an absent key matches a car whose registration is null, which would
bind the widget to whichever car happens to lack a plate.

Also drops an unguarded `devices[0].getData().registration` debug line
that would throw on an account with no paired vehicles, and logged a plate
on every keystroke in the picker. getVehicles in the widget API returns an
explicit {id, vin, registration, name} shape rather than raw Device
instances, and now says in a comment that it is not what feeds the picker.

Verified on a real Homey: an existing widget keeps working untouched, and
re-picking the car stores name|vin|registration.

Rides along with 3.3.0, which is pushed but not yet published.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants