Skip to content

Add gridX integration#167112

Draft
unl0ck wants to merge 20 commits intohome-assistant:devfrom
unl0ck:add-gridx-integration
Draft

Add gridX integration#167112
unl0ck wants to merge 20 commits intohome-assistant:devfrom
unl0ck:add-gridx-integration

Conversation

@unl0ck
Copy link
Copy Markdown

@unl0ck unl0ck commented Apr 1, 2026

Breaking change

Proposed change

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

Copilot AI review requested due to automatic review settings April 1, 2026 20:45
Copy link
Copy Markdown

@home-assistant home-assistant bot left a comment

Choose a reason for hiding this comment

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

Hi @unl0ck

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant
Copy link
Copy Markdown

home-assistant bot commented Apr 1, 2026

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Home Assistant integration for the gridX (GridBox) cloud platform, including config flow, coordinators, sensors, diagnostics, and a dedicated test suite.

Changes:

  • Introduces the gridx integration with config entry setup/unload, config flow (incl. reauth/reconfigure), and data update coordinators.
  • Adds a comprehensive sensor platform covering live and historical (daily total) metrics with translations/icons.
  • Adds diagnostics support and a full set of component tests (config flow, coordinators, sensors, diagnostics).

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
homeassistant/components/gridx/__init__.py Sets up/unloads the integration, creates the connector + coordinators, forwards platform setups.
homeassistant/components/gridx/config_flow.py Implements user setup, reauth, and reconfigure flows plus credential validation.
homeassistant/components/gridx/const.py Defines domain constants, OEM list, and polling intervals.
homeassistant/components/gridx/coordinator.py Adds live + historical DataUpdateCoordinators and fetch helpers with error handling.
homeassistant/components/gridx/sensor.py Defines all sensor entity descriptions and the coordinator-backed sensor entity implementation.
homeassistant/components/gridx/diagnostics.py Adds config entry diagnostics with redaction.
homeassistant/components/gridx/types.py Adds typed config entry + runtime data container.
homeassistant/components/gridx/manifest.json Declares the integration metadata and PyPI requirement (gridx-connector==3.0.0).
homeassistant/components/gridx/strings.json Adds config flow, entity, and exception translation strings.
homeassistant/components/gridx/icons.json Adds icon translations for entities.
homeassistant/components/gridx/quality_scale.yaml Declares integration quality scale status (Bronze→Platinum rules).
tests/components/gridx/conftest.py Adds shared fixtures and mocks for integration tests.
tests/components/gridx/test_config_flow.py Tests config flow user setup, duplicate abort, reauth, and reconfigure.
tests/components/gridx/test_coordinator.py Tests coordinator success paths and error handling.
tests/components/gridx/test_sensor.py Tests entity unique IDs, live/historical values, and missing-battery behavior.
tests/components/gridx/test_diagnostics.py Tests diagnostics payload and redaction behavior.
tests/components/gridx/__init__.py Marks the test package.

Comment on lines +38 to +44
except httpx.HTTPStatusError as err:
status = err.response.status_code if err.response else None
key = "invalid_auth" if status in (401, 403) else "cannot_connect"
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key=key,
) from err
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Raise UpdateFailed (not ConfigEntryAuthFailed) for non-auth HTTP status codes so transient server/network errors don’t incorrectly trigger a reauth flow.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +78
except httpx.HTTPStatusError as err:
status = err.response.status_code if err.response else None
key = "invalid_auth" if status in (401, 403) else "cannot_connect"
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key=key,
) from err
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Raise UpdateFailed (not ConfigEntryAuthFailed) for non-auth HTTP status codes so transient server/network errors don’t incorrectly trigger a reauth flow.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +60
async def _fetch_historical(connector: AsyncGridboxConnector) -> GridxHistoricalData:
"""Fetch today's historical totals."""
now = datetime.now().astimezone()
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
tomorrow = midnight + timedelta(days=1)

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Use Home Assistant’s dt_util helpers (e.g., dt_util.start_of_local_day()/dt_util.now()) to compute local midnight to avoid DST/timezone edge cases from manual datetime.now().astimezone().replace().

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +60
except PermissionError as err:
LOGGER.error("GridX authentication failed: %s", err)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
except httpx.HTTPStatusError as err:
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Raise ConfigEntryAuthFailed (not ConfigEntryNotReady) on authentication errors during setup so Home Assistant can start the reauth flow instead of repeatedly retrying the entry.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +28
def _load_oem_config(oem: str, username: str, password: str) -> dict[str, Any]:
"""Load the OEM config file bundled with gridx-connector and inject credentials."""
config_path = files("gridx_connector").joinpath("config", f"{oem}.config.json")
config: dict[str, Any] = json.loads(config_path.read_text())
config["login"]["username"] = username
config["login"]["password"] = password
return config
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Avoid duplicating the OEM config-loading logic by moving _load_oem_config to a shared module (or importing it from the integration package) so future changes don’t need to be made in two places.

