/wion

WiOn SmartSwitch/Outlet (KAB enterprises) protocol Implementation

Primary LanguageRust

WiOn SmartSwitch/Outlet (KAB enterprises protocol) Implementation

WiOn Switch

This is a very early implementation of KAB enterprises protocol in Rust that is used to control smart plugs and switches. They are sold under numerous brand names such as WiOn or ECOplugs.

Endianness

All multi-byte numerical's are transmitted in little endian order.

Device Discovery

The discovery of devices on the network is achieved by sending a UDP broadcast packet on either port 25 or 5888 to the local network where the device(s) reside.

Discovery Request

The payload of this broadcast is comprised of 128 bytes all set to zero except for six bytes starting at offset 24 with the following values, 0xE0, 0x07, 0x06, 0x07, 0x07, 0xE0. As far I've been able to surmise these bytes don't represent anything tangible, but they must be represented exactly as specified starting at offset 24. Below is a Hex dump representation of the payload:

0000   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0010   00 00 00 00 00 00 00 00 e0 07 06 07 07 e6 00 00
0020   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0030   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0040   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0050   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0060   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0070   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Discovery Response

The following is the response structure that will be returned from each device after the broadcast has been sent:

pub struct BroadcastResp {
    pub unknown: u32,
    pub version: [u8;6], //string
    pub dev_model: [u8;32],   //string
    pub dev_name: [u8;32],  //string
    pub dev_serial: [u8;34],  //string
    pub unknown2: u32,
    pub unknown3: u32,
    pub unknown4: u32,
    pub ssid: [u8;64],  //string
    pub wifi_pass: [u8;64],  //string
    pub unknown5: u32,
    pub unknown6: u32,
    pub unknown7: u32,
    pub zipcode: [u8;12],
    pub p2pm: [u8;16],  //string ip
    pub p2ps: [u8;16], //string ip
    pub paw: [u8;16], //string ip
    pub unknown8: u32,
    pub unknown9: u32,
    pub unknown10: u32,
    pub unknown11: u32,
    pub unknown12: u32,
    pub unknown13: u32,
    pub unknown14: u32,
    pub unknown15: u32,
    pub unknown16: u32,
    pub unknown17: u32,
    pub unknown18: u32,
    pub unknown19: u32,
    pub dev_mac: [u8;18], //string
    pub dev_ip: [u8;18], //string
    pub dev_port: u32,
  }
The total byte size of the response is always 408 bytes (because the max size of all dynamic fields is fixed. E.g dev_name is set to hard 32 byte length). You should also note that response includes the WiFi SSID the device is connected to and the WiFi password of that network. Both values are transmitted in clear text. Below is an example of a parsed response:
Unknown: 0x0
Version: 1.6.0
Model: ECO-74227201
Name: Basement
Serial: 74227201
Unknown2: 0x150117E1
Unknown3: 0x9FF0
Unknown4: 0xFFFF3B20
SSID: myhouse
WiFi Pass: mywifipass
Unknown5: 0x30300001
Unknown6: 0x31
Unknown7: 0x0
Zipcode: 90210
p2pm: 210.71.198.37
p2ps: 210.71.198.37
paw: 61.220.255.143
Unknown8: 0x1
Unknown9: 0x2
Unknown10: 0x3
Unknown11: 0x10101
Unknown12: 0x43530001
Unknown13: 0x4C347A71
Unknown14: 0x64434676
Unknown15: 0x0
Unknown16: 0x0
Unknown17: 0x0
Unknown18: 0x0
Unknown19: 0x0
MAC: 48:5c:2a:4d:a1:22
IP: 192.168.0.248
Port: 80

Basic Request/Response Header

The basic request/response header is the common header to all communication with the device.

pub struct Header {
    pub cmd: u32,
    pub req_conn_id: u32,
    pub cmd_type: u16,
    pub version: [u8;6], //Convert to String - Fixed length (unused bytes are nulled, 0x00)
    pub model: [u8;32],  //Convert to String - Fixed length (unused bytes are nulled, 0x00)
    pub dev_name: [u8;32],  //Convert to String - Fixed length (unused bytes are nulled, 0x00)
    pub serial: [u8;32],  //Convert to String - Fixed length (unused bytes are nulled, 0x00)
    pub resp_status: u32,
    pub seq_counter: u32,
    pub unknown: u32,
    pub resp_conn_id: u32,
}

