-
-
Notifications
You must be signed in to change notification settings - Fork 37.5k
Add new CentriConnect component #166933
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 new CentriConnect component #166933
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
b1b4fc2
Initial commit of CentriConnect component
gresrun 8426c88
Add suggested_display_precision to sensor entities
gresrun 2c85c7d
Merge branch 'dev' into centriconnect
gresrun 226fce3
Delete homeassistant/components/centriconnect/brand/logo@2x.png
gresrun dff57f4
Delete homeassistant/components/centriconnect/brand/icon.png
gresrun 7ba3659
Delete homeassistant/components/centriconnect/brand/icon@2x.png
gresrun 9cc0dcc
Delete homeassistant/components/centriconnect/brand/logo.png
gresrun 17e03ae
Delete tests/components/centriconnect/test_diagnostics.py
gresrun 2ec37d9
Delete tests/components/centriconnect/snapshots/test_diagnostics.ambr
gresrun 12f2a40
Delete homeassistant/components/centriconnect/diagnostics.py
gresrun d569fc7
Update diagnostics status from 'done' to 'todo'
gresrun 323263c
Merge branch 'dev' into centriconnect
gresrun 8ad3725
Remove logging statement in async_unload_entry
gresrun ff518fb
Import homeassistant.config_entries directly
gresrun d6951d8
Consolidate excpetion handling in config_flow
gresrun b4a6915
Improve exception handling in _async_setup
gresrun 57e481c
Remove the unneeded icons.json entry for battery_level
gresrun 59db06e
Add TODOs to move sensor logic down into library
gresrun 547d5c5
Move async_setup_entry above the sensor
gresrun 6d8bd46
Remove a few diagnostic sensors at the request of @joostlek
gresrun 26fba4c
Use Sentence case for all strings
gresrun 5b4c76a
Merge branch 'dev' into centriconnect
gresrun d87cae9
Fix the article in the docstring to use “a” instead of “an”
gresrun ea91ad1
Merge branch 'dev' into centriconnect
gresrun 3d86cac
Upgrade to aiocentriconnect v0.2.3 and use new properties
gresrun 48f0aa8
Remove deleted sensors from string.json
gresrun 3c9aa40
Update sensor snapshot with default battery sensor name
gresrun 50b23af
Import config_entries.SOURCE_USER directly
gresrun d8e5c2e
Mock CentriConnect client directly and parameterize tests
gresrun d795172
Merge branch 'dev' into centriconnect
gresrun 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
There are no files selected for viewing
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
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,30 @@ | ||
| """The CentriConnect/MyPropane API integration.""" | ||
|
|
||
| import logging | ||
|
|
||
| from homeassistant.const import Platform | ||
| from homeassistant.core import HomeAssistant | ||
|
|
||
| from .coordinator import CentriConnectConfigEntry, CentriConnectCoordinator | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| PLATFORMS: list[Platform] = [Platform.SENSOR] | ||
|
|
||
|
|
||
| async def async_setup_entry( | ||
| hass: HomeAssistant, entry: CentriConnectConfigEntry | ||
| ) -> bool: | ||
| """Set up CentriConnect/MyPropane API from a config entry.""" | ||
| coordinator = CentriConnectCoordinator(hass, entry) | ||
| await coordinator.async_config_entry_first_refresh() | ||
| entry.runtime_data = coordinator | ||
| await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
| return True | ||
|
|
||
|
|
||
| async def async_unload_entry( | ||
| hass: HomeAssistant, entry: CentriConnectConfigEntry | ||
| ) -> bool: | ||
| """Unload CentriConnect/MyPropane API integration platforms and coordinator.""" | ||
| 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,89 @@ | ||
| """Config flow for the CentriConnect/MyPropane API integration.""" | ||
|
|
||
| import logging | ||
| from typing import Any | ||
|
|
||
| from aiocentriconnect import CentriConnect | ||
| from aiocentriconnect.exceptions import ( | ||
| CentriConnectConnectionError, | ||
| CentriConnectDecodeError, | ||
| CentriConnectEmptyResponseError, | ||
| CentriConnectNotFoundError, | ||
| CentriConnectTooManyRequestsError, | ||
| ) | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||
| from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_USERNAME | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
|
||
| from .const import CENTRICONNECT_DEVICE_ID, DOMAIN | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| STEP_USER_DATA_SCHEMA = vol.Schema( | ||
| { | ||
| vol.Required(CONF_USERNAME): str, | ||
| vol.Required(CONF_DEVICE_ID): str, | ||
| vol.Required(CONF_PASSWORD): str, | ||
|
gresrun marked this conversation as resolved.
|
||
| } | ||
| ) | ||
|
|
||
|
|
||
| async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: | ||
| """Validate the user input allows us to connect. | ||
|
|
||
| Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. | ||
| """ | ||
| # Validate the user-supplied data can be used to set up a connection. | ||
| hub = CentriConnect( | ||
| data[CONF_USERNAME], | ||
| data[CONF_DEVICE_ID], | ||
| data[CONF_PASSWORD], | ||
| session=async_get_clientsession(hass), | ||
| ) | ||
|
|
||
| tank_data = await hub.async_get_tank_data() | ||
|
|
||
| # Return info to store in the config entry. | ||
| return { | ||
| "title": tank_data.device_name, | ||
| CENTRICONNECT_DEVICE_ID: tank_data.device_id, | ||
| } | ||
|
|
||
|
|
||
| class CentriConnectConfigFlow(ConfigFlow, domain=DOMAIN): | ||
| """Handle a config flow for CentriConnect/MyPropane API.""" | ||
|
|
||
| VERSION = 1 | ||
|
|
||
| async def async_step_user( | ||
| self, user_input: dict[str, Any] | None = None | ||
| ) -> ConfigFlowResult: | ||
| """Handle the initial step.""" | ||
| errors: dict[str, str] = {} | ||
| if user_input is not None: | ||
| try: | ||
| info = await validate_input(self.hass, user_input) | ||
| except CentriConnectConnectionError, CentriConnectTooManyRequestsError: | ||
| errors["base"] = "cannot_connect" | ||
| except CentriConnectNotFoundError: | ||
| errors["base"] = "invalid_auth" | ||
| except CentriConnectEmptyResponseError, CentriConnectDecodeError: | ||
| errors["base"] = "unknown" | ||
| except Exception: | ||
| _LOGGER.exception("Unexpected exception") | ||
| errors["base"] = "unknown" | ||
| else: | ||
| await self.async_set_unique_id( | ||
| unique_id=info[CENTRICONNECT_DEVICE_ID], raise_on_progress=True | ||
| ) | ||
| self._abort_if_unique_id_configured( | ||
| updates=user_input, reload_on_update=True | ||
| ) | ||
| return self.async_create_entry(title=info["title"], data=user_input) | ||
|
|
||
| return self.async_show_form( | ||
| step_id="user", data_schema=STEP_USER_DATA_SCHEMA, 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,5 @@ | ||
| """Constants for the CentriConnect/MyPropane API integration.""" | ||
|
|
||
| DOMAIN = "centriconnect" | ||
|
|
||
| CENTRICONNECT_DEVICE_ID = "device_id" | ||
|
gresrun marked this conversation as resolved.
|
||
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,88 @@ | ||
| """Coordinator for CentriConnect/MyPropane API integration. | ||
|
|
||
| Responsible for polling the device API endpoint and normalizing data for entities. | ||
| """ | ||
|
|
||
| from dataclasses import dataclass | ||
| from datetime import timedelta | ||
| import logging | ||
|
|
||
| from aiocentriconnect import CentriConnect, Tank | ||
| from aiocentriconnect.exceptions import CentriConnectConnectionError, CentriConnectError | ||
|
|
||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_USERNAME | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
| from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
|
||
| from .const import DOMAIN | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| COORDINATOR_NAME = f"{DOMAIN} Coordinator" | ||
| # Maximum update frequency is every 6 hours. The API will return 429 Too Many Requests if polled frequently. | ||
| # The device updates its data every 8-12 hours, so there's no need to poll more frequently. | ||
| UPDATE_INTERVAL = timedelta(hours=6) | ||
|
|
||
| type CentriConnectConfigEntry = ConfigEntry[CentriConnectCoordinator] | ||
|
|
||
|
|
||
| @dataclass | ||
| class CentriConnectDeviceInfo: | ||
| """Data about the CentriConnect device.""" | ||
|
|
||
| device_id: str | ||
| device_name: str | ||
| hardware_version: str | ||
| lte_version: str | ||
| tank_size: int | ||
| tank_size_unit: str | ||
|
|
||
|
|
||
| class CentriConnectCoordinator(DataUpdateCoordinator[Tank]): | ||
| """Data update coordinator for CentriConnect/MyPropane devices.""" | ||
|
|
||
| config_entry: CentriConnectConfigEntry | ||
| device_info: CentriConnectDeviceInfo | ||
|
|
||
| def __init__(self, hass: HomeAssistant, entry: CentriConnectConfigEntry) -> None: | ||
| """Initialize the CentriConnect data update coordinator.""" | ||
| super().__init__( | ||
| hass, | ||
| logger=_LOGGER, | ||
| name=COORDINATOR_NAME, | ||
| update_interval=UPDATE_INTERVAL, | ||
| config_entry=entry, | ||
| ) | ||
|
|
||
| self.api_client = CentriConnect( | ||
| entry.data[CONF_USERNAME], | ||
| entry.data[CONF_DEVICE_ID], | ||
| entry.data[CONF_PASSWORD], | ||
| session=async_get_clientsession(hass), | ||
| ) | ||
|
|
||
| async def _async_setup(self) -> None: | ||
| try: | ||
| tank_data = await self.api_client.async_get_tank_data() | ||
| except CentriConnectError as err: | ||
| raise UpdateFailed("Could not fetch device info") from err | ||
| self.device_info = CentriConnectDeviceInfo( | ||
|
gresrun marked this conversation as resolved.
|
||
| device_id=tank_data.device_id, | ||
| device_name=tank_data.device_name, | ||
| hardware_version=tank_data.hardware_version, | ||
| lte_version=tank_data.lte_version, | ||
|
gresrun marked this conversation as resolved.
|
||
| tank_size=tank_data.tank_size, | ||
| tank_size_unit=tank_data.tank_size_unit, | ||
| ) | ||
|
|
||
| async def _async_update_data(self) -> Tank: | ||
| """Fetch device state.""" | ||
| try: | ||
| state = await self.api_client.async_get_tank_data() | ||
| except CentriConnectConnectionError as err: | ||
| raise UpdateFailed(f"Error communicating with device: {err}") from err | ||
| except CentriConnectError as err: | ||
| raise UpdateFailed(f"Unexpected response: {err}") from err | ||
| return state | ||
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,37 @@ | ||
| """Defines a base CentriConnect entity.""" | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from homeassistant.helpers.device_registry import DeviceInfo | ||
| from homeassistant.helpers.entity import EntityDescription | ||
| from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
|
||
| from .const import DOMAIN | ||
| from .coordinator import CentriConnectCoordinator | ||
|
|
||
|
|
||
| class CentriConnectBaseEntity(CoordinatorEntity[CentriConnectCoordinator]): | ||
| """Defines a base CentriConnect entity.""" | ||
|
|
||
| _attr_has_entity_name = True | ||
|
|
||
| def __init__( | ||
| self, | ||
| coordinator: CentriConnectCoordinator, | ||
| description: EntityDescription, | ||
| ) -> None: | ||
| """Initialize the CentriConnect entity.""" | ||
| super().__init__(coordinator) | ||
| if TYPE_CHECKING: | ||
| assert coordinator.config_entry.unique_id | ||
|
|
||
| self._attr_device_info = DeviceInfo( | ||
| identifiers={(DOMAIN, coordinator.config_entry.unique_id)}, | ||
| name=coordinator.device_info.device_name, | ||
| serial_number=coordinator.device_info.device_id, | ||
| hw_version=coordinator.device_info.hardware_version, | ||
| sw_version=coordinator.device_info.lte_version, | ||
| manufacturer="CentriConnect", | ||
| ) | ||
| self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}" | ||
| self.entity_description = description |
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,68 @@ | ||
| { | ||
| "entity": { | ||
| "sensor": { | ||
| "alert_status": { | ||
| "default": "mdi:alert-circle-outline", | ||
| "state": { | ||
| "critical_level": "mdi:alert-circle", | ||
| "low_level": "mdi:alert-circle-outline", | ||
| "no_alert": "mdi:check-circle-outline" | ||
| } | ||
| }, | ||
| "altitude": { | ||
| "default": "mdi:altimeter" | ||
| }, | ||
| "battery_voltage": { | ||
| "default": "mdi:car-battery" | ||
| }, | ||
| "device_temperature": { | ||
| "default": "mdi:thermometer" | ||
| }, | ||
| "last_post_time": { | ||
| "default": "mdi:clock-end" | ||
| }, | ||
| "latitude": { | ||
| "default": "mdi:latitude" | ||
| }, | ||
| "longitude": { | ||
| "default": "mdi:longitude" | ||
| }, | ||
| "lte_signal_level": { | ||
| "default": "mdi:signal", | ||
| "range": { | ||
| "0": "mdi:signal-cellular-outline", | ||
| "25": "mdi:signal-cellular-1", | ||
| "50": "mdi:signal-cellular-2", | ||
| "75": "mdi:signal-cellular-3" | ||
| } | ||
| }, | ||
| "lte_signal_strength": { | ||
| "default": "mdi:signal-variant" | ||
| }, | ||
| "next_post_time": { | ||
| "default": "mdi:clock-start" | ||
| }, | ||
| "solar_level": { | ||
| "default": "mdi:sun-wireless" | ||
| }, | ||
| "solar_voltage": { | ||
| "default": "mdi:solar-power" | ||
| }, | ||
| "tank_level": { | ||
| "default": "mdi:gauge", | ||
| "range": { | ||
| "0": "mdi:gauge-empty", | ||
| "25": "mdi:gauge-low", | ||
| "50": "mdi:gauge", | ||
| "75": "mdi:gauge-full" | ||
| } | ||
| }, | ||
| "tank_remaining_volume": { | ||
| "default": "mdi:storage-tank-outline" | ||
| }, | ||
| "tank_size": { | ||
| "default": "mdi:storage-tank" | ||
| } | ||
| } | ||
| } | ||
| } |
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": "centriconnect", | ||
| "name": "CentriConnect/MyPropane", | ||
| "codeowners": ["@gresrun"], | ||
| "config_flow": true, | ||
| "documentation": "https://www.home-assistant.io/integrations/centriconnect", | ||
| "integration_type": "device", | ||
| "iot_class": "cloud_polling", | ||
| "quality_scale": "bronze", | ||
| "requirements": ["aiocentriconnect==0.2.3"] | ||
| } |
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.
Uh oh!
There was an error while loading. Please reload this page.