Copilot uses AI. Check for mistakes.


@pytest.fixture
async def setup_integration(hass: HomeAssistant, mock_gridx_connector: MagicMock):
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Add an explicit return type to this fixture (it returns a MockConfigEntry) to align with the repository’s typed-test conventions.

Suggested change
async def setup_integration(hass: HomeAssistant, mock_gridx_connector: MagicMock):
async def setup_integration(
hass: HomeAssistant, mock_gridx_connector: MagicMock
) -> MockConfigEntry:

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +21
@pytest.fixture
def config_entry(hass):
"""Return a mock GridX config entry."""
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Add type annotations for test parameters (e.g., hass: HomeAssistant, config_entry: MockConfigEntry) so the new tests follow the repository’s typed-test guideline.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +35
async def test_live_coordinator_success(hass, config_entry) -> None:
"""Test that live coordinator returns processed data."""
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Add type annotations for the hass/config_entry parameters in these async tests to keep new tests consistent with typed-test expectations.

Copilot uses AI. Check for mistakes.


@pytest.fixture
def mock_gridx_connector() -> Generator[MagicMock]:
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Specify the full Generator type parameters (Generator[MagicMock, None, None]) for this fixture’s return type so type checkers correctly understand the yielded value.

Suggested change
def mock_gridx_connector() -> Generator[MagicMock]:
def mock_gridx_connector() -> Generator[MagicMock, None, None]:

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +8
appropriate-polling: done
brands: done
common-modules: done
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Mark the brands quality scale rule as not-yet-done (or exempt) until the brands-repo PR is merged, since the PR description indicates the brand assets are still pending.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 7 comments.

Comment on lines +49 to +55
except PermissionError as err:
LOGGER.error("GridX authentication failed: %s", err)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
except httpx.HTTPStatusError as err:
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Raise ConfigEntryAuthFailed for authentication errors (not ConfigEntryNotReady) so Home Assistant can initiate the reauth flow instead of endlessly retrying setup.

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +81
live_coordinator = GridxLiveCoordinator(hass, entry, connector)
hist_coordinator = GridxHistoricalCoordinator(hass, entry, connector)

await live_coordinator.async_config_entry_first_refresh()
await hist_coordinator.async_config_entry_first_refresh()

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Ensure the httpx client/connector is closed if setup fails after creation (e.g., during the first coordinator refresh) to avoid leaking network resources on retries.

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +55
connector = await async_create_connector(config, httpx_client)
try:
data = await connector.retrieve_live_data()
finally:
await connector.close()
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Close the created httpx client if connector creation fails (since owns_httpx_client/connector.close won’t run) to prevent leaking an AsyncClient during config flow validation.

