KNX Interface

Smart Home Connect allows to interconnect with KNX home automation systems. Actually, it has initially been built for providing a web interface and additional connectivity for a KNX bus system. For connecting to a KNX system, it requires the knxdclient Python library and a running KNXD (KNX deamon, formerly known as EIBD). knxdclient is an asynchronous Python implementation of KNXD’s native client protocol (or at least relevant parts of it) and was originally developed as part of SHC.

Setting up KNXD

KNXD is curently only available for unixoid operating systems (esp. Linux). However, you don’t have to run the KNXD on the same machine as SHC, as the knxdclient library can connect to KNXD via a TCP network port. The installation instructions for KNXD depend on the specific OS and distribution:

On modern Debian and Ubuntu systems (≥ Debian 10 buster, including Raspbian, or Ubuntu 20.04 focal), KNXD is packaged in the official software repositories and can be installed with a simple:

sudo apt install knxd

For other systems, head over to https://github.com/knxd/knxd, select the git branch for your specific distribution and follow the instructions in the Building section of the README.md file.

Afterwards, KNXD must be configured to be started as a service with the correct command line arguments to connect to your KNX bus via some interface (typically IP or USB) and provide a “local-listener” on a TCP port or UNIX socket for SHC to connect to. A reference of all available arguments can be found at https://github.com/knxd/knxd/wiki/Command-line-parameters. The Debian and Ubuntu packages already include a systemd unit to run KNXD as a service with a local listener at TCP port 6720 and /run/knx. Additional commandline arguments for KNXD (e.g. for specifying the bus interface) can be configured in the /etc/default/knxd on these Linux distributions.

Configuration Examples

The following example demonstrates a minimal configuration for connecting an SHC bool variable to KNX group address 1/2/4 with datapoint type 1.xxx (which is typical for controlling light switches):

import shc
import shc.interfaces.knx
from shc.interfaces.knx import KNXGAD

knx_connection = shc.interfaces.knx.KNXConnector(host='localhost', port=6720)  # Connect to KNXD

my_group_address = knx_connection.group(KNXGAD(1,2,4), '1')  # datapoint type (dtp) '1' is a general bool (1.xxx)

my_variable = shc.Variable(bool, "my cool variable")\
    .connect(my_group_address)

# Start SHC event loop
shc.main()

Group Read Telegrams

If you have a device in your KNX system, which responds to group read telegrams (i.e. (R)EAD flag is set), you can initialize the value of the variable with the current state from the KNX system by using the init parameter of KNXConnector.group():

my_group_address = knx_connection.group(KNXGAD(1,2,4), '1', init=True)

It tells the KNXConnector to send a group read telegram to this group address upon startup of SHC. The response telegram will be consumed by SHC in the same way as usual group write telegrams.

SHC can respond to group read telegrams from other KNX devices itself by sending the current value of a Readable object. To enable this functionality, simply provide a default provider (see Connectable Objects) to the group address object, for example using the read and provide parameters of shc.base.Connectable.connect():

my_group_address = knx_connection.group(KNXGAD(1,2,4), '1')

my_variable = shc.Variable(bool, "my cool variable")\
    .connect(my_group_address, provide=True)

Datapoint Types

If the KNX group address represents a blind control (UP/DOWN), it is typically represented as KNX datapoint type ‘1.008’ (DPT_UpDown). This is only a special interpretation of a dpt ‘1.xxx’, so it is binary-compatible to this type. However, at least the author of this text often confuses the values, so there’s special support for this type in SHC:

my_blind_group_address = knx_connection.group(KNXGAD(1,2,6), '1.008')

On the KNX side, this group address object behaves just like the one defined above. On the SHC side, it does not take plain bool values, but instead members of the KNXUpDown enum, which are UP and DOWN. To satisfy SHC’s type checking, we must only connect it to other Connectable objects of type KNXUpDown or use the convert functionality (see Typing of Connectable Objects).

For a full list of supported KNX datapoint types and the corresponding SHC Python types, see KNXConnector.group().

Warning

For KNX group addresses, that represent events or commands (like Up/Down) instead of state (like On/Off), you typically don’t want to use SHC Variables! Please read the following section for more information.

Stateless Group Objects

In its modular publish-subscribe-based structure, SHC is inherently capable of dealing with stateless group addresses, i.e. group addresses that represent events or commands (like Up/Down) instead of state (like On/Off). However, as noted in the ‘Variables’ section you must not use shc.variables.Variable objects to connect such group addresses to other SHC Connectable objects, like Buttons in the web user interface: A Variable only forwards value updates to all its subscribers if the value changes. Thus, moving your blinds in the same direction twice from a web UI button, for example, will not work with a Variable between the button and the KNX group address.

