-
-
Notifications
You must be signed in to change notification settings - Fork 37.5k
Add eGauge integration #155279
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
Merged
Merged
Add eGauge integration #155279
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 f2350df
Add eGauge config flow implementation
neggert 5a558b8
Add eGauge data models
neggert 20e58d4
Add eGauge data update coordinator
neggert 44d3b0f
Add eGauge base entity class
neggert cae073a
Add eGauge power and energy sensors
neggert f0067c3
Implement eGauge integration setup
neggert 53bc842
Add eGauge quality scale configuration
neggert 9b7ec58
Add eGauge test infrastructure
neggert 8ad2c1a
Add eGauge config flow tests
neggert 43121d4
Add eGauge sensor tests
neggert 0240341
Add eGauge integration tests
neggert 7df9674
Fix eGauge library imports
neggert 4e359be
Move EgaugeConfigEntry to avoid circular import
neggert 4ec6c04
Add missing data_description strings
neggert d6be33c
Fix broken tests
neggert 86f28b8
Add snapshots
neggert 69596c5
Use httpx client
neggert 7b1098f
Run hassfest
neggert 01e3dec
update test requirements
neggert d531cdc
Update quality scale.
neggert a29ffa6
Accept hostname/IP and SSL options instead of base URL
neggert c8b9b6e
Add functional tests for eGauge sensors and coordinator
neggert 182e070
Update quality scale
neggert 413e853
Move _build_client_url to util.py
neggert 26f9580
Move client construction inside coordinator
neggert cb6853f
Declare serial_number and hostname as class variables
neggert e8d4350
Initialize _register_info as empty dict
neggert 3408752
Initialize _register_info to empty dict instead of None
neggert 1b71e53
Move populating device info into _async_setup
neggert 2405fcb
Remove reauth flow for now
neggert 5185e87
Rename LOGGER to _LOGGER
neggert 6412bd7
Minor fixes to config flow
neggert 4731c96
Use hostname as configentry title
neggert 0c954b2
Remove redundant last_reset property
neggert 838209d
Use SensorEntityDescription to simplify sensor classes
neggert 5003b0d
Remove explicit coordinator tests.
neggert bf3058b
Update comment
neggert c34d99e
Raise better exceptions for initial setup vs post-setup
neggert 74b311b
Add all quality scale rules
neggert f81e872
Merge branch 'dev' into egauge-integration
neggert ca76c68
Change ConfigEntryAuthError to ConfigEntryError
neggert a564836
Update quality scale
neggert a9bdb56
Move async_setup_entry before EgaugeSensor class definition
neggert 62e842f
Move register information out of EgaugeEntity and into EgaugeSensor
neggert cd7d2af
Remove unused test fixture
neggert 6e8ac32
Autouse mock_egauge_client test fixture
neggert 46bad61
Use pytestmark to always use init_integration fixture
neggert a663b29
Remove redundant assertions
neggert e46a462
Use snapshot to verify device attributes
neggert 856421f
Remove redundant tests.
neggert 2261a27
Test that sensors become available once errors resolve
neggert de953a0
Update egauge-async dependency to v0.4.0
neggert 90d9a67
Update EgaugeJsonClient to match new signature in egauge-async 0.4.0
neggert 410160b
Removed unused helper function
neggert 986e320
Improve exception handling.
neggert 0f71bdd
Use translation keys in entity names.
neggert af50a39
Merge branch 'dev' into egauge-integration
neggert 76dc571
Merge branch 'dev' into egauge-integration
neggert 529b282
Merge branch 'dev' into egauge-integration
neggert bac94d3
Create a sub-device for each register
neggert ebf6072
Use static config flow schema
neggert 7ae7784
Use const for coordinator update interval
neggert 28bb23e
Improve coordinator error handling
neggert 00fb680
Move EgaugeData into coordinator file
neggert 1356d39
entity-translations: done
neggert 7159f03
Use native units
neggert c8bbff6
Formatting
neggert 8f72896
Remove unused reauth strings
neggert 4dc40ef
Merge branch 'dev' into egauge-integration
neggert 2735741
Correctly handle missing register
neggert a76e514
Leave user_input None instead of empty dict
neggert 9706ee2
native_value always returns float
neggert 83072a0
Remove unused translation_keys
neggert 261cf03
Merge branch 'dev' into egauge-integration
neggert 501e1b5
Update snapshot
neggert f28eae1
_LOGGER -> LOGGER
neggert 781a3a0
Simplify register to sensor mapping
neggert 8fe3371
Simplify sensors
neggert c26eed6
Merge branch 'dev' into egauge-integration
neggert 2470e55
Remove redundant call to async_add_entities
neggert 00e2056
Improve robustness of config flow tests
neggert 0ec2905
Merge branch 'dev' into egauge-integration
neggert File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| 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) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
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, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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), | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
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 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Is there a way to figure out which model it is?