Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
582f464
Add initial eGauge integration structure
neggert Oct 21, 2025
f2350df
Add eGauge config flow implementation
neggert Oct 21, 2025
5a558b8
Add eGauge data models
neggert Oct 21, 2025
20e58d4
Add eGauge data update coordinator
neggert Oct 21, 2025
44d3b0f
Add eGauge base entity class
neggert Oct 21, 2025
cae073a
Add eGauge power and energy sensors
neggert Oct 21, 2025
f0067c3
Implement eGauge integration setup
neggert Oct 21, 2025
53bc842
Add eGauge quality scale configuration
neggert Oct 21, 2025
9b7ec58
Add eGauge test infrastructure
neggert Oct 21, 2025
8ad2c1a
Add eGauge config flow tests
neggert Oct 21, 2025
43121d4
Add eGauge sensor tests
neggert Oct 21, 2025
0240341
Add eGauge integration tests
neggert Oct 21, 2025
7df9674
Fix eGauge library imports
neggert Oct 21, 2025
4e359be
Move EgaugeConfigEntry to avoid circular import
neggert Oct 21, 2025
4ec6c04
Add missing data_description strings
neggert Oct 21, 2025
d6be33c
Fix broken tests
neggert Oct 21, 2025
86f28b8
Add snapshots
neggert Oct 21, 2025
69596c5
Use httpx client
neggert Oct 21, 2025
7b1098f
Run hassfest
neggert Oct 21, 2025
01e3dec
update test requirements
neggert Oct 21, 2025
d531cdc
Update quality scale.
neggert Oct 21, 2025
a29ffa6
Accept hostname/IP and SSL options instead of base URL
neggert Oct 23, 2025
c8b9b6e
Add functional tests for eGauge sensors and coordinator
neggert Oct 23, 2025
182e070
Update quality scale
neggert Oct 27, 2025
413e853
Move _build_client_url to util.py
neggert Oct 28, 2025
26f9580
Move client construction inside coordinator
neggert Oct 28, 2025
cb6853f
Declare serial_number and hostname as class variables
neggert Oct 28, 2025
e8d4350
Initialize _register_info as empty dict
neggert Oct 28, 2025
3408752
Initialize _register_info to empty dict instead of None
neggert Oct 28, 2025
1b71e53
Move populating device info into _async_setup
neggert Oct 28, 2025
2405fcb
Remove reauth flow for now
neggert Oct 28, 2025
5185e87
Rename LOGGER to _LOGGER
neggert Oct 28, 2025
6412bd7
Minor fixes to config flow
neggert Oct 28, 2025
4731c96
Use hostname as configentry title
neggert Oct 28, 2025
0c954b2
Remove redundant last_reset property
neggert Oct 28, 2025
838209d
Use SensorEntityDescription to simplify sensor classes
neggert Oct 28, 2025
5003b0d
Remove explicit coordinator tests.
neggert Oct 28, 2025
bf3058b
Update comment
neggert Oct 28, 2025
c34d99e
Raise better exceptions for initial setup vs post-setup
neggert Oct 28, 2025
74b311b
Add all quality scale rules
neggert Oct 28, 2025
f81e872
Merge branch 'dev' into egauge-integration
neggert Oct 28, 2025
ca76c68
Change ConfigEntryAuthError to ConfigEntryError
neggert Oct 30, 2025
a564836
Update quality scale
neggert Oct 30, 2025
a9bdb56
Move async_setup_entry before EgaugeSensor class definition
neggert Oct 30, 2025
62e842f
Move register information out of EgaugeEntity and into EgaugeSensor
neggert Oct 30, 2025
cd7d2af
Remove unused test fixture
neggert Oct 30, 2025
6e8ac32
Autouse mock_egauge_client test fixture
neggert Oct 30, 2025
46bad61
Use pytestmark to always use init_integration fixture
neggert Oct 30, 2025
a663b29
Remove redundant assertions
neggert Oct 30, 2025
e46a462
Use snapshot to verify device attributes
neggert Oct 30, 2025
856421f
Remove redundant tests.
neggert Oct 30, 2025
2261a27
Test that sensors become available once errors resolve
neggert Oct 30, 2025
de953a0
Update egauge-async dependency to v0.4.0
neggert Oct 31, 2025
90d9a67
Update EgaugeJsonClient to match new signature in egauge-async 0.4.0
neggert Oct 31, 2025
410160b
Removed unused helper function
neggert Oct 31, 2025
986e320
Improve exception handling.
neggert Oct 31, 2025
0f71bdd
Use translation keys in entity names.
neggert Oct 31, 2025
af50a39
Merge branch 'dev' into egauge-integration
neggert Oct 31, 2025
76dc571
Merge branch 'dev' into egauge-integration
neggert Nov 8, 2025
529b282
Merge branch 'dev' into egauge-integration
neggert Nov 15, 2025
bac94d3
Create a sub-device for each register
neggert Nov 29, 2025
ebf6072
Use static config flow schema
neggert Nov 29, 2025
7ae7784
Use const for coordinator update interval
neggert Nov 29, 2025
28bb23e
Improve coordinator error handling
neggert Nov 29, 2025
00fb680
Move EgaugeData into coordinator file
neggert Nov 29, 2025
1356d39
entity-translations: done
neggert Nov 29, 2025
7159f03
Use native units
neggert Nov 29, 2025
c8bbff6
Formatting
neggert Nov 29, 2025
8f72896
Remove unused reauth strings
neggert Nov 29, 2025
4dc40ef
Merge branch 'dev' into egauge-integration
neggert Nov 29, 2025
2735741
Correctly handle missing register
neggert Nov 29, 2025
a76e514
Leave user_input None instead of empty dict
neggert Dec 2, 2025
9706ee2
native_value always returns float
neggert Dec 2, 2025
83072a0
Remove unused translation_keys
neggert Dec 2, 2025
261cf03
Merge branch 'dev' into egauge-integration
neggert Dec 2, 2025
501e1b5
Update snapshot
neggert Dec 2, 2025
f28eae1
_LOGGER -> LOGGER
neggert Dec 2, 2025
781a3a0
Simplify register to sensor mapping
neggert Dec 3, 2025
8fe3371
Simplify sensors
neggert Dec 6, 2025
c26eed6
Merge branch 'dev' into egauge-integration
neggert Dec 6, 2025
2470e55
Remove redundant call to async_add_entities
neggert Dec 7, 2025
00e2056
Improve robustness of config flow tests
neggert Dec 7, 2025
0ec2905
Merge branch 'dev' into egauge-integration
neggert Dec 7, 2025
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
2 changes: 2 additions & 0 deletions CODEOWNERS

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

