Skip to content

feat: Add INTL region support and EU token refresh#177

Open
onpremcloudguy wants to merge 2 commits into
DasBasti:mainfrom
onpremcloudguy:onpremcloudguy-working_Intl_EU
Open

feat: Add INTL region support and EU token refresh#177
onpremcloudguy wants to merge 2 commits into
DasBasti:mainfrom
onpremcloudguy:onpremcloudguy-working_Intl_EU

Conversation

@onpremcloudguy

@onpremcloudguy onpremcloudguy commented Feb 28, 2026

Copy link
Copy Markdown

the previous version you committed didn't work for Intl, I have completed testing via VPN's in the EU and Japan and all tests have passed.

Add support for Hello Smart International (INTL) API used in Australia, Singapore, Israel, and other international markets.

  • Add SmartAuthenticationINTL class with 3-step auth flow
  • Implement EU token refresh via /login-app/api/v1/token
  • Add clear_credentials() security method
  • Add V2L, basic_status, trailer vehicle data
  • Add SmartRegion enum for region selection
  • Update .gitignore for dev/docker folders

Summary by CodeRabbit

  • New Features

    • International (INTL) region authentication and region-aware API handling
    • Ability to clear stored credentials from memory
    • Vehicle basic status (speed, direction, engine, park time) and trailer lamp status
    • Expanded battery telemetry (V2L, charge port/DC-DC status, instantaneous power, 20% range)
    • Air quality indicators (interior/exterior PM2.5 levels)
  • Improvements

    • Optimized EU token refresh and session exchange flow
    • Region-aware request signing and header generation
    • Password added to sensitive-field sanitization

Add support for Hello Smart International (INTL) API used in Australia, Singapore, Israel, and other international markets.

- Add SmartAuthenticationINTL class with 3-step auth flow
- Implement EU token refresh via /login-app/api/v1/token
- Add clear_credentials() security method
- Add V2L, basic_status, trailer vehicle data
- Add SmartRegion enum for region selection
- Update .gitignore for dev/docker folders
@coderabbitai

coderabbitai Bot commented Feb 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@onpremcloudguy has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 34 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 3f4a333 and a11cbb2.

📒 Files selected for processing (12)
  • .gitignore
  • pysmarthashtag/account.py
  • pysmarthashtag/api/authentication.py
  • pysmarthashtag/api/log_sanitizer.py
  • pysmarthashtag/api/utils.py
  • pysmarthashtag/const.py
  • pysmarthashtag/vehicle/basic_status.py
  • pysmarthashtag/vehicle/battery.py
  • pysmarthashtag/vehicle/climate.py
  • pysmarthashtag/vehicle/safety.py
  • pysmarthashtag/vehicle/trailer.py
  • pysmarthashtag/vehicle/vehicle.py
📝 Walkthrough

Walkthrough

Adds INTL-region support (authentication, signing, endpoints), region-aware header generation and credential clearing; introduces BasicStatus and Trailer vehicle models; extends Battery and Climate telemetry fields; updates SmartVehicle to include new models and adjusts parsing for regional differences.

Changes

Cohort / File(s) Summary
Region & Authentication Core
pysmarthashtag/const.py, pysmarthashtag/api/utils.py, pysmarthashtag/api/authentication.py, pysmarthashtag/account.py
Adds SmartRegion enum and endpoint EndpointUrls, INTL constants and EU refresh URL; implements SmartAuthenticationINTL with INTL login/refresh flows and INTL header generation (generate_intl_header); region-aware header selection and endpoint resolution in SmartAccount; adds credential clearing (clear_credentials).
Log Sanitization / Credential Safety
pysmarthashtag/api/log_sanitizer.py, pysmarthashtag/api/authentication.py, pysmarthashtag/account.py
Marks password as sensitive for logging; adds clear_credentials() to authentication and account layers and guards login when credentials were cleared.
Vehicle models — New
pysmarthashtag/vehicle/basic_status.py, pysmarthashtag/vehicle/trailer.py
Adds BasicStatus dataclass to parse basic vehicle status (speed, direction, engine, notifications, eGuard, park time, configuration) and Trailer dataclass to parse trailer lamp statuses; both include parsing helpers and from_vehicle_data classmethods.
Vehicle models — Extensions
pysmarthashtag/vehicle/battery.py, pysmarthashtag/vehicle/climate.py, pysmarthashtag/vehicle/vehicle.py
Extends Battery with many EV-related fields (V2L values, charge lid AC/DC status, DC-DC status, wireless alignment, instantaneous power, remaining_range_at_20_percent); adds three PM2.5 fields to Climate; integrates basic_status and trailer into SmartVehicle and populates them.
Parsing tweaks
pysmarthashtag/vehicle/safety.py
Suppresses missing-field logging for two rear door fields by setting log_missing=False in parsing calls.
Repo config
.gitignore
Adds dev/ and docker/ to ignored paths.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SmartAccount
    participant AuthINTL as SmartAuthenticationINTL
    participant INTL_Auth_Service as INTL Auth Service
    participant INTL_OAuth as INTL OAuth
    participant VehicleAPI as Vehicle API

    Client->>SmartAccount: initialize(region=INTL)
    SmartAccount->>AuthINTL: instantiate (region-aware)
    Client->>AuthINTL: login(username, password)
    AuthINTL->>INTL_Auth_Service: POST /login (credentials)
    INTL_Auth_Service-->>AuthINTL: intl_access_token, intl_id_token
    AuthINTL->>INTL_OAuth: POST /oauth/token (exchange)
    INTL_OAuth-->>AuthINTL: authorization/session token
    AuthINTL->>VehicleAPI: POST /session (exchange for vehicle API tokens)
    VehicleAPI-->>AuthINTL: api_access_token, api_refresh_token
    AuthINTL-->>SmartAccount: return tokens
    SmartAccount-->>Client: authenticated
