Matthew1471/Navien-API

Navien NPE Series

mreiling opened this issue · 11 comments

Hi. Your work here is awesome. This is the only code I have found that seems to communicate with Navien units. Unfortunately, this appears to be directed at a different unit than what I have. The NPE series hot water heater. Your code does in fact connect but Navien is returning different data than expected. I would love to help port this to the NPE series but am at a loss as to how to even start. Any thoughts?

I would like to second this comment/issue. The work here is great; is there any way to adapt it do the NPE series of water heaters? Would it at least be possible to get the raw data that is returned from the request, and perhaps we could parse it and figure out what's what? Thanks!

Still trying to figure this all out. I was able to get it to talk to the US server which use slightly different api calls. I can log in and get a list of devices so far. Once it switches to connecting on port 6001, I am baffled. Do you have any insight on how you figured out this binary protocol? I would love to contribute to this, or fork it for the NPE series.

I recently installed a NaviLink for use with my NPE and had a look at this and compared with what the latest Android app is doing. It appears that the US server being used (uscv2.naviensmartcontrol.com) does indeed use a different set of APIs. Interestingly enough, they inadvertently listed some details about the API endpoints under /Help at the host above, but there isn't any documentation. Using mitmproxy with the Android app has revealed that you can login with a POST to /api/requestDeviceList with x-www-form-urlencoded params userID and password in the POST body populated with your account credentials will return something like this:

{
    "msg": "MAINUSER_DEVICELIST_SUCCESS",
    "data": "[{\"PID\":\"xxxx\",\"GID\":\"xxxx\",\"AID\":\"0\",\"UID\":\"xxxx\",\"PCD\":\"1\",\"State\":\"1\",\"HwRev\":\"\",\"SwRev\":\"\",\"ConnectionTime\":\"2/17/2022 1:36:13 AM\",\"ServerIP\":\"13.10.5.75\",\"ServerPort\":\"6001\",\"AName\":\"REMOTE\",\"CountryCD\":\"1\",\"NickName\":\"xxxx\",\"NickName2\":\"\",\"ZipCode\":\"xxxx\",\"TimeZone\":\"GMTM08:00\",\"DeviceInfo\":\"2\",\"InstallerInfo\":\"\",\"MainUserID\":\"xxxx\",\"InstallerID\":\"\",\"isErrorAlarm\":\"1\",\"isETCAlarm\":\"1\"}]"
}

I spent a bit more time digging and it appears that the parsing done in the existing code in https://github.com/matthew1471/Navien-API/blob/2ba61d734f413f1e96fdf8dab616775b8b92b416/python/shared/NavienSmartControl.py#L152-L153
matches some data from the first response payload binary blob (message type 0x01?) returned from port 6001 for an NPE device, but it is a bit different as the NPE returns 52 bytes instead of the 42 bytes expected by the existing parser. I could only clearly identify the deviceid in the first 8 bytes corresponds with the GID value from the /api/requestDeviceList endpoint response. The Android app then sends another binary blob to port 6001 (contents unknown) and it receives a different type of payload (message type 0x02?) that seems to show the values from the status screen (domestic outlet set temp, domestic outlet temp, domestic flow rate, inlet temp, and current H/C) The gas usage stats are likely there as well, but do not appear to be encoded in an obvious way. Message type 0x02 also appears to have the recirculation pump schedule info, but I won't be able to test this as my unit isn't setup to use this at present.

It looks like another project https://github.com/ggiesen/PyNaviLink has discovered some of the same info, but they also have some potential details for the header used in the second request to port 6001 that the mobile app makes before receiving message type 0x02 back.

I spent some more time reviewing the state request/response data with an an existing mobile app implementation and have most of the payload decoded. Hopefully this is helpful:

Raw state/status request data (sanitized to remove deviceID)

00000000  07 99 00 a6 37 00 XX XX  XX XX XX XX XX XX 01 03
00000010  01 01 02 00 00 00 00 00  00 00 00 00 00 00 00 00
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000030  00 00 00 00 00
All requests share a hardcoded 6 byte prefix that contains the following:
07 99 00 a6 37 00
-stx = 07
-did = 99
-reserve = 00
-cmd = a6
-dataLength = 37
-dSid = 00
device ID = XX XX XX XX XX XX XX XX

