Skip to content

Commit 0a034b9

Browse files
bestycameNoRi2909odotreppe-abbovejoostlek
authored
Add Hanna integration (home-assistant#147085)
Co-authored-by: Norbert Rittel <[email protected]> Co-authored-by: Olivier d'Otreppe <[email protected]> Co-authored-by: Joostlek <[email protected]>
1 parent 6a8106c commit 0a034b9

File tree

17 files changed

+630
-0
lines changed

17 files changed

+630
-0
lines changed

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""The Hanna Instruments integration."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
from hanna_cloud import HannaCloudClient
8+
9+
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
10+
from homeassistant.core import HomeAssistant
11+
12+
from .coordinator import HannaConfigEntry, HannaDataCoordinator
13+
14+
PLATFORMS = [Platform.SENSOR]
15+
16+
17+
def _authenticate_and_get_devices(
18+
api_client: HannaCloudClient,
19+
email: str,
20+
password: str,
21+
) -> list[dict[str, Any]]:
22+
"""Authenticate and get devices in a single executor job."""
23+
api_client.authenticate(email, password)
24+
return api_client.get_devices()
25+
26+
27+
async def async_setup_entry(hass: HomeAssistant, entry: HannaConfigEntry) -> bool:
28+
"""Set up Hanna Instruments from a config entry."""
29+
api_client = HannaCloudClient()
30+
devices = await hass.async_add_executor_job(
31+
_authenticate_and_get_devices,
32+
api_client,
33+
entry.data[CONF_EMAIL],
34+
entry.data[CONF_PASSWORD],
35+
)
36+
37+
# Create device coordinators
38+
device_coordinators = {}
39+
for device in devices:
40+
coordinator = HannaDataCoordinator(hass, entry, device, api_client)
41+
await coordinator.async_config_entry_first_refresh()
42+
device_coordinators[coordinator.device_identifier] = coordinator
43+
44+
# Set runtime data
45+
entry.runtime_data = device_coordinators
46+
47+
# Forward the setup to the platforms
48+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
49+
return True
50+
51+
52+
async def async_unload_entry(hass: HomeAssistant, entry: HannaConfigEntry) -> bool:
53+
"""Unload a config entry."""
54+
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Config flow for Hanna Instruments integration."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
from typing import Any
7+
8+
from hanna_cloud import AuthenticationError, HannaCloudClient
9+
from requests.exceptions import ConnectionError as RequestsConnectionError, Timeout
10+
import voluptuous as vol
11+
12+
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
13+
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
14+
15+
from .const import DOMAIN
16+
17+
_LOGGER = logging.getLogger(__name__)
18+
19+
20+
class HannaConfigFlow(ConfigFlow, domain=DOMAIN):
21+
"""Handle a config flow for Hanna Instruments."""
22+
23+
VERSION = 1
24+
data_schema = vol.Schema(
25+
{vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}
26+
)
27+
28+
async def async_step_user(
29+
self, user_input: dict[str, Any] | None = None
30+
) -> ConfigFlowResult:
31+
"""Handle the setup flow."""
32+
33+
errors: dict[str, str] = {}
34+
35+
if user_input is not None:
36+
await self.async_set_unique_id(user_input[CONF_EMAIL])
37+
self._abort_if_unique_id_configured()
38+
client = HannaCloudClient()
39+
try:
40+
await self.hass.async_add_executor_job(
41+
client.authenticate,
42+
user_input[CONF_EMAIL],
43+
user_input[CONF_PASSWORD],
44+
)
45+
except (Timeout, RequestsConnectionError):
46+
errors["base"] = "cannot_connect"
47+
except AuthenticationError:
48+
errors["base"] = "invalid_auth"
49+
50+
if not errors:
51+
return self.async_create_entry(
52+
title=user_input[CONF_EMAIL],
53+
data=user_input,
54+
)
55+
56+
return self.async_show_form(
57+
step_id="user",
58+
data_schema=self.add_suggested_values_to_schema(
59+
self.data_schema, user_input
60+
),
61+
errors=errors,
62+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Constants for the Hanna integration."""
2+
3+
DOMAIN = "hanna"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Hanna Instruments data coordinator for Home Assistant.
2+
3+
This module provides the data coordinator for fetching and managing Hanna Instruments
4+
sensor data.
5+
"""
6+
7+
from datetime import timedelta
8+
import logging
9+
from typing import Any
10+
11+
from hanna_cloud import HannaCloudClient
12+
from requests.exceptions import RequestException
13+
14+
from homeassistant.config_entries import ConfigEntry
15+
from homeassistant.core import HomeAssistant
16+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
17+
18+
from .const import DOMAIN
19+
20+
type HannaConfigEntry = ConfigEntry[dict[str, HannaDataCoordinator]]
21+
22+
_LOGGER = logging.getLogger(__name__)
23+
24+
25+
class HannaDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
26+
"""Coordinator for fetching Hanna sensor data."""
27+
28+
def __init__(
29+
self,
30+
hass: HomeAssistant,
31+
config_entry: HannaConfigEntry,
32+
device: dict[str, Any],
33+
api_client: HannaCloudClient,
34+
) -> None:
35+
"""Initialize the Hanna data coordinator."""
36+
self.api_client = api_client
37+
self.device_data = device
38+
super().__init__(
39+
hass,
40+
_LOGGER,
41+
name=f"{DOMAIN}_{self.device_identifier}",
42+
config_entry=config_entry,
43+
update_interval=timedelta(seconds=30),
44+
)
45+
46+
@property
47+
def device_identifier(self) -> str:
48+
"""Return the device identifier."""
49+
return self.device_data["DID"]
50+
51+
def get_parameters(self) -> list[dict[str, Any]]:
52+
"""Get all parameters from the sensor data."""
53+
return self.api_client.parameters
54+
55+
def get_parameter_value(self, key: str) -> Any:
56+
"""Get the value for a specific parameter."""
57+
for parameter in self.get_parameters():
58+
if parameter["name"] == key:
59+
return parameter["value"]
60+
return None
61+
62+
async def _async_update_data(self) -> dict[str, Any]:
63+
"""Fetch latest sensor data from the Hanna API."""
64+
try:
65+
readings = await self.hass.async_add_executor_job(
66+
self.api_client.get_last_device_reading, self.device_identifier
67+
)
68+
except RequestException as e:
69+
raise UpdateFailed(f"Error communicating with Hanna API: {e}") from e
70+
except (KeyError, IndexError) as e:
71+
raise UpdateFailed(f"Error parsing Hanna API response: {e}") from e
72+
return readings
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Hanna Instruments entity base class for Home Assistant.
2+
3+
This module provides the base entity class for Hanna Instruments entities.
4+
"""
5+
6+
from homeassistant.helpers.device_registry import DeviceInfo
7+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
8+
9+
from .const import DOMAIN
10+
from .coordinator import HannaDataCoordinator
11+
12+
13+
class HannaEntity(CoordinatorEntity[HannaDataCoordinator]):
14+
"""Base class for Hanna entities."""
15+
16+
_attr_has_entity_name = True
17+
18+
def __init__(self, coordinator: HannaDataCoordinator) -> None:
19+
"""Initialize the entity."""
20+
super().__init__(coordinator)
21+
self._attr_device_info = DeviceInfo(
22+
identifiers={(DOMAIN, coordinator.device_identifier)},
23+
manufacturer=coordinator.device_data.get("manufacturer"),
24+
model=coordinator.device_data.get("DM"),
25+
name=coordinator.device_data.get("name"),
26+
serial_number=coordinator.device_data.get("serial_number"),
27+
sw_version=coordinator.device_data.get("sw_version"),
28+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"domain": "hanna",
3+
"name": "Hanna",
4+
"codeowners": ["@bestycame"],
5+
"config_flow": true,
6+
"documentation": "https://www.home-assistant.io/integrations/hanna",
7+
"iot_class": "cloud_polling",
8+
"quality_scale": "bronze",
9+
"requirements": ["hanna-cloud==0.0.6"]
10+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
rules:
2+
# Bronze
3+
action-setup:
4+
status: exempt
5+
comment: |
6+
This integration doesn't add actions.
7+
appropriate-polling:
8+
status: done
9+
brands: done
10+
common-modules: done
11+
config-flow-test-coverage: done
12+
config-flow: done
13+
dependency-transparency: done
14+
docs-actions: done
15+
docs-high-level-description: done
16+
docs-installation-instructions: done
17+
docs-removal-instructions: done
18+
entity-event-setup:
19+
status: exempt
20+
comment: |
21+
Entities of this integration does not explicitly subscribe to events.
22+
entity-unique-id: done
23+
has-entity-name: done
24+
runtime-data: done
25+
test-before-configure: done
26+
test-before-setup: done
27+
unique-config-entry: done
28+
29+
# Silver
30+
action-exceptions: todo
31+
config-entry-unloading: done
32+
docs-configuration-parameters:
33+
status: exempt
34+
comment: |
35+
This integration does not have any configuration parameters.
36+
docs-installation-parameters: done
37+
entity-unavailable: todo
38+
integration-owner: done
39+
log-when-unavailable: todo
40+
parallel-updates: todo
41+
reauthentication-flow: todo
42+
test-coverage: todo
43+
44+
# Gold
45+
devices: done
46+
diagnostics: todo
47+
discovery-update-info: todo
48+
discovery: todo
49+
docs-data-update: done
50+
docs-examples: todo
51+
docs-known-limitations: todo
52+
docs-supported-devices: done
53+
docs-supported-functions: done
54+
docs-troubleshooting: todo
55+
docs-use-cases: todo
56+
dynamic-devices: todo
57+
entity-category: todo
58+
entity-device-class: done
59+
entity-disabled-by-default: todo
60+
entity-translations: done
61+
exception-translations: todo
62+
icon-translations: todo
63+
reconfiguration-flow: todo
64+
repair-issues: todo
65+
stale-devices: todo
66+
67+
# Platinum
68+
async-dependency: todo
69+
inject-websession: todo
70+
strict-typing: todo

0 commit comments

Comments
 (0)