Loading
sequenceDiagram
    participant Client
    participant SmartAccount
    participant Utils as API Utils
    participant VehicleAPI as Vehicle API
    participant Parser as Vehicle Parsers

    Client->>SmartAccount: get_vehicle_data()
    SmartAccount->>Utils: generate_intl_header(device_id, access_token, params)
    Utils->>Utils: compute nonce & timestamp & signature (INTL_SECRET)
    Utils-->>SmartAccount: headers
    SmartAccount->>VehicleAPI: GET /vehicle (with INTL headers)
    VehicleAPI-->>SmartAccount: vehicle payload
    SmartAccount->>Parser: BasicStatus.from_vehicle_data / Trailer.from_vehicle_data / Battery/Climate parsing
    Parser-->>SmartAccount: parsed models
    SmartAccount-->>Client: return parsed models
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Poem

🐰
Hopped from EU to INTL wide and far,
Tokens, headers, endpoints — a shiny new car,
BasicStatus and Trailer come out to play,
Batteries hum and climates say “Hooray”,
I clear my credentials and leap away!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: adding INTL region support and EU token refresh, which are primary objectives reflected throughout the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 97.37% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
pysmarthashtag/vehicle/climate.py (1)

215-217: Reduce log noise for optional pollution fields.

On Line 215–Line 217, get_field_as_type uses its default log_missing=True. These keys are optional by market, so this can generate repetitive missing-field logs during normal polling. Consider setting log_missing=False for these three lookups.

