"""Support for KEBA charging stations.""" import asyncio import logging from keba_kecontact.connection import KebaKeContact import voluptuous as vol from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) DOMAIN = "keba" PLATFORMS = (Platform.BINARY_SENSOR, Platform.SENSOR, Platform.LOCK, Platform.NOTIFY) CONF_RFID = "rfid" CONF_FS = "failsafe" CONF_FS_TIMEOUT = "failsafe_timeout" CONF_FS_FALLBACK = "failsafe_fallback" CONF_FS_PERSIST = "failsafe_persist" CONF_FS_INTERVAL = "refresh_interval" MAX_POLLING_INTERVAL = 5 # in seconds MAX_FAST_POLLING_COUNT = 4 CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_RFID, default="00845500"): cv.string, vol.Optional(CONF_FS, default=False): cv.boolean, vol.Optional(CONF_FS_TIMEOUT, default=30): cv.positive_int, vol.Optional(CONF_FS_FALLBACK, default=6): cv.positive_int, vol.Optional(CONF_FS_PERSIST, default=0): cv.positive_int, vol.Optional(CONF_FS_INTERVAL, default=5): cv.positive_int, } ) }, extra=vol.ALLOW_EXTRA, ) _SERVICE_MAP = { "request_data": "async_request_data", "set_energy": "async_set_energy", "set_current": "async_set_current", "authorize": "async_start", "deauthorize": "async_stop", "enable": "async_enable_ev", "disable": "async_disable_ev", "set_failsafe": "async_set_failsafe", } async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Check connectivity and version of KEBA charging station.""" host = config[DOMAIN][CONF_HOST] rfid = config[DOMAIN][CONF_RFID] refresh_interval = config[DOMAIN][CONF_FS_INTERVAL] keba = KebaHandler(hass, host, rfid, refresh_interval) hass.data[DOMAIN] = keba # Wait for KebaHandler setup complete (initial values loaded) if not await keba.setup(): _LOGGER.error("Could not find a charging station at %s", host) return False # Set failsafe mode at start up of Home Assistant failsafe = config[DOMAIN][CONF_FS] timeout = config[DOMAIN][CONF_FS_TIMEOUT] if failsafe else 0 fallback = config[DOMAIN][CONF_FS_FALLBACK] if failsafe else 0 persist = config[DOMAIN][CONF_FS_PERSIST] if failsafe else 0 try: hass.loop.create_task(keba.set_failsafe(timeout, fallback, persist)) except ValueError as ex: _LOGGER.warning("Could not set failsafe mode %s", ex) # Register services to hass async def execute_service(call: ServiceCall) -> None: """Execute a service to KEBA charging station. This must be a member function as we need access to the keba object here. """ function_name = _SERVICE_MAP[call.service] function_call = getattr(keba, function_name) await function_call(call.data) for service in _SERVICE_MAP: hass.services.async_register(DOMAIN, service, execute_service) # Load components for platform in PLATFORMS: hass.async_create_task( discovery.async_load_platform(hass, platform, DOMAIN, {}, config) ) # Start periodic polling of charging station data keba.start_periodic_request() return True class KebaHandler(KebaKeContact): """Representation of a KEBA charging station connection.""" def __init__(self, hass, host, rfid, refresh_interval): """Initialize charging station connection.""" super().__init__(host, self.hass_callback) self._update_listeners = [] self._hass = hass self.rfid = rfid self.device_name = "keba" # correct device name will be set in setup() self.device_id = "keba_wallbox_" # correct device id will be set in setup() # Ensure at least MAX_POLLING_INTERVAL seconds delay self._refresh_interval = max(MAX_POLLING_INTERVAL, refresh_interval) self._fast_polling_count = MAX_FAST_POLLING_COUNT self._polling_task = None def start_periodic_request(self): """Start periodic data polling.""" self._polling_task = self._hass.loop.create_task(self._periodic_request()) async def _periodic_request(self): """Send periodic update requests.""" await self.request_data() if self._fast_polling_count < MAX_FAST_POLLING_COUNT: self._fast_polling_count += 1 _LOGGER.debug("Periodic data request executed, now wait for 2 seconds") await asyncio.sleep(2) else: _LOGGER.debug( "Periodic data request executed, now wait for %s seconds", self._refresh_interval, ) await asyncio.sleep(self._refresh_interval) _LOGGER.debug("Periodic data request rescheduled") self._polling_task = self._hass.loop.create_task(self._periodic_request()) async def setup(self, loop=None): """Initialize KebaHandler object.""" await super().setup(loop) # Request initial values and extract serial number await self.request_data() if ( self.get_value("Serial") is not None and self.get_value("Product") is not None ): self.device_id = f"keba_wallbox_{self.get_value('Serial')}" self.device_name = self.get_value("Product") return True return False def hass_callback(self, data): """Handle component notification via callback.""" # Inform entities about updated values for listener in self._update_listeners: listener() _LOGGER.debug("Notifying %d listeners", len(self._update_listeners)) def _set_fast_polling(self): _LOGGER.debug("Fast polling enabled") self._fast_polling_count = 0 self._polling_task.cancel() self._polling_task = self._hass.loop.create_task(self._periodic_request()) def add_update_listener(self, listener): """Add a listener for update notifications.""" self._update_listeners.append(listener) # initial data is already loaded, thus update the component listener() async def async_request_data(self, param): """Request new data in async way.""" await self.request_data() _LOGGER.debug("New data from KEBA wallbox requested") async def async_set_energy(self, param): """Set energy target in async way.""" try: energy = param["energy"] await self.set_energy(float(energy)) self._set_fast_polling() except (KeyError, ValueError) as ex: _LOGGER.warning("Energy value is not correct. %s", ex) async def async_set_current(self, param): """Set current maximum in async way.""" try: current = param["current"] await self.set_current(float(current)) # No fast polling as this function might be called regularly except (KeyError, ValueError) as ex: _LOGGER.warning("Current value is not correct. %s", ex) async def async_start(self, param=None): """Authorize EV in async way.""" await self.start(self.rfid) self._set_fast_polling() async def async_stop(self, param=None): """De-authorize EV in async way.""" await self.stop(self.rfid) self._set_fast_polling() async def async_enable_ev(self, param=None): """Enable EV in async way.""" await self.enable(True) self._set_fast_polling() async def async_disable_ev(self, param=None): """Disable EV in async way.""" await self.enable(False) self._set_fast_polling() async def async_set_failsafe(self, param=None): """Set failsafe mode in async way.""" try: timeout = param[CONF_FS_TIMEOUT] fallback = param[CONF_FS_FALLBACK] persist = param[CONF_FS_PERSIST] await self.set_failsafe(int(timeout), float(fallback), bool(persist)) self._set_fast_polling() except (KeyError, ValueError) as ex: _LOGGER.warning( ( "Values are not correct for: failsafe_timeout, failsafe_fallback" " and/or failsafe_persist: %s" ), ex, )