Suggested change
connector = await async_create_connector(config, httpx_client)
try:
data = await connector.retrieve_live_data()
finally:
await connector.close()
try:
connector = await async_create_connector(config, httpx_client)
try:
data = await connector.retrieve_live_data()
finally:
await connector.close()
finally:
await httpx_client.aclose()

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +170
schema = vol.Schema(
{
vol.Required(
CONF_USERNAME, default=entry.data.get(CONF_USERNAME, "")
): str,
vol.Required(CONF_PASSWORD): str,
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Avoid allowing username changes during reauth (or update the config entry unique_id accordingly), since the entry’s unique_id is based on the original username and changing it here can lead to confusing identity/duplicate-account handling.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +20
class GridxData:
"""Runtime data stored on the config entry."""

connector: Any
live_coordinator: GridxLiveCoordinator
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Use a typed connector protocol for runtime_data (e.g., GridxConnector) instead of Any to satisfy strict typing and improve type safety across the integration.

Copilot uses AI. Check for mistakes.
Comment on lines +590 to +594
self.entity_description: GridxSensorEntityDescription = description
self._attr_unique_id = f"{entry.entry_id}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
name="GridX GridBox",
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Base entity unique_id/device identifiers on a stable identifier (e.g., entry.unique_id) instead of entry.entry_id so entity/device registry entries survive remove/re-add and keep user customizations.

Copilot uses AI. Check for mistakes.
Comment on lines +623 to +627
self.entity_description: GridxSensorEntityDescription = description
self._attr_unique_id = f"{entry.entry_id}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
name="GridX GridBox",
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Base entity unique_id/device identifiers on a stable identifier (e.g., entry.unique_id) instead of entry.entry_id so entity/device registry entries survive remove/re-add and keep user customizations.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 2, 2026 03:31
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 25 changed files in this pull request and generated 8 comments.

Comment on lines +73 to +77
key = "invalid_auth" if status in (401, 403) else "cannot_connect"
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key=key,
) from err
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Apply the same HTTPStatusError handling for historical fetches so non-auth HTTP failures don’t raise ConfigEntryAuthFailed.

Suggested change
key = "invalid_auth" if status in (401, 403) else "cannot_connect"
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key=key,
) from err
if status in (401, 403):
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
raise UpdateFailed(f"Error fetching GridX historical data: {err}") from err

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +46
config = _load_oem_config(oem, username, password)
httpx_client = create_async_httpx_client(
hass,
auto_cleanup=False,
base_url=API_BASE_URL,
)

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Ensure the created httpx client is closed when connector creation/setup fails; currently httpx_client is leaked on exceptions before a connector exists to close it.

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +162
if user_input is not None:
try:
await _validate_credentials(
self.hass,
entry.data[CONF_OEM],
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
except PermissionError:
errors["base"] = "invalid_auth"
except httpx.HTTPStatusError as err:
status = err.response.status_code if err.response else None
errors["base"] = (
"invalid_auth" if status in (401, 403) else "cannot_connect"
)
except httpx.HTTPError:
errors["base"] = "cannot_connect"
except ConnectionError, TimeoutError, OSError:
errors["base"] = "cannot_connect"
except UNEXPECTED_AUTH_ERRORS:
LOGGER.exception("Unexpected error during GridX re-authentication")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
entry,
data_updates={
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Don’t allow username changes in the reauth step (or update the config entry unique_id and re-check duplicates) because the unique_id is derived from username but is never updated here.

Copilot uses AI. Check for mistakes.
Comment on lines +185 to +215
if user_input is not None:
try:
await _validate_credentials(
self.hass,
user_input[CONF_OEM],
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
except PermissionError:
errors["base"] = "invalid_auth"
except httpx.HTTPStatusError as err:
status = err.response.status_code if err.response else None
errors["base"] = (
"invalid_auth" if status in (401, 403) else "cannot_connect"
)
except httpx.HTTPError:
errors["base"] = "cannot_connect"
except ConnectionError, TimeoutError, OSError:
errors["base"] = "cannot_connect"
except UNEXPECTED_AUTH_ERRORS:
LOGGER.exception("Unexpected error during GridX reconfiguration")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
entry,
data_updates={
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_OEM: user_input[CONF_OEM],
},
)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Reconfigure currently accepts a new username without validating/updating the entry unique_id, which can desync duplicate detection from the configured account; either keep username fixed or update unique_id and abort on duplicates/mismatch.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +21
@dataclass
class GridxData:
"""Runtime data stored on the config entry."""

connector: Any
live_coordinator: GridxLiveCoordinator
hist_coordinator: GridxHistoricalCoordinator
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Type the stored connector as GridxConnector instead of Any to keep strict typing meaningful for runtime_data.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +62
except httpx.HTTPStatusError as err:
status = err.response.status_code if err.response else None
key = "invalid_auth" if status in (401, 403) else "cannot_connect"
LOGGER.error("Error connecting to GridX: %s", err)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key=key,
) from err
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Raise ConfigEntryAuthFailed for 401/403 HTTPStatusError cases so authentication errors trigger reauth instead of setup retries (use ConfigEntryNotReady only for transient connectivity issues).

Copilot uses AI. Check for mistakes.
Comment on lines +145 to +149
key="directConsumptionRate",
translation_key="direct_consumption_rate",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Remove SensorDeviceClass.POWER_FACTOR for these percentage rate sensors (keep unit as %), since POWER_FACTOR is intended for unitless -1..1 power factor values and can misclassify the entity.

Copilot uses AI. Check for mistakes.
Comment on lines +508 to +512
key="hist_selfConsumptionRate",
translation_key="hist_self_consumption_rate",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Remove SensorDeviceClass.POWER_FACTOR for historical percentage rate sensors (keep unit as %), since POWER_FACTOR is intended for unitless -1..1 power factor values.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 2, 2026 04:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 25 changed files in this pull request and generated 2 comments.

Comment on lines +40 to +74
config = _load_oem_config(oem, username, password)
httpx_client = create_async_httpx_client(
hass,
auto_cleanup=False,
base_url=API_BASE_URL,
)

try:
connector = await async_create_connector(config, httpx_client)
except PermissionError as err:
LOGGER.error("GridX authentication failed: %s", err)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
except httpx.HTTPStatusError as err:
status = err.response.status_code if err.response else None
key = "invalid_auth" if status in (401, 403) else "cannot_connect"
LOGGER.error("Error connecting to GridX: %s", err)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key=key,
) from err
except httpx.HTTPError as err:
LOGGER.error("Error connecting to GridX: %s", err)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
except (RuntimeError, TypeError, ValueError) as err:
LOGGER.error("Error connecting to GridX: %s", err)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Ensure the httpx client is closed if async_create_connector fails, since auto_cleanup=False means the client will otherwise leak on setup errors.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +21
class GridxData:
"""Runtime data stored on the config entry."""