♻️ Suggested change
-                retval["interior_PM25_level"] = get_field_as_type(pollutionStatus, "interiorPM25Level", int)
-                retval["interior_secondary_PM25_level"] = get_field_as_type(pollutionStatus, "interiorSecondPM25Level", int)
-                retval["exterior_PM25_level"] = get_field_as_type(pollutionStatus, "exteriorPM25Level", int)
+                retval["interior_PM25_level"] = get_field_as_type(
+                    pollutionStatus, "interiorPM25Level", int, log_missing=False
+                )
+                retval["interior_secondary_PM25_level"] = get_field_as_type(
+                    pollutionStatus, "interiorSecondPM25Level", int, log_missing=False
+                )
+                retval["exterior_PM25_level"] = get_field_as_type(
+                    pollutionStatus, "exteriorPM25Level", int, log_missing=False
+                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/vehicle/climate.py` around lines 215 - 217, The three
pollution-related optional fields are currently fetched with get_field_as_type
using its default log_missing=True which floods logs; update the three
assignments that set retval["interior_PM25_level"],
retval["interior_secondary_PM25_level"], and retval["exterior_PM25_level"] to
call get_field_as_type(pollutionStatus, "<fieldName>", int, log_missing=False)
(using the exact keys "interiorPM25Level", "interiorSecondPM25Level",
"exteriorPM25Level") so missing optional fields are not logged during normal
polling.
pysmarthashtag/const.py (1)

79-84: Use existing INTL constants in the INTL endpoint preset.

Line 79–Line 84 duplicate URL literals already declared as constants, which increases drift risk.

♻️ Proposed refactor
         return EndpointUrls(
-            server_url="https://sg-app-api.smart.com/iam/service/api/v1/login",
-            auth_url="https://sg-app-api.smart.com/iam/service/api/v1/oauth20/authorize",
-            login_url="https://sg-app-api.smart.com/iam/service/api/v1/login",
+            server_url=INTL_LOGIN_URL,
+            auth_url=INTL_OAUTH_URL,
+            login_url=INTL_LOGIN_URL,
             # INTL uses same data endpoints as EU
-            api_base_url="https://api.ecloudeu.com",
-            api_base_url_v2="https://apiv2.ecloudeu.com",
+            api_base_url=API_BASE_URL,
+            api_base_url_v2=API_BASE_URL_V2,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/const.py` around lines 79 - 84, The INTL endpoint preset
currently reuses hard-coded URL literals for server_url, auth_url, login_url,
api_base_url and api_base_url_v2; replace these duplicated string literals with
the existing INTL constants already defined elsewhere in the module (use the
corresponding INTL_* constant names instead of the literal URLs) so the preset
references the canonical constants and avoids drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pysmarthashtag/account.py`:
- Around line 87-90: The INTL signed header is built with the wrong device id:
replace use of auth.device_id with the INTL auth object's device_identifier when
calling utils.generate_intl_header (i.e., ensure the code path that uses
SmartAuthenticationINTL passes auth.device_identifier into device_id argument),
so generate_intl_header, SmartAuthenticationINTL, and the auth.device_identifier
field are used consistently for INTL signed vehicle API requests.

In `@pysmarthashtag/api/authentication.py`:
- Around line 750-755: The returned payload is assigning the IAM refresh token
key incorrectly — replace the value for "refresh_token" with the INTL refresh
token (self.intl_refresh_token) while keeping "api_refresh_token" as
api_refresh_token; update the same incorrect mapping in the other spot mentioned
(around the other return block). Locate the return dicts in authentication.py
(the function/method that builds the token response, and the similar block near
the second occurrence) and change the "refresh_token" value to
self.intl_refresh_token so INTL refresh flow uses the correct token while
leaving "api_refresh_token" and "api_access_token" unchanged.
- Around line 676-709: The code currently logs raw INTL auth artifacts (e.g.,
self.intl_user_id, self.intl_access_token, self.intl_id_token, auth_code and
full oauth_result) inside the INTL login flow; update the logging in the method
handling INTL login (the block that calls _LOGGER.info("INTL: Login
successful..."), _LOGGER.debug("INTL OAuth response code...") and
_LOGGER.debug("INTL: Got authCode...")) to stop emitting raw secrets and
identifiers: import and call sanitize_log_data() from api.log_sanitizer for any
user/id/token values you must log, or remove the value from the log message
entirely, and ensure errors log only sanitized oauth_result.get("message")
(sanitized) and not full token payloads.

In `@pysmarthashtag/api/utils.py`:
- Around line 11-15: EU_SECRET and INTL_SECRET are hard-coded secrets; remove
these literal values from pysmarthashtag.api.utils and load them from secure
configuration (e.g., environment variables or a secrets manager) instead. Change
the code that defines EU_SECRET and INTL_SECRET to read from environment
variables like SMARTHASHTAG_EU_SECRET and SMARTHASHTAG_INTL_SECRET
(base64-decode SMARTHASHTAG_EU_SECRET when required, and .encode("utf-8") or
accept bytes for INTL_SECRET), validate they are present and raise a clear error
if missing, and update any callers of EU_SECRET/INTL_SECRET to use the new
variables; also update deployment/docs and tests to inject mock secrets via env
vars or fixtures.

In `@pysmarthashtag/vehicle/basic_status.py`:
- Around line 149-152: The current try/except/finally suppresses non-KeyError
exceptions because the return is inside finally; move the return retval out of
the finally block in the function that constructs retval (the block with except
KeyError as e and _LOGGER.info) so that only KeyError is caught and logged via
_LOGGER.info while other exceptions (e.g., ValueError from
datetime.fromtimestamp calls) are allowed to propagate (or are explicitly
handled/re-raised). Ensure the except only handles KeyError, remove the finally
or keep it for cleanup only, and place the final "return retval" after the
try/except so errors aren't swallowed.

In `@pysmarthashtag/vehicle/trailer.py`:
- Around line 68-71: The finally block currently contains "return retval" which
suppresses exceptions and overrides the intended early return/exception
behavior; remove the return from the finally block and instead return retval
after the try/except/finally completes (or return/raise inside the try/except
branches as appropriate). Concretely, edit the function that uses the local
variable retval and the except KeyError as e handler so the except either sets
retval (e.g., to None) or re-raises the exception, then place a single "return
retval" statement after the try/except/finally so that exceptions are not
silenced and the early return behavior is preserved. Ensure no other code relies
on a return inside finally.

---

Nitpick comments:
In `@pysmarthashtag/const.py`:
- Around line 79-84: The INTL endpoint preset currently reuses hard-coded URL
literals for server_url, auth_url, login_url, api_base_url and api_base_url_v2;
replace these duplicated string literals with the existing INTL constants
already defined elsewhere in the module (use the corresponding INTL_* constant
names instead of the literal URLs) so the preset references the canonical
constants and avoids drift.

In `@pysmarthashtag/vehicle/climate.py`:
- Around line 215-217: The three pollution-related optional fields are currently
fetched with get_field_as_type using its default log_missing=True which floods
logs; update the three assignments that set retval["interior_PM25_level"],
retval["interior_secondary_PM25_level"], and retval["exterior_PM25_level"] to
call get_field_as_type(pollutionStatus, "<fieldName>", int, log_missing=False)
(using the exact keys "interiorPM25Level", "interiorSecondPM25Level",
"exteriorPM25Level") so missing optional fields are not logged during normal
polling.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44f28e5 and dec9b28.

📒 Files selected for processing (12)
  • .gitignore
  • pysmarthashtag/account.py
  • pysmarthashtag/api/authentication.py
  • pysmarthashtag/api/log_sanitizer.py
  • pysmarthashtag/api/utils.py
  • pysmarthashtag/const.py
  • pysmarthashtag/vehicle/basic_status.py
  • pysmarthashtag/vehicle/battery.py
  • pysmarthashtag/vehicle/climate.py
  • pysmarthashtag/vehicle/safety.py
  • pysmarthashtag/vehicle/trailer.py
  • pysmarthashtag/vehicle/vehicle.py

Comment thread pysmarthashtag/account.py
Comment on lines +87 to +90
return utils.generate_intl_header(
device_id=auth.device_id,
access_token=auth.api_access_token,
params=params,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the INTL device identifier for INTL signed headers.

Line 88 currently uses auth.device_id, but SmartAuthenticationINTL establishes INTL auth/session with device_identifier. Keep this identifier consistent for signed vehicle API requests.

🔧 Proposed fix
             return utils.generate_intl_header(
-                device_id=auth.device_id,
+                device_id=getattr(auth, "device_identifier", auth.device_id),
                 access_token=auth.api_access_token,
                 params=params,
                 method=method,
                 url=url,
                 body=body,
                 client_id=client_id,
             )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return utils.generate_intl_header(
device_id=auth.device_id,
access_token=auth.api_access_token,
params=params,
return utils.generate_intl_header(
device_id=getattr(auth, "device_identifier", auth.device_id),
access_token=auth.api_access_token,
params=params,
method=method,
url=url,
body=body,
client_id=client_id,
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/account.py` around lines 87 - 90, The INTL signed header is
built with the wrong device id: replace use of auth.device_id with the INTL auth
object's device_identifier when calling utils.generate_intl_header (i.e., ensure
the code path that uses SmartAuthenticationINTL passes auth.device_identifier
into device_id argument), so generate_intl_header, SmartAuthenticationINTL, and
the auth.device_identifier field are used consistently for INTL signed vehicle
API requests.