On the other hand, you also don’t need a variable in such cases, because there is no state to be cached and you will only use Connectable objects that do not need to read the current state (e.g. shc.web.widgets.StatelessButton instead of shc.web.widgets.ToggleButton). So you can simply connect the objects immediately to forward events between them. For example, to move your blind’s down via UI button:

some_button = shc.web.widgets.StatelessButton(shc.interfaces.knx.KNXUpDown.DOWN, "down")
my_blind_group_address = knx_connection.group(KNXGAD(1,2,6), '1.008')

some_button.connect(my_blind_group_address)

… or, to react to an incoming telegram on that group address:

my_blind_group_address = knx_connection.group(KNXGAD(1,2,6), '1.008')

@my_blind_group_address.trigger
@shc.handler()
async def on_blinds_move(direction, origin):
    if direction is shc.interfaces.knx.KNXUpDown.DOWN:
        print("Blinds are moving down")

Central Functions and Status Feedback

The modular structure of SHC also allows us to connect a Variable (or other Connectable objects) to multiple KNX group addresses. This is required for central functions and status-feedback addresses in KNX systems. In doing so, though, you have to be careful not to create any undesired effects, like feedback loops or unwanted central switching telegrams. This is where the send/receive parameters of the connect method method and—in more complicated cases—the shc.misc.TwoWayPipe will become handy.

Let’s assume that you have two central group addresses, one to switch (off) all lights on the ground floor and one for all lights:

central_lights_ground_floor = knx_connection.group(KNXGAD(0,0,5), '1')
central_lights_all = knx_connection.group(KNXGAD(0,0,1), '1')

group_light_kitchen = knx_connection.group(KNXGAD(1,2,1), '1')
group_light_living_room = knx_connection.group(KNXGAD(1,3,1), '1')
...

In the parameterization of the KNX application programs of your switching actuators and wall-mounted switches, you will use the individual lights’ group addresses as primary (send) group address and add the two central group addresses as multiple listening addresses. This way, the switching actuators of each light react to all three addresses and the wall-mounted switches will update their internal state and status LEDs correctly when one of the central functions is used.

In SHC, we can connect to group addresses in the same way, using send=False (resp. receive=False) to turn off forwarding values to KNX group address objects where undesired. But watch out, this simple approach will probably not yet do, what you expect:

# WARNING! This will probably not do, what you want
light_kitchen = shc.Variable(bool)\
    .connect(group_light_kitchen)\
    .connect(central_lights_ground_floor, send=False)\
    .connect(central_lights_all, send=False)

The problem with this approach is that SHC will route value updates from one of the group addresses to the others, in this case from central_lights_ground_floor and central_lights_all to group_light_kitchen: When a telegram is received by central_lights_all, the variable light_kitchen is updated with that value and publishes the new value to all subscribers, including group_light_kitchen, which will send the value as a new telegram the KNX bus. This is not required (because all relevant devices on the KNX bus are configured to listen to the central group address, anyway) and may even be harmful in some situations. While SHC does actually not send value updates back to their origin to prevent internal feedback loops, in this case the two group address objects are considered to be separate objects, so forwarding the value update is not suppressed. This is not considered a bug, as in other scenarios, you might actually want to reach this behaviour.

To prevent the undesired “cross-talk” between the connected group addresses, we can use a shc.misc.TwoWayPipe: It connects a number of Connectable objects to its left end with objects at its right end, without connecting the objects at either side among themselves. In our case, we want to connect the Variable object to all the group address objects without establishing connections between them:

light_kitchen = shc.Variable(bool)\
    .connect(shc.misc.TwoWayPipe(bool)
             .connect_right(group_light_kitchen)
             .connect_right(central_lights_ground_floor, send=False)
             .connect_right(central_lights_all, send=False)
             )

The usual internal feedback prevention of SHC will now ensure that value updates from the variable will only be send to the KNX bus if they did not pass the TwoWayPipe before, i.e. if they did not originate from the KNX bus.

The same trick can be used for status-feedback group addresses: Let’s say, you have a dimmer actuator that has two KNX datapoints: a boolean on/off state and a integer/range variable for the current dimming value. The two datapoints are internally linked, such that sending a false value to the boolean datapoint will set the float datapoint to 0 and so on. The dimmer actor will probably provide additional status-feedback datapoints, which can transmit the current state of both datapoints to separate group addresses when the state changed internally. Let’s further assume that we connected the datapoints to the four group addresses 1/4/1 (on/off), 2/4/1 (on/off state-feedback), 1/4/2 (value), 2/4/2 (value state-feedback). Then we can connect these group addresses to SHC variables in the following way:

