Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for KEBA charging stations."""
2 
3 import asyncio
4 import logging
5 
6 from keba_kecontact.connection import KebaKeContact
7 import voluptuous as vol
8 
9 from homeassistant.const import CONF_HOST, Platform
10 from homeassistant.core import HomeAssistant, ServiceCall
11 from homeassistant.helpers import discovery
13 from homeassistant.helpers.typing import ConfigType
14 
15 _LOGGER = logging.getLogger(__name__)
16 
17 DOMAIN = "keba"
18 PLATFORMS = (Platform.BINARY_SENSOR, Platform.SENSOR, Platform.LOCK, Platform.NOTIFY)
19 
20 CONF_RFID = "rfid"
21 CONF_FS = "failsafe"
22 CONF_FS_TIMEOUT = "failsafe_timeout"
23 CONF_FS_FALLBACK = "failsafe_fallback"
24 CONF_FS_PERSIST = "failsafe_persist"
25 CONF_FS_INTERVAL = "refresh_interval"
26 
27 MAX_POLLING_INTERVAL = 5 # in seconds
28 MAX_FAST_POLLING_COUNT = 4
29 
30 CONFIG_SCHEMA = vol.Schema(
31  {
32  DOMAIN: vol.Schema(
33  {
34  vol.Required(CONF_HOST): cv.string,
35  vol.Optional(CONF_RFID, default="00845500"): cv.string,
36  vol.Optional(CONF_FS, default=False): cv.boolean,
37  vol.Optional(CONF_FS_TIMEOUT, default=30): cv.positive_int,
38  vol.Optional(CONF_FS_FALLBACK, default=6): cv.positive_int,
39  vol.Optional(CONF_FS_PERSIST, default=0): cv.positive_int,
40  vol.Optional(CONF_FS_INTERVAL, default=5): cv.positive_int,
41  }
42  )
43  },
44  extra=vol.ALLOW_EXTRA,
45 )
46 
47 _SERVICE_MAP = {
48  "request_data": "async_request_data",
49  "set_energy": "async_set_energy",
50  "set_current": "async_set_current",
51  "authorize": "async_start",
52  "deauthorize": "async_stop",
53  "enable": "async_enable_ev",
54  "disable": "async_disable_ev",
55  "set_failsafe": "async_set_failsafe",
56 }
57 
58 
59 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
60  """Check connectivity and version of KEBA charging station."""
61  host = config[DOMAIN][CONF_HOST]
62  rfid = config[DOMAIN][CONF_RFID]
63  refresh_interval = config[DOMAIN][CONF_FS_INTERVAL]
64  keba = KebaHandler(hass, host, rfid, refresh_interval)
65  hass.data[DOMAIN] = keba
66 
67  # Wait for KebaHandler setup complete (initial values loaded)
68  if not await keba.setup():
69  _LOGGER.error("Could not find a charging station at %s", host)
70  return False
71 
72  # Set failsafe mode at start up of Home Assistant
73  failsafe = config[DOMAIN][CONF_FS]
74  timeout = config[DOMAIN][CONF_FS_TIMEOUT] if failsafe else 0
75  fallback = config[DOMAIN][CONF_FS_FALLBACK] if failsafe else 0
76  persist = config[DOMAIN][CONF_FS_PERSIST] if failsafe else 0
77  try:
78  hass.loop.create_task(keba.set_failsafe(timeout, fallback, persist))
79  except ValueError as ex:
80  _LOGGER.warning("Could not set failsafe mode %s", ex)
81 
82  # Register services to hass
83  async def execute_service(call: ServiceCall) -> None:
84  """Execute a service to KEBA charging station.
85 
86  This must be a member function as we need access to the keba
87  object here.
88  """
89  function_name = _SERVICE_MAP[call.service]
90  function_call = getattr(keba, function_name)
91  await function_call(call.data)
92 
93  for service in _SERVICE_MAP:
94  hass.services.async_register(DOMAIN, service, execute_service)
95 
96  # Load components
97  for platform in PLATFORMS:
98  hass.async_create_task(
99  discovery.async_load_platform(hass, platform, DOMAIN, {}, config)
100  )
101 
102  # Start periodic polling of charging station data
103  keba.start_periodic_request()
104 
105  return True
106 
107 
108 class KebaHandler(KebaKeContact):
109  """Representation of a KEBA charging station connection."""
110 
111  def __init__(self, hass, host, rfid, refresh_interval):
112  """Initialize charging station connection."""
113  super().__init__(host, self.hass_callbackhass_callback)
114 
115  self._update_listeners_update_listeners = []
116  self._hass_hass = hass
117  self.rfidrfid = rfid
118  self.device_namedevice_name = "keba" # correct device name will be set in setup()
119  self.device_iddevice_id = "keba_wallbox_" # correct device id will be set in setup()
120 
121  # Ensure at least MAX_POLLING_INTERVAL seconds delay
122  self._refresh_interval_refresh_interval = max(MAX_POLLING_INTERVAL, refresh_interval)
123  self._fast_polling_count_fast_polling_count = MAX_FAST_POLLING_COUNT
124  self._polling_task_polling_task = None
125 
127  """Start periodic data polling."""
128  self._polling_task_polling_task = self._hass_hass.loop.create_task(self._periodic_request_periodic_request())
129 
130  async def _periodic_request(self):
131  """Send periodic update requests."""
132  await self.request_data()
133 
134  if self._fast_polling_count_fast_polling_count < MAX_FAST_POLLING_COUNT:
135  self._fast_polling_count_fast_polling_count += 1
136  _LOGGER.debug("Periodic data request executed, now wait for 2 seconds")
137  await asyncio.sleep(2)
138  else:
139  _LOGGER.debug(
140  "Periodic data request executed, now wait for %s seconds",
141  self._refresh_interval_refresh_interval,
142  )
143  await asyncio.sleep(self._refresh_interval_refresh_interval)
144 
145  _LOGGER.debug("Periodic data request rescheduled")
146  self._polling_task_polling_task = self._hass_hass.loop.create_task(self._periodic_request_periodic_request())
147 
148  async def setup(self, loop=None):
149  """Initialize KebaHandler object."""
150  await super().setup(loop)
151 
152  # Request initial values and extract serial number
153  await self.request_data()
154  if (
155  self.get_value("Serial") is not None
156  and self.get_value("Product") is not None
157  ):
158  self.device_iddevice_id = f"keba_wallbox_{self.get_value('Serial')}"
159  self.device_namedevice_name = self.get_value("Product")
160  return True
161 
162  return False
163 
164  def hass_callback(self, data):
165  """Handle component notification via callback."""
166 
167  # Inform entities about updated values
168  for listener in self._update_listeners_update_listeners:
169  listener()
170 
171  _LOGGER.debug("Notifying %d listeners", len(self._update_listeners_update_listeners))
172 
173  def _set_fast_polling(self):
174  _LOGGER.debug("Fast polling enabled")
175  self._fast_polling_count_fast_polling_count = 0
176  self._polling_task_polling_task.cancel()
177  self._polling_task_polling_task = self._hass_hass.loop.create_task(self._periodic_request_periodic_request())
178 
179  def add_update_listener(self, listener):
180  """Add a listener for update notifications."""
181  self._update_listeners_update_listeners.append(listener)
182 
183  # initial data is already loaded, thus update the component
184  listener()
185 
186  async def async_request_data(self, param):
187  """Request new data in async way."""
188  await self.request_data()
189  _LOGGER.debug("New data from KEBA wallbox requested")
190 
191  async def async_set_energy(self, param):
192  """Set energy target in async way."""
193  try:
194  energy = param["energy"]
195  await self.set_energy(float(energy))
196  self._set_fast_polling_set_fast_polling()
197  except (KeyError, ValueError) as ex:
198  _LOGGER.warning("Energy value is not correct. %s", ex)
199 
200  async def async_set_current(self, param):
201  """Set current maximum in async way."""
202  try:
203  current = param["current"]
204  await self.set_current(float(current))
205  # No fast polling as this function might be called regularly
206  except (KeyError, ValueError) as ex:
207  _LOGGER.warning("Current value is not correct. %s", ex)
208 
209  async def async_start(self, param=None):
210  """Authorize EV in async way."""
211  await self.start(self.rfidrfid)
212  self._set_fast_polling_set_fast_polling()
213 
214  async def async_stop(self, param=None):
215  """De-authorize EV in async way."""
216  await self.stop(self.rfidrfid)
217  self._set_fast_polling_set_fast_polling()
218 
219  async def async_enable_ev(self, param=None):
220  """Enable EV in async way."""
221  await self.enable(True)
222  self._set_fast_polling_set_fast_polling()
223 
224  async def async_disable_ev(self, param=None):
225  """Disable EV in async way."""
226  await self.enable(False)
227  self._set_fast_polling_set_fast_polling()
228 
229  async def async_set_failsafe(self, param=None):
230  """Set failsafe mode in async way."""
231  try:
232  timeout = param[CONF_FS_TIMEOUT]
233  fallback = param[CONF_FS_FALLBACK]
234  persist = param[CONF_FS_PERSIST]
235  await self.set_failsafe(int(timeout), float(fallback), bool(persist))
236  self._set_fast_polling_set_fast_polling()
237  except (KeyError, ValueError) as ex:
238  _LOGGER.warning(
239  (
240  "Values are not correct for: failsafe_timeout, failsafe_fallback"
241  " and/or failsafe_persist: %s"
242  ),
243  ex,
244  )
def async_enable_ev(self, param=None)
Definition: __init__.py:219
def async_set_failsafe(self, param=None)
Definition: __init__.py:229
def add_update_listener(self, listener)
Definition: __init__.py:179
def __init__(self, hass, host, rfid, refresh_interval)
Definition: __init__.py:111
def async_disable_ev(self, param=None)
Definition: __init__.py:224
None execute_service(RuntimeEntryData entry_data, UserService service, ServiceCall call)
Definition: manager.py:670
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:59