Comment on lines +676 to +709
_LOGGER.info("INTL: Login successful for user %s", self.intl_user_id)

except (KeyError, ValueError, TypeError) as e:
raise SmartAPIError(f"Could not parse INTL login response: {e}")

# Step 2: Exchange accessToken for authCode via OAuth
# Note: OAuth uses just accessToken in URL param, and idToken in Xs-Auth-Token header
_LOGGER.debug("INTL: Step 2 - Exchanging accessToken for authCode")

oauth_url = f"{INTL_OAUTH_URL}?accessToken={self.intl_access_token}"

oauth_headers = self._generate_intl_headers(self.intl_id_token)

r_oauth = await client.get(
oauth_url,
headers=oauth_headers,
)

try:
oauth_result = r_oauth.json()
_LOGGER.debug("INTL OAuth response code: %s", oauth_result.get("code"))

if oauth_result.get("code") != "200":
error_msg = oauth_result.get("message", "Unknown error")
_LOGGER.error("INTL OAuth failed: %s", error_msg)
raise SmartAPIError(f"INTL OAuth failed: {error_msg}")

# Result is directly the authCode string (e.g., "CODE-xxx")
auth_code = oauth_result.get("result")
if isinstance(auth_code, dict):
auth_code = auth_code.get("authCode", auth_code)

_LOGGER.debug("INTL: Got authCode: %s...", str(auth_code)[:30] if auth_code else "None")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stop logging raw INTL auth/user artifacts.

Line 676, Line 708, Line 745, and Line 804 log sensitive identity/auth values directly. Remove these values or sanitize before logging.

🔒 Proposed logging hardening
-                _LOGGER.info("INTL: Login successful for user %s", self.intl_user_id)
+                _LOGGER.info("INTL: Login successful")

-                _LOGGER.debug("INTL: Got authCode: %s...", str(auth_code)[:30] if auth_code else "None")
+                _LOGGER.debug("INTL: OAuth authCode received")

-                _LOGGER.info("INTL: Successfully authenticated, user ID: %s", api_user_id)
+                _LOGGER.info("INTL: Successfully authenticated")

-                _LOGGER.info("INTL: Token refresh successful for user %s", self.intl_user_id)
+                _LOGGER.info("INTL: Token refresh successful")
As per coding guidelines: Use `sanitize_log_data()` from `api.log_sanitizer` when logging sensitive data.

Also applies to: 745-746, 804-805

🧰 Tools
🪛 Ruff (0.15.2)