42 changes: 42 additions & 0 deletions homeassistant/components/egauge/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Integration for eGauge energy monitors."""

from __future__ import annotations

from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr

from .const import DOMAIN, MANUFACTURER, MODEL
from .coordinator import EgaugeConfigEntry, EgaugeDataCoordinator

PLATFORMS = [Platform.SENSOR]


async def async_setup_entry(hass: HomeAssistant, entry: EgaugeConfigEntry) -> bool:
"""Set up eGauge from a config entry."""

coordinator = EgaugeDataCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()

# Store coordinator in runtime_data
entry.runtime_data = coordinator

# Set up main device
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, coordinator.serial_number)},
name=coordinator.hostname,
manufacturer=MANUFACTURER,
model=MODEL,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a way to figure out which model it is?

serial_number=coordinator.serial_number,
)

# Setup sensor platform
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: EgaugeConfigEntry) -> bool:
"""Unload eGauge config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
77 changes: 77 additions & 0 deletions homeassistant/components/egauge/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Config flow to configure the eGauge integration."""

from __future__ import annotations

from typing import Any

from egauge_async.exceptions import EgaugeAuthenticationError, EgaugePermissionError
from egauge_async.json.client import EgaugeJsonClient
from httpx import ConnectError
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.helpers.httpx_client import get_async_client

from .const import DOMAIN, LOGGER

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_SSL, default=True): bool,
vol.Required(CONF_VERIFY_SSL, default=False): bool,
}
)


class EgaugeFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle an eGauge config flow."""

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initiated by the user."""
errors: dict[str, str] = {}

if user_input is not None:
client = EgaugeJsonClient(
host=user_input[CONF_HOST],
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
client=get_async_client(
self.hass, verify_ssl=user_input[CONF_VERIFY_SSL]
),
use_ssl=user_input[CONF_SSL],
)
try:
serial_number = await client.get_device_serial_number()
hostname = await client.get_hostname()
except EgaugeAuthenticationError:
errors["base"] = "invalid_auth"
except EgaugePermissionError:
errors["base"] = "missing_permission"
except ConnectError:
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=hostname, data=user_input)

return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, user_input
),
errors=errors,
)
10 changes: 10 additions & 0 deletions homeassistant/components/egauge/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Constants for the eGauge integration."""

import logging

DOMAIN = "egauge"
LOGGER = logging.getLogger(__package__)

