Skip to content
Open
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ TEST_PLAN.md
BT_ADAPTER_PINNING.md
BT
.vscode/
Decompiled/
OneControlAppLogs/
ha_config/
docker-compose.ha-dev.yml
ha-onecontrol.sln
docs/IDS_CAN_CSHARP_REFERENCE.md
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ During setup, the integration discovers OneControl gateways via BLE advertisemen
| - | - |
| **Push-to-Pair** | Has a physical "Connect" button on the RV control panel |
| **PIN (legacy)** | No Connect button — uses only the 6-digit PIN sticker |
| **CAN-to-Ethernet Bridge (experimental)** | IDS / CAN_TO_ETHERNET_GATEWAY on your local network |


### Push-to-Pair gateways (newer)
Expand Down Expand Up @@ -77,6 +78,24 @@ This approach is experimental. If you are attempting this, use the `pairing\_te
- Flash the pairing helper to the **exact device** that will serve as the proxy — bonds are not transferable between ESP32 units

- OTA-flash the production proxy firmware **without erasing flash** after bonding

## Experimental: CAN-to-Ethernet Bridge

The integration now includes an experimental Ethernet path for older OneControl systems that expose CAN traffic through an IDS CAN-to-Ethernet bridge.

**Important Note on Device Names:** The legacy CAN-to-Ethernet bridge uses an older IDS-CAN protocol that does not appear to support device metadata retrieval over the network. To get proper device names (instead of generic names like "Switch 1"), you must manually import the device names from the official app's diagnostic data.

### How to get your Device Names:
1. Open the official OneControl app on your Android/iOS device while connected to your RV.
2. Generate a diagnostics package through the app settings and email it to yourself.
3. Extract the zip file on your computer and open both `DeviceManifestV1.json` and `DeviceSnapshotV1.json` in a text editor.
4. During the Home Assistant configuration flow, you will be prompted for these files. Copy and paste the entire contents of both files into their respective fields. The integration will use this data to automatically name your devices as they are discovered!

### Setup Instructions:
1. In the HA config flow, select **CAN-to-Ethernet bridge** as the connection type.
2. The integration listens for UDP advertisements and pre-fills host/port when found.
3. If discovery does not find the bridge, enter the host and port manually. Default bridge values are commonly `192.168.1.1` and `6969`.

## Supported Devices

- **Switches** — Relay-controlled devices (lights, water pump, water heaters, tank heater)
Expand Down
18 changes: 16 additions & 2 deletions custom_components/ha_onecontrol/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

import logging
import re
from typing import TYPE_CHECKING

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er

from .const import DOMAIN
from .coordinator import OneControlCoordinator

if TYPE_CHECKING:
from .coordinator import OneControlCoordinator

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -42,7 +45,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"hourmeter",
)
pattern = re.compile(
r"^([0-9a-f]{12})_("
r"^([a-z0-9\.]+)_("
+ "|".join(re.escape(t) for t in _DEVICE_TYPES)
+ r")_([0-9a-f]{2})([0-9a-f]{2})$"
)
Expand Down Expand Up @@ -94,6 +97,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up OneControl from a config entry."""
from .coordinator import OneControlCoordinator

hass.data.setdefault(DOMAIN, {})

existing: OneControlCoordinator | None = hass.data[DOMAIN].get(entry.entry_id)
Expand All @@ -110,6 +115,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

coordinator = OneControlCoordinator(hass, entry)

entry.async_on_unload(entry.add_update_listener(_async_update_listener))

# Store coordinator for platform setup
hass.data[DOMAIN][entry.entry_id] = coordinator
_LOGGER.info(
Expand All @@ -130,6 +137,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
from .coordinator import OneControlCoordinator

unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

if unload_ok:
Expand All @@ -142,3 +151,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_disconnect()

return unload_ok


async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry when options are updated."""
await hass.config_entries.async_reload(entry.entry_id)
47 changes: 16 additions & 31 deletions custom_components/ha_onecontrol/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import OneControlCoordinator
from .entity_helpers import build_gateway_device_info
from .protocol.events import GeneratorStatus

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -87,12 +87,9 @@ def __init__(self, coordinator: OneControlCoordinator, address: str) -> None:
super().__init__(coordinator)
mac = address.replace(":", "").lower()
self._attr_unique_id = f"{mac}_gateway_connected"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, address)},
name=f"OneControl {address}",
manufacturer="Lippert / LCI",
model="BLE Gateway",
connections={("bluetooth", address)},
self._attr_device_info = build_gateway_device_info(
address,
getattr(coordinator, "_connection_type", "ble"),
)

@property
Expand All @@ -114,12 +111,9 @@ def __init__(self, coordinator: OneControlCoordinator, address: str) -> None:
super().__init__(coordinator)
mac = address.replace(":", "").lower()
self._attr_unique_id = f"{mac}_gateway_authenticated"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, address)},
name=f"OneControl {address}",
manufacturer="Lippert / LCI",
model="BLE Gateway",
connections={("bluetooth", address)},
self._attr_device_info = build_gateway_device_info(
address,
getattr(coordinator, "_connection_type", "ble"),
)

@property
Expand All @@ -146,12 +140,9 @@ def __init__(self, coordinator: OneControlCoordinator, address: str) -> None:
super().__init__(coordinator)
mac = address.replace(":", "").lower()
self._attr_unique_id = f"{mac}_in_motion_lockout"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, address)},
name=f"OneControl {address}",
manufacturer="Lippert / LCI",
model="BLE Gateway",
connections={("bluetooth", address)},
self._attr_device_info = build_gateway_device_info(
address,
getattr(coordinator, "_connection_type", "ble"),
)

@property
Expand Down Expand Up @@ -192,12 +183,9 @@ def __init__(self, coordinator: OneControlCoordinator, address: str) -> None:
super().__init__(coordinator)
mac = address.replace(":", "").lower()
self._attr_unique_id = f"{mac}_data_healthy"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, address)},
name=f"OneControl {address}",
manufacturer="Lippert / LCI",
model="BLE Gateway",
connections={("bluetooth", address)},
self._attr_device_info = build_gateway_device_info(
address,
getattr(coordinator, "_connection_type", "ble"),
)

@property
Expand Down Expand Up @@ -236,12 +224,9 @@ def __init__(
self._key = f"{table_id:02x}:{device_id:02x}"
mac = address.replace(":", "").lower()
self._attr_unique_id = f"{mac}_gen_quiet_{device_id:02x}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, address)},
name=f"OneControl {address}",
manufacturer="Lippert / LCI",
model="BLE Gateway",
connections={("bluetooth", address)},
self._attr_device_info = build_gateway_device_info(
address,
getattr(coordinator, "_connection_type", "ble"),
)
self._unsub = coordinator.register_event_callback(self._on_event)

Expand Down
20 changes: 7 additions & 13 deletions custom_components/ha_onecontrol/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import OneControlCoordinator
from .entity_helpers import build_gateway_device_info

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -57,12 +57,9 @@ def __init__(self, coordinator: OneControlCoordinator, address: str) -> None:
super().__init__(coordinator)
mac = address.replace(":", "").lower()
self._attr_unique_id = f"{mac}_clear_lockout"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, address)},
name=f"OneControl {address}",
manufacturer="Lippert / LCI",
model="BLE Gateway",
connections={("bluetooth", address)},
self._attr_device_info = build_gateway_device_info(
address,
getattr(coordinator, "_connection_type", "ble"),
)

@property
Expand Down Expand Up @@ -98,12 +95,9 @@ def __init__(self, coordinator: OneControlCoordinator, address: str) -> None:
super().__init__(coordinator)
mac = address.replace(":", "").lower()
self._attr_unique_id = f"{mac}_refresh_metadata"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, address)},
name=f"OneControl {address}",
manufacturer="Lippert / LCI",
model="BLE Gateway",
connections={("bluetooth", address)},
self._attr_device_info = build_gateway_device_info(
address,
getattr(coordinator, "_connection_type", "ble"),
)

@property
Expand Down
11 changes: 4 additions & 7 deletions custom_components/ha_onecontrol/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

Expand All @@ -42,6 +41,7 @@
HVAC_SETPOINT_DEBOUNCE_S,
)
from .coordinator import OneControlCoordinator
from .entity_helpers import build_gateway_device_info
from .protocol.events import HvacZone

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -118,12 +118,9 @@ def __init__(
self._key = f"{table_id:02x}:{device_id:02x}"
mac = address.replace(":", "").lower()
self._attr_unique_id = f"{mac}_climate_{device_id:02x}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, address)},
name=f"OneControl {address}",
manufacturer="Lippert / LCI",
model="BLE Gateway",
connections={("bluetooth", address)},
self._attr_device_info = build_gateway_device_info(
address,
getattr(coordinator, "_connection_type", "ble"),
)
self._unsub = coordinator.register_event_callback(self._on_event)
self._setpoint_debounce_handle: asyncio.TimerHandle | None = None
Expand Down
Loading