[warning] 679-679: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 679-679: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 701-701: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/api/authentication.py` around lines 676 - 709, The code
currently logs raw INTL auth artifacts (e.g., self.intl_user_id,
self.intl_access_token, self.intl_id_token, auth_code and full oauth_result)
inside the INTL login flow; update the logging in the method handling INTL login
(the block that calls _LOGGER.info("INTL: Login successful..."),
_LOGGER.debug("INTL OAuth response code...") and _LOGGER.debug("INTL: Got
authCode...")) to stop emitting raw secrets and identifiers: import and call
sanitize_log_data() from api.log_sanitizer for any user/id/token values you must
log, or remove the value from the log message entirely, and ensure errors log
only sanitized oauth_result.get("message") (sanitized) and not full token
payloads.

Comment on lines +750 to +755
return {
"access_token": api_access_token,
"refresh_token": api_refresh_token,
"api_access_token": api_access_token,
"api_refresh_token": api_refresh_token,
"api_user_id": api_user_id,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

INTL refresh_token payload currently returns the wrong token type.

Line 752 and Line 862 return api_refresh_token as refresh_token, but INTL refresh flow depends on IAM refresh token (self.intl_refresh_token). This breaks refresh continuity after object re-instantiation.

🔁 Proposed token mapping fix
         return {
             "access_token": api_access_token,
-            "refresh_token": api_refresh_token,
+            "refresh_token": self.intl_refresh_token,
             "api_access_token": api_access_token,
             "api_refresh_token": api_refresh_token,
             "api_user_id": api_user_id,
             "expires_at": expires_at,
         }
@@
                 return {
                     "access_token": api_access_token,
-                    "refresh_token": api_refresh_token,
+                    "refresh_token": self.intl_refresh_token,
                     "api_access_token": api_access_token,
                     "api_refresh_token": api_refresh_token,
                     "api_user_id": api_user_id,
                     "expires_at": expires_at,
                 }

Also applies to: 860-865

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/api/authentication.py` around lines 750 - 755, The returned
payload is assigning the IAM refresh token key incorrectly — replace the value
for "refresh_token" with the INTL refresh token (self.intl_refresh_token) while
keeping "api_refresh_token" as api_refresh_token; update the same incorrect
mapping in the other spot mentioned (around the other return block). Locate the
return dicts in authentication.py (the function/method that builds the token
response, and the similar block near the second occurrence) and change the
"refresh_token" value to self.intl_refresh_token so INTL refresh flow uses the
correct token while leaving "api_refresh_token" and "api_access_token"
unchanged.

Comment on lines +11 to +15
# EU signing secret (base64 encoded)
EU_SECRET = base64.b64decode("NzRlNzQ2OWFmZjUwNDJiYmJlZDdiYmIxYjM2YzE1ZTk=")

# INTL (International) signing secret - used for Australia, Singapore, Israel, etc.
INTL_SECRET = "30fb4a7b7fab4e2e8b52120d0087efdd".encode("utf-8")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Move signing secrets out of source code.

Line 12 and Line 15 commit signing secrets directly in the repository. This is a security risk and already surfaced by secret scanning.

🔐 Proposed direction
+import os
 import base64
 import hashlib
 import hmac
@@
-# EU signing secret (base64 encoded)
-EU_SECRET = base64.b64decode("NzRlNzQ2OWFmZjUwNDJiYmJlZDdiYmIxYjM2YzE1ZTk=")
+# EU signing secret (base64 encoded) - load from environment/secret manager
+EU_SECRET = base64.b64decode(os.environ["SMART_EU_SIGNING_SECRET_B64"])
@@
-# INTL (International) signing secret - used for Australia, Singapore, Israel, etc.
-INTL_SECRET = "30fb4a7b7fab4e2e8b52120d0087efdd".encode("utf-8")
+# INTL signing secret - load from environment/secret manager
+INTL_SECRET = os.environ["SMART_INTL_SIGNING_SECRET"].encode("utf-8")
🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 15-15: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/api/utils.py` around lines 11 - 15, EU_SECRET and INTL_SECRET
are hard-coded secrets; remove these literal values from
pysmarthashtag.api.utils and load them from secure configuration (e.g.,
environment variables or a secrets manager) instead. Change the code that
defines EU_SECRET and INTL_SECRET to read from environment variables like
SMARTHASHTAG_EU_SECRET and SMARTHASHTAG_INTL_SECRET (base64-decode
SMARTHASHTAG_EU_SECRET when required, and .encode("utf-8") or accept bytes for
INTL_SECRET), validate they are present and raise a clear error if missing, and
update any callers of EU_SECRET/INTL_SECRET to use the new variables; also
update deployment/docs and tests to inject mock secrets via env vars or
fixtures.

Comment on lines +149 to +152
except KeyError as e:
_LOGGER.info(f"Basic status info not available: {e}")
finally:
return retval

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd basic_status.py --type f

Repository: DasBasti/pySmartHashtag

Length of output: 105


🏁 Script executed:

cat -n pysmarthashtag/vehicle/basic_status.py | sed -n '130,165p'

Repository: DasBasti/pySmartHashtag

Length of output: 1249


🏁 Script executed:

cat -n pysmarthashtag/vehicle/basic_status.py | sed -n '80,155p'

Repository: DasBasti/pySmartHashtag

Length of output: 4226


🏁 Script executed:

cat -n pysmarthashtag/vehicle/basic_status.py | head -10

Repository: DasBasti/pySmartHashtag

Length of output: 427


🏁 Script executed:

rg "get_field_as_type" --type py -A 3 -B 1 | head -40

Repository: DasBasti/pySmartHashtag

Length of output: 2613


🏁 Script executed:

cat -n pysmarthashtag/models.py | grep -A 20 "def get_field_as_type"

Repository: DasBasti/pySmartHashtag

Length of output: 972


🏁 Script executed:

rg "except.*:\s*\n\s*_LOGGER" pysmarthashtag/vehicle/ --type py -B 5 -A 5

Repository: DasBasti/pySmartHashtag

Length of output: 258


🏁 Script executed:

rg "except KeyError" pysmarthashtag/vehicle/ --type py -A 10

Repository: DasBasti/pySmartHashtag

Length of output: 2723


🏁 Script executed:

cat -n pysmarthashtag/vehicle/basic_status.py | sed -n '118,125p'

Repository: DasBasti/pySmartHashtag

Length of output: 525


Move return outside finally block to prevent exception suppression.

The finally block at line 151–152 will suppress any exception raised during parsing that isn't a KeyError—for example, ValueError from datetime.fromtimestamp() at lines 120, 137, and 147. This hides errors and makes troubleshooting difficult.

Fix
-        except KeyError as e:
-            _LOGGER.info(f"Basic status info not available: {e}")
-        finally:
-            return retval
+        except KeyError as e:
+            _LOGGER.info(f"Basic status info not available: {e}")
+        return retval
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except KeyError as e:
_LOGGER.info(f"Basic status info not available: {e}")
finally:
return retval
except KeyError as e:
_LOGGER.info(f"Basic status info not available: {e}")
return retval
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 152-152: return inside finally blocks cause exceptions to be silenced

(B012)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/vehicle/basic_status.py` around lines 149 - 152, The current
try/except/finally suppresses non-KeyError exceptions because the return is
inside finally; move the return retval out of the finally block in the function
that constructs retval (the block with except KeyError as e and _LOGGER.info) so
that only KeyError is caught and logged via _LOGGER.info while other exceptions
(e.g., ValueError from datetime.fromtimestamp calls) are allowed to propagate
(or are explicitly handled/re-raised). Ensure the except only handles KeyError,
remove the finally or keep it for cleanup only, and place the final "return
retval" after the try/except so errors aren't swallowed.