Rest of status request follows starting at array position 14 (byte 15):
commandCount = 01
currentControlChannel = 03 (NPE device is connected to Navilink serial port 3)
deviceNumber = 01 (first device on the serial bus)
controlSorting = 01  (1 for deviceInfo requests, and 2 for deviceControlWeekly and deviceControl)
infoItem = 02 matched to enum STATE and set to KDEnum.ControlType.STATE (This field is only used when info is requested as per this request)
controlItem = 00 matched to enum UNKNOWN and set to KDEnum.ControlType.UNKNOWN (This field is only used when controlling)
controlValue = 00 (this is set to 0 for info requests, 1 for setting weekly schedule info or another value when controlling)
controlValue_WeeklyDay = 00
controlValue_WeeklyCount = 00
controlValue_WeeklyDay_1_Hour = 00
controlValue_WeeklyDay_1_Minute = 00
controlValue_WeeklyDay_1_Flag = 00
controlValue_WeeklyDay_2_Hour = 00
controlValue_WeeklyDay_2_Minute = 00
controlValue_WeeklyDay_2_Flag = 00
controlValue_WeeklyDay_3_Hour = 00
controlValue_WeeklyDay_3_Minute = 00
controlValue_WeeklyDay_3_Flag = 00
controlValue_WeeklyDay_4_Hour = 00
controlValue_WeeklyDay_4_Minute = 00
controlValue_WeeklyDay_4_Flag = 00
controlValue_WeeklyDay_5_Hour = 00
controlValue_WeeklyDay_5_Minute = 00
controlValue_WeeklyDay_5_Flag = 00
controlValue_WeeklyDay_6_Hour = 00
controlValue_WeeklyDay_6_Minute = 00
controlValue_WeeklyDay_6_Flag = 00
controlValue_WeeklyDay_7_Hour = 00
controlValue_WeeklyDay_7_Minute = 00
controlValue_WeeklyDay_7_Flag = 00
controlValue_WeeklyDay_8_Hour = 00
controlValue_WeeklyDay_8_Minute = 00
controlValue_WeeklyDay_8_Flag = 00
controlValue_WeeklyDay_9_Hour = 00
controlValue_WeeklyDay_9_Minute = 00
controlValue_WeeklyDay_9_Flag = 00
controlValue_WeeklyDay_10_Hour = 00
controlValue_WeeklyDay_10_Minute = 00
controlValue_WeeklyDay_10_Flag = 00

Raw state/status response data to request shown above (sanitized to remove deviceID)

00000000  XX XX XX XX XX XX XX XX  01 02 0e 00 13 05 1c 00
00000010  01 01 03 01 00 00 01 2f  b3 22 c7 40 00 00 7d 7b
00000020  2b 00 3b 00 20 20 01 02  02 02 00 01 00 00 00 00
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000040  00 00 00 00 00 00 00 00  00 00 00 02 00 00 00 00
00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000060  00 00 00 00 00 00 00 00  00 00 00 03 00 00 00 00
00000070  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000080  00 00 00 00 00 00 00 00  00 00 00 04 00 00 00 00
00000090  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
000000A0  00 00 00 00 00 00 00 00  00 00 00 05 00 00 00 00
000000B0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
000000C0  00 00 00 00 00 00 00 00  00 00 00 06 00 00 00 00
000000D0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
000000E0  00 00 00 00 00 00 00 00  00 00 00 07 00 00 00 00
000000F0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000100  00 00 00 00 00 00 00 00  00 00 00 20 20 20 20
Common response header:
deviceID = XX XX XX XX XX XX XX XX
countryCD = 01
controlType = 02 matched to enum STATE and set to KDEnum.ControlType.STATE
swVersionMajor = 0e & 0xFF = 14 dec
swVersionMinor = 00 & 0xFF = 0

Status response details start at array position 12 (byte 13):
controllerVersion = 13 05
pannelVersion = 1c 00
deviceSorting = 01 matched to enum NPE and set to KDEnum.DeviceSorting.NPE
deviceCount = 01
currentChannel = 03 (device is connected to serial port 3 on the Navilink)
deviceNumber = 01
errorCD = (00 & 0xFF) + (00 & 0xFF) * 256 = 0
operationDeviceNumber = 01
AverageCalorimeter = 2f & 0xFF = 2F (47 dec, this is converted in the app by 47 / 2 = 23.5 %)
gasInstantUse = (b3 & 0xFF) + (22 & 0xFF) * 256 = 0xB3 + (0x22 * 256) = 179 + 8704 = 8883 (This is converted from kilocalories in the app by 8883 * 3.968 = 35,247.744 and rounded to 35,247.7 BTU. If converted accurately, should actually be 35,227.02 BTU)
gasAccumulatedUse = c7 40 00 00 treat as little endian and convert to int = 16583 (This is converted from m^3/10 in the app by 16583 * 35.314667 / 10.0 = 58,562.3122861 and rounded to 58562.3 ft^3)
hotWaterSettingTemperature = 7d & 0xFF = 125 (F)
hotWaterCurrentTemperature = 7b & 0xFF = 123 (F)
hotWaterFlowRate = (2b & 0xFF) + (00 & 0xFF) * 256 = 43 + 0 =  43 (This is converted from LPM/10 in the app by 43 / 3.785 / 10.0 = 1.1360 and rounded to 1.1 GPM. If converted accurately is 1.13594 GPM, but with rounding it wouldn't matter)
hotWaterTemperature = 3b & 0xFF = 59 (F, this is actually the inlet temperature per the app)
heatSettingTemperature = 00 & 0xFF = 0
currentWorkingFluidTemperature = 20 & 0xFF = 32 dec (probably a default value when not using external recirc)
currentReturnWaterTemperature = 20 & 0xFF = 32 dec (probably a default value when not using external recirc)
powerStatus = 01 matched to enum ON and set to KDEnum.OnOFFFlag.ON
heatStatus = 02 matched to enum OFF and set to KDEnum.OnOFFFlag.OFF
useOnDemand = 02 matched to enum OFF and set to KDEnum.OnOFFFlag.OFF
weeklyControl = 02 matched to enum OFF and set to KDEnum.OnOFFFlag.OFF
totalDaySequence = 00
Loops 7 times (0-6), one for each day of week contained in the status response
-matches day.daySequence to enums defined in KDWeekly.DayOfWeek based on values 01-07 in binary data
-for array position 43 (byte 44) = 01:
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
--weeklyTotalCount = 00 (loops again from 0 to day.weeklyTotalCount max of 10 on/off times per day of week)
---reads 3 byte sets of hour, minute and KDEnum.OnOFFFlag enum
00 00 00 
00 00 00 
00 00 00 
00 00 00 
00 00 00 
00 00 00 
00 00 00 
00 00 00 
00 00 00 
00 00 00
hotWaterAverageTemperature = 20 & 0xFF = 32 dec (probably a default value when not using external recirc)
inletAverageTemperature = 20 & 0xFF = 32 dec (probably a default value when not using external recirc)
supplyAverageTemperature = 20 & 0xFF = 32 dec (probably a default value when not using external recirc)
returnAverageTemperature = 20 & 0xFF = 32 dec (probably a default value when not using external recirc)

Would read an additional two bytes for recirculationSettingTemperature and recirculationCurrentTemperature if recirculation was enabled (the status response would be two bytes larger)

Initial communication on app startup
Raw TCP connection to uscv2.naviensmartcontrol.com port 6001 (separate from user authentication flow, no session parameters used)
send: userID$iPhone1.0$deviceID in ASCII

Raw channel information response data (sanitized to remove deviceID)

00000000  XX XX XX XX XX XX XX XX  01 01 0e 00 04 01 00 00
00000010  01 00 00 00 00 00 01 00  00 02 02 00 00 01 00 00
00000020  00 00 00 01 00 00 02 03  01 01 02 62 b6 20 20 03
00000030  01 00 00 01
Common response header:
deviceID = XX XX XX XX XX XX XX XX
countryCD = 01
controlType = 01 matched to enum CHANNEL_INFOMATION and set to KDEnum.ControlType.CHANNEL_INFOMATION
swVersionMajor = 0e & 0xFF = 14 dec
swVersionMinor = 00 & 0xFF = 0

Channel information response starts at array position 12 (byte 13):
channelUse = 04 matched to enum CHANNEL_3_USE and set to KDEnum.ChannelUse.CHANNEL_3_USE (NPE device is connected to NaviLink serial port 3)
computed firmware version = swVersionMajor * 100 + swVersionMinor = 1400 dec since this is < 1500, b1 = 13 (otherwise b1 = 15)
loops 3 times (index 0-2), one loop per channel (13 bytes per channel)
01 00 00 01 00 00 00 00 00 01 00 00 02 
-channel.channel = 01
-channel.deviceSorting = 00 matched to enum NO_DEVICE and set to KDEnum.DeviceSorting.NO_DEVICE
-channel.deviceCount = 00
-channel.deviceTempFlag = 01 matched to enum CELSIUS and set to KDEnum.TemperatureType.CELSIUS
-channel.mininumSettingWaterTemperature = 00 & 0xFF = 0
-channel.maxinumSettingWaterTemperature = 00 & 0xFF = 0
-channel.heatingMininumSettingWaterTemperature = 00 & 0xFF = 0
-channel.heatingMaxinumSettingWaterTemperature = 00 & 0xFF = 0
-channel.useOnDemand = 00 matched to enum UNKNOWN and set to KDEnum.OnDemandFlag.UNKNOWN
-channel.heatingControl = 01 matched to enum SUPPLY and set to KDEnum.HeatingControl.SUPPLY
-channel.wwsdFlag = 00 matched to enum OK and set to KDEnum.WWSDFlag.OK
-channel.commercialLock derived from wwsdFlag hex value, matched to enum OK and set to KDEnum.CommercialLockFlag.OK
-channel.hotwaterPossibility derived from wwsdFlag hex value, matched to enum OFF and set to KDEnum.NFBWaterFlag.OFF
-channel.recirculationPossibility derived from wwsdFlag hex value, matched to enum OFF and set to KDEnum.RecirculationFlag.OFF
-channel.highTemperature = 00 matched to enum TEMPERATURE_60 and set to KDEnum.HighTemperature.TEMPERATURE_60
-channel.useWarmWater = 02 matched to enum OFF and set to KDEnum.OnOFFFlag.OFF 
-would also read two more bytes for channel.mininumSettingRecirculationTemperature and channel.maxinumSettingRecirculationTemperature if the computed firmware version is >1500, but the app does not properly evaluate this condition (bug)
02 00 00 01 00 00 00 00 00 01 00 00 02
03 01 01 02 62 b6 20 20 03 01 00 00 01
-channel.channel = 03
-channel.deviceSorting = 01 matched to enum NPE and set to KDEnum.DeviceSorting.NPE
-channel.deviceCount = 01
-channel.deviceTempFlag = 02 matched to enum FAHRENHEIT and set to KDEnum.TemperatureType.FAHRENHEIT
-channel.mininumSettingWaterTemperature = 62 & 0xFF = 98 dec
-channel.maxinumSettingWaterTemperature = b6 & 0xFF = 182 dec
-channel.heatingMininumSettingWaterTemperature = 20 & 0xFF = 32 dec (probably a default value as this feature is not in use)
-channel.heatingMaxinumSettingWaterTemperature = 20 & 0xFF = 32 dec (probably a default value as this feature is not in use)
-channel.useOnDemand = 03 matched to enum WARMUP and set to KDEnum.OnDemandFlag.WARMUP
-channel.heatingControl = 01 matched to enum SUPPLY and set to KDEnum.HeatingControl.SUPPLY
-channel.wwsdFlag = 00 matched to enum OK and set to KDEnum.WWSDFlag.OK
-channel.commercialLock derived from wwsdFlag hex value, matched to enum OK and set to KDEnum.CommercialLockFlag.OK
-channel.hotwaterPossibility derived from wwsdFlag hex value, matched to enum OFF and set to KDEnum.NFBWaterFlag.OFF
-channel.recirculationPossibility derived from wwsdFlag hex value, matched to enum OFF and set to KDEnum.RecirculationFlag.OFF
-channel.highTemperature = 00 matched to enum TEMPERATURE_60 and set to KDEnum.HighTemperature.TEMPERATURE_60
-channel.useWarmWater = 01 matched to enum ON KDEnum.OnOFFFlag.ON
-would also read two more bytes for channel.mininumSettingRecirculationTemperature and channel.maxinumSettingRecirculationTemperature if the computed firmware version is >1500, but the app does not properly evaluate this condition (bug)

A few control samples

Turning off power

00000000  07 99 00 a6 37 00 XX XX  XX XX XX XX XX XX 01 03
00000010  01 02 00 01 02 00 00 00  00 00 00 00 00 00 00 00
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000030  00 00 00 00 00
Rest of status request follows starting at array position 14 (byte 15):
commandCount = 01
currentControlChannel = 03 (NPE device is connected to Navilink serial port 3)
deviceNumber = 01 (first device on the serial bus)
controlSorting = 02 (1 for deviceInfo requests, and 2 for deviceControlWeekly and deviceControl)
infoItem = 00 (this field is set to 0 when controlling)
controlItem = 01 matched to enum POWER
controlValue = 02 This corresponds with enum OFF in KDEnum.OnOFFFlag

Turning back on

00000000  07 99 00 a6 37 00 XX XX  XX XX XX XX XX XX 01 03
00000010  01 02 00 01 01 00 00 00  00 00 00 00 00 00 00 00
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000030  00 00 00 00 00
Rest of status request follows starting at array position 14 (byte 15):
commandCount = 01
currentControlChannel = 03 (NPE device is connected to Navilink serial port 3)
deviceNumber = 01 (first device on the serial bus)
controlSorting = 02 (1 for deviceInfo requests, and 2 for deviceControlWeekly and deviceControl)
infoItem = 00 (this field is set to 0 when controlling)
controlItem = 01 matched to enum POWER
controlValue = 01 This corresponds with enum ON in KDEnum.OnOFFFlag

Setting temperature down to 120

00000000  07 99 00 a6 37 00 XX XX  XX XX XX XX XX XX 01 03
00000010  01 02 00 03 78 00 00 00  00 00 00 00 00 00 00 00
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000030  00 00 00 00 00
Rest of status request follows starting at array position 14 (byte 15):
commandCount = 01
currentControlChannel = 03 (NPE device is connected to Navilink serial port 3)
deviceNumber = 01 (first device on the serial bus)
controlSorting = 02 (deviceControl)
infoItem = 00 (this field is set to 0 when controlling)
controlItem = 03 matched to enum WATER_TEMPERATURE
controlValue = 78 (120 dec)

An EMS monthly sample:

Initial trend sample raw request

00000000  07 99 00 a6 37 00 XX XX  XX XX XX XX XX XX 01 03
00000010  01 01 03 00 00 00 00 00  00 00 00 00 00 00 00 00
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000030  00 00 00 00 00
Rest of status request follows starting at array position 14 (byte 15):
commandCount = 01
currentControlChannel = 03 (NPE device is connected to Navilink serial port 3)
deviceNumber = 01 (first device on the serial bus)
controlSorting = 01 (deviceInfo)
infoItem = 03 matched to KDEnum.ControlType.TREND_SAMPLE
controlItem = 00
controlValue = 00

Trend sample response

00000000  XX XX XX XX XX XX XX XX  01 03 0e 00 13 05 1c 00
00000010  01 01 03 01 00 00 00 e8  05 00 00 f7 40 00 00 3b
00000020  3a 42 00 00 00 00 00
Common response header:
deviceID = XX XX XX XX XX XX XX XX
countryCD = 01
controlType = 03 matched to enum KDEnum.ControlType.TREND_SAMPLE
swVersionMajor = 0e & 0xFF = 14 dec
swVersionMinor = 00 & 0xFF = 0

The app does not use the following (status-like) response details starting at array position 12 (byte 13):
controllerVersion = 13 05
pannelVersion = 1c 00
deviceSorting = 01 matched to enum NPE and set to KDEnum.DeviceSorting.NPE
deviceCount = 01
currentChannel = 03 (device is connected to serial port 3 on the Navilink)
deviceNumber = 01

Trend sample information response starts at array position 20 (byte 21):
modelInfo = 00 00 00
totalOperatedTime = e8 05 00 00 treat as little endian and convert to int = 1512 (hour)
totalGasAccumulateSum = f7 40 00 00 treat as little endian and convert to int = 16631 (this is not displayed, but likely corresponds with gasAccumulatedUse used in the state response and would be converted from m^3/10 in the app by 16631 * 35.314667 / 10.0 = 58,731.8226877 and rounded to 58,731.8 ft^3)
totalHotWaterAccumulateSum = 3b 3a 42 00 treat as little endian and convert to int (this is not displayed)
totalCHOperatedTime = 00 00 00 00 (heating is not used)
if the response payload was >39, would read another four bytes for totalDHWUsageTime

Another status request for TREND_MONTH is sent following the above

00000000  07 99 00 a6 37 00 XX XX  XX XX XX XX XX XX 01 03
00000010  01 01 04 00 00 00 00 00  00 00 00 00 00 00 00 00
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000030  00 00 00 00 00
Rest of status request follows starting at array position 14 (byte 15):
commandCount = 01
currentControlChannel = 03 (NPE device is connected to Navilink serial port 3)
deviceNumber = 01 (first device on the serial bus)
controlSorting = 01 (deviceInfo)
infoItem = 04 matched to KDEnum.ControlType.TREND_MONTH
controlItem = 00
controlValue = 00

The response to trend month contains all data for daily total gas usage, daily domestic usage time, daily domestic usage Cnt, daily total hot button cnt

00000000  XX XX XX XX XX XX XX XX  01 04 0e 00 13 05 1c 00
00000010  01 01 03 01 1f 01 00 00  00 00 00 00 00 00 00 00
00000020  00 00 00 00 00 00 00 00  00 00 00 02 00 00 00 00
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000040  00 03 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000050  00 00 00 00 00 00 00 04  00 00 00 00 00 00 00 00
00000060  00 00 00 00 00 00 00 00  00 00 00 00 00 05 00 00
00000070  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000080  00 00 00 06 00 00 00 00  00 00 00 00 00 00 00 00
00000090  00 00 00 00 00 00 00 00  00 07 00 00 00 00 00 00
000000A0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 08
000000B0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
000000C0  00 00 00 00 00 09 00 00  00 00 00 00 00 00 00 00
000000D0  00 00 00 00 00 00 00 00  00 00 00 0a 00 00 00 00
000000E0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
000000F0  00 0b 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000100  00 00 00 00 00 00 00 0c  00 00 00 00 00 00 00 00
00000110  00 00 00 00 00 00 00 00  00 00 00 00 00 0d 00 00
00000120  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000130  00 00 00 0e 00 00 00 00  00 00 00 00 00 00 00 00
00000140  00 00 00 00 00 00 00 00  00 0f 00 00 00 01 00 00
00000150  00 00 00 00 00 02 00 00  00 00 00 20 20 00 00 10
00000160  00 00 00 09 00 00 00 00  00 00 00 06 00 00 00 00
00000170  00 20 20 00 00 11 00 00  00 05 00 00 00 6c 09 00
00000180  00 04 00 00 00 00 00 20  20 01 00 12 00 00 00 08
00000190  00 00 00 00 00 00 00 05  00 00 00 00 00 20 20 00
000001A0  00 13 00 00 00 17 00 00  00 69 12 00 00 09 00 00
000001B0  00 00 00 20 20 02 00 14  00 00 00 0b 00 00 00 27
000001C0  09 00 00 0c 00 00 00 00  00 20 20 01 00 15 00 00
000001D0  00 04 00 00 00 d4 08 00  00 04 00 00 00 00 00 20
000001E0  20 01 00 16 00 00 00 00  00 00 00 00 00 00 00 00
000001F0  00 00 00 00 00 00 00 00  00 17 00 00 00 00 00 00
00000200  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 18
00000210  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000220  00 00 00 00 00 19 00 00  00 00 00 00 00 00 00 00
00000230  00 00 00 00 00 00 00 00  00 00 00 1a 00 00 00 00
00000240  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000250  00 1b 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000260  00 00 00 00 00 00 00 1c  00 00 00 00 00 00 00 00
00000270  00 00 00 00 00 00 00 00  00 00 00 00 00 1d 00 00
00000280  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000290  00 00 00 1e 00 00 00 00  00 00 00 00 00 00 00 00
000002A0  00 00 00 00 00 00 00 00  00 1f 00 00 00 00 00 00
000002B0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00
Common response header:
deviceID = XX XX XX XX XX XX XX XX
countryCD = 01
controlType = 04 matched to enum KDEnum.ControlType.TREND_MONTH
swVersionMajor = 0e & 0xFF = 14 dec
swVersionMinor = 00 & 0xFF = 0

The app does not use the following (status-like) response details starting at array position 12 (byte 13):
controllerVersion = 13 05
pannelVersion = 1c 00
deviceSorting = 01 matched to enum NPE and set to KDEnum.DeviceSorting.NPE
deviceCount = 01
currentChannel = 03 (device is connected to serial port 3 on the Navilink)
deviceNumber = 01

Trend month information response starts at array position 20 (byte 21):
totalDaySequence = 1f = 31 dec
Loops 31 times and reads the following
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
-trendData.sequence = 01
-trendData.modelInfo = 00 00 00
-trendData.gasAccumulatedUse = 00 00 00 00 treat as little endian and convert to int = 0 converted from m^3/10 in the app by 0 * 35.314667 / 10.0 = 0 and rounded to 0 ft^3
-trendData.hotWaterAccumulatedUse = 00 00 00 00 treat as little endian and convert to int = 0 (not shown in the app for NPE, but for others that do this is converted in the app by 0 / 3.785 / 10.0 = 0 and rounded to 0 G)
-trendData.hotWaterOperatedCount = 00 00 => ((00 & 0xFF) + (00 & 0xFF) * 256 = 0 dec
-trendData.onDemandUseCount = 00 00 => ((00 & 0xFF) + (00 & 0xFF) * 256 = 0 dec
-trendData.heatAccumulatedUse = 00 00 => ((00 & 0xFF) + (00 & 0xFF) * 256 = 0 dec
-trendData.outdoorAirMaxTemperature = 00
-trendData.outdoorAirMinTemperature = 00
-trendData.dHWAccumulatedUse = 00 00 => ((00 & 0xFF) + (00 & 0xFF) * 256 = 0 dec
...
14 00 00 00 0b 00 00 00 27 09 00 00 0c 00 00 00 00 00 20 20 01 00
-trendData.sequence = 14
-trendData.modelInfo = 00 00 00
-trendData.gasAccumulatedUse = 0b 00 00 00 treat as little endian and convert to int = 11 converted from m^3/10 in the app by 11 * 35.314667 / 10.0 = 38.8461337 and rounded to 38.9 ft^3
-trendData.hotWaterAccumulatedUse = 27 09 00 00 treat as little endian and convert to int = 2343 (not shown in the app for NPE, but for others that do this is converted in the app by 2343 / 3.785 / 10.0 = 61.902246 and rounded to 61.9 G)
-trendData.hotWaterOperatedCount = 0c 00 => ((0c & 0xFF) + (00 & 0xFF) * 256 = 12 dec converted in the app by 12 * 10 = 120
-trendData.onDemandUseCount = 00 00 => ((00 & 0xFF) + (00 & 0xFF) * 256 = 0 dec
-trendData.heatAccumulatedUse = 00 00 => ((00 & 0xFF) + (00 & 0xFF) * 256 = 00 dec
-trendData.outdoorAirMaxTemperature = 20 = 32 dec (probably a default value as this feature is not in use)
-trendData.outdoorAirMinTemperature = 20 = 32 dec (probably a default value as this feature is not in use)
-trendData.dHWAccumulatedUse = 01 00 => ((01 & 0xFF) + (00 & 0xFF) * 256 = 1 dec

An EMS yearly sample:

Request for TREND_YEAR

00000000  07 99 00 a6 37 00 XX XX  XX XX XX XX XX XX 01 03
00000010  01 01 05 00 00 00 00 00  00 00 00 00 00 00 00 00
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000030  00 00 00 00 00 
Rest of status request follows starting at array position 14 (byte 15):
commandCount = 01
currentControlChannel = 03 (NPE device is connected to Navilink serial port 3)
deviceNumber = 01 (first device on the serial bus)
controlSorting = 01 (deviceInfo)
infoItem = 05 matched to KDEnum.ControlType.TREND_YEAR
controlItem = 00
controlValue = 00

The response to trend year contains all data for monthly total gas usage, monthly domestic usage time, monthly domestic usage Cnt, monthly total hot button cnt

00000000  XX XX XX XX XX XX XX XX  01 05 0e 00 13 05 1c 00
00000010  01 01 03 01 18 01 00 00  00 00 00 00 00 00 00 00
00000020  00 00 00 00 00 00 00 00  00 00 00 02 00 00 00 00
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000040  00 03 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000050  00 00 00 00 00 00 00 04  00 00 00 00 00 00 00 00
00000060  00 00 00 00 00 00 00 00  00 00 00 00 00 05 00 00
00000070  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000080  00 00 00 06 00 00 00 00  00 00 00 00 00 00 00 00
00000090  00 00 00 00 00 00 00 00  00 07 00 00 00 00 00 00
000000A0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 08
000000B0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
000000C0  00 00 00 00 00 09 00 00  00 00 00 00 00 00 00 00
000000D0  00 00 00 00 00 00 00 00  00 00 00 0a 00 00 00 00
000000E0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
000000F0  00 0b 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000100  00 00 00 00 00 00 00 0c  00 00 00 00 00 00 00 00
00000110  00 00 00 00 00 00 00 00  00 00 00 00 00 0d 00 00
00000120  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000130  00 00 00 0e 00 00 00 00  00 00 00 00 00 00 00 00
00000140  00 00 00 00 00 00 00 00  00 0f 00 00 00 00 00 00
00000150  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 10
00000160  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00000170  00 00 00 00 00 11 00 00  00 00 00 00 00 00 00 00
00000180  00 00 00 00 00 00 00 00  00 00 00 12 00 00 00 00
00000190  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
000001A0  00 13 00 00 00 00 00 00  00 00 00 00 00 00 00 00
000001B0  00 00 00 00 00 00 00 14  00 00 00 00 00 00 00 00
000001C0  00 00 00 00 00 00 00 00  00 00 00 00 00 15 00 00
000001D0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
000001E0  00 00 00 16 00 00 00 00  00 00 00 00 00 00 00 00
000001F0  00 00 00 00 00 00 00 00  00 17 00 00 00 00 00 00
00000200  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 18
00000210  00 00 00 45 00 00 00 d0  2d 00 00 2f 00 00 00 00
00000220  00 20 20 05 00
Common response header:
deviceID = XX XX XX XX XX XX XX XX
countryCD = 01
controlType = 05 matched to enum KDEnum.ControlType.TREND_YEAR
swVersionMajor = 0e & 0xFF = 14 dec
swVersionMinor = 00 & 0xFF = 0

The app does not use the following (status-like) response details starting at array position 12 (byte 13):
controllerVersion = 13 05
pannelVersion = 1c 00
deviceSorting = 01 matched to enum NPE and set to KDEnum.DeviceSorting.NPE
deviceCount = 01
currentChannel = 03 (device is connected to serial port 3 on the Navilink)
deviceNumber = 01

Trend year information response starts at array position 20 (byte 21):
totalDaySequence = 18 = 24 dec (rolling month by month over two years. Last sequence is most recent)
Loops 24 times and reads 22 bytes
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
-trendData.sequence = 01 (1 dec)
-trendData.modelInfo = 00 00 00
-trendData.gasAccumulatedUse = 00 00 00 00 treat as little endian and convert to int = 0 converted from m^3/10 in the app by 0 * 35.314667 / 10.0 = 0 and rounded to 0 ft^3
-trendData.hotWaterAccumulatedUse = 00 00 00 00 treat as little endian and convert to int = 0 (not shown in the app for NPE, but for others that do this is converted in the app by 0 / 3.785 / 10.0 = 0 and rounded to 0 G)
-trendData.hotWaterOperatedCount = 00 00 => ((00 & 0xFF) + (00 & 0xFF) * 256 = 0 dec
-trendData.onDemandUseCount = 00 00 => ((00 & 0xFF) + (00 & 0xFF) * 256 = 0 dec
-trendData.heatAccumulatedUse = 00 00 => ((00 & 0xFF) + (00 & 0xFF) * 256 = 0 dec
-trendData.outdoorAirMaxTemperature = 00 & 0xFF
-trendData.outdoorAirMinTemperature = 00 & 0xFF
-trendData.dHWAccumulatedUse = 00 00 => ((00 & 0xFF) + (00 & 0xFF) * 256 = 0 dec
...
18 00 00 00 45 00 00 00 d0 2d 00 00 2f 00 00 00 00 00 20 20 05 00
-trendData.sequence = 18 (24 dec)
-trendData.modelInfo = 00 00 00
-trendData.gasAccumulatedUse = 45 00 00 00 treat as little endian and convert to int = 69 converted from m^3/10 in the app by 69 * 35.314667 / 10.0 = 243.6712023 and rounded to 243.7 ft^3
-trendData.hotWaterAccumulatedUse = d0 2d 00 00 treat as little endian and convert to int = 11728 (not shown in the app for NPE, but for others that do this is converted in the app by 11728 / 3.785 / 10.0 = 309.854689 and rounded to 309.9 G)
-trendData.hotWaterOperatedCount = 2f 00 => ((2f & 0xFF) + (00 & 0xFF) * 256 = 47 dec converted in the app by 47 * 10 = 470
-trendData.onDemandUseCount = 00 00 => ((00 & 0xFF) + (00 & 0xFF) * 256 = 0 dec
-trendData.heatAccumulatedUse = 00 00 => ((00 & 0xFF) + (00 & 0xFF) * 256 = 0 dec
-trendData.outdoorAirMaxTemperature = 20 & 0xFF = 32 dec (probably a default value as this feature is not in use)
-trendData.outdoorAirMinTemperature = 20 & 0xFF = 32 dec (probably a default value as this feature is not in use)
-trendData.dHWAccumulatedUse = 05 00 => ((05 & 0xFF) + (00 & 0xFF) * 256 = 5 dec (hours)

FYI: I have created a new repo to better organize my protocol decoding notes as well as to start making the appropriate code updates to utilize this information.

Hi folks, just a quick update: I have completed the Python module and CLI rewrite over on https://github.com/rudybrian/PyNavienSmartControl This updated implementation supports individual NPE, NCB, NHB, NFB, NFC, NPN, NPE2. NCB-H, NVW as well as cascaded NPE, NHB, NFB, NPN, NPE2 and NVW device types.

Hi @rudybrian, this looks really good! Unfortunately I moved homes in 2020 so no longer have access to a Navien boiler. I will archive my project to prevent future confusion. Really impressive work though that you have created 🥇