Toggle Switch On or Off

The switch can be turned by loading and transmitting the following structure to the device:

//Header minimum fields
head.cmd = 327702; // CMD_BASCI_MODIFY_SWITCH
head.req_conn_id = rng.gen::(); ; // needs to be changed each time or device is flaky with fast changes.  
                                          using rand now,
head.cmd_type = 0x02;
// must have model or the  device will not on turn
head.model = [0x45, 0x43, 0x4F, 0x2D, 0x37, 0x38, 0x30, 0x30, 0x34, 0x42, 0x30, 0x31, 0x00, 0x00,
              0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
              0x00, 0x00, 0x00, 0x00];  // translates to "ECO-78004B01" with null pads.
head.seq_counter = 0x55555555; // should be incremented, but it doesn't really matter, used for tracking

//additional fields after the basic "header" head.operation = 0x02; // 0x02 for write, 0x00 for read head.rw_byte = 1; // 1 = on, 0 = off

You'll notice that certain fields within the Header structure need not be set. These are the minimum required for the switch to be turned on and off. Its important that you set the model field to your specific device. The model field is a combination of "ECO-" and the serial number of your device. Use the discovery packet to identified this field. There are two additional fields that are added to the basic Header. They are are operation and rw_byte. The operation field specifies the operation as read or write. These are signified by 0x02 for write and 0x00 for read. The rw_byte is the value that is to be written. In this case writing 0 means switch off and 1 means switch on. Below is an example response: you will receive from the device (128 bytes):

[Cmd: 0x50016, Req Conn ID: 0x84DD0000, cmd_type: 0x0,
 Version: 1.6.0, Model: ECO-78004B01, Dev_name: Basement test, Serial: 78004B01,
 Resp_Status: 0x7E11DC5E, Seq Counter: 1431655765, Unknown: 0 Resp Conn ID: 0x0]

Note that the last two bytes (operation, rw_bytes) of the structure are only present on "Requests". Thus the base size of a request is 130 bytes, while the base response packet size is 128 bytes.

The way you toggle the switch off is to use the same populated Header structure as above, except you set the rwByte field to 0 as shown below:

head.rw_byte = 0;  // 1 = on, 0 = off

Get Switch Status

Retrieving the switch status: CMD_BASCI_GET_SWITCH_STATUS:

head.cmd = 327703; // CMD_BASCI_GET_SWITCH_STATUS
head.req_conn_id = rng.gen::(); ; // needs to be changed each time or device is flaky with fast changes.  
                                          using rand now,
head.cmd_type = 0x00; // set to zero?  works set to 2 also.  
// must have model or the  device will not on turn
head.model = [0x45, 0x43, 0x4F, 0x2D, 0x37, 0x38, 0x30, 0x30, 0x34, 0x42, 0x30, 0x31, 0x00, 0x00,
              0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
              0x00, 0x00, 0x00, 0x00];  // translates to "ECO-78004B01" with null pads.
head.seq_counter = 0x55555555; // should be incremented, but it doesn't really matter, used for tracking

//additional fields after the basic "header" None

The response appears as follows:

[Cmd: 0x50017, Req Conn ID: 0x785B0000, cmd_type: 0x2,
 Version: 1.6.0, Model: ECO-78004B01, Dev_name: Basement test, Serial: 78004B01,
 Resp_Status: 0x7E11E25D, Seq Counter: 1431655765, Unknown: 0 Resp Conn ID: 0x0,
 Operation: 1, rwByte: 0]

Notice that the two additional fields added to the response packet. These are the same additional fields that are used for toggling the switch on and off. The rwByte signifies if the switch is on or off. 0 = off, 1 = on.

Get Rom Status

WIP - Below is the additional payload (42 bytes) form a get rom status request. This is just the additional fields. Does not include the basic header. Needs to be decoded.

0000D0070101000000000000000033080101000000000000000000938CC1FA766FFC64D7B750B0000000

Scheduling

These devices contain the ability to autonomously manage set points for turning on and off at specified times. The WiOn product has the ability to store 12 schedules per device. Other Kab protocol based devices may have more or less. The header for scheduling is the same as the basic Header with the following additional fields:

tableEntryCount: u8, // contains the total number of populated "tableEntry*" structs
                        that are following.  When I refer populated its means
                        there are x number of TableEntry structs represented in the packet.
entryNum: u8,  // the entry number in the table. 0-11 for wion.  
                  only populated when theres valid timer in the entry
unknown: u16,
counterType: u8  // the type of counter type.  See Appendix
After this preamble header you get into the scheduling specific data structures. The first structure is tableEntry and then its followed n (max of 11 for WiOn) number of tableEntryNext structures. The reason that theres two structure's is the unknown field in tableEntry is four bytes while the same unknown field in tableEntryNext is only a single byte. I have yet to figure out this discrepancy.
//tableEntry structure
daysOfTheWeek: u8,
unknownBlob: [u8;8],
startYear: u16, // the year.  E.g the start year of timmr. eg. 2017
startMonth: u8, // month number 1-12
startDay: u8, // day of the month.  1-31
startTimeInSecs: u32, // the time the switch should turn on in seconds that represent military time.
                  E.g. 75600 = 21:00 hours
unknown: u16,
endYear: u16, // the year.  E.g the end year of timer. eg. 2017
endMonth: u8, // end month number 1-12
endDay: u8, // end day of the month.  1-31   
unknown: u16,
endTimeInSecs: u32 // // the time the switch should turn off in seconds that represent military time.
                E.g. 75600 = 21:00 hours
The following is the tableEntryNext structure which can be represented x number of times as dictated by the tableEntryCount field in the header:
// tableEntryNext structure
entryNum: u8, // the entry number in the table. 0-11 for WiOn.  only populated when theres
                 valid timer in the entry
unknown: u8,
counterType: u8 , // the type of counter type.  See Appendix
struct tableEntry,  //  x number of times per the  tableEntryCount field.

Appendix

Known Commands

Below is a list of the known commands:

public static final int CMD_BASCI_CONNECT_ROUTER = 327697;
public static final int CMD_BASCI_END_UPGRADE = 328195;
public static final int CMD_BASCI_GET_RDM_STATUS = 327713;
public static final int CMD_BASCI_GET_SETTING = 327685;
public static final int CMD_BASCI_GET_SWITCH_STATUS = 327703;
public static final int CMD_BASCI_GET_WIFI_HOT = 327698;
public static final int CMD_BASCI_HEART_PAKG = 262144;
public static final int CMD_BASCI_MODIFY_ALIAS_PSW = 327699;
public static final int CMD_BASCI_MODIFY_AREAINFOR = 327716;
public static final int CMD_BASCI_MODIFY_IP_PORT = 327700;
public static final int CMD_BASCI_MODIFY_SWITCH = 327702;
public static final int CMD_BASCI_MODIFY_TIMEZONE = 327701;
public static final int CMD_BASCI_NIGHTLAMP_SETTING = 327715;
public static final int CMD_BASCI_POST_ASTROMICTABLE = 327704;
public static final int CMD_BASCI_POST_SETTING = 327684;
public static final int CMD_BASCI_REDA_PWR_OFFSET = 327730;
public static final int CMD_BASCI_RESTART = 327682;
public static final int CMD_BASCI_RESTORE = 327683;
public static final int CMD_BASCI_SCHEDULE_RANDON = 327714;
public static final int CMD_BASCI_SET_RDM_STATUS = 327712;
public static final int CMD_BASCI_START_UPGRADE = 328193;
public static final int CMD_BASCI_WRITE_PWR_OFFSET = 327731;
public static final int CMD_GET_SETTING_STATU = 327941;
public static final int CMD_GET_TODAY_TASKTAB = 327940;
public static final int CMD_SCHEDULE_ADD = 327936;
public static final int CMD_SCHEDULE_DELETE = 327938;
public static final int CMD_SCHEDULE_EDIT = 327937;
public static final int CMD_SCHEDULE_GETALL = 327939;

daysOfTheWeek

The field within the schedule structure specified by daysOfTheWeek (u8) is decoded with the following bit masks:

Sunday = 0x40
Monday = 0x20
Tuesday = 0x10
Wednesday = 0x08
Thursday = 0x04
Friday = 0x02
Saturday = 0x01
Since this is a mask, multiple values are possible. E.g. Monday, Tuesday, and Saturday can be represented within the byte,

counterType

Schedules have diffferent counter types. These are the known ones:

programable_timer = 0x02
countDownTimer = 0x00;