After installing everything we need to develop a binding for a new enOcean profile into OpenHAB, let's start to implement such a new profile
OpenHAB binding
OpenHAB has an architecture based on a bus where the OpenHAB runtime components write messages and receive notifications. Bindings are one of those components specifically designed to communicate with a external devices using a certain protocol.
Here is an overview of the OpenHAB architecture
There is a tutorial here that provides an in-depth description of how to create a new binding. However, I'm not going to create a new binding from scratch but extend the existing Aleoncean binding.
First things first
Before proceeding, I need to select the proper profile to adhere to. After reading through the enOcean equipment profiles document (available here), I selected the D2-01 profile, that is described as "Electronic switches and dimmers with energy".
I am going to implement for the moment only the part that relates to the switches. In particular, I will implement the following frames
CMD 0x01: Actuator set output
This message controls switching / dimming of one or all channels of an actuator
CMD 0x03: Actuator status query
This message requests the status of one or all channels of an actuator
CMD 0x04: Actuator status response
This message is sent by an actuator if one of the following event occurs
- 1. status of a channel has changed
- 2. a "Actuator status query" message has been received
Implementing the enOcean profile
Important note: here I'm NOT referrig to the Aleoncean binding library (available here) but the Aleoncean library (available here) that provides the framework the binding has been built on
To implement the new enOcean profile, I need to modify the Aleoncean library. First of all, let's setup the development environment
- 1. Since I've not been able to import the Aleoncean library project into Eclipse, I downloaded and installed Maven to build it (see this tutorial for a detailed explanation about how to install Maven)
- 2. download the Aleoncean library from here. Unzip the file (for example in C:\Documents and Settings\Administrator\aleoncean-master)
- 3. open a command window in the Aleoncean source directory (C:\Documents and Settings\Administrator\aleoncean-master) and type "mvn". The library should build without errors
Now we can start with the changes to the source code. I created a new class name RemoteDeviceEEPD20100, which extends the StandardDevice class and implements the RemoteDevice interface.
First of all, I will override the method to access a specific device value based on the settings in the items configuration file. The Aleoncean binding communicates with the device classes through methods called
@Override
public Object getByParameter(final DeviceParameter parameter) throws IllegalDeviceParameterException;
@Override
public void setByParameter(final DeviceParameter parameter, final Object value) throws IllegalDeviceParameterException;
For example, if in the items configuration file there is a line like this
Switch Input_Flame "Flame detected" (gGF) {aleoncean="REMOTEID=01:85:3A:3A,TYPE=RD_D2-01-00,PARAMETER=SWITCH_0"}
the binding will create the proper device class based on the content of the TYPE field and invoke the getDeviceParameter method using the content of the PARAMETER field as argument
getDeviceParameter(DeviceParameter.SWITCH_0);
to get the current status of the actuator 0. The same happens when the binding needs to set the value
setDeviceParameter(DeviceParameter.SWITCH_0, true);
First of all, let's add the new values to the DeviceParameter enum
public enum DeviceParameter {
BUTTON_DIM_A,
BUTTON_DIM_B,
...
TEMPERATURE_CONTROL_CUR_TEMP,
WINDOW_HANDLE_POSITION,
// New code begins here
SWITCH_0,
SWITCH_1,
SWITCH_2,
SWITCH_3,
SWITCH_4,
SWITCH_5,
SWITCH_6,
SWITCH_7,
SWITCH_8,
SWITCH_9,
SWITCH_10,
SWITCH_11,
SWITCH_12,
SWITCH_13,
SWITCH_14,
SWITCH_15,
SWITCH_16,
SWITCH_17,
SWITCH_18,
SWITCH_19,
SWITCH_20,
SWITCH_21,
SWITCH_22,
SWITCH_23,
SWITCH_24,
SWITCH_25,
SWITCH_26,
SWITCH_27,
SWITCH_28,
SWITCH_29,
// New code ends here
TMP_RECV_SERVICE_ON,
TMP_RECV_ENERGY_INPUT_ENABLED,
I added 30 switches because this is the maximum allows by the enOcean protocol. Real sensor will typically have much less switches.
I then added a member variable to store actuators status
private boolean on[];
which is initialized in the constructor
public RemoteDeviceEEPD20100(final ESP3Connector conn,
final EnOceanId addressRemote,
final EnOceanId addressLocal) {
super(conn, addressRemote, addressLocal);
on = new boolean[MAX_OUTPUTS];
}
Next, I implemented the access methods that get and set the status of the actuators
private boolean isOn(int index) {
if ((index < 0) || (index >= MAX_OUTPUTS))
return false;
return on[index];
}
private void setOn(final DeviceParameterUpdatedInitiation initiation, final IOChannel channel, final Boolean on) {
int minIdx = getIndexOfIOChannel(IOChannel.OUTPUT_CHANNEL_00);
int maxIdx = getIndexOfIOChannel(IOChannel.OUTPUT_CHANNEL_1D);
int index = getIndexOfIOChannel(channel);
if ((index < minIdx) || (index > maxIdx))
return;
final Boolean oldOn = this.on[index];
this.on[index] = on;
fireParameterChanged(getDeviceParameterSwitch(index), initiation, oldOn, on);
}
I can now implement the getByParameter and setByParameter
@Override
public Object getByParameter(final DeviceParameter parameter) throws IllegalDeviceParameterException {
switch (parameter) {
case ENERGY_WS:
return getEnergy();
case POWER_W:
return getPower();
case SWITCH:
return isOn(0);
default:
int minIdx = getIndexOfDeviceParameter(DeviceParameter.SWITCH_0);
int maxIdx = getIndexOfDeviceParameter(DeviceParameter.SWITCH_29);
int index = getIndexOfDeviceParameter(parameter);
if ((index >= minIdx) && (index < maxIdx))
return isOn(index-minIdx);
return super.getByParameter(parameter);
}
}
@Override
public void setByParameter(final DeviceParameter parameter, final Object value) throws IllegalDeviceParameterException {
assert DeviceParameter.getSupportedClass(parameter).isAssignableFrom(value.getClass());
LOGGER.debug("Invoking RemoteDeviceEEPD20100::setByParameter {} {}", parameter, value);
switch (parameter) {
case SWITCH:
switchOnOff(DeviceParameterUpdatedInitiation.SET_PARAMETER, DeviceParameter.SWITCH_0, (Boolean) value);
break;
default:
int minIdx = getIndexOfDeviceParameter(DeviceParameter.SWITCH_0);
int maxIdx = getIndexOfDeviceParameter(DeviceParameter.SWITCH_29);
int index = getIndexOfDeviceParameter(parameter);
if ((index >= minIdx) && (index <= maxIdx))
switchOnOff(DeviceParameterUpdatedInitiation.SET_PARAMETER, parameter, (Boolean) value);
else
super.setByParameter(parameter, value);
}
}
The setByParameter method call the switchOnOff method, that created the radio packet and sent it out. This method creates an instance of the UserDataEEPD201CMD01 class, which has the logic to create the stream of bytes to send over the air
public void switchOnOff(final DeviceParameterUpdatedInitiation initiation, final DeviceParameter channel, final boolean on) {
int index = getIndexOfDeviceParameter(channel) - getIndexOfDeviceParameter(DeviceParameter.SWITCH_0);
LOGGER.warn("Invoking RemoteDeviceEEPD20100::switchOnOff {}, {}", channel, index);
UserDataEEPD201CMD01 userData = new UserDataEEPD201CMD01();
userData.setDimValue(DimValue.SWITCH_TO_NEW_OUT_VALUE);
userData.setIOChannel(getIOChannel(index));
userData.setOutputValueOnOff(on);
send(userData);
setOn(initiation, getIOChannel(index), on);
}
The last feature to implement is to handle the incoming radio packet. I overrode the parseRadioPacket method. This method is called by the Aleoncean binding whenever a radio packet is received. This is my implementation
@Override
public void parseRadioPacket(RadioPacket packet) {
if (!packet.getSenderId().equals(getAddressRemote())) {
LOGGER.warn("Got a package that sender ID does not fit (senderId={}, expected={}).",
packet.getSenderId(), getAddressRemote());
return;
}
if (packet instanceof RadioPacketVLD) {
parseRadioPacketVLD((RadioPacketVLD) packet);
} else if (packet instanceof RadioPacketUTE) {
parseRadioPacketUTE((RadioPacketUTE) packet);
} else {
LOGGER.warn("Don't know how to handle radio choice 0x%02X.", packet.getChoice());
}
}
private void parseRadioPacketVLD(final RadioPacketVLD packet) {
final UserDataEEPD201 userData = UserDataEEPD201Factory.createFromUserDataRaw(packet.getUserDataRaw());
if (userData instanceof UserDataEEPD201CMD01
|| userData instanceof UserDataEEPD201CMD02
|| userData instanceof UserDataEEPD201CMD03
|| userData instanceof UserDataEEPD201CMD05
|| userData instanceof UserDataEEPD201CMD06) {
LOGGER.warn("This command (0x%02X) shoule be sent to an actuator... Skip it.", userData.getCmd());
} else if (userData instanceof UserDataEEPD201CMD04) {
LOGGER.warn("Actuator status response received");
handleIncomingActuatorStatusResponse((UserDataEEPD201CMD04) userData);
} else if (userData instanceof UserDataEEPD201CMD07) {
LOGGER.warn("Actuator measurement response received");
handleIncomingActuatorMeasurementResponse((UserDataEEPD201CMD07) userData);
} else {
LOGGER.warn("Unexpected user data received (CMD=0x%02X).", userData.getCmd());
}
}
public void handleIncomingActuatorStatusResponse(final UserDataEEPD201CMD04 userData) {
try {
handleIncomingOutputValue(userData.getIOChannel(), userData.getOutputValueOnOff());
} catch (UserDataScaleValueException ex) {
LOGGER.warn("Something went wrong on status response handling.", ex);
}
}
public void handleIncomingOutputValue(IOChannel channel, boolean on) {
LOGGER.warn("{} - Received new output value: {} for channel: {}", getAddressRemote(), on, channel);
setOn(DeviceParameterUpdatedInitiation.RADIO_PACKET, channel, on);
}
The last step is to make the library aware of the new profile, we need to change the DeviceFactory class. First, let's add the string that will identify the new profile in the items definition file
private static final String RD_A50802 = "RD_A5-08-02";
private static final String RD_A52001 = "RD_A5-20-01";
private static final String RD_D20100 = "RD_D2-01-00"; //New line
private static final String RD_D20108 = "RD_D2-01-08";
private static final String RD_F60201 = "RD_F6-02-01";
and add the profile to the map of known profiles
MAP.put(RD_A50401, RemoteDeviceEEPA50401.class);
MAP.put(RD_A50802, RemoteDeviceEEPA50802.class);
MAP.put(RD_A52001, RemoteDeviceEEPA52001.class);
MAP.put(RD_D20100, RemoteDeviceEEPD20100.class); //New line
MAP.put(RD_D20108, RemoteDeviceEEPD20108.class);
MAP.put(RD_F60201, RemoteDeviceEEPF60201.class);
That's all as far as implementation of the new profile is concerned. Now we can build the library by running the command "mvn" from the directory where the Aleoncean source code has been unzipped (this is the directory that contains the pom.xml file, in this tutorial is "C:\Documents and Settings\Administrator\aleoncean-master").
If everything is ok, the new library will be created in the "target" folder.
Copy the file "aleoncean-0.0.1-SNAPSHOT-jar-with-dependencies.jar" in the folder "C:\Documents and Settings\Administrator\OpenHAB\org.openhab.binding.aleoncean\lib".
Build the OpenHAB runtime to include the changes to the Aleoncean library.
In next post I will try to send command to my Raspberry board.. stay tuned! For the moment, I attached the complete source code for the Aleoncean library, Aleoncean binding and OpenHAB (the latter has only the differences with the git version, so first download and install OpenHAB source from official git repository, then apply these patches)