dimmer_hallway_onoff = shc.Variable(bool)\
    .connect(shc.misc.TwoWayPipe(bool)
             .connect_right(knx_connection.group(KNXGAD(1,4,1), '1'))              # on/off send
             .connect_right(knx_connection.group(KNXGAD(2,4,1), '1'), send=False)  # on/off state-feedback
             .connect_right(central_lights_ground_floor, send=False)               # on/off central function (as seen above)
             )
dimmer_hallway_value = shc.Variable(shc.datatypes.RangeUInt8)\
    .connect(shc.misc.TwoWayPipe(shc.datatypes.RangeUInt8)
             .connect_right(knx_connection.group(KNXGAD(1,4,2), '5.001'))              # value send
             .connect_right(knx_connection.group(KNXGAD(2,4,2), '5.001'), send=False)  # value state-feedback
             )

Emulating a dimming actuator

With SHC, you can create a KNX dimming actuator yourself, i.e. listen for incoming “Control Dimming” telegrams (KNX datapoint type 3.007) and adjust a percentage Variable in SHC accordingly. This can be used to control non-KNX dimmable devices (such as a Tasmota-based LED strip) using a KNX wall switch with dimming function.

In theory, the KNX “Control Dimming” datatype supports differently sized steps, which can be directly applied to the value, via a FadeStepAdapter. We only need to convert the KNXControlDimming values to FadeStep values (via ConvertSubscription, in the simplest way):

led_strip_brightness = shc.Variable(RangeFloat1, "LED strip brightness")
led_strip_dimming_group = knx_interface.group(KNXGAD(4, 2, 9), "3")

led_strip_brightness\
    .connect(shc.misc.FadeStepAdapter(
        shc.misc.ConvertSubscription(led_strip_dimming_group, FadeStep)))

However, at least some KNX wall switches only send start- and stop commands for dimming, i.e. a single dimming step of +100% or -100%. Then, they expect the dimming actuator to apply this step gradually and stop at the current value when a stop command is received. To emulate this behaviour with SHC, we need to replace the FadeStepAdapter with a FadeStepRamp:

led_strip_brightness = shc.Variable(RangeFloat1, "LED strip brightness")
led_strip_dimming_group = knx_interface.group(KNXGAD(4, 2, 9), "3")

led_strip_brightness\
    .connect(shc.timer.FadeStepRamp(
        shc.misc.ConvertSubscription(led_strip_dimming_group, FadeStep),
        ramp_duration=datetime.timedelta(seconds=5), max_frequency=5.0))
#           ↑ Full range dimming duration: 5 sec           ↑ max value update rate: 5 Hz    (⇒ 25 dimming steps)

interfaces.knx Module Reference

class shc.interfaces.knx.KNXConnector(host: str = 'localhost', port: int = 6720, sock: Optional[str] = None, auto_reconnect: bool = True, read_init_after_reconnect: bool = True, failsafe_start: bool = False)

SHC interface for connecting with a KNX home automation bus via KNX deamon (KNXD).

The interface allows to interact bidirectional with KNX group addresses (i.e. send and receive KNX group write telegrams). For this purpose a Connectable object can be created for each group address, using the group() method.

The connection to the KNX bus is established by using KNXDs native client protocol (not via KNX over UDP protocol), either via TCP port or via UNIX domain socket. Thus, KNXD must be started with either the -i or -u option (or be run by systemd with an appropriate config for taking care of this). By default, the KNXConnector tries to connect to KNXDs default TCP port 6720 at localhost. The parameters host, port, and sock can be used to specify another host/port or connect via a local UNIX domain socket instead.

Parameters:
  • host – Hostname for connecting to KNXD via TCP. Defaults to ‘localhost’

  • port – TCP port where KNXD is listening for client connections at the specified host. Defaults to 6720.

  • sock – Path to the KNXD UNIX domain socket. If given, it is used instead of the TCP connection to host/port.

  • auto_reconnect – If True (default), the interface tries to reconnect automatically with exponential backoff (5 * 1.25^n seconds sleep), when connection to KNXD is lost. Otherwise, the complete SHC system is shut down on connection errors.

  • read_init_after_reconnect – If True (default), the group read telegrams for initialization (according to the init parameter of group()) are resent when reconnecting after a connection loss to compensate for possibly missed value updates on the KNX bus.

  • failsafe_start – If True, the KNXD client allows SHC to start up, even if the connection to KNXD can not be established in the first try. The connection is retried in background with exponential backoff (see auto_reconnect option). Otherwise (default), the first connection attempt on startup is not retried and will shutdown the SHC application on failure, even if auto_reconnect is True.

