Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Keba charging station/wallbox as component #24484

Merged
merged 28 commits into from Aug 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a282e7b
Add Keba charging station wallbox component
dannerph Jun 11, 2019
88bf0ee
Added start/stop commands (ena 0 and ena 1)
dannerph Jun 12, 2019
3fa430a
added refresh_interval parameter and fixed authorization
dannerph Jul 6, 2019
60abecf
fixed max line length
dannerph Jul 6, 2019
2305759
deactivate failsafe mode if not set in configuration
dannerph Jul 6, 2019
3805ec6
extracted I/O code to pypi library
dannerph Jul 18, 2019
c2c3611
updated services.yaml
dannerph Jul 18, 2019
6c1f158
pinned version of requirements
dannerph Jul 18, 2019
d4bc8e4
fixed typos, indent and comments
dannerph Jul 20, 2019
4023518
simplified sensor generation, fixed unique_id and name of sensors
dannerph Jul 20, 2019
26d7abb
cleaned up data extraction
dannerph Jul 20, 2019
9cd9092
flake8 fixes
dannerph Jul 20, 2019
ad7079c
added fast polling, fixed unique_id, code cleanup
dannerph Jul 21, 2019
f01508e
updated requirements
dannerph Jul 21, 2019
4646056
fixed pylint
dannerph Jul 21, 2019
d2915b0
integrated code styling suggestions
dannerph Aug 7, 2019
9d31bb4
fixed pylint
dannerph Aug 7, 2019
8d33aa0
code style changes according to suggestions and pylint fixes
dannerph Aug 7, 2019
c46672b
formatted with black
dannerph Aug 7, 2019
3e53460
clarefied variables
dannerph Aug 8, 2019
375fdd3
Update homeassistant/components/keba/__init__.py
dannerph Aug 8, 2019
b020fce
Update homeassistant/components/keba/__init__.py
dannerph Aug 8, 2019
648eaa1
Update homeassistant/components/keba/__init__.py
dannerph Aug 8, 2019
2dc9453
Update homeassistant/components/keba/__init__.py
dannerph Aug 8, 2019
9a58535
fixed behaviour if no charging station was found
dannerph Aug 8, 2019
dffe0c4
Merge branch 'keba-charging-station' of https://github.com/dannerph/h…
dannerph Aug 8, 2019
99f7684
fix pylint
dannerph Aug 8, 2019
a381282
Update homeassistant/components/keba/__init__.py
dannerph Aug 19, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Expand Up @@ -309,6 +309,7 @@ omit =
homeassistant/components/joaoapps_join/*
homeassistant/components/juicenet/*
homeassistant/components/kankun/switch.py
homeassistant/components/keba/*
homeassistant/components/keenetic_ndms2/device_tracker.py
homeassistant/components/keyboard/*
homeassistant/components/keyboard_remote/*
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -143,6 +143,7 @@ homeassistant/components/ipma/* @dgomes
homeassistant/components/iqvia/* @bachya
homeassistant/components/irish_rail_transport/* @ttroy50
homeassistant/components/jewish_calendar/* @tsvi
homeassistant/components/keba/* @dannerph
homeassistant/components/knx/* @Julius2342
homeassistant/components/kodi/* @armills
homeassistant/components/konnected/* @heythisisnate
Expand Down
229 changes: 229 additions & 0 deletions homeassistant/components/keba/__init__.py
@@ -0,0 +1,229 @@
"""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
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
dannerph marked this conversation as resolved.
Show resolved Hide resolved

_LOGGER = logging.getLogger(__name__)

DOMAIN = "keba"
SUPPORTED_COMPONENTS = ["binary_sensor", "sensor", "lock"]

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": "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, config):
"""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):
"""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 domain in SUPPORTED_COMPONENTS:
hass.async_create_task(
discovery.async_load_platform(hass, domain, 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):
"""Constructor."""
super().__init__(host, self.hass_callback)

self._update_listeners = []
self._hass = hass
self.rfid = rfid
self.device_name = "keba_wallbox_"

# 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:
self.device_name = f"keba_wallbox_{self.get_value('Serial')}"
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_set_energy(self, param):
"""Set energy target in async way."""
try:
energy = param["energy"]
await self.set_energy(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(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:
timout = param[CONF_FS_TIMEOUT]
fallback = param[CONF_FS_FALLBACK]
persist = param[CONF_FS_PERSIST]
await self.set_failsafe(timout, fallback, persist)
self._set_fast_polling()
except (KeyError, ValueError) as ex:
_LOGGER.warning(
"failsafe_timeout, failsafe_fallback and/or "
"failsafe_persist value are not correct. %s",
ex,
)
108 changes: 108 additions & 0 deletions homeassistant/components/keba/binary_sensor.py
@@ -0,0 +1,108 @@
"""Support for KEBA charging station binary sensors."""
import logging

from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_PLUG,
DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_SAFETY,
)

from . import DOMAIN

_LOGGER = logging.getLogger(__name__)


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the KEBA charging station platform."""
if discovery_info is None:
return

keba = hass.data[DOMAIN]

sensors = [
KebaBinarySensor(keba, "Online", "Wallbox", DEVICE_CLASS_CONNECTIVITY),
KebaBinarySensor(keba, "Plug", "Plug", DEVICE_CLASS_PLUG),
KebaBinarySensor(keba, "State", "Charging state", DEVICE_CLASS_POWER),
KebaBinarySensor(keba, "Tmo FS", "Failsafe Mode", DEVICE_CLASS_SAFETY),
]
async_add_entities(sensors)


class KebaBinarySensor(BinarySensorDevice):
"""Representation of a binary sensor of a KEBA charging station."""

def __init__(self, keba, key, sensor_name, device_class):
"""Initialize the KEBA Sensor."""
self._key = key
self._keba = keba
self._name = sensor_name
self._device_class = device_class
self._is_on = None
self._attributes = {}

@property
def should_poll(self):
"""Deactivate polling. Data updated by KebaHandler."""
return False

@property
def unique_id(self):
"""Return the unique ID of the binary sensor."""
return f"{self._keba.device_name}_{self._name}"

@property
def name(self):
"""Return the name of the device."""
return self._name

@property
def device_class(self):
"""Return the class of this sensor."""
return self._device_class

@property
def is_on(self):
"""Return true if sensor is on."""
return self._is_on

@property
def device_state_attributes(self):
"""Return the state attributes of the binary sensor."""
return self._attributes

async def async_update(self):
"""Get latest cached states from the device."""
if self._key == "Online":
self._is_on = self._keba.get_value(self._key)

elif self._key == "Plug":
self._is_on = self._keba.get_value("Plug_plugged")
self._attributes["plugged_on_wallbox"] = self._keba.get_value(
"Plug_wallbox"
)
self._attributes["plug_locked"] = self._keba.get_value("Plug_locked")
self._attributes["plugged_on_EV"] = self._keba.get_value("Plug_EV")

elif self._key == "State":
self._is_on = self._keba.get_value("State_on")
self._attributes["status"] = self._keba.get_value("State_details")
self._attributes["max_charging_rate"] = str(
self._keba.get_value("Max curr")
)

elif self._key == "Tmo FS":
self._is_on = not self._keba.get_value("FS_on")
self._attributes["failsafe_timeout"] = str(self._keba.get_value("Tmo FS"))
self._attributes["fallback_current"] = str(self._keba.get_value("Curr FS"))
elif self._key == "Authreq":
self._is_on = self._keba.get_value(self._key) == 0

def update_callback(self):
"""Schedule a state update."""
self.async_schedule_update_ha_state(True)

async def async_added_to_hass(self):
"""Add update callback after being added to hass."""
self._keba.add_update_listener(self.update_callback)