Skip to content
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7881899
Add gridX integration
unl0ck Apr 1, 2026
ade1a7c
Refactor GridX integration for improved structure and readability
unl0ck Apr 1, 2026
8a29178
Fix GridX typing and lazy imports
unl0ck Apr 1, 2026
6356b0f
Bump gridx-connector to 3.0.1
unl0ck Apr 1, 2026
6d25e9d
Regenerate hassfest files for gridx strict-typing
unl0ck Apr 2, 2026
7735abb
Fix mypy strict-typing errors and manifest formatting
unl0ck Apr 2, 2026
dc3bd4b
Fix remaining unparenthesized except blocks for mypy compatibility
unl0ck Apr 2, 2026
cc873af
Fix mypy unreachable and ruff format issues
unl0ck Apr 2, 2026
bdd72e9
Bump gridx-connector to 3.0.2
unl0ck Apr 3, 2026
00ec599
Address Copilot review comments
unl0ck Apr 6, 2026
00656f5
Fix missing trailing newline in strings.json
unl0ck Apr 6, 2026
b640fc0
Move load_oem_config to client.py; add comprehensive test coverage
unl0ck Apr 7, 2026
26024ce
Fix quality_scale.yaml: use shorthand notation for async-dependency a…
unl0ck Apr 7, 2026
693831f
Address PR review comments
unl0ck Apr 7, 2026
3bbcd39
Add no_systems error for empty accounts; fix last_reset test assertions
unl0ck Apr 7, 2026
1d2ed13
Remove unused ignore_missing_translations for config_entry_reauth
unl0ck Apr 8, 2026
0ab8fc3
Merge branch 'dev' into add-gridx-integration
unl0ck Apr 8, 2026
a0281e3
Remove round() from TOTAL_INCREASING sensors; aggregate multi-system …
unl0ck Apr 8, 2026
bb4061a
Add test coverage for historical multi-system aggregation
unl0ck Apr 8, 2026
0fa8084
Merge branch 'dev' into add-gridx-integration
unl0ck Apr 8, 2026
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
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ homeassistant.components.google_weather.*
homeassistant.components.govee_ble.*
homeassistant.components.gpsd.*
homeassistant.components.greeneye_monitor.*
homeassistant.components.gridx.*
homeassistant.components.group.*
homeassistant.components.guardian.*
homeassistant.components.habitica.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

96 changes: 96 additions & 0 deletions homeassistant/components/gridx/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""The GridX integration."""

from __future__ import annotations

import httpx

from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.httpx_client import create_async_httpx_client

from .client import async_create_connector, load_oem_config
from .const import CONF_OEM, DOMAIN, LOGGER
from .coordinator import GridxHistoricalCoordinator, GridxLiveCoordinator
from .types import GridxConfigEntry, GridxData

PLATFORMS = [Platform.SENSOR]
API_BASE_URL = "https://api.gridx.de"


async def async_setup_entry(hass: HomeAssistant, entry: GridxConfigEntry) -> bool:
"""Set up GridX from a config entry."""
username: str = entry.data[CONF_USERNAME]
password: str = entry.data[CONF_PASSWORD]
oem: str = entry.data[CONF_OEM]

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)
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.
except PermissionError as err:
await httpx_client.aclose()
LOGGER.error("GridX authentication failed: %s", err)
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
except httpx.HTTPStatusError as err:
Comment on lines +36 to +43
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 +36 to +43
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.
await httpx_client.aclose()
status = err.response.status_code if err.response else None
LOGGER.error("Error connecting to GridX: %s", err)
if status in (401, 403):
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
Comment on lines +43 to +55
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.
except httpx.HTTPError as err:
await httpx_client.aclose()
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:
await httpx_client.aclose()
LOGGER.error("Error connecting to GridX: %s", err)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err

live_coordinator = GridxLiveCoordinator(hass, entry, connector)
hist_coordinator = GridxHistoricalCoordinator(hass, entry, connector)

try:
await live_coordinator.async_config_entry_first_refresh()
await hist_coordinator.async_config_entry_first_refresh()
except Exception:
await connector.close()
raise

Comment on lines +71 to +80
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.
entry.runtime_data = GridxData(
connector=connector,
live_coordinator=live_coordinator,
hist_coordinator=hist_coordinator,
)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: GridxConfigEntry) -> bool:
"""Unload a GridX config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
await entry.runtime_data.connector.close()
return unload_ok
54 changes: 54 additions & 0 deletions homeassistant/components/gridx/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Client helpers for the GridX integration."""

from __future__ import annotations

from importlib import import_module
from importlib.resources import files
import json
from typing import Any, Protocol

import httpx


class GridxConnector(Protocol):
"""Protocol for the GridX connector used by the integration."""

async def retrieve_live_data(self) -> list[dict[str, Any]]:
"""Retrieve live data for all systems."""

async def retrieve_historical_data(
self,
*,
start: str,
end: str,
resolution: str,
) -> list[dict[str, Any]]:
"""Retrieve historical data for all systems."""

async def close(self) -> None:
"""Close the connector and any owned clients."""


def load_oem_config(oem: str, username: str, password: str) -> dict[str, Any]:
"""Load OEM connector config 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


async def async_create_connector(
config: dict[str, Any],
httpx_client: httpx.AsyncClient,
) -> GridxConnector:
"""Create a GridX connector without importing the dependency at module import time."""
connector_module = import_module("gridx_connector.async_connector")
async_gridbox_connector = connector_module.AsyncGridboxConnector

connector: GridxConnector = await async_gridbox_connector.create(
config,
httpx_client=httpx_client,
owns_httpx_client=True,
)
return connector
Loading
Loading