Skip to content
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 Iskra integration #121488

Merged
merged 23 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5ef3eaa
Add iskra integration
iskrakranj Aug 27, 2024
1fe5f9b
iskra non resettable counters naming fix
iskrakranj Aug 27, 2024
78ab9e2
added iskra config_flow test
iskrakranj Aug 27, 2024
9099259
fixed iskra integration according to code review
iskrakranj Aug 27, 2024
b28f2f5
changed iskra config flow test
iskrakranj Aug 27, 2024
0aadb80
iskra integration, fixed codeowners
iskrakranj Aug 27, 2024
1934a98
Removed counters code & minor fixes
iskrakranj Aug 27, 2024
08ad866
added comment
iskrakranj Aug 27, 2024
e6258d7
Update homeassistant/components/iskra/__init__.py
iskrakranj Sep 2, 2024
a90fc5e
Updated Iskra integration according to review
iskrakranj Sep 2, 2024
1ece1f3
Update homeassistant/components/iskra/strings.json
iskrakranj Sep 2, 2024
ede1e06
Updated iskra integration according to review
iskrakranj Sep 2, 2024
f627555
minor iskra integration change
iskrakranj Sep 2, 2024
385d919
iskra integration changes according to review
iskrakranj Sep 3, 2024
050eee6
iskra integration changes according to review
iskrakranj Sep 3, 2024
7bdc483
Changed iskra integration according to review
iskrakranj Sep 4, 2024
0c9ad4a
added iskra config_flow range validation
iskrakranj Sep 4, 2024
4908f1e
Fixed tests for iskra integration
iskrakranj Sep 4, 2024
9d335a8
Update homeassistant/components/iskra/coordinator.py
joostlek Sep 4, 2024
476b5a6
Update homeassistant/components/iskra/config_flow.py
iskrakranj Sep 4, 2024
a39e56a
Fixed iskra integration according to review
iskrakranj Sep 4, 2024
6457a6c
Changed voluptuous schema for iskra integration and added data_descri…
iskrakranj Sep 4, 2024
6250a13
Iskra integration tests lint error fix
iskrakranj Sep 4, 2024
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
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,8 @@ build.json @home-assistant/supervisor
/tests/components/iron_os/ @tr4nt0r
/homeassistant/components/isal/ @bdraco
/tests/components/isal/ @bdraco
/homeassistant/components/iskra/ @iskramis
joostlek marked this conversation as resolved.
Show resolved Hide resolved
/tests/components/iskra/ @iskramis
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
/homeassistant/components/israel_rail/ @shaiu
Expand Down
100 changes: 100 additions & 0 deletions homeassistant/components/iskra/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""The iskra integration."""

from __future__ import annotations

from pyiskra.adapters import Modbus, RestAPI
from pyiskra.devices import Device
from pyiskra.exceptions import DeviceConnectionError, DeviceNotSupported, NotAuthorised

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_ADDRESS,
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_PROTOCOL,
CONF_USERNAME,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr

from .const import DOMAIN, MANUFACTURER
from .coordinator import IskraDataUpdateCoordinator

PLATFORMS: list[Platform] = [Platform.SENSOR]


type IskraConfigEntry = ConfigEntry[list[IskraDataUpdateCoordinator]]


async def async_setup_entry(hass: HomeAssistant, entry: IskraConfigEntry) -> bool:
"""Set up iskra device from a config entry."""
conf = entry.data
adapter = None

if conf[CONF_PROTOCOL] == "modbus_tcp":
adapter = Modbus(
ip_address=conf[CONF_HOST],
protocol="tcp",
port=conf[CONF_PORT],
modbus_address=conf[CONF_ADDRESS],
)
elif conf[CONF_PROTOCOL] == "rest_api":
authentication = None
if (username := conf.get(CONF_USERNAME)) is not None and (
password := conf.get(CONF_PASSWORD)
joostlek marked this conversation as resolved.
Show resolved Hide resolved
) is not None:
authentication = {
"username": username,
"password": password,
}
adapter = RestAPI(ip_address=conf[CONF_HOST], authentication=authentication)

# Try connecting to the device and create pyiskra device object
try:
base_device = await Device.create_device(adapter)
except DeviceConnectionError as e:
raise ConfigEntryNotReady("Cannot connect to the device") from e
except NotAuthorised as e:
raise ConfigEntryNotReady("Not authorised to connect to the device") from e
except DeviceNotSupported as e:
raise ConfigEntryNotReady("Device not supported") from e

Check warning on line 63 in homeassistant/components/iskra/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/__init__.py#L58-L63

Added lines #L58 - L63 were not covered by tests

# Initialize the device
await base_device.init()

# if the device is a gateway, add all child devices, otherwise add the device itself.
if base_device.is_gateway:

Check warning on line 69 in homeassistant/components/iskra/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/__init__.py#L69

Added line #L69 was not covered by tests
# Add the gateway device to the device registry
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(

Check warning on line 72 in homeassistant/components/iskra/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/__init__.py#L71-L72

Added lines #L71 - L72 were not covered by tests
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, base_device.serial)},
manufacturer=MANUFACTURER,
name=base_device.model,
model=base_device.model,
sw_version=base_device.fw_version,
)

coordinators = [

Check warning on line 81 in homeassistant/components/iskra/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/__init__.py#L81

Added line #L81 was not covered by tests
IskraDataUpdateCoordinator(hass, child_device)
for child_device in base_device.get_child_devices()
]
else:
coordinators = [IskraDataUpdateCoordinator(hass, base_device)]

Check warning on line 86 in homeassistant/components/iskra/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/__init__.py#L86

Added line #L86 was not covered by tests

for coordinator in coordinators:
await coordinator.async_config_entry_first_refresh()

Check warning on line 89 in homeassistant/components/iskra/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/__init__.py#L88-L89

Added lines #L88 - L89 were not covered by tests

entry.runtime_data = coordinators

Check warning on line 91 in homeassistant/components/iskra/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/__init__.py#L91

Added line #L91 was not covered by tests

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

Check warning on line 93 in homeassistant/components/iskra/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/__init__.py#L93

Added line #L93 was not covered by tests

return True

Check warning on line 95 in homeassistant/components/iskra/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/__init__.py#L95

Added line #L95 was not covered by tests


async def async_unload_entry(hass: HomeAssistant, entry: IskraConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

Check warning on line 100 in homeassistant/components/iskra/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/__init__.py#L100

Added line #L100 was not covered by tests
253 changes: 253 additions & 0 deletions homeassistant/components/iskra/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
"""Config flow for iskra integration."""

from __future__ import annotations

import logging
from typing import Any

from pyiskra.adapters import Modbus, RestAPI
from pyiskra.exceptions import (
DeviceConnectionError,
DeviceTimeoutError,
InvalidResponseCode,
NotAuthorised,
)
from pyiskra.helper import BasicInfo
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_ADDRESS,
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_PROTOCOL,
CONF_USERNAME,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import (
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)


STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PROTOCOL, default="rest_api"): SelectSelector(
SelectSelectorConfig(
options=["rest_api", "modbus_tcp"],
mode=SelectSelectorMode.LIST,
translation_key="protocol",
),
),
}
)

STEP_AUTHENTICATION_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)

# CONF_ADDRESS validation is done later in code, as if ranges are set in voluptuous it turns into a slider
STEP_MODBUS_TCP_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_PORT, default=10001): vol.All(
vol.Coerce(int), vol.Range(min=0, max=65535)
),
vol.Required(CONF_ADDRESS, default=33): NumberSelector(
NumberSelectorConfig(min=1, max=255, mode=NumberSelectorMode.BOX)
),
}
)


async def test_rest_api_connection(host: str, user_input: dict[str, Any]) -> BasicInfo:
"""Check if the RestAPI requires authentication."""

rest_api = RestAPI(ip_address=host, authentication=user_input)
try:
basic_info = await rest_api.get_basic_info()
except NotAuthorised as e:
raise NotAuthorised from e
except (DeviceConnectionError, DeviceTimeoutError, InvalidResponseCode) as e:
raise CannotConnect from e
except Exception as e:
_LOGGER.error("Unexpected exception: %s", e)
raise UnknownException from e

return basic_info


async def test_modbus_connection(host: str, user_input: dict[str, Any]) -> BasicInfo:
"""Test the Modbus connection."""
modbus_api = Modbus(
ip_address=host,
protocol="tcp",
port=user_input[CONF_PORT],
modbus_address=user_input[CONF_ADDRESS],
)

try:
basic_info = await modbus_api.get_basic_info()
except NotAuthorised as e:
raise NotAuthorised from e

Check warning on line 104 in homeassistant/components/iskra/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/config_flow.py#L104

Added line #L104 was not covered by tests
except (DeviceConnectionError, DeviceTimeoutError, InvalidResponseCode) as e:
raise CannotConnect from e
except Exception as e:
_LOGGER.error("Unexpected exception: %s", e)
raise UnknownException from e

return basic_info


class IskraConfigFlowFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for iskra."""