MANUFACTURER = "eGauge Systems"
MODEL = "eGauge Energy Monitor"
COORDINATOR_UPDATE_INTERVAL_SECONDS = 30
105 changes: 105 additions & 0 deletions homeassistant/components/egauge/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Data update coordinator for eGauge energy monitors."""

from __future__ import annotations

from dataclasses import dataclass
from datetime import timedelta

from egauge_async.exceptions import (
EgaugeAuthenticationError,
EgaugeException,
EgaugePermissionError,
)
from egauge_async.json.client import EgaugeJsonClient
from egauge_async.json.models import RegisterInfo
from httpx import ConnectError

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import COORDINATOR_UPDATE_INTERVAL_SECONDS, DOMAIN, LOGGER

type EgaugeConfigEntry = ConfigEntry[EgaugeDataCoordinator]


@dataclass
class EgaugeData:
"""Data from eGauge device."""

measurements: dict[str, float] # Instantaneous values (W, V, A, etc.)
counters: dict[str, float] # Cumulative values (Ws)
register_info: dict[str, RegisterInfo] # Metadata for all registers


class EgaugeDataCoordinator(DataUpdateCoordinator[EgaugeData]):
"""Class to manage fetching eGauge data."""

serial_number: str
hostname: str

def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
logger=LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=COORDINATOR_UPDATE_INTERVAL_SECONDS),
config_entry=config_entry,
)
self.client = EgaugeJsonClient(
host=config_entry.data[CONF_HOST],
username=config_entry.data[CONF_USERNAME],
password=config_entry.data[CONF_PASSWORD],
client=get_async_client(
hass, verify_ssl=config_entry.data[CONF_VERIFY_SSL]
),
use_ssl=config_entry.data[CONF_SSL],
)
# Populated in _async_setup
self._register_info: dict[str, RegisterInfo] = {}

async def _async_setup(self) -> None:
try:
self.serial_number = await self.client.get_device_serial_number()
self.hostname = await self.client.get_hostname()
self._register_info = await self.client.get_register_info()
except (
EgaugeAuthenticationError,
EgaugePermissionError,
EgaugeException,
) as err:
# EgaugeAuthenticationError and EgaugePermissionError will raise ConfigEntryAuthFailed once reauth is implemented
raise ConfigEntryError from err
Comment thread
neggert marked this conversation as resolved.
except ConnectError as err:
raise UpdateFailed(f"Error fetching device info: {err}") from err

async def _async_update_data(self) -> EgaugeData:
"""Fetch data from eGauge device."""
try:
measurements = await self.client.get_current_measurements()
counters = await self.client.get_current_counters()
except (
EgaugeAuthenticationError,
EgaugePermissionError,
EgaugeException,
) as err:
# will raise ConfigEntryAuthFailed once reauth is implemented
raise ConfigEntryError("Error fetching device info: {err}") from err
except ConnectError as err:
raise UpdateFailed(f"Error fetching device info: {err}") from err

return EgaugeData(
measurements=measurements,
counters=counters,
register_info=self._register_info,
)
35 changes: 35 additions & 0 deletions homeassistant/components/egauge/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Base entity for the eGauge integration."""

from __future__ import annotations

from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN, MANUFACTURER, MODEL
from .coordinator import EgaugeDataCoordinator


class EgaugeEntity(CoordinatorEntity[EgaugeDataCoordinator]):
"""Base entity for eGauge sensors."""

_attr_has_entity_name = True

def __init__(
self,
coordinator: EgaugeDataCoordinator,
register_name: str,
) -> None:
"""Initialize the eGauge entity."""
super().__init__(coordinator)

register_identifier = f"{coordinator.serial_number}_{register_name}"
register_name = f"{coordinator.hostname} {register_name}"

# Device info using coordinator's cached data
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, register_identifier)},
name=register_name,
manufacturer=MANUFACTURER,
model=MODEL,
via_device=(DOMAIN, coordinator.serial_number),
)
11 changes: 11 additions & 0 deletions homeassistant/components/egauge/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"domain": "egauge",
"name": "eGauge",
"codeowners": ["@neggert"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/egauge",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["egauge-async==0.4.0"]
}
74 changes: 74 additions & 0 deletions homeassistant/components/egauge/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register custom actions
appropriate-polling: done
brands: done
common-modules: done
config-flow: done
config-flow-test-coverage: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not register custom actions
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Integration does not subscribe to events
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
Comment thread
neggert marked this conversation as resolved.

# Silver
action-exceptions:
status: exempt
comment: Integration does not register actions
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: Integration does not expose configuration options
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done

# Gold
devices: done
diagnostics: todo
discovery: todo
discovery-update-info: todo
docs-data-update: done
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: Integration only has essential entities
entity-translations: done
exception-translations: todo
icon-translations:
status: exempt
comment: Integration uses standard device class icons
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo

# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo
Loading
Loading