Skip to content

Instantly share code, notes, and snippets.

@memoos
Last active April 28, 2024 19:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save memoos/7e0c8558029fa771053308591e06df14 to your computer and use it in GitHub Desktop.
Save memoos/7e0c8558029fa771053308591e06df14 to your computer and use it in GitHub Desktop.
Small demo application to emulate Stiebel Eltron FEK

CAN Protocol

According to my current knowledge (credits to [1] for the good docs)

  1. Each device has their own CAN-Id and is sending on that ID only and only handling messages that are send to this receiver address (see receiver address field in payload).

  2. The device send can frames with the length of 7 bytes

  3. In the payload bytes of the can messages seem to be 4 fields: which I call Message type (4 bit), receiver address (11 Bits), data_key (8 or 16 bit) and value (16 bit)

  4. There are 2 types of message stucture:

    Short format: (if byte 2 != 0xfa)

    Byte 0 0 0 1 2 3 4 5 6
    Bit 7654 3210 7 6543210 all all all
    Meaning receiver bits 10-7 msg type ? receiver bits 6-0 data_key value hi value lo zero zero

    Long format: (if byte 2 == 0xfa)

    Byte 0 0 1 1 2 3 4 5 6
    Bit 7654 3210 7 6543210 all all all all all
    Meaning receiver bits 10-7 msg type ? receiver bits 6-0 0xfa data_key hi data_key lo value hi value lo
  5. message type

    According to [1] there are the following types:

    • 0: write
    • 1: read request
    • 2: read response
    • 3: ack
    • 4: write ack
    • 5: write response
    • 6: system
    • 7: system response

    I personally only observed types 0, 1, 2, 6 and 7

    • write messages are used to change a value
    • read request messages are used to request data from an other device
    • read response is send by the other device in response to the read request with the requested data_key and the current value
    • system: initialisation request/heartbeat messages are send with that message type
    • system response: used for responses by a device to initialisation/heartbeat requests
  6. Reveiver

    The receiver CAN-Id is encoded divided into 2 parts in bytes 0 and 1 of the payload. For the lower 7 bits there is a special meaning if all bits are set (byte 1 has value 0x7F), which means that the message is addressed to all receivers matching the their first 4 bits (?).

  7. data_key

    A good list of available datakeys and their meaning can be found at [2]. Unfortunately it seems to miss some keys of newer WPM models. In this context the following keys seems to be of relevance:

    • 0xfe: INITIALISATION: Value is typically 0x0100, some devices also send 0x0200 after first initialisation/heartbeat
    • 0x0005: ROOM SET TEMPERATURE in 0.1°C for old WPM models
    • 0x0008: ROOM SET TEMPERATURE night in 0.1°C for old WPM models
    • 0x0011: ACTUAL ROOM TEMPERATURE in 0.1°C for old WPM models
    • 0x0199: Software number
    • 0x019a: Software version
    • 0x4ecd: Value is written by WPM to FEK, meaning is unclear. I used it as a triggerpoint to write TEMPERATUR and HUMIDITY values to WPM
    • 0x4ec7: ACTUAL ROOM TEMPERATURE heating circuit 1 in 0.1°C for WPM4
    • 0x4ec8: ACTUAL rel HUMIDITY heating curcuit 1 in 0x1% for WPM4
  8. value

    Value encoding depends on the data_key. value 0x8000 seems to have the special meaning "no data".

[1] http://juerg5524.ch/data/readme.txt

[2] http://juerg5524.ch/data/ElsterTable.inc

FEK Simulation