VERSION = 1
host: str
protocol: str

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initiated by the user."""
errors: dict[str, str] = {}
if user_input is not None:
self.host = user_input[CONF_HOST]
self.protocol = user_input[CONF_PROTOCOL]
if self.protocol == "rest_api":
# Check if authentication is required.
try:
device_info = await test_rest_api_connection(self.host, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except NotAuthorised:
# Proceed to authentication step.
return await self.async_step_authentication()
except UnknownException:
errors["base"] = "unknown"
# If the connection was not successful, show an error.

# If the connection was successful, create the device.
if not errors:
return await self._create_entry(
host=self.host,
protocol=self.protocol,
device_info=device_info,
user_input=user_input,
)

if self.protocol == "modbus_tcp":
# Proceed to modbus step.
return await self.async_step_modbus_tcp()

return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
)

async def async_step_authentication(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the authentication step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
device_info = await test_rest_api_connection(self.host, user_input)
# If the connection failed, abort.
except CannotConnect:
errors["base"] = "cannot_connect"

Check warning on line 171 in homeassistant/components/iskra/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/config_flow.py#L171

Added line #L171 was not covered by tests
# If the authentication failed, show an error and authentication form again.
except NotAuthorised:
errors["base"] = "invalid_auth"
except UnknownException:
errors["base"] = "unknown"

Check warning on line 176 in homeassistant/components/iskra/config_flow.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iskra/config_flow.py#L175-L176

Added lines #L175 - L176 were not covered by tests

# if the connection was successful, create the device.
if not errors:
return await self._create_entry(
self.host,
self.protocol,
device_info=device_info,
user_input=user_input,
)

# If there's no user_input or there was an error, show the authentication form again.
return self.async_show_form(
step_id="authentication",
data_schema=STEP_AUTHENTICATION_DATA_SCHEMA,
errors=errors,
)

async def async_step_modbus_tcp(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the Modbus TCP step."""
errors: dict[str, str] = {}

