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
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ homeassistant.components.imap.*
homeassistant.components.imgw_pib.*
homeassistant.components.immich.*
homeassistant.components.incomfort.*
homeassistant.components.inels.*
homeassistant.components.input_button.*
homeassistant.components.input_select.*
homeassistant.components.input_text.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ build_from:
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.1
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.1
codenotary:
signer: [email protected]
base_image: [email protected]
cosign:
base_identity: https://github.com/home-assistant/docker/.*
identity: https://github.com/home-assistant/core/.*
Expand Down
95 changes: 95 additions & 0 deletions homeassistant/components/inels/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""The iNELS integration."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from inelsmqtt import InelsMqtt
from inelsmqtt.devices import Device
from inelsmqtt.discovery import InelsDiscovery

from homeassistant.components import mqtt as ha_mqtt
from homeassistant.components.mqtt import (
ReceiveMessage,
async_prepare_subscribe_topics,
async_subscribe_topics,
async_unsubscribe_topics,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady

from .const import LOGGER, PLATFORMS

type InelsConfigEntry = ConfigEntry[InelsData]


@dataclass
class InelsData:
"""Represents the data structure for INELS runtime data."""

mqtt: InelsMqtt
devices: list[Device]


async def async_setup_entry(hass: HomeAssistant, entry: InelsConfigEntry) -> bool:
"""Set up iNELS from a config entry."""

async def mqtt_publish(topic: str, payload: str, qos: int, retain: bool) -> None:
"""Publish an MQTT message using the Home Assistant MQTT client."""
await ha_mqtt.async_publish(hass, topic, payload, qos, retain)

async def mqtt_subscribe(
sub_state: dict[str, Any] | None,
topic: str,
callback_func: Callable[[str, str], None],
) -> dict[str, Any]:
"""Subscribe to MQTT topics using the Home Assistant MQTT client."""

@callback
def mqtt_message_received(msg: ReceiveMessage) -> None:
"""Handle iNELS mqtt messages."""
# Payload is always str at runtime since we don't set encoding=None
# HA uses UTF-8 by default
callback_func(msg.topic, msg.payload) # type: ignore[arg-type]

topics = {
"inels_subscribe_topic": {
"topic": topic,
"msg_callback": mqtt_message_received,
}
}

sub_state = async_prepare_subscribe_topics(hass, sub_state, topics)
await async_subscribe_topics(hass, sub_state)
return sub_state

async def mqtt_unsubscribe(sub_state: dict[str, Any]) -> None:
async_unsubscribe_topics(hass, sub_state)

if not await ha_mqtt.async_wait_for_mqtt_client(hass):
LOGGER.error("MQTT integration not available")
raise ConfigEntryNotReady("MQTT integration not available")

inels_mqtt = InelsMqtt(mqtt_publish, mqtt_subscribe, mqtt_unsubscribe)
devices: list[Device] = await InelsDiscovery(inels_mqtt).start()

# If no devices are discovered, continue with the setup
if not devices:
LOGGER.info("No devices discovered")

entry.runtime_data = InelsData(mqtt=inels_mqtt, devices=devices)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: InelsConfigEntry) -> bool:
"""Unload a config entry."""
await entry.runtime_data.mqtt.unsubscribe_topics()
entry.runtime_data.mqtt.unsubscribe_listeners()

return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
73 changes: 73 additions & 0 deletions homeassistant/components/inels/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Config flow for iNELS."""

from __future__ import annotations

from typing import Any

from homeassistant.components import mqtt
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo

from .const import DOMAIN, TITLE


class INelsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle of iNELS config flow."""

VERSION = 1

async def async_step_mqtt(
self, discovery_info: MqttServiceInfo
) -> ConfigFlowResult:
"""Handle a flow initialized by MQTT discovery."""
if self._async_in_progress():
return self.async_abort(reason="already_in_progress")

# Validate the message, abort if it fails.
if not discovery_info.topic.endswith("/gw"):
# Not an iNELS discovery message.
return self.async_abort(reason="invalid_discovery_info")
if not discovery_info.payload:
# Empty payload, unexpected payload.
return self.async_abort(reason="invalid_discovery_info")

return await self.async_step_confirm_from_mqtt()

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
try:
if not mqtt.is_connected(self.hass):
return self.async_abort(reason="mqtt_not_connected")
except KeyError:
return self.async_abort(reason="mqtt_not_configured")

return await self.async_step_confirm_from_user()

async def step_confirm(
self, step_id: str, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm the setup."""

if user_input is not None:
await self.async_set_unique_id(DOMAIN)
return self.async_create_entry(title=TITLE, data={})

return self.async_show_form(step_id=step_id)

async def async_step_confirm_from_mqtt(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm the setup from MQTT discovered."""
return await self.step_confirm(
step_id="confirm_from_mqtt", user_input=user_input
)

async def async_step_confirm_from_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm the setup from user add integration."""
return await self.step_confirm(
step_id="confirm_from_user", user_input=user_input
)
14 changes: 14 additions & 0 deletions homeassistant/components/inels/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Constants for the iNELS integration."""

import logging

from homeassistant.const import Platform

DOMAIN = "inels"
TITLE = "iNELS"

PLATFORMS: list[Platform] = [
Platform.SWITCH,
]

LOGGER = logging.getLogger(__package__)
61 changes: 61 additions & 0 deletions homeassistant/components/inels/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Base class for iNELS components."""

from __future__ import annotations

from inelsmqtt.devices import Device

from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity

from .const import DOMAIN


class InelsBaseEntity(Entity):
"""Base iNELS entity."""

_attr_should_poll = False
_attr_has_entity_name = True

def __init__(
self,
device: Device,
key: str,
index: int,
) -> None:
"""Init base entity."""
self._device = device
self._device_id = device.unique_id
self._attr_unique_id = self._device_id

# The referenced variable to read from
self._key = key
# The index of the variable list to read from. '-1' for no index
self._index = index

info = device.info()
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.unique_id)},
manufacturer=info.manufacturer,
model=info.model_number,
name=device.title,
sw_version=info.sw_version,
)

async def async_added_to_hass(self) -> None:
"""Add subscription of the data listener."""
# Register the HA callback
self._device.add_ha_callback(self._key, self._index, self._callback)
# Subscribe to MQTT updates
self._device.mqtt.subscribe_listener(
self._device.state_topic, self._device.unique_id, self._device.callback
)

def _callback(self) -> None:
"""Get data from broker into the HA."""
if hasattr(self, "hass"):
self.schedule_update_ha_state()

@property
def available(self) -> bool:
"""Return if entity is available."""
return self._device.is_available
15 changes: 15 additions & 0 deletions homeassistant/components/inels/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"entity": {
"switch": {
"bit": {
"default": "mdi:power-socket-eu"
},
"simple_relay": {
"default": "mdi:power-socket-eu"
},
"relay": {
"default": "mdi:power-socket-eu"
}
}
}
}
13 changes: 13 additions & 0 deletions homeassistant/components/inels/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"domain": "inels",
"name": "iNELS",
"codeowners": ["@epdevlab"],
"config_flow": true,
"dependencies": ["mqtt"],
"documentation": "https://www.home-assistant.io/integrations/inels",
"iot_class": "local_push",
"mqtt": ["inels/status/#"],
"quality_scale": "bronze",
"requirements": ["elkoep-aio-mqtt==0.1.0b4"],
"single_config_entry": true
}
Loading
Loading