mirror of https://github.com/home-assistant/core
245 lines
8.4 KiB
Python
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,
|
|
)
|