Skip to content

Commit 2fce7db

Browse files
authored
Add iNELS integration (#125595)
1 parent 6e49911 commit 2fce7db

23 files changed

+1280
-0
lines changed

.strict-typing

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ homeassistant.components.imap.*
278278
homeassistant.components.imgw_pib.*
279279
homeassistant.components.immich.*
280280
homeassistant.components.incomfort.*
281+
homeassistant.components.inels.*
281282
homeassistant.components.input_button.*
282283
homeassistant.components.input_select.*
283284
homeassistant.components.input_text.*

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: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""The iNELS integration."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Callable
6+
from dataclasses import dataclass
7+
from typing import Any
8+
9+
from inelsmqtt import InelsMqtt
10+
from inelsmqtt.devices import Device
11+
from inelsmqtt.discovery import InelsDiscovery
12+
13+
from homeassistant.components import mqtt as ha_mqtt
14+
from homeassistant.components.mqtt import (
15+
ReceiveMessage,
16+
async_prepare_subscribe_topics,
17+
async_subscribe_topics,
18+
async_unsubscribe_topics,
19+
)
20+
from homeassistant.config_entries import ConfigEntry
21+
from homeassistant.core import HomeAssistant, callback
22+
from homeassistant.exceptions import ConfigEntryNotReady
23+
24+
from .const import LOGGER, PLATFORMS
25+
26+
type InelsConfigEntry = ConfigEntry[InelsData]
27+
28+
29+
@dataclass
30+
class InelsData:
31+
"""Represents the data structure for INELS runtime data."""
32+
33+
mqtt: InelsMqtt
34+
devices: list[Device]
35+
36+
37+
async def async_setup_entry(hass: HomeAssistant, entry: InelsConfigEntry) -> bool:
38+
"""Set up iNELS from a config entry."""
39+
40+
async def mqtt_publish(topic: str, payload: str, qos: int, retain: bool) -> None:
41+
"""Publish an MQTT message using the Home Assistant MQTT client."""
42+
await ha_mqtt.async_publish(hass, topic, payload, qos, retain)
43+
44+
async def mqtt_subscribe(
45+
sub_state: dict[str, Any] | None,
46+
topic: str,
47+
callback_func: Callable[[str, str], None],
48+
) -> dict[str, Any]:
49+
"""Subscribe to MQTT topics using the Home Assistant MQTT client."""
50+
51+
@callback
52+
def mqtt_message_received(msg: ReceiveMessage) -> None:
53+
"""Handle iNELS mqtt messages."""
54+
# Payload is always str at runtime since we don't set encoding=None
55+
# HA uses UTF-8 by default
56+
callback_func(msg.topic, msg.payload) # type: ignore[arg-type]
57+
58+
topics = {
59+
"inels_subscribe_topic": {
60+
"topic": topic,
61+
"msg_callback": mqtt_message_received,
62+
}
63+
}
64+
65+
sub_state = async_prepare_subscribe_topics(hass, sub_state, topics)
66+
await async_subscribe_topics(hass, sub_state)
67+
return sub_state
68+
69+
async def mqtt_unsubscribe(sub_state: dict[str, Any]) -> None:
70+
async_unsubscribe_topics(hass, sub_state)
71+
72+
if not await ha_mqtt.async_wait_for_mqtt_client(hass):
73+
LOGGER.error("MQTT integration not available")
74+
raise ConfigEntryNotReady("MQTT integration not available")
75+
76+
inels_mqtt = InelsMqtt(mqtt_publish, mqtt_subscribe, mqtt_unsubscribe)
77+
devices: list[Device] = await InelsDiscovery(inels_mqtt).start()
78+
79+
# If no devices are discovered, continue with the setup
80+
if not devices:
81+
LOGGER.info("No devices discovered")
82+
83+
entry.runtime_data = InelsData(mqtt=inels_mqtt, devices=devices)
84+
85+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
86+
87+
return True
88+
89+
90+
async def async_unload_entry(hass: HomeAssistant, entry: InelsConfigEntry) -> bool:
91+
"""Unload a config entry."""
92+
await entry.runtime_data.mqtt.unsubscribe_topics()
93+
entry.runtime_data.mqtt.unsubscribe_listeners()
94+
95+
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Config flow for iNELS."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
from homeassistant.components import mqtt
8+
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
9+
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
10+
11+
from .const import DOMAIN, TITLE
12+
13+
14+
class INelsConfigFlow(ConfigFlow, domain=DOMAIN):
15+
"""Handle of iNELS config flow."""
16+
17+
VERSION = 1
18+
19+
async def async_step_mqtt(
20+
self, discovery_info: MqttServiceInfo
21+
) -> ConfigFlowResult:
22+
"""Handle a flow initialized by MQTT discovery."""
23+
if self._async_in_progress():
24+
return self.async_abort(reason="already_in_progress")
25+
26+
# Validate the message, abort if it fails.
27+
if not discovery_info.topic.endswith("/gw"):
28+
# Not an iNELS discovery message.
29+
return self.async_abort(reason="invalid_discovery_info")
30+
if not discovery_info.payload:
31+
# Empty payload, unexpected payload.
32+
return self.async_abort(reason="invalid_discovery_info")
33+
34+
return await self.async_step_confirm_from_mqtt()
35+
36+
async def async_step_user(
37+
self, user_input: dict[str, Any] | None = None
38+
) -> ConfigFlowResult:
39+
"""Handle a flow initialized by the user."""
40+
try:
41+
if not mqtt.is_connected(self.hass):
42+
return self.async_abort(reason="mqtt_not_connected")
43+
except KeyError:
44+
return self.async_abort(reason="mqtt_not_configured")
45+
46+
return await self.async_step_confirm_from_user()
47+
48+
async def step_confirm(
49+
self, step_id: str, user_input: dict[str, Any] | None = None
50+
) -> ConfigFlowResult:
51+
"""Confirm the setup."""
52+
53+
if user_input is not None:
54+
await self.async_set_unique_id(DOMAIN)
55+
return self.async_create_entry(title=TITLE, data={})
56+
57+
return self.async_show_form(step_id=step_id)
58+
59+
async def async_step_confirm_from_mqtt(
60+
self, user_input: dict[str, Any] | None = None
61+
) -> ConfigFlowResult:
62+
"""Confirm the setup from MQTT discovered."""
63+
return await self.step_confirm(
64+
step_id="confirm_from_mqtt", user_input=user_input
65+
)
66+
67+
async def async_step_confirm_from_user(
68+
self, user_input: dict[str, Any] | None = None
69+
) -> ConfigFlowResult:
70+
"""Confirm the setup from user add integration."""
71+
return await self.step_confirm(
72+
step_id="confirm_from_user", user_input=user_input
73+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Constants for the iNELS integration."""
2+
3+
import logging
4+
5+
from homeassistant.const import Platform
6+
7+
DOMAIN = "inels"
8+
TITLE = "iNELS"
9+
10+
PLATFORMS: list[Platform] = [
11+
Platform.SWITCH,
12+
]
13+
14+
LOGGER = logging.getLogger(__package__)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Base class for iNELS components."""
2+
3+
from __future__ import annotations
4+
5+
from inelsmqtt.devices import Device
6+
7+
from homeassistant.helpers.device_registry import DeviceInfo
8+
from homeassistant.helpers.entity import Entity
9+
10+
from .const import DOMAIN
11+
12+
13+
class InelsBaseEntity(Entity):
14+
"""Base iNELS entity."""
15+
16+
_attr_should_poll = False
17+
_attr_has_entity_name = True
18+
19+
def __init__(
20+
self,
21+
device: Device,
22+
key: str,
23+
index: int,
24+
) -> None:
25+
"""Init base entity."""
26+
self._device = device
27+
self._device_id = device.unique_id
28+
self._attr_unique_id = self._device_id
29+
30+
# The referenced variable to read from
31+
self._key = key
32+
# The index of the variable list to read from. '-1' for no index
33+
self._index = index
34+
35+
info = device.info()
36+
self._attr_device_info = DeviceInfo(
37+
identifiers={(DOMAIN, device.unique_id)},
38+
manufacturer=info.manufacturer,
39+
model=info.model_number,
40+
name=device.title,
41+
sw_version=info.sw_version,
42+
)
43+
44+
async def async_added_to_hass(self) -> None:
45+
"""Add subscription of the data listener."""
46+
# Register the HA callback
47+
self._device.add_ha_callback(self._key, self._index, self._callback)
48+
# Subscribe to MQTT updates
49+
self._device.mqtt.subscribe_listener(
50+
self._device.state_topic, self._device.unique_id, self._device.callback
51+
)
52+
53+
def _callback(self) -> None:
54+
"""Get data from broker into the HA."""
55+
if hasattr(self, "hass"):
56+
self.schedule_update_ha_state()
57+
58+
@property
59+
def available(self) -> bool:
60+
"""Return if entity is available."""
61+
return self._device.is_available
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"entity": {
3+
"switch": {
4+
"bit": {
5+
"default": "mdi:power-socket-eu"
6+
},
7+
"simple_relay": {
8+
"default": "mdi:power-socket-eu"
9+
},
10+
"relay": {
11+
"default": "mdi:power-socket-eu"
12+
}
13+
}
14+
}
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"domain": "inels",
3+
"name": "iNELS",
4+
"codeowners": ["@epdevlab"],
5+
"config_flow": true,
6+
"dependencies": ["mqtt"],
7+
"documentation": "https://www.home-assistant.io/integrations/inels",
8+
"iot_class": "local_push",
9+
"mqtt": ["inels/status/#"],
10+
"quality_scale": "bronze",
11+
"requirements": ["elkoep-aio-mqtt==0.1.0b4"],
12+
"single_config_entry": true
13+
}

0 commit comments

Comments
 (0)