connector: Any
live_coordinator: GridxLiveCoordinator
hist_coordinator: GridxHistoricalCoordinator
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Replace the connector field type from Any to the integration’s GridxConnector Protocol (or a more specific type) to keep strict typing meaningful.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 3, 2026 10:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 25 changed files in this pull request and generated 5 comments.

Comment on lines +37 to +41
except httpx.HTTPStatusError as err:
status = err.response.status_code if err.response else None
key = "invalid_auth" if status in (401, 403) else "cannot_connect"
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Raise UpdateFailed (not ConfigEntryAuthFailed) for non-401/403 HTTPStatusError responses so temporary connectivity/server errors don’t trigger a reauth flow.

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +75
except httpx.HTTPStatusError as err:
status = err.response.status_code if err.response else None
key = "invalid_auth" if status in (401, 403) else "cannot_connect"
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Raise UpdateFailed (not ConfigEntryAuthFailed) for non-401/403 HTTPStatusError responses so historical polling failures don’t incorrectly force reauthentication.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +58
now = datetime.now().astimezone()
midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
tomorrow = midnight + timedelta(days=1)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Compute the local midnight using Home Assistant’s dt_util helpers (respecting the configured time zone) instead of datetime.now().astimezone().

Copilot uses AI. Check for mistakes.
)

try:
connector = await async_create_connector(config, httpx_client)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Ensure the created httpx client is closed if async_create_connector fails (e.g., wrap connector creation in a try/except that calls await httpx_client.aclose() before re-raising) to avoid leaking sessions when setup errors occur.

Suggested change
connector = await async_create_connector(config, httpx_client)
try:
connector = await async_create_connector(config, httpx_client)
except BaseException:
await httpx_client.aclose()
raise

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +11
"loggers": ["gridx_connector"],
"quality_scale": "gold",
"requirements": ["gridx-connector==3.0.2"]
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

Align the PR description with the pinned dependency version (code pins gridx-connector==3.0.2 while the description mentions 3.0.0).

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 6, 2026 12:47
unl0ck added 16 commits April 8, 2026 05:55
Adds support for the gridX energy management platform. gridX provides
a cloud-based platform for monitoring and managing energy systems
including solar, batteries, EV chargers, heat pumps, and smart heaters.

- Config flow with OEM + username/password authentication
- Live data coordinator (solar, consumption, grid, battery, EV, heatpump, heater)
- Historical data coordinator (self-sufficiency, autarchy)
- 40+ sensors across all categories
- Diagnostics support
- Async implementation with injectable httpx session (Platinum quality scale)
- Full test coverage (20 tests)

Depends on gridx-connector==3.0.0 (PyPI)
- Updated manifest.json for better formatting and clarity.
- Refactored sensor.py to enhance type hints and improve value extraction logic.
- Cleaned up string.json for consistency and added error handling messages.
- Adjusted types.py to import AsyncGridboxConnector from the correct module.
- Updated requirements files to specify gridx-connector version 3.0.0.
- Enhanced test files for better readability and consistency in type hints.
- Improved diagnostics and sensor tests for better coverage and clarity.
Version 3.0.2 fixes the missing gridx_connector_api module in the
PyPI package (the 3.0.1 wheel omitted it due to a CI build issue).
- coordinator: use dt_util.start_of_local_day() instead of manual datetime;
  raise UpdateFailed for non-auth HTTP errors, ConfigEntryAuthFailed for 401/403
