Skip to content

Instantly share code, notes, and snippets.

@pshirshov
Last active November 3, 2024 23:35
Show Gist options
  • Save pshirshov/28c57ff4e3074df91a599dfc0e171c44 to your computer and use it in GitHub Desktop.
Save pshirshov/28c57ff4e3074df91a599dfc0e171c44 to your computer and use it in GitHub Desktop.
myenergi udp protocol: reverse engineering
import std.mem;
import std.sys;
import std.core;
using MyDevice = u32;
// convention:
// _lowercase - zero certainty what is that
// _CamelCase - have an idea what is that
// CamelCase - we are certain what is that
// lowercase - not a field, impl detail
struct HarviClamp {
std::assert(std::mem::read_unsigned($, 1) == 8, "harvi record starts with 0x8");
padding[1];
u8 _wtf1; // ? 17-33-49
std::assert(_wtf1 == 17 || _wtf1 == 33 || _wtf1 == 49, "wtf1");
u8 ClampType;
u8 _wtf2; // ? varies but always is either an increasing sequence or 0-0-0
s16 Power_Watts;
s16 Current_CentiAmps;
std::print(" CT tpe={} pw={} amps={}", ClampType, Power_Watts, Current_CentiAmps / 100.0);
std::print(" wtf1 = {}, wtf2 = {}", _wtf1, _wtf2);
};
struct HarviRecord<auto PayloadSize, auto PayloadEnd> {
u32 HarviSerial [[color("00ff00")]];
std::print(" HarviSerial={}", HarviSerial);
HarviClamp HarviClamps[while($ < PayloadEnd-2)];
// varies between packets, tend to be the same within one packet (but not always).
// Frequently it's 0-0 for all entries in all harvis
// Seconds since last reading?..
u16 _end_bytes[1];
std::print("harvi last bytes: {}", _end_bytes);
};
struct ZappiRecord<auto PayloadSize, auto PayloadEnd> {
};
struct SharedRecord<auto PayloadSize, auto PayloadEnd> {
u32 _stable_id;
u32 _zero_or_wtf;
u32 _four;
u16 _device_type;
u16 _record_type;
std::print(" devtpe {}, rectpe {}",
_device_type, _record_type);
if (_record_type == 276) {
std::assert(PayloadSize == 30, "");
u16 _voltage;
std::print(" voltage {}",
_voltage / 10.0);
} else if (_record_type == 8456) {
std::assert(PayloadSize == 42 || PayloadSize == 50, "");
} else if (_record_type == 108) {
std::assert(PayloadSize == 38, "");
} else if (_record_type == 206) {
std::assert(PayloadSize == 56, "");
} else if (_record_type == 4360) {
std::assert(PayloadSize == 31, "");
} else if (_record_type == 276) {
std::assert(PayloadSize == 30, "");
} else if (_record_type == 276) {
std::assert(PayloadSize == 30, "");
} else {
std::print(" UKNOWN 13584 RECORD TYPE: {} of size {}", _record_type, PayloadSize);
//std::assert(false, std::format("unknown rectype {} with sz {}", _record_type, PayloadSize));
}
};
struct MyEnergiRecord
{
u8 PayloadSize [[color("0000FF")]];
u32 start = $;
u32 record_end = start + PayloadSize;
u8 NetId [[color("00ffff")]];
u16 _PacketType;
u8 DevId;
bool debug = true || (_PacketType == 14128);
u8 _wtf1; // 0xff => good known device, 0x1 => some shit, 0x4 => last record,
std::assert(_wtf1 == 0xFF || _wtf1 == 4 || _wtf1 == 1, "");
if (debug) {
std::print("tpe {}, NetId {}, DevId {}, sz {}",
_PacketType, NetId , DevId, PayloadSize);
if (_PacketType == 13584 || _PacketType == 31097 || _PacketType == 14128) {
MyDevice DevSerial [[color("880000")]];
std::print(" DevSerial {}", DevSerial);
if (_PacketType == 14128) {
HarviRecord<PayloadSize, record_end> HarviRecord;
} else if(_PacketType == 13584) {
SharedRecord<PayloadSize, record_end> SharedRecord;
} else if (_PacketType == 31097) {
ZappiRecord<PayloadSize, record_end> ZappiRecord;
}
} else {
std::print(" UKNOWN PACKET TYPE: {}", _PacketType);
}
}
u8 _unknown_end[PayloadSize - ($ - addressof(NetId))] [[color("aaaaaa")]] ;
if (debug) std::print(" {} {}", sizeof(_unknown_end), _unknown_end);
};
struct MyenergiPkt
{
u32 MAGIC;
std::assert(std::mem::read_unsigned($, 4) == 0x00000000, "myenergi message starts with 0x0 in second octet");
padding [4];
u32 Serial;
u32 _wtf1;
u32 Timestamp ;
u32 _wtf2;
u16 _FirmwareVersion;
u8 _NetworkId;
u8 _wtf3;
u16 PacketSize [[color("0000FF")]];
u16 _wtf4;
MyEnergiRecord Records[while (!std::mem::eof())];
};
MyenergiPkt pkt @ 0x0;
@pshirshov
Copy link
Author

pshirshov commented Oct 21, 2024

Stream processing:

ROUTER=router.lan IMHEX=/Applications/ImHex.app/Contents/MacOS/imhex MEIP=192.168.0.200 MEIF=br1 && \
  ssh $ROUTER 'tcpdump -i '$MEIF' -w - src '$MEIP' or dst '$MEIP' 2>/dev/null | tshark -r - -T fields -e data 2>/dev/null' | \
  grep c0cacefa | \
  xargs -I % -S 8192 bash -c 'tmpf=$(mktemp) && echo % | xxd -r -p > $tmpf && echo $tmpf && echo % && '$IMHEX' --pl run --pattern ./proto.hexpat -v -i $tmpf'

@pshirshov
Copy link
Author

Local communication is done via multicast, some initial research is done here: https://github.com/PeterTM/mqtt-energi
All myenergi devices must be in the same network.
sudo tcpdump -i $MEIF -n -p multicast

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment