Skip to content

Commit

Permalink
Merge 800de4b into a387710
Browse files Browse the repository at this point in the history
  • Loading branch information
crazyfx1 committed Feb 11, 2021
2 parents a387710 + 800de4b commit 3914230
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 238 deletions.
198 changes: 132 additions & 66 deletions home-assistant-plugin/custom_components/xknx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import voluptuous as vol
from xknx import XKNX
from xknx.core.telegram_queue import TelegramQueue
from xknx.devices import DateTime, ExposeSensor
from xknx.dpt import DPTArray, DPTBase, DPTBinary
from xknx.exceptions import XKNXException
from xknx.io import (
Expand All @@ -17,27 +18,34 @@
from xknx.telegram.apci import GroupValueResponse, GroupValueWrite

from homeassistant.const import (
CONF_ENTITY_ID,
CONF_HOST,
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import callback
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ServiceCallType

from .const import DOMAIN, SupportedPlatforms
from .expose import create_knx_exposure
from .factory import create_knx_device
from .schema import (
BinarySensorSchema,
ClimateSchema,
ConnectionSchema,
CoverSchema,
ExposeSchema,
FanSchema,
LightSchema,
NotifySchema,
SceneSchema,
Expand Down Expand Up @@ -67,7 +75,6 @@
SERVICE_XKNX_ATTR_TYPE = "type"
SERVICE_XKNX_ATTR_REMOVE = "remove"
SERVICE_XKNX_EVENT_REGISTER = "event_register"
SERVICE_XKNX_EXPOSURE_REGISTER = "exposure_register"

CONFIG_SCHEMA = vol.Schema(
{
Expand Down Expand Up @@ -130,6 +137,9 @@
vol.Optional(SupportedPlatforms.weather.value): vol.All(
cv.ensure_list, [WeatherSchema.SCHEMA]
),
vol.Optional(SupportedPlatforms.fan.value): vol.All(
cv.ensure_list, [FanSchema.SCHEMA]
),
}
),
)
Expand Down Expand Up @@ -163,40 +173,19 @@
}
)

SERVICE_XKNX_EXPOSURE_REGISTER_SCHEMA = vol.Any(
ExposeSchema.SCHEMA.extend(
{
vol.Optional(SERVICE_XKNX_ATTR_REMOVE, default=False): cv.boolean,
}
),
vol.Schema(
# for removing only `address` is required
{
vol.Required(SERVICE_XKNX_ATTR_ADDRESS): cv.string,
vol.Required(SERVICE_XKNX_ATTR_REMOVE): vol.All(cv.boolean, True),
},
extra=vol.ALLOW_EXTRA,
),
)


async def async_setup(hass, config):
"""Set up the KNX component."""
try:
hass.data[DOMAIN] = KNXModule(hass, config)
hass.data[DOMAIN].async_create_exposures()
await hass.data[DOMAIN].start()
except XKNXException as ex:
_LOGGER.warning("Could not connect to KNX interface: %s", ex)
hass.components.persistent_notification.async_create(
f"Could not connect to KNX interface: <br><b>{ex}</b>", title="KNX"
)

if CONF_XKNX_EXPOSE in config[DOMAIN]:
for expose_config in config[DOMAIN][CONF_XKNX_EXPOSE]:
hass.data[DOMAIN].exposures.append(
create_knx_exposure(hass, hass.data[DOMAIN].xknx, expose_config)
)

for platform in SupportedPlatforms:
if platform.value in config[DOMAIN]:
for device_config in config[DOMAIN][platform.value]:
Expand Down Expand Up @@ -229,14 +218,6 @@ async def async_setup(hass, config):
schema=SERVICE_XKNX_EVENT_REGISTER_SCHEMA,
)

async_register_admin_service(
hass,
DOMAIN,
SERVICE_XKNX_EXPOSURE_REGISTER,
hass.data[DOMAIN].service_exposure_register_modify,
schema=SERVICE_XKNX_EXPOSURE_REGISTER_SCHEMA,
)

async def reload_service_handler(service_call: ServiceCallType) -> None:
"""Remove all KNX components and load new ones from config."""

Expand Down Expand Up @@ -271,7 +252,6 @@ def __init__(self, hass, config):
self.config = config
self.connected = False
self.exposures = []
self.service_exposures = {}

self.init_xknx()
self._knx_event_callback: TelegramQueue.Callback = self.register_callback()
Expand Down Expand Up @@ -314,7 +294,7 @@ def connection_config(self):
if CONF_XKNX_ROUTING in self.config[DOMAIN]:
return self.connection_config_routing()
# config from xknx.yaml always has priority later on
return ConnectionConfig()
return ConnectionConfig(auto_reconnect=True)

def connection_config_routing(self):
"""Return the connection_config if routing is configured."""
Expand All @@ -340,6 +320,34 @@ def connection_config_tunneling(self):
auto_reconnect=True,
)

@callback
def async_create_exposures(self):
"""Create exposures."""
if CONF_XKNX_EXPOSE not in self.config[DOMAIN]:
return
for to_expose in self.config[DOMAIN][CONF_XKNX_EXPOSE]:
expose_type = to_expose.get(ExposeSchema.CONF_XKNX_EXPOSE_TYPE)
entity_id = to_expose.get(CONF_ENTITY_ID)
attribute = to_expose.get(ExposeSchema.CONF_XKNX_EXPOSE_ATTRIBUTE)
default = to_expose.get(ExposeSchema.CONF_XKNX_EXPOSE_DEFAULT)
address = to_expose.get(ExposeSchema.CONF_XKNX_EXPOSE_ADDRESS)
if expose_type.lower() in ["time", "date", "datetime"]:
exposure = KNXExposeTime(self.xknx, expose_type, address)
exposure.async_register()
self.exposures.append(exposure)
else:
exposure = KNXExposeSensor(
self.hass,
self.xknx,
expose_type,
entity_id,
attribute,
default,
address,
)
exposure.async_register()
self.exposures.append(exposure)

async def telegram_received_cb(self, telegram):
"""Call invoked after a KNX telegram was received."""
data = None
Expand Down Expand Up @@ -388,38 +396,6 @@ async def service_event_register_modify(self, call):
group_address,
)

async def service_exposure_register_modify(self, call):
"""Service for adding or removing an exposure to KNX bus."""
group_address = call.data.get(SERVICE_XKNX_ATTR_ADDRESS)

if call.data.get(SERVICE_XKNX_ATTR_REMOVE):
try:
removed_exposure = self.service_exposures.pop(group_address)
except KeyError:
_LOGGER.warning(
"Service exposure_register could not remove exposure for '%s'",
group_address,
)
else:
removed_exposure.shutdown()
return

if group_address in self.service_exposures:
replaced_exposure = self.service_exposures.pop(group_address)
_LOGGER.warning(
"Service exposure_register replacing already registered exposure for '%s' - %s",
group_address,
replaced_exposure.device.name,
)
replaced_exposure.shutdown()
exposure = create_knx_exposure(self.hass, self.xknx, call.data)
self.service_exposures[group_address] = exposure
_LOGGER.debug(
"Service exposure_register registered exposure for '%s' - %s",
group_address,
exposure.device.name,
)

async def service_send_to_knx_bus(self, call):
"""Service for sending an arbitrary KNX message to the KNX bus."""
attr_payload = call.data.get(SERVICE_XKNX_ATTR_PAYLOAD)
Expand All @@ -442,3 +418,93 @@ def calculate_payload(attr_payload):
payload=GroupValueWrite(calculate_payload(attr_payload)),
)
await self.xknx.telegrams.put(telegram)


class KNXExposeTime:
"""Object to Expose Time/Date object to KNX bus."""

def __init__(self, xknx: XKNX, expose_type: str, address: str):
"""Initialize of Expose class."""
self.xknx = xknx
self.expose_type = expose_type
self.address = address
self.device = None

@callback
def async_register(self):
"""Register listener."""
self.device = DateTime(
self.xknx,
name=self.expose_type.capitalize(),
broadcast_type=self.expose_type.upper(),
localtime=True,
group_address=self.address,
)


class KNXExposeSensor:
"""Object to Expose Home Assistant entity to KNX bus."""

def __init__(self, hass, xknx, expose_type, entity_id, attribute, default, address):
"""Initialize of Expose class."""
self.hass = hass
self.xknx = xknx
self.type = expose_type
self.entity_id = entity_id
self.expose_attribute = attribute
self.expose_default = default
self.address = address
self.device = None

@callback
def async_register(self):
"""Register listener."""
if self.expose_attribute is not None:
_name = self.entity_id + "__" + self.expose_attribute
else:
_name = self.entity_id
self.device = ExposeSensor(
self.xknx,
name=_name,
group_address=self.address,
value_type=self.type,
)
async_track_state_change_event(
self.hass, [self.entity_id], self._async_entity_changed
)

async def _async_entity_changed(self, event):
"""Handle entity change."""
new_state = event.data.get("new_state")
if new_state is None:
return
if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
return

if self.expose_attribute is not None:
new_attribute = new_state.attributes.get(self.expose_attribute)
old_state = event.data.get("old_state")

if old_state is not None:
old_attribute = old_state.attributes.get(self.expose_attribute)
if old_attribute == new_attribute:
# don't send same value sequentially
return
await self._async_set_knx_value(new_attribute)
else:
await self._async_set_knx_value(new_state.state)

async def _async_set_knx_value(self, value):
"""Set new value on xknx ExposeSensor."""
if value is None:
if self.expose_default is None:
return
value = self.expose_default

if self.type == "binary":
if value == STATE_ON:
value = True
elif value == STATE_OFF:
value = False

await self.device.set(value)
7 changes: 4 additions & 3 deletions home-assistant-plugin/custom_components/xknx/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@ class ColorTempModes(Enum):
class SupportedPlatforms(Enum):
"""Supported platforms."""

cover = "cover"
light = "light"
binary_sensor = "binary_sensor"
climate = "climate"
switch = "switch"
cover = "cover"
fan = "fan"
light = "light"
notify = "notify"
scene = "scene"
sensor = "sensor"
switch = "switch"
weather = "weather"


Expand Down

0 comments on commit 3914230

Please sign in to comment.