group(addr: GroupAddress, dpt: str, init: bool = False) KNXGroupVar

Create a Connectable object for sending and receiving KNX telegrams for a given group address.

The returned object is Subscribable for receiving updates (group write and group response telegrams) from the KNX system and Writable to send a new value to the KNX system. It is also optionally Reading. If a default_provider is set (e.g. via the read/provide parameter of connect), this KNXConnector actively responds to group read telegrams from the KNX system by sending a group response with the read value.

To ensure correct data encoding, the KNX datapoint type of the group address must be specified. It must be equal to the datapoint type of other KNX devices’ datapoints which are connected to this group address. The returned Connectable’s type is derived from the KNX datapoint type. The following KNX datapoint types (DPT) are supported:

KNX DPT

Python type

‘1’

bool

‘1.008’

KNXUpDown

‘3’

KNXControlDimming

‘4’

str

‘5’

int

‘5.001’

shc.datatypes.RangeUInt8

‘5.003’

shc.datatypes.AngleUInt8

‘5.004’

shc.datatypes.RangeInt0To100

‘6’

int

‘7’

int

‘8’

int

‘9’

float

‘10’

knxdclient.KNXTime

‘11’

datetime.date

‘12’

int

‘13’

int

‘14’

float

‘16’

str

‘17’

int

‘19’

datetime.datetime

‘20.102’

KNXHVACMode

When group is called multiple times with the same group address, a reference to the same Connectable object is returned. This ensures, that dispatching of incoming messages and local feedback (see below) always work correctly and checks on the origin of a new value don’t behave unexpectedly. However, the datapoint type given in all calls for the same group address must match. Otherwise, a ValueError is raised.

The Connectable object for each group address features an internal local feedback. This means, that every new value written to the object is being published to all other local subscribers, after being transmitted to the KNX system. Thus, the KNX group address behaves “bus-like”, for the connected objects within SHC: When one connected object sends a new value to the KNX bus, its received by all KNX devices and all other connected objects as well, which is important for central functions etc.

Parameters:
  • addr – The KNX group address to connect to, represented as a KNXGAD object

  • dpt – The KNX datapoint type (DPT) number as string according to the table above

  • init – If True, the interface will send a group read telegram to this group address after SHC’s startup. This can be used to initialize subscribed SHC variables with the current value from the KNX system, if there’s a KNX device responding to read requests for this group address (i.e. which has the read flag set on the relevant datapoint).

Returns:

The Connectable object representing the group address

Raises:

ValueError – If group has been called before with the same group address but a different datapoint type

shc.interfaces.knx.KNXGAD

alias of GroupAddress

class shc.interfaces.knx.KNXHVACMode(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

Python enum representation of the KNX datapoint type 20.102 “DPT_HVACMode”, a 8-bit enum of heating/ventilation/AC operating modes.

The value mapping corresponds to KNX’ native value encoding of this datatype.

AUTO = 0
BUILDING_PROTECTION = 4
COMFORT = 1
ECONOMY = 3
STANDBY = 2
class shc.interfaces.knx.KNXUpDown(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

Python enum representation of the KNX datapoint type 1.008 “DPT_UpDown”, a 1-bit value for controlling blinds etc.

Values of this type can also be used as bool values, using the native KNX value mapping (to datapoint type 1.001).

DOWN = True
UP = False
namedtuple shc.interfaces.knx.KNXControlDimming(increase: bool, step_exponent: int)

Python NamedTuple representation of the KNX datapoint type 3.007 “DPT_Control_Dimming” or 3.008 “DPT_Control_Blinds”.

Fields:
  1.  increase (bool) – True: Increase dimmer brightness / lower blinds; False: Decrese dimmer brightness / raise blinds

  2.  step_exponent (int) – 0: Break dimmming action; 1-7 define step size = 2^(1-stepcode)

classmethod from_step(value: FadeStep) KNXControlDimming
property step: FadeStep

Relevant classes from knxdclient

class knxdclient.KNXTime(time: time, weekday: Optional[int])

Python representation of a KNX ‘time of day’ packet. In addition to the actual time, it contains a weekday number (from 0-6).

class knxdclient.GroupAddress(main: int, middle: int, sub: int)

A KNX group address in the three-layer (main/middle/sub) notation