-
-
Notifications
You must be signed in to change notification settings - Fork 37.8k
Add Hanna integration #147085
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
Add Hanna integration #147085
Changes from 8 commits
073e72e
524aa9a
06ef095
ff89533
45df6dc
dd9530e
5bffc3a
623607a
95ecc53
ef61866
665839d
fae064d
bcec574
1746628
719b2c9
9150cbd
f095cce
6b30df6
4512afd
2bc1b9b
74b85d9
caf9b9d
2401e14
8d882cf
e458b79
b384d1d
d3bcb74
cbb7888
eda78ca
6eee8bd
04320d8
71e8519
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| """The Hanna Instruments integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import Platform | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.helpers.config_validation import config_entry_only_config_schema | ||
| from homeassistant.helpers.typing import ConfigType | ||
| from homeassistant.loader import async_get_integration | ||
|
|
||
| from .const import DOMAIN | ||
| from .coordinator import HannaDataCoordinator, HannaMainCoordinator | ||
|
|
||
| PLATFORMS = [Platform.SENSOR] | ||
|
|
||
| CONFIG_SCHEMA = config_entry_only_config_schema(DOMAIN) | ||
|
|
||
|
|
||
| async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: | ||
| """Set up the Hanna Instruments component.""" | ||
| _ = await async_get_integration(hass, DOMAIN) | ||
| hass.data.setdefault(DOMAIN, {}) | ||
| return True | ||
|
|
||
|
|
||
| async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
|
bestycame marked this conversation as resolved.
Outdated
|
||
| """Set up Hanna Instruments from a config entry.""" | ||
|
|
||
| # Create main coordinator | ||
| main_coordinator = HannaMainCoordinator(hass, entry) | ||
| await main_coordinator.async_authenticate( | ||
| entry.data["email"], entry.data["password"], entry.data["code"] | ||
| ) | ||
|
bestycame marked this conversation as resolved.
Outdated
|
||
| await main_coordinator.async_config_entry_first_refresh() | ||
|
|
||
| # Create device coordinators | ||
| devices = await main_coordinator.async_get_devices() | ||
| device_coordinators = {} | ||
| for device in devices: | ||
| coordinator = HannaDataCoordinator(hass, main_coordinator, device, entry) | ||
| await coordinator.async_config_entry_first_refresh() | ||
| device_coordinators[coordinator.device_identifier] = coordinator | ||
|
|
||
| # Set runtime data | ||
| entry.runtime_data = { | ||
| "main_coordinator": main_coordinator, | ||
| "device_coordinators": device_coordinators, | ||
| } | ||
|
|
||
| # Forward the setup to the platforms | ||
| await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
| return True | ||
|
|
||
|
|
||
| async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
| """Unload a config entry.""" | ||
| # Unload platforms | ||
| unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) | ||
|
|
||
| # Clean up coordinators | ||
| if unload_ok and entry.runtime_data: | ||
| # Clean up device coordinators | ||
| for coordinator in entry.runtime_data["device_coordinators"].values(): | ||
| await coordinator.async_shutdown() | ||
|
|
||
| # Clean up main coordinator | ||
| await entry.runtime_data["main_coordinator"].async_shutdown() | ||
|
|
||
| # Clear runtime data | ||
| entry.runtime_data = None | ||
|
|
||
| return unload_ok | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| """Config flow for Hanna Instruments integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from typing import Any | ||
|
|
||
| from hanna_cloud import HannaCloudClient | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||
| from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD, CONF_SCAN_INTERVAL | ||
|
|
||
| from .const import DEFAULT_ENCRYPTION_KEY, DOMAIN | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class HannaConfigFlow(ConfigFlow, domain=DOMAIN): | ||
| """Handle a config flow for Hanna Instruments.""" | ||
|
|
||
| VERSION = 1 | ||
| data_schema = vol.Schema( | ||
| { | ||
| vol.Required(CONF_EMAIL): str, | ||
| vol.Required(CONF_PASSWORD): str, | ||
| vol.Required(CONF_CODE, default=DEFAULT_ENCRYPTION_KEY): str, | ||
|
bestycame marked this conversation as resolved.
Outdated
|
||
| vol.Required(CONF_SCAN_INTERVAL, default=5): int, | ||
|
bestycame marked this conversation as resolved.
Outdated
|
||
| } | ||
| ) | ||
|
|
||
| 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 None: | ||
| return self.async_show_form( | ||
| step_id="user", | ||
| data_schema=self.data_schema, | ||
| errors=errors, | ||
| ) | ||
|
|
||
| try: | ||
| client = HannaCloudClient() | ||
| await self.hass.async_add_executor_job( | ||
| client.authenticate, | ||
| user_input[CONF_EMAIL], | ||
| user_input[CONF_PASSWORD], | ||
| user_input[CONF_CODE], | ||
| ) | ||
| except Exception: | ||
|
bestycame marked this conversation as resolved.
Outdated
|
||
| _LOGGER.exception("Unexpected exception") | ||
| errors["base"] = "invalid_auth" | ||
| return self.async_show_form( | ||
| step_id="user", | ||
| data_schema=self.data_schema, | ||
| errors=errors, | ||
| ) | ||
|
bestycame marked this conversation as resolved.
Outdated
|
||
| return self.async_create_entry( | ||
| title=user_input[CONF_EMAIL], | ||
| data=user_input, | ||
| ) | ||
|
|
||
| async def async_step_reconfigure( | ||
|
bestycame marked this conversation as resolved.
Outdated
|
||
| self, user_input: dict[str, Any] | None = None | ||
| ) -> ConfigFlowResult: | ||
| """Handle reconfiguration.""" | ||
| errors: dict[str, str] = {} | ||
|
|
||
| if user_input is None: | ||
| return self.async_show_form( | ||
| step_id="reconfigure", | ||
| data_schema=self.data_schema, | ||
| errors=errors, | ||
| ) | ||
|
|
||
| try: | ||
| client = HannaCloudClient() | ||
| await self.hass.async_add_executor_job( | ||
| client.authenticate, | ||
| user_input[CONF_EMAIL], | ||
| user_input[CONF_PASSWORD], | ||
| user_input[CONF_CODE], | ||
| ) | ||
| except Exception: | ||
| _LOGGER.exception("Unexpected exception") | ||
| errors["base"] = "invalid_auth" | ||
| return self.async_show_form( | ||
| step_id="reconfigure", | ||
| data_schema=self.data_schema, | ||
| errors=errors, | ||
| ) | ||
|
|
||
| # Update the existing entry | ||
| reconfigure_entry = self._get_reconfigure_entry() | ||
| return self.async_update_reload_and_abort( | ||
| reconfigure_entry, | ||
| data=user_input, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| """Constants for the Hanna integration.""" | ||
|
|
||
| DOMAIN = "hanna" | ||
|
|
||
| # This key is NOT private. It is found in the JavaScript code of the Hanna Cloud webapp at https://www.hannacloud.com | ||
| DEFAULT_ENCRYPTION_KEY = "MzJmODBmMDU0ZTAyNDFjYWM0YTVhOGQxY2ZlZTkwMDM=" |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,174 @@ | ||||||
| """Hanna Instruments data coordinator for Home Assistant. | ||||||
|
|
||||||
| This module provides the data coordinator for fetching and managing Hanna Instruments | ||||||
| sensor data. | ||||||
| """ | ||||||
|
|
||||||
| from datetime import datetime, timedelta | ||||||
| import logging | ||||||
| from typing import Any | ||||||
|
|
||||||
| from hanna_cloud import HannaCloudClient | ||||||
| from requests.exceptions import RequestException | ||||||
|
|
||||||
| from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.helpers.device_registry import DeviceInfo | ||||||
| from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||||||
|
|
||||||
| _LOGGER = logging.getLogger(__name__) | ||||||
|
|
||||||
|
|
||||||
| class HannaMainCoordinator(DataUpdateCoordinator): | ||||||
| """Main coordinator for Hanna Instruments authentication and device management.""" | ||||||
|
|
||||||
| def __init__( | ||||||
| self, | ||||||
| hass: HomeAssistant, | ||||||
| config_entry: ConfigEntry, | ||||||
| ) -> None: | ||||||
| """Initialize the main Hanna coordinator.""" | ||||||
| self.api_client = HannaCloudClient() | ||||||
| self._devices: dict[str, dict] = {} | ||||||
| update_interval = timedelta(minutes=config_entry.data.get("update_interval", 1)) | ||||||
|
||||||
| update_interval = timedelta(minutes=config_entry.data.get("update_interval", 1)) | |
| update_interval = timedelta(minutes=config_entry.data.get("scan_interval", 1)) |
Copilot
AI
Jun 26, 2025
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.
In the HannaDataCoordinator, the update interval is also obtained from the key 'update_interval'. To ensure consistency with the config flow (which uses 'scan_interval'), update this key accordingly.
| update_interval = timedelta(minutes=config_entry.data.get("update_interval", 1)) | |
| update_interval = timedelta(minutes=config_entry.data.get("scan_interval", 1)) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "domain": "hanna", | ||
| "name": "Hanna", | ||
| "codeowners": ["@bestycame"], | ||
| "config_flow": true, | ||
| "documentation": "https://www.home-assistant.io/integrations/hanna", | ||
| "iot_class": "cloud_polling", | ||
| "quality_scale": "bronze", | ||
| "requirements": ["hanna-cloud==0.0.4"], | ||
| "single_config_entry": true | ||
|
bestycame marked this conversation as resolved.
Outdated
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.