-
-
Notifications
You must be signed in to change notification settings - Fork 8
feat: Add INTL region support and reconfigure flow #397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
| ) | ||
|
|
@@ -40,6 +40,8 @@ | |
| MIN_SCAN_INTERVAL, | ||
| NAME, | ||
| REGION_CUSTOM, | ||
| REGION_EU, | ||
| REGION_INTL, | ||
| REGIONS, | ||
| ) | ||
|
|
||
|
|
@@ -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( | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.pyRepository: 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}")
PYRepository: 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.pyRepository: DasBasti/SmartHashtag Length of output: 1302 🏁 Script executed: # Look for NAME constant definition
rg "^NAME\s*=" custom_components/smarthashtag/config_flow.py -A 1Repository: 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.pyRepository: 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.pyRepository: DasBasti/SmartHashtag Length of output: 1267 🏁 Script executed: # Check _finish_reconfigure implementation
rg -A 25 "async def _finish_reconfigure" custom_components/smarthashtag/config_flow.pyRepository: 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.pyRepository: 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 pyRepository: 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 2Repository: 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.pyRepository: 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.pyRepository: 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 pyRepository: 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.
🔧 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 |
||
|
|
||
| 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, | ||
|
|
@@ -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, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Localize the new vehicle field label in German.
💬 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| "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": { | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normalize region value before endpoint branching.
entry.data.get(CONF_REGION, DEFAULT_REGION)does not replace explicitNone. Usingentry.data.get(CONF_REGION) or DEFAULT_REGIONavoids falling into the unrecognized-region path for null values.🔧 Suggested tweak
📝 Committable suggestion
🤖 Prompt for AI Agents