Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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"
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

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."""

_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
Loading
Loading