Setup no longer depends on the bike being reachable. The coordinator is built from an address and resolves the BLEDevice per connect attempt, so a parked or switched-off bike loads the entry instead of raising ConfigEntryNotReady and leaving every entity unavailable. Parser state is persisted through helpers.storage.Store and restored before the platforms are set up, so entities carry their last values and the VIN on their first state write. What survives is a whitelist: battery and EBM counters yes, motor and assist no - a restored speed reading is indistinguishable from live data on a parked bike. The connection switch position is restored too, so a restart no longer wakes a bike the user deliberately disconnected. Also fixes three defects found while building this: - Store.async_delay_save debounces rather than throttles, so re-arming on every notification postponed the write for as long as the bike stayed connected and nothing reached disk except on a clean shutdown. - establish_connection had no disconnected_callback, so a dropped link left _is_connected True: the connectivity sensor lied and the poll never retried. - _connect read self._client back across the 200ms handshake, which a concurrent teardown could clear underneath it. Connecting now starts on the bike's advertisement instead of the next poll tick, and an unreachable bike says why - distinguishing "switched off" from "seen only by a passive proxy that cannot connect". Renames the connection switch to Auto-connect and drops the hardcoded English name on the connectivity sensor, which had been defeating its translation key. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FyabWZzd7HoLpyBwEa5Zzh
100 lines
3.6 KiB
Python
100 lines
3.6 KiB
Python
"""Switch platform for MySmartBike BLE integration."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from homeassistant.components.switch import SwitchEntity
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
|
|
from .const import DOMAIN
|
|
from .coordinator import MySmartBikeCoordinator
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up MySmartBike BLE switch entities."""
|
|
coordinator: MySmartBikeCoordinator = entry.runtime_data
|
|
async_add_entities([MySmartBikeConnectionSwitch(coordinator, entry)])
|
|
|
|
|
|
class MySmartBikeConnectionSwitch(CoordinatorEntity[MySmartBikeCoordinator], SwitchEntity):
|
|
"""Switch to control BLE connection to the bike."""
|
|
|
|
_attr_has_entity_name = True
|
|
|
|
def __init__(
|
|
self,
|
|
coordinator: MySmartBikeCoordinator,
|
|
entry: ConfigEntry,
|
|
) -> None:
|
|
"""Initialize the switch."""
|
|
super().__init__(coordinator)
|
|
self._attr_unique_id = f"{entry.entry_id}_connection"
|
|
# Build device info with optional serial number (VIN)
|
|
self._attr_device_info = {
|
|
"identifiers": {(DOMAIN, entry.entry_id)},
|
|
}
|
|
if coordinator.vin:
|
|
self._attr_device_info["serial_number"] = coordinator.vin
|
|
if coordinator.protocol_version:
|
|
self._attr_device_info["sw_version"] = coordinator.protocol_version
|
|
self._attr_translation_key = "connection"
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Return True - the connection wish can always be changed."""
|
|
return True
|
|
|
|
@property
|
|
def is_on(self) -> bool:
|
|
"""Return True if connection is desired (not manually disconnected).
|
|
|
|
Restored from storage on startup, so a bike the user deliberately
|
|
disconnected is not woken again by a Home Assistant restart.
|
|
"""
|
|
return not self.coordinator._manual_disconnect
|
|
|
|
@property
|
|
def icon(self) -> str:
|
|
"""Return the icon."""
|
|
return "mdi:bluetooth-connect" if self.is_on else "mdi:bluetooth-off"
|
|
|
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
"""Turn on the switch - request connection to the bike.
|
|
|
|
The switch reflects the *wish* to be connected, so it stays on even when
|
|
the bike is currently unreachable - the coordinator keeps retrying.
|
|
"""
|
|
try:
|
|
await self.coordinator.async_reconnect()
|
|
except Exception as ex:
|
|
error_msg = str(ex).lower()
|
|
if "not reachable" in error_msg:
|
|
_LOGGER.warning("Cannot connect - bike not reachable. Will auto-connect when available.")
|
|
else:
|
|
_LOGGER.error("Failed to connect to bike: %s", ex)
|
|
finally:
|
|
self.async_write_ha_state()
|
|
|
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
"""Turn off the switch - disconnect from the bike.
|
|
|
|
WARNING: This will turn off the bike after ~5 minutes! It must be manually
|
|
turned on again or connected to power.
|
|
"""
|
|
_LOGGER.warning("Disconnecting from bike - it will turn off after ~5 minutes")
|
|
try:
|
|
await self.coordinator.async_disconnect()
|
|
self.async_write_ha_state()
|
|
except Exception as ex:
|
|
_LOGGER.error("Failed to disconnect from bike: %s", ex)
|