Comment on lines +68 to +71
except KeyError as e:
_LOGGER.info(f"Trailer status info not available: {e}")
finally:
return retval

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, verify the file exists and check the specific lines mentioned
echo "=== File check ==="
if [ -f "pysmarthashtag/vehicle/trailer.py" ]; then
  echo "File found"
  wc -l pysmarthashtag/vehicle/trailer.py
else
  echo "File not found"
  exit 1
fi

echo -e "\n=== Lines 50-75 (context around lines 68-71) ==="
sed -n '50,75p' pysmarthashtag/vehicle/trailer.py | cat -n

echo -e "\n=== Check for return statements in finally blocks ==="
rg -nU 'finally:\n\s*return' pysmarthashtag/vehicle/trailer.py

Repository: DasBasti/pySmartHashtag

Length of output: 1579


Avoid return in finally here—it suppresses exceptions and overrides the early return at line 54.

A return statement in the finally block (lines 70–71) overrides the early return None at line 54 and suppresses the caught KeyError exception, silencing potential parsing failures.

Suggested fix
        except KeyError as e:
            _LOGGER.info(f"Trailer status info not available: {e}")
-       finally:
-           return retval
+       return retval
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 71-71: return inside finally blocks cause exceptions to be silenced

(B012)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/vehicle/trailer.py` around lines 68 - 71, The finally block
currently contains "return retval" which suppresses exceptions and overrides the
intended early return/exception behavior; remove the return from the finally
block and instead return retval after the try/except/finally completes (or
return/raise inside the try/except branches as appropriate). Concretely, edit
the function that uses the local variable retval and the except KeyError as e
handler so the except either sets retval (e.g., to None) or re-raises the
exception, then place a single "return retval" statement after the
try/except/finally so that exceptions are not silenced and the early return
behavior is preserved. Ensure no other code relies on a return inside finally.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
pysmarthashtag/account.py (1)

87-95: ⚠️ Potential issue | 🟠 Major

Use device_identifier for INTL signed headers.

Line 88 uses auth.device_id, but SmartAuthenticationINTL establishes the INTL auth/session with device_identifier. For consistency with INTL signing, use the INTL-specific identifier.

🔧 Proposed fix
         if self._is_intl_region():
             # INTL region uses different headers and signing
             client_id = getattr(auth, 'api_client_id', None)
             return utils.generate_intl_header(
-                device_id=auth.device_id,
+                device_id=getattr(auth, "device_identifier", auth.device_id),
                 access_token=auth.api_access_token,
                 params=params,
                 method=method,
                 url=url,
                 body=body,
                 client_id=client_id,
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/account.py` around lines 87 - 95, The INTL-signed header call
is using auth.device_id but SmartAuthenticationINTL establishes the session with
a different identifier; update the call to utils.generate_intl_header to pass
the INTL-specific identifier (auth.device_identifier) instead of auth.device_id
so signing uses the correct device token; locate the call in account.py where
generate_intl_header is invoked and replace the device_id argument to reference
auth.device_identifier (ensure any tests or callers expecting device_id are
adjusted to the INTL field if needed).
pysmarthashtag/api/authentication.py (2)

676-676: ⚠️ Potential issue | 🟠 Major

Avoid logging sensitive identifiers directly.

Lines 676, 708, 745, and 804 log user IDs and partial auth codes directly. These should be removed or sanitized per coding guidelines.

🔒 Proposed logging hardening
-                _LOGGER.info("INTL: Login successful for user %s", self.intl_user_id)
+                _LOGGER.info("INTL: Login successful")
-                _LOGGER.debug("INTL: Got authCode: %s...", str(auth_code)[:30] if auth_code else "None")
+                _LOGGER.debug("INTL: OAuth authCode received")
-                _LOGGER.info("INTL: Successfully authenticated, user ID: %s", api_user_id)
+                _LOGGER.info("INTL: Successfully authenticated")
-                _LOGGER.info("INTL: Token refresh successful for user %s", self.intl_user_id)
+                _LOGGER.info("INTL: Token refresh successful")

Based on learnings: Use sanitize_log_data() from api.log_sanitizer when logging sensitive data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/api/authentication.py` at line 676, Replace direct logging of
sensitive identifiers with sanitized values using sanitize_log_data from
api.log_sanitizer: update calls that use _LOGGER.info/_LOGGER.debug which
include self.intl_user_id and any auth code variables (e.g., partial auth code
variables logged in the same class/methods where the "INTL: Login successful for
user %s" message appears) to pass sanitize_log_data(self.intl_user_id) or
sanitize_log_data(auth_code_var) instead, or remove the log entry entirely if
not needed; import sanitize_log_data at the top of the module and ensure every
occurrence that logs user IDs or auth codes uses that sanitizer.

750-757: ⚠️ Potential issue | 🟠 Major

Consider returning intl_refresh_token for refresh continuity.

The return dict sets refresh_token to api_refresh_token, but INTL refresh flow depends on self.intl_refresh_token. If the auth object is re-instantiated with stored tokens, the INTL refresh won't work because intl_refresh_token won't be available.

🔁 Proposed token mapping fix
         return {
             "access_token": api_access_token,
-            "refresh_token": api_refresh_token,
+            "refresh_token": self.intl_refresh_token,
             "api_access_token": api_access_token,
             "api_refresh_token": api_refresh_token,
             "api_user_id": api_user_id,
             "expires_at": expires_at,
         }

Also apply the same fix at line 862.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/api/authentication.py` around lines 750 - 757, The returned
token dict currently maps "refresh_token" to api_refresh_token which breaks INTL
refresh flow; change the mapping so "refresh_token" uses self.intl_refresh_token
(and include "intl_refresh_token": self.intl_refresh_token) while keeping
"api_refresh_token": api_refresh_token and "api_access_token": api_access_token;
update the return that builds {"access_token": api_access_token,
"refresh_token": ..., "api_access_token": ..., "api_refresh_token": ...,
"api_user_id": api_user_id, "expires_at": expires_at} to use
self.intl_refresh_token for the refresh_token and add the intl_refresh_token
key, and make the same corresponding change for the duplicate token-returning
block later in the file that uses the same variables (api_access_token,
api_refresh_token, api_user_id, expires_at).
🧹 Nitpick comments (2)
pysmarthashtag/api/authentication.py (2)

678-679: Chain exceptions with raise ... from for better tracebacks.

When re-raising exceptions, use raise ... from err to preserve the exception chain and improve debugging.

♻️ Proposed fix
             except (KeyError, ValueError, TypeError) as e:
-                raise SmartAPIError(f"Could not parse INTL login response: {e}")
+                raise SmartAPIError(f"Could not parse INTL login response: {e}") from e

Apply the same pattern at lines 711 and 748.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/api/authentication.py` around lines 678 - 679, Change the bare
re-raises of parsing exceptions to chain the original exception using "raise ...
from e": in the except blocks (e.g., the block that catches (KeyError,
ValueError, TypeError) and raises SmartAPIError("Could not parse INTL login
response: {e}")), update the raise to use "raise SmartAPIError(...) from e".
Apply the same pattern for the other similar except blocks referenced in the
file (the ones at the locations corresponding to lines around 711 and 748) so
all SmartAPIError raises preserve the original exception chain.

549-550: Consider moving imports to module level.

The time and uuid imports are placed inside the method. While functional, module-level imports are more conventional and slightly more efficient.

♻️ Suggested improvement

Move the imports to the top of the file with other imports:

 import asyncio
 import datetime
 import json
 import logging
 import math
 import secrets
 import ssl
+import time
+import uuid
 from collections import defaultdict

Then remove the local imports in _generate_intl_headers and _generate_vehicle_api_headers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/api/authentication.py` around lines 549 - 550, Move the local
imports of time and uuid out of the methods and into the module-level imports at
the top of the file (add "import time" and "import uuid" alongside the other
imports), then remove the in-function "import time" and "import uuid" lines from
_generate_intl_headers and _generate_vehicle_api_headers so both functions use
the module-level symbols; verify there are no other local import duplicates and
run tests to ensure no naming conflicts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pysmarthashtag/api/authentication.py`:
- Line 579: The parameter annotation in _generate_vehicle_api_headers currently
uses an implicit Optional via body: str = None; change the signature to
explicitly declare Optional[str] (e.g., body: Optional[str] = None) and add the
corresponding typing import (from typing import Optional) or use the explicit
union syntax (str | None) if the project targets Python 3.10+; update any
references or type checks in the function if needed to match the new annotation.

In `@pysmarthashtag/const.py`:
- Around line 35-46: The SmartRegion enum currently inherits from Enum but Enum
is not imported; add the missing import for Enum (from the standard library enum
module) at the top of the file so SmartRegion (class SmartRegion) can reference
Enum without causing a NameError.

---

Duplicate comments:
In `@pysmarthashtag/account.py`:
- Around line 87-95: The INTL-signed header call is using auth.device_id but
SmartAuthenticationINTL establishes the session with a different identifier;
update the call to utils.generate_intl_header to pass the INTL-specific
identifier (auth.device_identifier) instead of auth.device_id so signing uses
the correct device token; locate the call in account.py where
generate_intl_header is invoked and replace the device_id argument to reference
auth.device_identifier (ensure any tests or callers expecting device_id are
adjusted to the INTL field if needed).

In `@pysmarthashtag/api/authentication.py`:
- Line 676: Replace direct logging of sensitive identifiers with sanitized
values using sanitize_log_data from api.log_sanitizer: update calls that use
_LOGGER.info/_LOGGER.debug which include self.intl_user_id and any auth code
variables (e.g., partial auth code variables logged in the same class/methods
where the "INTL: Login successful for user %s" message appears) to pass
sanitize_log_data(self.intl_user_id) or sanitize_log_data(auth_code_var)
instead, or remove the log entry entirely if not needed; import
sanitize_log_data at the top of the module and ensure every occurrence that logs
user IDs or auth codes uses that sanitizer.
- Around line 750-757: The returned token dict currently maps "refresh_token" to
api_refresh_token which breaks INTL refresh flow; change the mapping so
"refresh_token" uses self.intl_refresh_token (and include "intl_refresh_token":
self.intl_refresh_token) while keeping "api_refresh_token": api_refresh_token
and "api_access_token": api_access_token; update the return that builds
{"access_token": api_access_token, "refresh_token": ..., "api_access_token":
..., "api_refresh_token": ..., "api_user_id": api_user_id, "expires_at":
expires_at} to use self.intl_refresh_token for the refresh_token and add the
intl_refresh_token key, and make the same corresponding change for the duplicate
token-returning block later in the file that uses the same variables
(api_access_token, api_refresh_token, api_user_id, expires_at).

---

Nitpick comments:
In `@pysmarthashtag/api/authentication.py`:
- Around line 678-679: Change the bare re-raises of parsing exceptions to chain
the original exception using "raise ... from e": in the except blocks (e.g., the
block that catches (KeyError, ValueError, TypeError) and raises
SmartAPIError("Could not parse INTL login response: {e}")), update the raise to
use "raise SmartAPIError(...) from e". Apply the same pattern for the other
similar except blocks referenced in the file (the ones at the locations
corresponding to lines around 711 and 748) so all SmartAPIError raises preserve
the original exception chain.
- Around line 549-550: Move the local imports of time and uuid out of the
methods and into the module-level imports at the top of the file (add "import
time" and "import uuid" alongside the other imports), then remove the
in-function "import time" and "import uuid" lines from _generate_intl_headers
and _generate_vehicle_api_headers so both functions use the module-level
symbols; verify there are no other local import duplicates and run tests to
ensure no naming conflicts.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dec9b28 and 3f4a333.

📒 Files selected for processing (3)
  • pysmarthashtag/account.py
  • pysmarthashtag/api/authentication.py
  • pysmarthashtag/const.py


return headers

def _generate_vehicle_api_headers(self, body: str = None) -> dict[str, str]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix implicit Optional type hint.

PEP 484 prohibits implicit Optional. The parameter body: str = None should use explicit Optional[str].

🔧 Proposed fix
-    def _generate_vehicle_api_headers(self, body: str = None) -> dict[str, str]:
+    def _generate_vehicle_api_headers(self, body: Optional[str] = None) -> dict[str, str]:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _generate_vehicle_api_headers(self, body: str = None) -> dict[str, str]:
def _generate_vehicle_api_headers(self, body: Optional[str] = None) -> dict[str, str]:
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 579-579: PEP 484 prohibits implicit Optional

Convert to Optional[T]

(RUF013)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pysmarthashtag/api/authentication.py` at line 579, The parameter annotation
in _generate_vehicle_api_headers currently uses an implicit Optional via body:
str = None; change the signature to explicitly declare Optional[str] (e.g.,
body: Optional[str] = None) and add the corresponding typing import (from typing
import Optional) or use the explicit union syntax (str | None) if the project
targets Python 3.10+; update any references or type checks in the function if
needed to match the new annotation.

Comment thread pysmarthashtag/const.py
@onpremcloudguy onpremcloudguy force-pushed the onpremcloudguy-working_Intl_EU branch from 3f4a333 to a11cbb2 Compare February 28, 2026 06:56
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.

1 participant