Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions custom_components/smarthashtag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from pysmarthashtag.account import SmartAccount
from pysmarthashtag.const import EndpointUrls
from pysmarthashtag.const import EndpointUrls, SmartRegion, get_endpoint_urls_for_region

from .const import (
CONF_API_BASE_URL,
CONF_API_BASE_URL_V2,
CONF_REGION,
DEFAULT_REGION,
REGION_CUSTOM,
REGION_EU,
REGION_INTL,
)
from .coordinator import SmartHashtagDataUpdateCoordinator

Expand Down Expand Up @@ -57,7 +60,7 @@ async def async_setup_entry(
"""
# Determine endpoint URLs based on region or custom settings
endpoint_urls = None
region = entry.data.get(CONF_REGION)
region = entry.data.get(CONF_REGION, DEFAULT_REGION)

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

Normalize region value before endpoint branching.

entry.data.get(CONF_REGION, DEFAULT_REGION) does not replace explicit None. Using entry.data.get(CONF_REGION) or DEFAULT_REGION avoids falling into the unrecognized-region path for null values.

🔧 Suggested tweak
-    region = entry.data.get(CONF_REGION, DEFAULT_REGION)
+    region = entry.data.get(CONF_REGION) or DEFAULT_REGION
📝 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
region = entry.data.get(CONF_REGION, DEFAULT_REGION)
region = entry.data.get(CONF_REGION) or DEFAULT_REGION
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@custom_components/smarthashtag/__init__.py` at line 63, The region variable
assignment uses entry.data.get(CONF_REGION, DEFAULT_REGION) which doesn't guard
against an explicit None value; change the logic where region is set (the
assignment for region that reads entry.data.get and later used in endpoint
branching) to normalize None to the default, e.g. use
entry.data.get(CONF_REGION) or DEFAULT_REGION (or an explicit None check) so
explicit null values don't fall through into the unrecognized-region branch when
evaluating region for endpoint selection.


if region == REGION_CUSTOM:
# Use custom endpoints if provided
Expand All @@ -68,8 +71,14 @@ async def async_setup_entry(
api_base_url=custom_api_base_url or None,
api_base_url_v2=custom_api_base_url_v2 or None,
)
# For EU region (default) or unrecognized region, endpoint_urls remains None
# and SmartAccount will use default EU endpoints
elif region == REGION_INTL:
# Use international endpoints
endpoint_urls = get_endpoint_urls_for_region(SmartRegion.INTL)
elif region == REGION_EU:
# Use EU endpoints (default)
endpoint_urls = get_endpoint_urls_for_region(SmartRegion.EU)
# If region is None or unrecognized, endpoint_urls remains None
# and SmartAccount will use default endpoints

entry.runtime_data = SmartHashtagDataUpdateCoordinator(
hass=hass,
Expand Down
163 changes: 161 additions & 2 deletions custom_components/smarthashtag/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import selector
from pysmarthashtag.account import SmartAccount
from pysmarthashtag.const import EndpointUrls
from pysmarthashtag.const import EndpointUrls, SmartRegion, get_endpoint_urls_for_region
from pysmarthashtag.models import (
SmartAPIError,
)
Expand All @@ -40,6 +40,8 @@
MIN_SCAN_INTERVAL,
NAME,
REGION_CUSTOM,
REGION_EU,
REGION_INTL,
REGIONS,
)

Expand Down Expand Up @@ -167,6 +169,9 @@ async def async_step_custom_endpoints(
self.init_info[CONF_API_BASE_URL] = api_base_url
self.init_info[CONF_API_BASE_URL_V2] = api_base_url_v2
self.init_info[CONF_VEHICLES] = list(vehicles)
# If reconfiguring, skip vehicle selection and update directly
if self._is_reconfigure:
return await self._finish_reconfigure()
return await self.async_step_vehicle()

return self.async_show_form(
Expand Down Expand Up @@ -194,6 +199,155 @@ async def async_step_custom_endpoints(
errors=_errors,
)

@property
def _is_reconfigure(self) -> bool:
"""Check if we are in a reconfigure flow."""
return self.init_info.get("_reconfigure", False)

async def async_step_reconfigure(
self,
user_input: dict | None = None,
) -> config_entries.FlowResult:
"""Handle reconfiguration of credentials and region."""
_errors = {}
reconfigure_entry = self._get_reconfigure_entry()

if user_input is not None:
# Check if custom endpoints are selected and redirect to custom step
if user_input.get(CONF_REGION) == REGION_CUSTOM:
self.init_info = user_input
self.init_info["_reconfigure"] = True
return await self.async_step_custom_endpoints()

try:
vehicles = await self._test_credentials(
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
region=user_input.get(CONF_REGION, DEFAULT_REGION),
)
except SmartAPIError as exception:
LOGGER.warning(exception)
_errors["base"] = "auth"
else:
self.init_info = user_input
self.init_info["_reconfigure"] = True
self.init_info[CONF_VEHICLES] = list(vehicles)
return await self._finish_reconfigure()

return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema(
{
vol.Required(
CONF_USERNAME,
default=(user_input or {}).get(
CONF_USERNAME, reconfigure_entry.data.get(CONF_USERNAME)
),
): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.EMAIL,
autocomplete="username",
),
),
vol.Required(
CONF_PASSWORD,
): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.PASSWORD,
autocomplete="current-password",
),
),
vol.Optional(
CONF_REGION,
default=(user_input or {}).get(
CONF_REGION,
reconfigure_entry.data.get(CONF_REGION, DEFAULT_REGION),
),
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
selector.SelectOptionDict(value=k, label=v)
for k, v in REGIONS.items()
],
mode=selector.SelectSelectorMode.DROPDOWN,
),
),
}
),
errors=_errors,
)

async def _finish_reconfigure(self) -> config_entries.FlowResult:
"""Complete reconfiguration by selecting a vehicle and updating the entry."""
reconfigure_entry = self._get_reconfigure_entry()
vehicles = self.init_info[CONF_VEHICLES]

# Build updated data, removing internal flags
data_updates = {
CONF_USERNAME: self.init_info[CONF_USERNAME],
CONF_PASSWORD: self.init_info[CONF_PASSWORD],
CONF_REGION: self.init_info.get(CONF_REGION, DEFAULT_REGION),
}

# Include custom endpoint URLs if present
if CONF_API_BASE_URL in self.init_info:
data_updates[CONF_API_BASE_URL] = self.init_info[CONF_API_BASE_URL]
if CONF_API_BASE_URL_V2 in self.init_info:
data_updates[CONF_API_BASE_URL_V2] = self.init_info[CONF_API_BASE_URL_V2]

# If only one vehicle or if the current vehicle is still available, auto-select
current_vehicle = reconfigure_entry.data.get(CONF_VEHICLE)
if len(vehicles) == 1:
data_updates[CONF_VEHICLE] = vehicles[0]
data_updates[CONF_VEHICLES] = vehicles
elif current_vehicle and current_vehicle in vehicles:
data_updates[CONF_VEHICLE] = current_vehicle
data_updates[CONF_VEHICLES] = vehicles
else:
# Multiple vehicles and current one not found - show vehicle picker
return await self.async_step_reconfigure_vehicle()

return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates=data_updates,
)
Comment on lines +298 to +313

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
set -euo pipefail

# First, verify the file exists and check its basic structure
if [ ! -f "custom_components/smarthashtag/config_flow.py" ]; then
  echo "File not found"
  exit 1
fi

wc -l custom_components/smarthashtag/config_flow.py

Repository: DasBasti/SmartHashtag

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Run the verification script provided in the review comment
python - <<'PY'
from pathlib import Path

path = Path("custom_components/smarthashtag/config_flow.py")
text = path.read_text()

def get_block(func_name: str) -> str:
    start = text.find(f"async def {func_name}")
    if start == -1:
        return f"Function {func_name} not found"
    next_async = text.find("\n    async def ", start + 1)
    next_def = text.find("\n    def ", start + 1)
    # Find the earliest next function
    candidates = [x for x in [next_async, next_def] if x != -1]
    end = min(candidates) if candidates else len(text)
    return text[start:end]

for fn in ("async_step_vehicle", "_finish_reconfigure", "async_step_reconfigure_vehicle"):
    try:
        block = get_block(fn)
        print(f"\n[{fn}]")
        for token in ("async_set_unique_id(", "async_update_entry(", "title=", "unique_id="):
            print(f"  {token} -> {token in block}")
    except Exception as e:
        print(f"[{fn}] Error: {e}")
PY

Repository: DasBasti/SmartHashtag

Length of output: 450


🏁 Script executed:

# Check the imports and constants at the top of the file
head -50 custom_components/smarthashtag/config_flow.py

Repository: DasBasti/SmartHashtag

Length of output: 1302


🏁 Script executed:

# Look for NAME constant definition
rg "^NAME\s*=" custom_components/smarthashtag/config_flow.py -A 1

Repository: DasBasti/SmartHashtag

Length of output: 47


🏁 Script executed:

# Check the lines 298-313 and 322-338 in context
sed -n '295,340p' custom_components/smarthashtag/config_flow.py

Repository: DasBasti/SmartHashtag

Length of output: 2071


🏁 Script executed:

# Look for async_step_vehicle function
rg -A 30 "async def async_step_vehicle" custom_components/smarthashtag/config_flow.py

Repository: DasBasti/SmartHashtag

Length of output: 1267


🏁 Script executed:

# Check _finish_reconfigure implementation
rg -A 25 "async def _finish_reconfigure" custom_components/smarthashtag/config_flow.py

Repository: DasBasti/SmartHashtag

Length of output: 1384


🏁 Script executed:

# Check async_step_reconfigure_vehicle implementation
rg -A 30 "async def async_step_reconfigure_vehicle" custom_components/smarthashtag/config_flow.py

Repository: DasBasti/SmartHashtag

Length of output: 1390


🏁 Script executed:

# Verify that async_update_entry is the correct Home Assistant API method
rg "async_update_entry\|async_update_entry" custom_components/smarthashtag/ --type py

Repository: DasBasti/SmartHashtag

Length of output: 47


🏁 Script executed:

# Check if there are any other places in the codebase where entry title/unique_id are updated
rg "async_update_entry|async_set_unique_id" custom_components/smarthashtag/ --type py -B 2 -A 2

Repository: DasBasti/SmartHashtag

Length of output: 996


🏁 Script executed:

# Verify Home Assistant imports and check what methods are available on config_entries
rg "from homeassistant" custom_components/smarthashtag/config_flow.py

Repository: DasBasti/SmartHashtag

Length of output: 280


🏁 Script executed:

# Check the exact signature and usage pattern of async_update_entry in Home Assistant context
rg -B 5 -A 5 "async_update_entry" custom_components/smarthashtag/select.py

Repository: DasBasti/SmartHashtag

Length of output: 518


🏁 Script executed:

# Look for the hass.config_entries import and verify the method chain
rg "hass\.config_entries" custom_components/smarthashtag/ -B 2 -A 2 --type py

Repository: DasBasti/SmartHashtag

Length of output: 2174


🏁 Script executed:

# Verify if there are any integration tests or manifest that show Home Assistant version
cat custom_components/smarthashtag/manifest.json 2>/dev/null || echo "manifest.json not found"

Repository: DasBasti/SmartHashtag

Length of output: 437


Update config entry identity when vehicle changes during reconfigure.

CONF_VEHICLE can be changed in reconfigure flows (_finish_reconfigure and async_step_reconfigure_vehicle), but entry title and unique_id are never updated. During setup (async_step_vehicle), identity is set to f"{NAME} {vehicle}", but reconfigure paths skip this step. Changing the vehicle without updating the entry identity leaves stale metadata that can create duplicate-prevention issues.

🔧 Suggested fix
@@
-        return self.async_update_reload_and_abort(
+        new_identity = f"{NAME} {data_updates[CONF_VEHICLE]}"
+        if (
+            reconfigure_entry.title != new_identity
+            or reconfigure_entry.unique_id != new_identity
+        ):
+            self.hass.config_entries.async_update_entry(
+                reconfigure_entry,
+                title=new_identity,
+                unique_id=new_identity,
+            )
+
+        return self.async_update_reload_and_abort(
             reconfigure_entry,
             data_updates=data_updates,
         )
@@
-            return self.async_update_reload_and_abort(
+            new_identity = f"{NAME} {user_input[CONF_VEHICLE]}"
+            if (
+                reconfigure_entry.title != new_identity
+                or reconfigure_entry.unique_id != new_identity
+            ):
+                self.hass.config_entries.async_update_entry(
+                    reconfigure_entry,
+                    title=new_identity,
+                    unique_id=new_identity,
+                )
+
+            return self.async_update_reload_and_abort(
                 reconfigure_entry,
                 data_updates=data_updates,
             )

Also applies to: Lines 322-338

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

In `@custom_components/smarthashtag/config_flow.py` around lines 298 - 313, When a
vehicle is changed during reconfigure (in async_step_reconfigure_vehicle and
_finish_reconfigure), update the config entry's title and unique_id to match the
new vehicle so metadata doesn't stay stale; after you determine the selected
vehicle (where you currently set data_updates[CONF_VEHICLE] and
data_updates[CONF_VEHICLES]) call
hass.config_entries.async_update_entry(reconfigure_entry, title=f"{NAME}
{vehicle}", unique_id=...) to set the same identity that async_step_vehicle
establishes (use the same f"{NAME} {vehicle}" pattern for title and derive the
unique_id the same way as in async_step_vehicle) before returning from
async_update_reload_and_abort or finishing the reconfigure flow.


async def async_step_reconfigure_vehicle(
self,
user_input: dict | None = None,
) -> config_entries.FlowResult:
"""Handle vehicle selection during reconfigure."""
reconfigure_entry = self._get_reconfigure_entry()

if user_input is not None:
data_updates = {
CONF_USERNAME: self.init_info[CONF_USERNAME],
CONF_PASSWORD: self.init_info[CONF_PASSWORD],
CONF_REGION: self.init_info.get(CONF_REGION, DEFAULT_REGION),
CONF_VEHICLE: user_input[CONF_VEHICLE],
CONF_VEHICLES: self.init_info[CONF_VEHICLES],
}
if CONF_API_BASE_URL in self.init_info:
data_updates[CONF_API_BASE_URL] = self.init_info[CONF_API_BASE_URL]
if CONF_API_BASE_URL_V2 in self.init_info:
data_updates[CONF_API_BASE_URL_V2] = self.init_info[CONF_API_BASE_URL_V2]

return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates=data_updates,
)

return self.async_show_form(
step_id="reconfigure_vehicle",
data_schema=vol.Schema(
{
vol.Required(CONF_VEHICLE): vol.In(
self.init_info[CONF_VEHICLES]
)
}
),
)

async def _test_credentials(
self,
username: str,
Expand All @@ -212,7 +366,12 @@ async def _test_credentials(
api_base_url=custom_api_base_url or None,
api_base_url_v2=custom_api_base_url_v2 or None,
)
# For EU region (default) or other regions, use default endpoints
elif region and region != REGION_CUSTOM:
# Predefined region
if region == REGION_EU:
endpoint_urls = get_endpoint_urls_for_region(SmartRegion.EU)
elif region == REGION_INTL:
endpoint_urls = get_endpoint_urls_for_region(SmartRegion.INTL)

client = SmartAccount(
username=username,
Expand Down
4 changes: 3 additions & 1 deletion custom_components/smarthashtag/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
NAME = "Smart"
DOMAIN = "smarthashtag"
DOMAIN_DATA = f"{DOMAIN}_data"
VERSION = "0.8.0"
VERSION = "0.8.0-intl"

ATTRIBUTION = "Data provided by http://smart.com/"
ISSUE_URL = "https://github.com/DasBasti/SmartHashtag/issues"
Expand Down Expand Up @@ -51,10 +51,12 @@

# Region options
REGION_EU = "eu"
REGION_INTL = "intl"
REGION_CUSTOM = "custom"

REGIONS = {
REGION_EU: "Europe (Hello Smart EU)",
REGION_INTL: "International (Hello Smart International - Australia, Singapore, etc.)",
REGION_CUSTOM: "Custom Endpoints",
}

Expand Down
2 changes: 1 addition & 1 deletion custom_components/smarthashtag/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/DasBasti/SmartHashtag/issues",
"requirements": ["pysmarthashtag==0.8.1"],
"version": "0.8.0"
"version": "0.8.0-intl"
}
18 changes: 17 additions & 1 deletion custom_components/smarthashtag/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,29 @@
"api_base_url": "API-Basis-URL",
"api_base_url_v2": "API-Basis-URL v2"
}
},
"reconfigure": {
"description": "Aktualisieren Sie Ihre Smart-Kontodaten oder ändern Sie die Region. Die Integration wird nach dem Speichern neu geladen.",
"data": {
"username": "Benutzername",
"password": "Passwort",
"region": "Region / Endpunkt"
}
},
"reconfigure_vehicle": {
"description": "Fahrzeug-VIN für das aktualisierte Konto auswählen",
"data": {
"vehicle": "Vehicle Identification Number"
}
Comment on lines +34 to +38

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

Localize the new vehicle field label in German.

reconfigure_vehicle.data.vehicle is still English ("Vehicle Identification Number"), which creates mixed-language UI in de.

💬 Suggested translation fix
       "reconfigure_vehicle": {
         "description": "Fahrzeug-VIN für das aktualisierte Konto auswählen",
         "data": {
-          "vehicle": "Vehicle Identification Number"
+          "vehicle": "Fahrzeug-Identifikationsnummer (VIN)"
         }
       }
📝 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
"reconfigure_vehicle": {
"description": "Fahrzeug-VIN für das aktualisierte Konto auswählen",
"data": {
"vehicle": "Vehicle Identification Number"
}
"reconfigure_vehicle": {
"description": "Fahrzeug-VIN für das aktualisierte Konto auswählen",
"data": {
"vehicle": "Fahrzeug-Identifikationsnummer (VIN)"
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@custom_components/smarthashtag/translations/de.json` around lines 34 - 38,
The German translation file contains an untranslated label: update
reconfigure_vehicle.data.vehicle from English to German so the UI is consistent;
locate the "reconfigure_vehicle" object and replace the value of "vehicle"
(currently "Vehicle Identification Number") with the appropriate German text
(for example "Fahrzeug-Identifikationsnummer" or another agreed German phrasing)
so the de.json entry is fully localized.

}
},
"error": {
"auth": "Authentifizierung fehlgeschlagen, bitte überprüfen Sie Ihre Anmeldeinformationen und versuchen Sie es erneut.",
"custom_endpoints_required": "Mindestens eine benutzerdefinierte Endpunkt-URL muss angegeben werden"
},
"abort": {}
"abort": {
"reconfigure_successful": "Konto erfolgreich aktualisiert. Die Integration wird neu geladen."
}
},
"options": {
"step": {
Expand Down
18 changes: 17 additions & 1 deletion custom_components/smarthashtag/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,29 @@
"api_base_url": "API Base URL",
"api_base_url_v2": "API Base URL v2"
}
},
"reconfigure": {
"description": "Update your Smart account credentials or change region. The integration will reload after saving.",
"data": {
"username": "Username",
"password": "Password",
"region": "Region / Endpoint"
}
},
"reconfigure_vehicle": {
"description": "Select vehicle VIN for the updated account",
"data": {
"vehicle": "Vehicle identification number"
}
}
},
"error": {
"auth": "Auth failed, please check your username and password",
"custom_endpoints_required": "At least one custom endpoint URL must be provided"
},
"abort": {}
"abort": {
"reconfigure_successful": "Account updated successfully. The integration will reload."
}
},
"options": {
"step": {
Expand Down
Loading