Essential for FEK simulation seems to be that you answer all system initialization requests, register every few minutes (I took every 420s) at your WPM with a system initialization request and send values for ROOM TEMPERATURE and HUMIDITY. To register at your WPM you send a short message with data_key 0xfe and value 0x0100 to your WPM can_id (0x480 for WPM4, 0x180 for old WPMs).

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdint.h>
#include "can4linux/can4linux/can4linux.h"
uint16_t can_id = 0x401;
void set_baud(int fd, int baud){
command_par_t cmd;
config_par_t cfg;
cmd.cmd = CMD_STOP;
ioctl(fd, CAN_IOCTL_COMMAND, &cmd);
cfg.target = CONF_TIMING;
cfg.val1 = baud;
ioctl(fd, CAN_IOCTL_CONFIG, &cfg);
cmd.cmd = CMD_START;
ioctl(fd, CAN_IOCTL_COMMAND, &cmd);
}
void can_send(int fd, canmsg_t msg){
if(write(fd, &msg, 1) != 1){
printf("Failed to send can message\n");
}
}
void print_msg(canmsg_t msg) {
printf("Received with ret=%d: %12lu.%06lu id=0x%05lX len=%d flags=0x%02x msg=", 1, msg.timestamp.tv_sec, msg.timestamp.tv_usec, msg.id, msg.length, msg.flags);
for(int i=0;i<msg.length;i++){
printf("%02X ", msg.data[i]);
}
putchar(10);
if (msg.length == 7){
uint16_t can_key = 8 * (msg.data[0] & 0xF0) + (msg.data[1] & 0x7F);
uint16_t data_key = msg.data[2] == 0xfa ? ((uint16_t)msg.data[3])<<8 | msg.data[4] : msg.data[2];
uint16_t value = msg.data[2] == 0xfa ? ((uint16_t)msg.data[5])<<8 | msg.data[6] : ((uint16_t)msg.data[3])<<8 | msg.data[4];
printf("Receiver 0x%05lX, key=0x%04X, value=%d(%X) ", can_key, data_key, value, value);
switch(msg.data[0] & 0xf){
case 0:
printf("write");
break;
case 1:
printf("read");
break;
case 2:
printf("response");
break;
case 3:
printf("ack");
break;
case 4:
printf("write ack");
break;
case 5:
printf("write respond");
break;
case 6:
printf("system");
break;
case 7:
printf("system respond");
break;
}
if (msg.data[1] == 0x79) {
printf(" broadcast");
}
putchar(10);
}
}
int main(int argc, char *argv[]) {
int fd,n;
canmsg_t msg;
unsigned int last_ts = 0, last_val_ts = 0;
fd = open("/dev/can0", O_RDWR);
if (fd < 0) {
perror("open\n");
return -1;
}
printf("Opened fd=%d\n", fd);
printf("set baudrate to %d Kbit/s\n", 20);
set_baud(fd, 20);
while ((n = read(fd, &msg, 1)) >= 0) {
if (msg.length == 7) {
uint16_t can_key = 8 * (msg.data[0] & 0xF0) + (msg.data[1] & 0x7F);
uint16_t data_key = msg.data[2] == 0xfa ? ((uint16_t)msg.data[3])<<8 | msg.data[4] : msg.data[2];
uint16_t value = msg.data[2] == 0xfa ? ((uint16_t)msg.data[5])<<8 | msg.data[6] : ((uint16_t)msg.data[3])<<8 | msg.data[4];
//broadcast for us or specifically at our id
if ((msg.data[1] == 0x79 && (8*(msg.data[0] & 0xF0)) == can_id & 0x780) || can_key == can_id) {
if (((msg.data[0] & 0xf) == 0x6) && data_key == 0xfe && value == 0x100) {
canmsg_t reply;
reply.id = can_id;
reply.length = 0x7;
reply.data[0] = ((msg.id >> 3) & 0xf0) | 0x7;
reply.data[1] = (msg.id & 0xFF);
reply.data[2] = 0xfe;
reply.data[3] = 0x01;
reply.data[4] = 0;
reply.data[5] = 0;
reply.data[6] = 0;
can_send(fd, reply);
printf("Send broadcast response to %x\n", msg.id);
print_msg(msg);
}
// implemented here as it seems to be needed for some wpm, but not needed for WPM4
else if((msg.data[0] & 0xf) == 0x1 && data_key == 0x5){ //raumsoll
canmsg_t reply;
reply.id = can_id;
reply.length = 0x7;
reply.data[0] = ((msg.id >> 3) & 0xf0) | 0x2;
reply.data[1] = (msg.id & 0xFF);
reply.data[2] = 0xfa;
reply.data[3] = 0;
reply.data[4] = 0x05;
reply.data[5] = 0;
reply.data[6] = 0xdc; //22°C
can_send(fd, reply);
printf("Send raumsoll response\n");
print_msg(msg);
}
// implemented here as it seems to be needed for some wpm, but not needed for WPM4
else if((msg.data[0] & 0xf) == 0x1 && data_key == 0x8){ //raumsoll nacht
canmsg_t reply;
reply.id = can_id;
reply.length = 0x7;
reply.data[0] = ((msg.id >> 3) & 0xf0) | 0x2;
reply.data[1] = (msg.id & 0xFF);
reply.data[2] = 0xfa;
reply.data[3] = 0;
reply.data[4] = 0x08;
reply.data[5] = 0;
reply.data[6] = 0xc8; //20°C
can_send(fd, reply);
printf("Send raumsoll nacht response\n");
print_msg(msg);
}
// implemented here as it seems to be needed for some wpm, but not needed for WPM4
else if((msg.data[0] & 0xf == 0x1) && data_key == 0x11){ //raumist
canmsg_t reply;
reply.id = can_id;
reply.length = 0x7;
reply.data[0] = ((msg.id >> 3) & 0xf0) | 0x2;
reply.data[1] = (msg.id & 0xFF);
reply.data[2] = 0xfa;
reply.data[3] = 0;
reply.data[4] = 0x11;
reply.data[5] = 0;
reply.data[6] = 0xc7; //19.9°C
can_send(fd, reply);
printf("Send raumist response\n");
print_msg(msg);
}
else if((msg.data[0] & 0xf) == 0x1 && data_key == 0x199){ //softwarenummer
canmsg_t reply;
reply.id = can_id;
reply.length = 0x7;
reply.data[0] = ((msg.id >> 3) & 0xf0) | 0x2;
reply.data[1] = (msg.id & 0xFF);
reply.data[2] = 0xfa;
reply.data[3] = 0x01;
reply.data[4] = 0x99;
reply.data[5] = 0x01;
reply.data[6] = 0xa0; //41.6
can_send(fd, reply);
printf("Send nummer response\n");
print_msg(msg);
}
else if(((msg.data[0] & 0xf) == 0x1) && data_key == 0x19a){ //softwareversion
canmsg_t reply;
reply.id = can_id;
reply.length = 0x7;
reply.data[0] = ((msg.id >> 3) & 0xf0) | 0x2;
reply.data[1] = (msg.id & 0xFF);
reply.data[2] = 0xfa;
reply.data[3] = 0x01;
reply.data[4] = 0x9a;
reply.data[5] = 0x0;
reply.data[6] = 0x03; //0.3
can_send(fd, reply);
printf("Send version response\n");
print_msg(msg);
} else if (((msg.data[0] & 0xf) == 0x7) && data_key == 0xfe && value == 0x100) { // registration success
printf("Got reg success\n");
print_msg(msg);
}
else if (((msg.data[0] & 0xf) == 0x0) && data_key == 0x4ecd){ // treat as request roomtemp
printf("Got 4ecd\n");
print_msg(msg);
canmsg_t s;
/* //another id with roomtemp, but seems to be not used by WPM4
s.id = can_id;
s.length = 0x7;
s.data[0] = ((0x601 >> 3) & 0xf0);
s.data[1] = (0x601 & 0xFF);
s.data[2] = 0xfa; // write raumist
s.data[3] = 0x4e;
s.data[4] = 0x63;
s.data[5] = 0x0;
s.data[6] = 0xc5; // 19.7
can_send(fd, s);
printf("set raumtemp to 19.7\n");
*/
s.id = can_id;
s.length = 0x7;
s.data[0] = ((0x601 >> 3) & 0xf0);
s.data[1] = (0x601 & 0xFF);
s.data[2] = 0xfa; // write raumist
s.data[3] = 0x4e;
s.data[4] = 0xc7;
s.data[5] = 0x0;
s.data[6] = 0xc6; // 19.8
can_send(fd, s);
printf("set raumtemp HK1 to 19.8\n");
s.id = can_id;
s.length = 0x7;
s.data[0] = ((0x601 >> 3) & 0xf0);
s.data[1] = (0x601 & 0xFF);
s.data[2] = 0xfa; // write raumfeuchte
s.data[3] = 0x4e;
s.data[4] = 0xc8;
s.data[5] = 0x01;
s.data[6] = 0xeb; // 49.1
can_send(fd, s);
printf("set raumfeuchte HK1 to 49.1\n");
}
else if ((msg.data[0] & 0xf) == 0x1){
canmsg_t reply;
reply.id = can_id;
reply.length = 0x7;
reply.data[0] = ((msg.id >> 3) & 0xf0) | 0x2;
reply.data[1] = (msg.id & 0xFF);
reply.data[2] = msg.data[2];
reply.data[3] = msg.data[3];
reply.data[4] = msg.data[3];
reply.data[5] = 0x80;
reply.data[6] = 0x00; //0.3
can_send(fd, reply);
printf("Unknown request (%x, %x, %x) reply unknown\n", (msg.data[0] & 0xf), data_key, value);
print_msg(msg);
print_msg(reply);
}
else{
printf("Unknown %x, %x, %x\n", (msg.data[0] & 0xf), data_key, value);
print_msg(msg);
}
}
if ((last_ts + 420) < msg.timestamp.tv_sec) {
last_ts = msg.timestamp.tv_sec;
//register at wpm
canmsg_t s;
s.id = can_id;
s.length = 0x7;
s.data[0] = 0x96;
s.data[1] = 0x00;
s.data[2] = 0xfe;
s.data[3] = 0x01;
s.data[4] = 0x0;
s.data[5] = 0x0;
s.data[6] = 0x0;
can_send(fd, s);
printf("Send init request\n");
}
if ((last_val_ts + 60) < msg.timestamp.tv_sec){
last_val_ts = msg.timestamp.tv_sec;
//no periodic message needed as it seems
}
}
}
if (n < 0) {
perror("Read error\n");
return 1;
}
close(fd);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment