Skip to content
Closed
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
85 changes: 85 additions & 0 deletions homeassistant/components/rexense/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""The Rexense integration using external rexense-wsclient library."""

from __future__ import annotations

from functools import partial
import logging

from aiorexense import RexenseWebsocketClient

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.dispatcher import dispatcher_send

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

type RexenseConfigEntry = ConfigEntry[RexenseWebsocketClient]


async def async_setup_entry(
hass: HomeAssistant,
entry: RexenseConfigEntry,
) -> bool:
"""Set up the Rexense integration after the config entry is created."""
host = entry.data[CONF_HOST]
port = entry.data.get(CONF_PORT, 80)
model = entry.data.get(CONF_MODEL, "")
device_id = entry.unique_id or entry.data["device_id"]
sw_build_id = entry.data.get("sw_build_id", "unknown")
feature_map = entry.data.get("feature_map", [])

ws_url = f"ws://{host}:{port}/rpc"
session = aiohttp_client.async_get_clientsession(hass)

# Create the client and configure the Home Assistant dispatcher callback.
client = RexenseWebsocketClient(
device_id=device_id,
model=model,
url=ws_url,
sw_build_id=sw_build_id,
feature_map=feature_map,
session=session,
)

client.on_update = partial(
dispatcher_send, hass, f"{DOMAIN}_{client.device_id}_update"
)
client.signal_update = f"{DOMAIN}_{device_id}_update"

try:
await client.connect()
except Exception as err:
_LOGGER.error(
"Failed to connect to Rexense device %s (%s): %s",
device_id,
host,
err,
)
raise ConfigEntryNotReady from err

entry.runtime_data = client
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client

# Forward to sensor and switch platforms
await hass.config_entries.async_forward_entry_setups(entry, ["sensor"])
_LOGGER.debug("Rexense integration setup complete for device %s", device_id)
return True


async def async_unload_entry(
hass: HomeAssistant,
entry: RexenseConfigEntry,
) -> bool:
"""Unload the Rexense integration and disconnect the client."""
client = entry.runtime_data
unload_ok = await hass.config_entries.async_unload_platforms(
entry, ["sensor"]
)
if unload_ok:
await client.disconnect()
return unload_ok
205 changes: 205 additions & 0 deletions homeassistant/components/rexense/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
"""Config flow for Rexense integration."""

from __future__ import annotations

import logging
from typing import Any

from aiohttp import ClientError
from aiorexense.api import get_basic_info
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_PORT
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo

from .const import DEFAULT_PORT, DOMAIN

_LOGGER = logging.getLogger(__name__)


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

def __init__(self) -> None:
"""Initialize the config flow."""
self.device_data: dict[str, Any] = {}

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle manual user configuration (from UI)."""
errors: dict[str, str] = {}
if user_input is not None:
host = user_input[CONF_HOST]
port = user_input.get(CONF_PORT, DEFAULT_PORT)
session = aiohttp_client.async_get_clientsession(self.hass)
try:
device_id, model, sw_build_id, feature_map = await get_basic_info(
host, port, session
)
except (TimeoutError, ClientError) as err:
_LOGGER.error(
"Error connecting to Rexense device at %s:%s - %s", host, port, err
)
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(device_id)
self._abort_if_unique_id_configured(
updates={CONF_HOST: host, CONF_PORT: port}
)

self.device_data = {
CONF_HOST: host,
CONF_PORT: port,
"device_id": device_id,
CONF_MODEL: model,
"sw_build_id": sw_build_id,
"feature_map": feature_map,
}

return self.async_create_entry(
title=f"{model} ({device_id})",
data=self.device_data,
)

data_schema = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}
)
return self.async_show_form(
step_id="user", data_schema=data_schema, errors=errors
)

async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of an existing entry."""
host: str
port: int
model: str
device_id: str
sw_build_id: str
feature_map: list[Any]

# Retrieve the ConfigEntry being reconfigured
entry_id = self.context.get("entry_id")
if not entry_id:
# Should never happen, but abort if we can't find it
return self.async_abort(reason="unknown")

entry = self.hass.config_entries.async_get_entry(entry_id)
if entry is None:
return self.async_abort(reason="unknown")

# On form submission, write back updated data
if user_input is not None:
host = self.device_data[CONF_HOST]
port = self.device_data.get(CONF_PORT, DEFAULT_PORT)
device_id = str(self.device_data.get("device_id", ""))
model = self.device_data.get(CONF_MODEL, "Rexense Device")
sw_build_id = self.device_data.get("sw_build_id", "")
feature_map = self.device_data.get("feature_map", [])
# Update the entry with new data
self.hass.config_entries.async_update_entry(
entry,
data={
CONF_HOST: host,
CONF_PORT: port,
"device_id": device_id,
CONF_MODEL: model,
"sw_build_id": sw_build_id,
"feature_map": feature_map,
},
)

# Populate self.device_data so downstream steps can read it
host = entry.data[CONF_HOST]
port = entry.data.get(CONF_PORT, DEFAULT_PORT)
model = entry.data.get(CONF_MODEL, "")
device_id = str(entry.data.get("device_id", ""))
sw_build_id = entry.data.get("sw_build_id", "")
feature_map = entry.data.get("feature_map", [])

self.device_data = {
CONF_HOST: host,
CONF_PORT: port,
"device_id": device_id,
CONF_MODEL: model,
"sw_build_id": sw_build_id,
"feature_map": feature_map,
}

data_schema = vol.Schema(
{
vol.Required(CONF_HOST, default=host): cv.string,
vol.Optional(CONF_PORT, default=port): cv.port,
}
)
_LOGGER.debug("Reconfiguring Rexense device %s at %s:%s", device_id, host, port)

return self.async_show_form(
step_id="reconfigure",
data_schema=data_schema,
description_placeholders={
"device_id": device_id,
CONF_MODEL: model,
CONF_PORT: host,
},
errors={},
)

async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle a flow initiated by zeroconf discovery."""
if discovery_info.type != "_rexense._tcp.local.":
return self.async_abort(reason="not_rexense_service")
host = discovery_info.host or (
discovery_info.addresses[0] if discovery_info.addresses else None
)
if not host:
return self.async_abort(reason="no_host_found")
port = discovery_info.port or DEFAULT_PORT

session = aiohttp_client.async_get_clientsession(self.hass)
try:
device_id, model, sw_build_id, feature_map = await get_basic_info(
host, port, session
)
except (TimeoutError, ClientError):
return self.async_abort(reason="cannot_connect")

await self.async_set_unique_id(device_id)
self._abort_if_unique_id_configured(updates={CONF_HOST: host, CONF_PORT: port})

self.device_data = {
CONF_HOST: host,
CONF_PORT: port,
"device_id": device_id,
CONF_MODEL: model,
"sw_build_id": sw_build_id,
"feature_map": feature_map,
}
return await self.async_step_discovery_confirm()

async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the confirmation step for discovered device."""
if user_input is not None:
return self.async_create_entry(
title=f"{self.device_data['model']} ({self.device_data['device_id']})",
data=self.device_data,
)

return self.async_show_form(
step_id="discovery_confirm",
description_placeholders={
CONF_HOST: self.device_data[CONF_HOST],
CONF_MODEL: self.device_data[CONF_MODEL],
},
)
Loading
Loading