Skip to content
Draft
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The integration communicates directly with the spa controller over TCP on the lo
- Local, asynchronous TCP communication with the spa controller
- Home Assistant UI configuration flow
- Automatic reconnection after network or bridge interruptions
- Optional Elfin EW11 software restart from Home Assistant
- Current and target water temperature
- Heating state and heat mode
- Ready, Rest and Ready-in-Rest control
Expand Down Expand Up @@ -82,6 +83,7 @@ The exact Elfin configuration interface varies by model and firmware. The adapte
├── config_flow.py
├── const.py
├── diagnostics.py
├── elfin.py
├── event.py
├── manifest.json
├── models.py
Expand Down Expand Up @@ -194,12 +196,15 @@ Some protocol-oriented entities are disabled by default and can be enabled from
Management and diagnostic actions include:

- Restart stream
- Restart Elfin bridge (disabled by default)
- Synchronise clock
- Refresh fault log
- Refresh device configuration
- Clear controller notification
- Advance a pump, blower or light to its next state

The **Restart Elfin bridge** button sends the same local management request as the EW11 web interface's Restart control (`CID 20003`). It is separate from **Restart stream**, which only rebuilds Home Assistant's TCP connection. The current implementation uses the stock EW11 HTTP Basic credentials `admin` / `admin` and is disabled by default so installations using other bridge hardware are unaffected.

### Fault-log event

The fault-log event entity reports decoded controller fault information when a fault entry is received. Event attributes can include the message code, description, severity, controller time and age of the stored entry.
Expand Down Expand Up @@ -257,6 +262,8 @@ Check that:

Verify the serial wiring, polarity and bridge settings. A TCP connection alone does not prove that valid RS-485 data is reaching Home Assistant.

If an EW11 remains reachable on the network and port `4257` accepts a TCP connection but the incoming data is no longer valid `0x7E`-framed spa traffic, enable the diagnostic **Restart Elfin bridge** button and restart the adapter. This performs a software restart of the EW11 without factory-resetting its configuration. The normal background reconnect loop should reconnect to the spa stream once the adapter is back online.

Enable the diagnostic entities and inspect the integration logs:

```yaml
Expand Down
32 changes: 30 additions & 2 deletions custom_components/spa_pool/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.const import CONF_HOST, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
Expand All @@ -36,6 +37,7 @@
MAX_LIGHTS,
MAX_PUMPS,
)
from .elfin import ElfinRestartError, async_restart_elfin
from .models import SpaState
from .protocol import (
SettingsCode,
Expand All @@ -51,6 +53,7 @@ class SpaPoolButtonAction(Enum):
"""Action performed by a stateless Spa Pool button."""

RESTART_STREAM = auto()
RESTART_ELFIN_BRIDGE = auto()
SYNC_CLOCK = auto()
REFRESH_FAULT_LOG = auto()
REFRESH_DEVICE_CONFIGURATION = auto()
Expand All @@ -76,6 +79,15 @@ class SpaPoolButtonEntityDescription(ButtonEntityDescription):
action=SpaPoolButtonAction.RESTART_STREAM,
requires_stream=False,
),
SpaPoolButtonEntityDescription(
key="restart_elfin_bridge",
translation_key="restart_elfin_bridge",
device_class=ButtonDeviceClass.RESTART,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
action=SpaPoolButtonAction.RESTART_ELFIN_BRIDGE,
requires_stream=False,
),
SpaPoolButtonEntityDescription(
key="sync_clock",
translation_key="sync_clock",
Expand Down Expand Up @@ -164,7 +176,6 @@ async def async_setup_entry(
SpaPoolToggleButtonEntity(
entry=entry,
name=f"Light {index + 1} next mode",
# Preserve the unique ID used by the previous light-mode button.
unique_key=f"light_{index + 1}_next_mode",
toggle_item=ToggleItem(ToggleItem.LIGHT_1 + index),
icon="mdi:palette-outline",
Expand Down Expand Up @@ -237,6 +248,10 @@ async def async_press(self) -> None:
await self._async_restart_stream()
return

if action is SpaPoolButtonAction.RESTART_ELFIN_BRIDGE:
await self._async_restart_elfin_bridge()
return

try:
if action is SpaPoolButtonAction.SYNC_CLOCK:
await self._async_sync_clock()
Expand Down Expand Up @@ -288,6 +303,19 @@ async def _async_restart_stream(self) -> None:
"Unable to restart the spa status stream"
) from err

async def _async_restart_elfin_bridge(self) -> None:
"""Restart an Elfin EW11 through its management interface."""

try:
await async_restart_elfin(
async_get_clientsession(self.hass),
str(self._entry.data[CONF_HOST]),
)
except ElfinRestartError as err:
raise HomeAssistantError(
"Unable to restart the Elfin bridge"
) from err

async def _async_sync_clock(self) -> None:
"""Send the current Home Assistant local time to the spa."""

Expand Down
70 changes: 70 additions & 0 deletions custom_components/spa_pool/elfin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Elfin EW11 management helpers."""

from __future__ import annotations

import asyncio
import json
import logging

from aiohttp import BasicAuth, ClientConnectionError, ClientSession, ServerDisconnectedError

_LOGGER = logging.getLogger(__name__)

_ELFIN_RESTART_CID = 20003
_ELFIN_WEB_PORT = 80
_ELFIN_USERNAME = "admin"
_ELFIN_PASSWORD = "admin"
_ELFIN_REQUEST_TIMEOUT = 5.0


class ElfinRestartError(Exception):
"""Raised when the Elfin restart request cannot be sent."""


async def async_restart_elfin(
session: ClientSession,
host: str,
) -> None:
"""Restart an Elfin EW11 through its local management API.

The request mirrors the EW11 web interface's Restart button:
``POST /cmd`` with ``CID 20003`` and an empty payload. Stock EW11
firmware protects the endpoint with HTTP Basic authentication.

A server-side disconnect after the request has been submitted is expected:
the adapter may reboot before it finishes the HTTP response.
"""

url = f"http://{host}:{_ELFIN_WEB_PORT}/cmd"
body = "msg=" + json.dumps(
{"CID": _ELFIN_RESTART_CID, "PL": {}},
separators=(",", ":"),
)

try:
async with asyncio.timeout(_ELFIN_REQUEST_TIMEOUT):
async with session.post(
url,
data=body,
headers={"Content-Type": "application/json;charset=utf-8"},
auth=BasicAuth(_ELFIN_USERNAME, _ELFIN_PASSWORD),
allow_redirects=False,
) as response:
if response.status == 401:
raise ElfinRestartError(
"Elfin management credentials were rejected"
)
if response.status >= 400:
raise ElfinRestartError(
f"Elfin restart request returned HTTP {response.status}"
)
await response.read()
except ServerDisconnectedError:
_LOGGER.debug(
"Elfin %s disconnected while processing the restart request",
host,
)
except (ClientConnectionError, TimeoutError) as err:
raise ElfinRestartError(
f"Unable to reach the Elfin management interface at {host}"
) from err
3 changes: 3 additions & 0 deletions custom_components/spa_pool/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
"restart_stream": {
"name": "Restart stream"
},
"restart_elfin_bridge": {
"name": "Restart Elfin bridge"
},
"sync_clock": {
"name": "Synchronise clock"
},
Expand Down
3 changes: 3 additions & 0 deletions custom_components/spa_pool/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
"restart_stream": {
"name": "Restart stream"
},
"restart_elfin_bridge": {
"name": "Restart Elfin bridge"
},
"sync_clock": {
"name": "Synchronise clock"
},
Expand Down