- __init__: raise ConfigEntryAuthFailed for auth errors; close httpx_client
  on all error paths during connector creation
- config_flow: close httpx_client on connector creation failure; reauth only
  asks for password (username locked to existing entry); reconfigure updates
  unique_id when username changes
- types: type connector as GridxConnector instead of Any
- sensor: remove SensorDeviceClass.POWER_FACTOR from percentage rate sensors;
  use entry.unique_id for stable entity/device identifiers
- tests: add unique_id to MockConfigEntry fixtures; update entity lookups to
  use entry.unique_id; fix reauth tests to only submit password
- client: add load_oem_config() as shared public helper for OEM config injection
- __init__: use load_oem_config from client, remove duplicated private function
- config_flow: use load_oem_config from client, remove duplicated private function
- test_init: add setup error path tests (PermissionError, HTTPStatus 401/500,
  HTTPError, RuntimeError) with proper ignore_missing_translations markers
- test_coordinator: add HTTPStatusError/HTTPError/RuntimeError coverage for
  both live and historical coordinators (8 new tests)
- test_config_flow: add Generator return type; add httpx error / unexpected
  error / reauth http error tests (5 new tests)
- test_sensor: fix battery docstring; add value_fn TypeError/ValueError and
  last_reset edge case tests (4 new tests)
…nd inject-websession

Upstream hassfest now requires a comment when using dict notation (status: done).
Switch to shorthand notation to avoid the validation error.
- __init__: close connector if coordinator first_refresh fails to avoid
  leaking the httpx client when setup errors occur after connector creation
- config_flow: update unique_id and title in async_update_reload_and_abort
  during reconfigure so username changes are fully persisted on the entry
- conftest: fix Generator return type (UP043: remove redundant None defaults)
- test_config_flow: fix Generator return type; add test verifying that
  reconfigure with username change updates unique_id and title
- config_flow: introduce _NoSystemsFoundError and a no_systems error key
  so users with valid credentials but no assigned GridBox get a clear
  message instead of the misleading 'cannot_connect' error; handled in
  all three flow steps (user, reauth, reconfigure)
- strings.json: add no_systems error string
- test_sensor: assert last_reset attribute is None in both last_reset
  edge-case tests to verify the actual property behaviour
The config_entry_reauth translations now exist in upstream, so the
ignore_missing_translations fixtures in test_setup_permission_error and
test_setup_http_status_401 are no longer needed and were causing test
teardown errors.
Copilot AI review requested due to automatic review settings April 8, 2026 03:56
@unl0ck unl0ck force-pushed the add-gridx-integration branch from dc26e5c to 1d2ed13 Compare April 8, 2026 03:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 26 changed files in this pull request and generated 4 comments.

Comment on lines +51 to +53
if not results:
raise UpdateFailed("GridX returned no live data")
return results[0]
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Handle multiple returned systems instead of always selecting results[0], e.g., by creating one device/coordinator per system (with a stable system identifier) or explicitly validating that only one system is present and raising an error otherwise.

Copilot uses AI. Check for mistakes.
if not results:
raise UpdateFailed("GridX returned no historical data")

total = results[0].get("total", {})
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Handle multiple returned systems instead of always selecting results[0], otherwise accounts with more than one system will only ever expose the first system’s historical totals.

Suggested change
total = results[0].get("total", {})
total: dict[str, Any] = {}
for result in results:
result_total = result.get("total", {})
if not isinstance(result_total, dict):
continue
for key, value in result_total.items():
current_value = total.get(key)
if isinstance(value, int | float) and isinstance(current_value, int | float):
total[key] = current_value + value
elif isinstance(value, int | float):
total[key] = value
elif key not in total:
total[key] = value

Copilot uses AI. Check for mistakes.
Comment on lines +181 to +188
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda d: (
round(d["gridMeterReadingPositive"] / 3600, 2)
if d.get("gridMeterReadingPositive") is not None
else None
),
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Avoid rounding cumulative energy readings before storing state, since rounding can introduce apparent non-monotonic values and reduce accuracy for TOTAL_INCREASING sensors; prefer returning the full-precision conversion and control display via suggested precision instead.

Copilot uses AI. Check for mistakes.
Comment on lines +193 to +200
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda d: (
round(d["gridMeterReadingNegative"] / 3600, 2)
if d.get("gridMeterReadingNegative") is not None
else None
),
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Avoid rounding cumulative energy readings before storing state, since rounding can introduce apparent non-monotonic values and reduce accuracy for TOTAL_INCREASING sensors; prefer returning the full-precision conversion and control display via suggested precision instead.

