core/homeassistant/components/keba/__init__.py

245 lines
8.4 KiB
Python

"""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,
)