# If there's user_input, check the connection.
if user_input is not None:
# convert to integer
user_input[CONF_ADDRESS] = int(user_input[CONF_ADDRESS])

try:
device_info = await test_modbus_connection(self.host, user_input)

# If the connection failed, show an error.
except CannotConnect:
errors["base"] = "cannot_connect"
except UnknownException:
errors["base"] = "unknown"

# If the connection was successful, create the device.
if not errors:
return await self._create_entry(
host=self.host,
protocol=self.protocol,
device_info=device_info,
user_input=user_input,
)

# If there's no user_input or there was an error, show the modbus form again.
return self.async_show_form(
step_id="modbus_tcp",
data_schema=STEP_MODBUS_TCP_DATA_SCHEMA,
errors=errors,
)

async def _create_entry(
self,
host: str,
protocol: str,
device_info: BasicInfo,
user_input: dict[str, Any],
) -> ConfigFlowResult:
"""Create the config entry."""

await self.async_set_unique_id(device_info.serial)
self._abort_if_unique_id_configured()

return self.async_create_entry(
title=device_info.model,
data={CONF_HOST: host, CONF_PROTOCOL: protocol, **user_input},
)


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""


class UnknownException(HomeAssistantError):
"""Error to indicate an unknown exception occurred."""
25 changes: 25 additions & 0 deletions homeassistant/components/iskra/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Constants for the iskra integration."""

DOMAIN = "iskra"
MANUFACTURER = "Iskra d.o.o"

# POWER
ATTR_TOTAL_APPARENT_POWER = "total_apparent_power"
ATTR_TOTAL_REACTIVE_POWER = "total_reactive_power"
ATTR_TOTAL_ACTIVE_POWER = "total_active_power"
ATTR_PHASE1_POWER = "phase1_power"
ATTR_PHASE2_POWER = "phase2_power"
ATTR_PHASE3_POWER = "phase3_power"

# Voltage
ATTR_PHASE1_VOLTAGE = "phase1_voltage"
ATTR_PHASE2_VOLTAGE = "phase2_voltage"
ATTR_PHASE3_VOLTAGE = "phase3_voltage"

# Current
ATTR_PHASE1_CURRENT = "phase1_current"
ATTR_PHASE2_CURRENT = "phase2_current"
ATTR_PHASE3_CURRENT = "phase3_current"

# Frequency
ATTR_FREQUENCY = "frequency"
Loading
Loading