Copilot uses AI. Check for mistakes.
unl0ck added 2 commits April 8, 2026 07:06
…historical totals

- sensor.py: remove round() from gridMeterReadingPositive/Negative value_fn
  and add suggested_display_precision=2 to avoid non-monotonic TOTAL_INCREASING
  state values
- coordinator.py: aggregate _fetch_historical totals across all returned systems
  instead of always using results[0]
Copilot AI review requested due to automatic review settings April 8, 2026 05:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 26 changed files in this pull request and generated 3 comments.

Comment on lines +120 to +125
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication."""
return await self.async_step_reauth_confirm()

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Rename the unused entry_data parameter (e.g., to _ or _entry_data) to avoid Ruff's unused-argument violation.

Copilot uses AI. Check for mistakes.
Comment on lines +588 to +592
self._attr_unique_id = f"{entry.unique_id}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(entry.unique_id))},
name="GridX GridBox",
manufacturer="gridX / Viessmann",
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Base entity unique_ids/device identifiers on a stable value (e.g., entry.entry_id or an immutable system/device id) so a username change during reconfigure doesn’t create a whole new set of entities/devices in the registry.

Copilot uses AI. Check for mistakes.
Comment on lines +621 to +625
self._attr_unique_id = f"{entry.unique_id}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(entry.unique_id))},
name="GridX GridBox",
manufacturer="gridX / Viessmann",
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Apply the same stable-id approach for historical sensors as well; otherwise reconfiguring the username will orphan the existing historical entities and create new ones.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 8, 2026 11:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 26 changed files in this pull request and generated 4 comments.

async def _fetch_historical(connector: GridxConnector) -> GridxHistoricalData:
"""Fetch today's historical totals."""
midnight = dt_util.start_of_local_day()
tomorrow = midnight + timedelta(days=1)
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Compute the end timestamp for the historical query using a calendar-day boundary (e.g., next local midnight via start_of_local_day) instead of midnight + timedelta(days=1) to avoid DST off-by-one-hour ranges.

Suggested change
tomorrow = midnight + timedelta(days=1)
tomorrow = dt_util.start_of_local_day(dt_util.now() + timedelta(days=1))

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +13
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import HomeAssistant

from .types import GridxConfigEntry

TO_REDACT: set[str] = {CONF_PASSWORD}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Redact the configured username (likely an email address) in diagnostics output in addition to the password to avoid leaking PII in support bundles.

Suggested change
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import HomeAssistant
from .types import GridxConfigEntry
TO_REDACT: set[str] = {CONF_PASSWORD}
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .types import GridxConfigEntry
TO_REDACT: set[str] = {CONF_PASSWORD, CONF_USERNAME}

Copilot uses AI. Check for mistakes.
Comment on lines +207 to +213
new_unique_id = new_username.lower()
if new_unique_id != entry.unique_id:
await self.async_set_unique_id(new_unique_id)
self._abort_if_unique_id_configured()
return self.async_update_reload_and_abort(
entry,
unique_id=new_unique_id,
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Avoid changing the config entry unique_id on reconfigure (or migrate entity unique_ids accordingly), since entity unique_ids are derived from entry.unique_id and a change will create new entities and orphan existing ones.

Suggested change
new_unique_id = new_username.lower()
if new_unique_id != entry.unique_id:
await self.async_set_unique_id(new_unique_id)
self._abort_if_unique_id_configured()
return self.async_update_reload_and_abort(
entry,
unique_id=new_unique_id,
return self.async_update_reload_and_abort(
entry,

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +96
try:
await _validate_credentials(self.hass, oem, username, password)
except PermissionError:
errors["base"] = "invalid_auth"
except httpx.HTTPStatusError as err:
status = err.response.status_code if err.response else None
errors["base"] = (
"invalid_auth" if status in (401, 403) else "cannot_connect"
)
except httpx.HTTPError:
errors["base"] = "cannot_connect"
except ConnectionError, TimeoutError, OSError:
errors["base"] = "cannot_connect"
except _NoSystemsFoundError:
errors["base"] = "no_systems"
except UNEXPECTED_AUTH_ERRORS:
LOGGER.exception("Unexpected error during GridX credential validation")
errors["base"] = "unknown"
else:
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Factor the repeated credential-validation exception-to-error mapping into a shared helper to prevent the three steps from drifting and to simplify future changes.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants