Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
073e72e
Add Hanna integration
bestycame Jun 18, 2025
524aa9a
Merge branch 'dev' into add_hanna_integration
bestycame Jun 18, 2025
06ef095
Update homeassistant/components/hanna/strings.json
bestycame Jun 18, 2025
ff89533
Update homeassistant/components/hanna/strings.json
bestycame Jun 18, 2025
45df6dc
Update homeassistant/components/hanna/strings.json
bestycame Jun 18, 2025
dd9530e
Merge branch 'dev' into add_hanna_integration
bestycame Jun 19, 2025
5bffc3a
Merge branch 'dev' into add_hanna_integration
bestycame Jun 19, 2025
623607a
Merge branch 'home-assistant:dev' into add_hanna_integration
bestycame Jun 19, 2025
95ecc53
refactor following PR review comment
odotreppe-abbove Jul 18, 2025
ef61866
Correct case of sensor names
odotreppe-abbove Jul 22, 2025
665839d
Refactor Hanna integration to streamline authentication and device ma…
odotreppe-abbove Jul 25, 2025
fae064d
WIP
odotreppe-abbove Sep 16, 2025
bcec574
Various updates following PR review
odotreppe-abbove Oct 1, 2025
1746628
Remove some logging
odotreppe-abbove Oct 14, 2025
719b2c9
update the type annotation
odotreppe-abbove Oct 14, 2025
9150cbd
Update following PR review
odotreppe-abbove Oct 14, 2025
f095cce
update looping on Sensor Description
odotreppe-abbove Oct 14, 2025
6b30df6
Removed dead code
odotreppe-abbove Oct 14, 2025
4512afd
self.add_suggested_values_to_schema(self.data_schema, user_input)
odotreppe-abbove Oct 14, 2025
2bc1b9b
Update quality scale
odotreppe-abbove Oct 15, 2025
74b85d9
Update quality scale and define async_shutdown function in coordinato…
odotreppe-abbove Oct 15, 2025
caf9b9d
removed dead code and async_shutdown function in coordinator as it i…
odotreppe-abbove Oct 15, 2025
2401e14
Merge branch 'dev' into add_hanna_integration
bestycame Oct 15, 2025
8d882cf
Extend test coverage
odotreppe-abbove Oct 16, 2025
e458b79
Improve test_error_scenarios following review
odotreppe-abbove Oct 16, 2025
b384d1d
Bump to hanna-cloud 0.0.6
odotreppe-abbove Oct 16, 2025
d3bcb74
Following review comments
odotreppe-abbove Nov 12, 2025
cbb7888
Added device classes and remove unnecessery icons
odotreppe-abbove Nov 12, 2025
eda78ca
Merge branch 'dev' into add_hanna_integration
joostlek Nov 20, 2025
6eee8bd
Fix
joostlek Nov 20, 2025
04320d8
Fix
joostlek Nov 20, 2025
71e8519
Fix
joostlek Nov 20, 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.

54 changes: 54 additions & 0 deletions homeassistant/components/hanna/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""The Hanna Instruments integration."""

from __future__ import annotations

from typing import Any

from hanna_cloud import HannaCloudClient

from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant

from .coordinator import HannaConfigEntry, HannaDataCoordinator

PLATFORMS = [Platform.SENSOR]


def _authenticate_and_get_devices(
api_client: HannaCloudClient,
email: str,
password: str,
) -> list[dict[str, Any]]:
"""Authenticate and get devices in a single executor job."""
api_client.authenticate(email, password)
return api_client.get_devices()


async def async_setup_entry(hass: HomeAssistant, entry: HannaConfigEntry) -> bool:
"""Set up Hanna Instruments from a config entry."""
api_client = HannaCloudClient()
devices = await hass.async_add_executor_job(
_authenticate_and_get_devices,
api_client,
entry.data[CONF_EMAIL],
entry.data[CONF_PASSWORD],
)

# Create device coordinators
device_coordinators = {}
for device in devices:
coordinator = HannaDataCoordinator(hass, entry, device, api_client)
await coordinator.async_config_entry_first_refresh()
device_coordinators[coordinator.device_identifier] = coordinator

# Set runtime data
entry.runtime_data = 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: HannaConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
62 changes: 62 additions & 0 deletions homeassistant/components/hanna/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Config flow for Hanna Instruments integration."""

from __future__ import annotations

import logging
from typing import Any

from hanna_cloud import AuthenticationError, HannaCloudClient
from requests.exceptions import ConnectionError as RequestsConnectionError, Timeout
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD

from .const import 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}
)

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the setup flow."""

errors: dict[str, str] = {}

if user_input is not None:
await self.async_set_unique_id(user_input[CONF_EMAIL])
self._abort_if_unique_id_configured()
client = HannaCloudClient()
try:
await self.hass.async_add_executor_job(
client.authenticate,
user_input[CONF_EMAIL],
user_input[CONF_PASSWORD],
)
except (Timeout, RequestsConnectionError):
errors["base"] = "cannot_connect"
Comment thread
bestycame marked this conversation as resolved.
except AuthenticationError:
errors["base"] = "invalid_auth"

if not errors:
return self.async_create_entry(
title=user_input[CONF_EMAIL],
data=user_input,
)

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

DOMAIN = "hanna"
72 changes: 72 additions & 0 deletions homeassistant/components/hanna/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Hanna Instruments data coordinator for Home Assistant.

This module provides the data coordinator for fetching and managing Hanna Instruments
sensor data.
"""

from datetime import 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.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN

type HannaConfigEntry = ConfigEntry[dict[str, HannaDataCoordinator]]

_LOGGER = logging.getLogger(__name__)


class HannaDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Coordinator for fetching Hanna sensor data."""

def __init__(
self,
hass: HomeAssistant,
config_entry: HannaConfigEntry,
device: dict[str, Any],
api_client: HannaCloudClient,
) -> None:
"""Initialize the Hanna data coordinator."""
self.api_client = api_client
self.device_data = device
super().__init__(
hass,
_LOGGER,
name=f"{DOMAIN}_{self.device_identifier}",
config_entry=config_entry,
update_interval=timedelta(seconds=30),
)

@property
def device_identifier(self) -> str:
"""Return the device identifier."""
return self.device_data["DID"]

def get_parameters(self) -> list[dict[str, Any]]:
"""Get all parameters from the sensor data."""
return self.api_client.parameters

def get_parameter_value(self, key: str) -> Any:
"""Get the value for a specific parameter."""
for parameter in self.get_parameters():
if parameter["name"] == key:
return parameter["value"]
return None
Comment thread
bestycame marked this conversation as resolved.

async def _async_update_data(self) -> dict[str, Any]:
"""Fetch latest sensor data from the Hanna API."""
try:
readings = await self.hass.async_add_executor_job(
self.api_client.get_last_device_reading, self.device_identifier
)
except RequestException as e:
raise UpdateFailed(f"Error communicating with Hanna API: {e}") from e
except (KeyError, IndexError) as e:
raise UpdateFailed(f"Error parsing Hanna API response: {e}") from e
return readings
28 changes: 28 additions & 0 deletions homeassistant/components/hanna/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Hanna Instruments entity base class for Home Assistant.

This module provides the base entity class for Hanna Instruments entities.
"""

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

from .const import DOMAIN
from .coordinator import HannaDataCoordinator


class HannaEntity(CoordinatorEntity[HannaDataCoordinator]):
"""Base class for Hanna entities."""
Comment thread
bestycame marked this conversation as resolved.

_attr_has_entity_name = True

def __init__(self, coordinator: HannaDataCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.device_identifier)},
manufacturer=coordinator.device_data.get("manufacturer"),
model=coordinator.device_data.get("DM"),
name=coordinator.device_data.get("name"),
serial_number=coordinator.device_data.get("serial_number"),
sw_version=coordinator.device_data.get("sw_version"),
)
10 changes: 10 additions & 0 deletions homeassistant/components/hanna/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"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.6"]
}
70 changes: 70 additions & 0 deletions homeassistant/components/hanna/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration doesn't add actions.
appropriate-polling:
status: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly 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

# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: |
This integration does not have any configuration parameters.
docs-installation-parameters: done
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: todo

# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: done
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo

# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo
Comment thread
bestycame marked this conversation as resolved.
Loading
Loading