Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exact mapping from Solax Android application #108

Open
nazar-pc opened this issue Jan 11, 2023 · 35 comments
Open

Exact mapping from Solax Android application #108

nazar-pc opened this issue Jan 11, 2023 · 35 comments

Comments

@nazar-pc
Copy link

Solax Power in private conversation refused to disclose meaning of the parameters of the ReadRealTimeData endpoint, so I decided to go through the source code of the Android app (version 0.4.3) and extract everything I can from there, thankfully all the logic there is client-side in JS (though JS is minified, so it takes some effort).

This will result in precise mapping the same exact way mobile app interprets data and will remove the need to guess values. With that information everything Android application supports will be also possible to support here in exactly the same way.

I'm only focusing on reading ReadRealTimeData values for now, but it would be possible to potentially replicate all the logic application supports if it is desirable (I'm a user of this library indirectly through Home Assistant, having ability to switch Work Mode, charge/discharge time and other things would be useful, but it would be a bit tricky to expose everything properly given how many inverter models they have and features are different for different inverters).

Once I have basic version compiled, I'll open source it in GitHub repo and comment it here, this is just for tracking and discussion purposes for now.

cc @fxstein as you commented on home-assistant/home-assistant.io#25151

@nazar-pc
Copy link
Author

nazar-pc commented Jan 11, 2023

As a primer, for my inverter X1 Hybrid G4 the correct and full mapping looks like this:

function read16BitSigned(n) {
  if (n < 32768) {
    return n;
  } else {
    return n - 65536;
  }
}

function read32BitUnsigned(a, b) {
  return a + 65536 * a;
}

function read32BitSigned(a, b) {
  if (a < 32768) {
    return a + 65536 * a;
  } else {
    return a + 65536 * a - 4294967296;
  }
}

{
  Yield_Today: Data[13] / 10,
  Yield_Total: read32BitUnsigned(Data[11], Data[12]) / 10,
  PowerDc1: Data[8],
  PowerDc2: Data[9],
  BAT_Power: read16BitSigned(Data[16]),
  feedInPower: read32BitSigned(Data[32], Data[33]),
  GridAPower: read16BitSigned(Data[2]),
  FeedInEnergy: read32BitUnsigned(Data[34], Data[35]) / 100,
  ConsumeEnergy: read32BitUnsigned(Data[36], Data[37]) / 100,
  RunMode: Data[10],
  EPSAPower: read16BitSigned(Data[28]),
  Vdc1: Data[4] / 10,
  Vdc2: Data[5] / 10,
  Idc1: Data[6] / 10,
  Idc2: Data[7] / 10,
  EPSAVoltage: Data[29] / 10,
  EPSACurrent: read16BitSigned(Data[30]) / 10,
  BatteryCapacity: Data[18],
  BatteryVoltage: Data[14] / 100,
  BatteryTemperature: read16BitSigned(Data[17]),
  GridAVoltage: Data[0] / 10,
  GridACurrent: read16BitSigned(Data[1]) / 10,
  FreqacA: Data[3] / 100,
}

As you can see, current version of the library doesn't read AC power and other values correctly when it is negative, resulting in ridiculous values like 62618 instead of correct value −2918.

@nazar-pc
Copy link
Author

Published all the documentation I was able to gather: https://github.com/nazar-pc/solax-local-api-docs

New release of the app came out 3 days ago (0.4.4), but according to changelog nothing major happened there, so I didn't bother looking into it.

@nazar-pc nazar-pc changed the title FYI I'm working on extracting exact mapping from Solax Android application Exact mapping from Solax Android application Jan 15, 2023
@fxstein
Copy link

fxstein commented Jan 15, 2023

Very much appreciated! I just got a set of X3 Hybrids up and running. Will be testing and integrating in about 2 weeks.

@nazar-pc
Copy link
Author

BTW, what would be the right place to request more sensors in HA? I had to derive a bunch of things like load power, charge/discharge rate and remaining time, totals for stored/used battery power for energy management. I can provide templates and describe rationale, just want to make sure I post it in the correct place.

@fxstein
Copy link

fxstein commented Jan 16, 2023

Not an expert on this component but: In general sensors are created dynamically in HA.
Checkout

for name, mapping in cls.response_decoder().items():
in this component's code to see where it tells HA about the sensors. As long as the decoder provides them, HA will create the entities. Special care needs to be given in the naming.
Hope this is helpful.

@nazar-pc
Copy link
Author

This library just extracts data. HA integration though should be free to add more sensors, which have derived values (for totals or change rate not provided by inverter itself it'll need access to older values for instance).

@fxstein
Copy link

fxstein commented Jan 16, 2023

You can create sensors either from data or from calculations. HA does not care. It's the integration that delivers the data and its classification.

eg

"Grid 1 Voltage": (0, Units.V, div10),

But I am not the author of this integration. Just happen to watch it closely as I will need it soon.

@nazar-pc
Copy link
Author

I understand that, my point is that it would be helpful if integration did provide some additional calculated sensors on top of raw values this library gets from the inverter. I just assumed you may be working on updating the library and integration afterwards, in which case I'd love to see some new sensors added.

I might look into it myself, but I don't feel very comfortable working with Python.

@fxstein
Copy link

fxstein commented Jan 16, 2023

FYI There is also a bunch of modbus info out there that might come in handy:

https://library.loxone.com/detail/solax-x3-hybrid-(neoom-kjubee)-917/overview

@nazar-pc
Copy link
Author

Yeah, Solax even sent me docs for it. But it doesn't seem to provide any new information and it is much less convenient to work with than to issue an HTTP request.

@squishykid
Copy link
Owner

I'm currently working on including this new information in the next release of this library.

@johny-mnemonic
Copy link

@squishykid would you please add EV chargers as well, not just inverters?

@nazar-pc Thanks a lot for extracting the mapping! ❤️
While going through the app code did you notice how the changes are done? Is it somehow possible to use this local API to also send commands, not just to read values?
That would be beneficial as with the ability to also send commands one would not need to buy Modbus converter to control the Inverter/Charger.

@nazar-pc
Copy link
Author

nazar-pc commented Mar 7, 2023

Yes, as long as UI allows to change settings (it does) it is certainly possible. The challenge is that API is kind of ugly and confusing, so changing critical parameters is potentially dangerous. Also limits configured in the app do not actually correspond to limits configured in the inverter itself (which leads to confusing errors), which is why I focused on reading data first rather than modifying.

I'd love to see ability to change some setting like lowering charging current depending on some rules in Home Assistant for instance, but Python is not my strong side, so unless someone volunteers to do the hard work, I can try to extract up to date mappings (they are likely also model-dependent).

@fxstein
Copy link

fxstein commented Mar 7, 2023

FYI I have been digging into their private APIs last weekend since their public API was hard down. First of all, all the private API endpoints continued to work while the public API was down.

I used Safari and Chrome to inspect the web interface, rather than the mobile App. Then used ARC to assemble the API calls to retrieve the data. You first need to login into the web App with a simple call:

curl "https://www.solaxcloud.com/phoebus/login/loginNew" \
  -X POST \
  -d "username=yourusername&userpwd=yourencodedpassword" \
  -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" 

if successful the response will include a token at the very end of the json document:

{
 ...
  "token": "your-magic-token"
}

You need to use that token for all the realtime calls like this:

curl "https://www.solaxcloud.com/phoebus/device/getInverterFromRedis" \
  -X POST \
  -d "inverterSN=yourinverterSN" \ 
  -H "token: your-magic-token" \
  -H "content-type: application/x-www-form-urlencoded" 

inverterSN is the serial number of the inverter independent of whatever communication module or its registration code.

if successful the response will look like this:

{
  "inverterSn": "yourinvertersn",
  "wsn": "yourregistrationcode",
  "moduleName": null,
  "userId": "youruserid",
  "siteId": "yoursiteid",
  "firmId": "afirmid",
  "firmwareVer": null,
  "ratedPower": 10.0,
  "idc1": 1.3,
  "idc2": 1.2,
  "idc3": null,
  "idc4": null,
  "idc5": null,
  "idc6": null,
  "idc7": null,
  "idc8": null,
  "idc9": null,
  "idc10": null,
  "idc11": null,
  "idc12": null,
  "vdc1": 461.0,
  "vdc2": 448.8,
  "vdc3": null,
  "vdc4": null,
  "vdc5": null,
  "vdc6": null,
  "vdc7": null,
  "vdc8": null,
  "vdc9": null,
  "vdc10": null,
  "vdc11": null,
  "vdc12": null,
  "iac1": 1.7,
  "vac1": 237.0,
  "gridpower": 1015.0,
  "temperature": 33.0,
  "todayyield": null,
  "yieldtoday": 37.0,
  "yieldtotal": 831.4,
  "feedinpower": 3361.0,
  "powerdc1": 652.0,
  "powerdc2": 533.0,
  "powerdc3": null,
  "powerdc4": null,
  "powerdc5": null,
  "powerdc6": null,
  "powerdc7": null,
  "powerdc8": null,
  "powerdc9": null,
  "powerdc10": null,
  "powerdc11": null,
  "powerdc12": null,
  "pac1": 334.0,
  "pac2": 338.0,
  "pac3": 342.0,
  "iac2": 1.7,
  "iac3": 1.7,
  "vac2": 236.5,
  "vac3": 237.3,
  "fac1": 50.0,
  "fac2": 49.99,
  "fac3": 50.0,
  "feedinenergy": 413.27,
  "consumeenergy": 3197.13,
  "uploadTime": timestamp,
  "uploadTimeValue": "formatted date & time",
  "type": null,
  "status": null,
  "version": "3.003.01",
  "inverterNum": null,
  "inverterTemperature": "44",
  "batVoltage1": 479.6,
  "batCurrent1": 0.0,
  "batPower1": 0.0,
  "chargePower": null,
  "temperBoard1": 27.0,
  "surplusEnergy1": 17.8,
  "inputEnergy1": 329.3,
  "outputEnergy1": 288.2,
  "batteryType": "1",
  "batteryStatus": "1",
  "batteryCapacity": 80.0,
  "chargecutVoltage": 524.6,
  "disChargecutVoltage": 361.0,
  "epsYieldToday": 0.0,
  "relay1Status": null,
  "relay1Power": null,
  "relay1Energy": null,
  "relay1Signal": null,
  "relay1answer": null,
  "relay2Status": null,
  "relay2Power": null,
  "relay2Energy": null,
  "relay2Signal": null,
  "relay2answer": null,
  "inverterStatus": 2.0,
  "inverterStatusValue": null,
  "systemSwitch": 1.0,
  "safety": null,
  "powerFactor": 1.0,
  "exportControl": null,
  "powerLimit": null,
  "inverterType": "14",
  "userName": "yourusername",
  "firmName": "afirmname",
  "currentYieldIncome": null,
  "totalYieldIncome": null,
  "faultType": null,
  "wMachineStyle": null,
  "epsVoltageA": 0.0,
  "epsCurrentA": 0.0,
  "epsApowerActive": 0.0,
  "epsVoltageB": 0.0,
  "epsCurrentB": 0.0,
  "epsBpowerActive": 0.0,
  "epsVoltageC": 0.0,
  "epsCurrentC": 0.0,
  "epsCpowerActive": 0.0,
  "epsApowerS": 0.0,
  "epsBpowerS": 0.0,
  "epsCpowerS": 0.0,
  "feedinPowerMeter2": 0.0,
  "feedinEnergyMeter2": 0.0,
  "consumeEnergyMeter2": 0.0,
  "managerBootloaderVersion": "1.09",
  "seller": null,
  "batteryBrand": "91",
  "batteryMasterVer": "1.02",
  "batterySlaveNum": "8",
  "batterySlaveVer1": "2.06",
  "batterySlaveVer2": "2.06",
  "batterySlaveVer3": "2.06",
  "batterySlaveVer4": "2.06",
  "batterySlaveVer5": "2.06",
  "batterySlaveVer6": "2.06",
  "batterySlaveVer7": "2.06",
  "batterySlaveVer8": "2.06",
  "batterySlaveVer9": "0.00",
  "batterySlaveVer10": "0.00",
  "batterySlaveVer11": "0.00",
  "batterySlaveVer12": "0.00",
  "batterySlaveVer13": "0.00",
  "batterySlaveVer14": "0.00",
  "batterySlaveVer15": "0.00",
  "batterySlaveVer16": "0.00",
  "batterySlaveType1": "0",
  "batterySlaveType2": "0",
  "batterySlaveType3": "0",
  "batterySlaveType4": "0",
  "batterySlaveType5": "0",
  "batterySlaveType6": "0",
  "batterySlaveType7": "0",
  "batterySlaveType8": "0",
  "batterySlaveType9": "0",
  "batterySlaveType10": "0",
  "batterySlaveType11": "0",
  "batterySlaveType12": "0",
  "batterySlaveType13": "0",
  "batterySlaveType14": "0",
  "batterySlaveType15": "0",
  "batterySlaveType16": "0",
  "iacL1N": null,
  "vacL1N": null,
  "iacL2N": null,
  "vacL2N": null,
  "genFreq": null,
  "genPower": null,
  "genL1Vol": null,
  "genL2Vol": null,
  "loadCurrentL1N": null,
  "loadCurrentL2N": null,
  "loadVoltL1N": null,
  "loadVoltL2N": null,
  "outputEnergy": null,
  "loadFlag": 0,
  "gridInputYieldTotal": 154.2,
  "batChargeYieldTotal": null,
  "pvYieldTotal": 772.6,
  "epsYieldTotal": 0.6,
  "vbatteryType": null,
  "dataReference": 0,
  "bmsBatVoltage": 480.3,
  "bmsBatCurrent": 0.0,
  "bmsBatPower": 0.0,
  "inverterMasterVer": "1.29",
  "inverterSlaveVer": "0.00",
  "managerVer": "1.27",
  "chargeVer": "0.00",
  "yieldIncr": 0.10000000000000142,
  "feedinEnergyIncr": 0.3299999999999841,
  "consumerEnergyIncr": 0.0,
  "pvEnergyInc": 0.10000000000002274,
  "consumerEnergyTmp": null,
  "feedinEnergyTmp": null,
  "selfUseIncome": 0.0,
  "feedIncome": 0.04751669999999771,
  "consumerIncome": 0.0,
  "fiveMinuteVal": 15,
  "stateMessage1": null,
  "stateMessage2": null,
  "energyThroughout": "639453",
  "masterSN": "yourbatterymastersn",
  "slave1_2SN": "yourbatterymastersn",
  "slave3_4SN": "yournextbatterysn1",
  "slave5_6SN": "yournextbatterysn2",
  "slave7_8SN": "yournextbatterysn3",
  "slave9_10SN": "",
  "slave11_12SN": "",
  "slave13_14SN": "",
  "slave15_16SN": "",
  "isBatteryAlarm": 0,
  "isBatteryAlarmValue": null,
  "feedinpower1": null,
  "feedinpower2": null,
  "chargingRequset": 0,
  "voltageHighPackageNum": 5,
  "voltageHighCellNum": 2,
  "voltageLowPackageNum": 2,
  "voltageLowCellNum": 1,
  "tempHighPackageNum": 2,
  "tempHighCellNum": 7,
  "tempLowPackageNum": 7,
  "tempLowCellNum": 5,
  "dayTotalYield": null,
  "monthTotalYield": null,
  "yearTotalYield": null,
  "generationStatus": null,
  "batterySN": null,
  "siteName": "yoursitename",
  "leaseMode": 0,
  "lockMode": 0,
  "pvPower": null,
  "powerdc": null,
  "vdc": null,
  "idc": null,
  "batterySlaveVer": null,
  "batterySlaveType": null,
  "slaveSN": null,
  "deviceAddress": null,
  "protocolTag": null,
  "producer": null,
  "model": null,
  "softwareVersion": null,
  "uploadSn": null,
  "protocolNo": null,
  "protocolVersion": null,
  "uab": null,
  "ubc": null,
  "uca": null,
  "reactivePower": null,
  "apparentPower": null,
  "reactiveYieldTotal": null,
  "reactiveYieldToday": null,
  "mpptNum": null,
  "pvModel": null,
  "busVoltage": 0.0,
  "dcPhaseA": null,
  "dcPhaseB": null,
  "dcPhaseC": null,
  "gfci": null,
  "subInverterStatus": null,
  "inverterTemperature2": null,
  "inverterTemperature3": null,
  "boostTemperature": null,
  "inductorTmperature": null,
  "insulationResistance": null,
  "fanState": null,
  "faultTime": null,
  "lastFaultType": null,
  "faultCode": null,
  "warningCode": null,
  "boostTemp1": null,
  "boostTemp2": null,
  "boostTemp3": null,
  "boostTemp4": null,
  "inverterTemp1": null,
  "inverterTemp2": null,
  "inverterTemp3": null,
  "acTemp1": null,
  "acTemp2": null,
  "acTemp3": null,
  "derateState": null,
  "fan1Speed": null,
  "fan2Speed": null,
  "fan3Speed": null,
  "fan4Speed": null,
  "masterSlaveType": null,
  "inverterAddress": null,
  "runState": null,
  "gridL1Vol": null,
  "gridL1Cur": null,
  "gridL2Vol": null,
  "gridL2Cur": null,
  "gridFreq": null,
  "genL1Cur": null,
  "genL2Cur": null,
  "atsRunMode": null,
  "atsRunState": null,
  "atsErrorCode": null,
  "versionSlave": null,
  "versionMaster": null,
  "gridpriority": null,
  "manualpriority": null,
  "dischargeThroughput": null,
  "chargeThroughput": null,
  "tzMsg": null,
  "cbcMsg": null,
  "relayMsg": null,
  "pwrLimitMsg": null,
  "mpptCommType": null,
  "mpptMsg": null,
  "logData1": null,
  "logData2": null,
  "logData3": null,
  "logData4": null,
  "connTimeCntDown": null,
  "outputSwitch": null,
  "burnInFlag": null,
  "runTime": null,
  "remoteOff": null,
  "busVolt": null,
  "externdOvertCurrent": null,
  "debugMsg1": null,
  "debugMsg2": null,
  "debugMsg3": null,
  "debugMsg4": null,
  "inverterSlaveStatus": null,
  "consumEnergyToday": 10.65,
  "feedinEnergyToday": 58.9,
  "inputEnergyChargeToday": 2.5,
  "outputEnergyChargeToday": 2.1,
  "inverterConsumeTotal": null,
  "inverterConsumeToday": null,
  "rgmYieldTotal": null,
  "rgmYieldToday": null,
  "arcVersion": null,
  "epsActivePower": 0,
  "epspower": 0.0,
  "batteryCellVoltageHigh2": 3.339,
  "batteryCellVoltageLow2": 3.330,
  "batteryCellVoltageDValue": null,
  "bmslost": 0,
  "inverterError": 0.0,
  "minCapacity": null,
  "epsvoltage": null,
  "epscurrent": null,
  "epsfrequency": 0.0,
  "bmsdisChargemaxCurrent": null,
  "batteryCellTemperatureHigh": 20.0,
  "batteryCellTemperatureLow": 15.5,
  "batteryCellVoltageHigh": 3.3,
  "batteryCellVoltageLow": 3.3,
  "batteryHealth": "14",
  "bmscommunication": null,
  "userChargemaxCurrent": 0.0, // incorrect
  "userDischargemaxCurrent": 35.0, // incorrect
  "workMode": null,
  "bmserror": 0.0,
  "managerRrror": 0.0,
  "bmschargemaxCurrent": null,
  "meterFunction": null,
  "meter1ComState": 1.0,
  "meter2ComState": 0.0,
  "managerError": 0.0,
  "bmserror2": null
}

Some of the values are incorrect like userChargemaxCurrent and userDischargemaxCurrent that I have set to different values in the app. I have replaced identifiable things like user or site information with madeup strings.

@nazar-pc
Copy link
Author

nazar-pc commented Mar 7, 2023

This is not the same as local access that this library is about. Also those "real time data" are not actually real time from what I saw, they are snapshots made every 5 minutes. So it is off-topic here (and please put the text under <details></details>, it beaks readability of the rest of the comments due to its length).

@johny-mnemonic
Copy link

@fxstein good job on digging into it. As @nazar-pc points out though their cloud API is of low interest for us as it is slow and unreliable.
Could you please try whether the local API uses the same authentication technique? It is accessed by the same app or web client so there is high chance it works the same 🤞
If you confirm that, we might be able to try changing some safe values like charger mode in case @nazar-pc manages to extract the mappings from the app.

@fxstein
Copy link

fxstein commented Mar 7, 2023

Changing inverter settings uses a different login method and API. I have just started playing with that. The login is different as it also requires the installer password to change most settings.

curl "https://abroad.solaxcloud.com/proxy//settingnew/paramSet" \
  -X POST \
  -d "MIME Type: application/x-www-form-urlencoded\noptType: setReg\nnum: 1\nsn: yourreistrationcode\ninverterSn: yourinvertersn\ndeviceType: 1\ntokenId: this-is-a-different-api-token\nData: [{\"reg\":200,\"val\":80}]" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "Accept: application/json, text/plain, */*" 

in this case register 200 is the maximum charge level for the charger, setting it to 80% in this example.

I have not had time to dig deeper into its login process or other settings, but Safari or Chrome developer shows you the details of every call when you hit the save button next to any setting.

@fxstein
Copy link

fxstein commented Mar 7, 2023

@nazar-pc changed the comment. My apologies for the length of it.

I understand that their online APIs are too slow - painfully aware of that. But I figured it can help with mapping data.
My system got installed with the LAN adapters (installer meant it well wiring everything up) but because of that I currently have no local API to play with and since I have 3 systems in master-slave setup, the simplified modbus protocol does not work either.
Waiting for the dongles to get swapped out for the WiFi/WLan versions...

@nazar-pc
Copy link
Author

nazar-pc commented Mar 7, 2023

For reading I extracted close to 100% of data. I think updating something like charging mode and charge/discharge percentages is relatively safe, but I'll have to look closer into it if someone wants to tackle implementing it. If different inverters have different mappings though the danger is that you might be editing one setting, but accidentally changing another to arbitrary value. You can trip circuit breaker, put the whole house offline or do other kinds of fun things that we better avoid.

Good thing is that it is all in the app. Bad thing is that application is poorly designed and code is minified with no documentation, so it is not exactly trivial to make sense of it. But certainly possible.

Another fun exercise would be to run mobile UI on desktop, then it is even easier to inspect things comparing to just reading the code, but I'm not in the mood (should be not too difficult though).

@johny-mnemonic
Copy link

@fxstein I see. That's unfortunate. If only those LAN adapters worked the same as WiFi, providing local API or Modbus TCP. That would be much better than going WiFi...
I am asking not because of my Inverter where I already have Modbus working but I also have the Solax EV charger (X3-EVC-11K). It has single Modbus interface and WiFi integrated into it. As it is connected to the inverter by the Modbus it seems my only option to integrate it is the local API...
Off-course if we can make this local API working it will help others that haven't invested into Modbus yet or do not have the ability to easily route required cabling to the inverter. I can help with testing as I have Pocket WiFi on mine and I can read from local API using curl.
@nazar-pc yep, charging mode is safe. The worst that can happen is that you will charge or discharge your battery in wrong time. That's low risk suited for playing with I would say :-)

@wills106
Copy link

wills106 commented Mar 7, 2023

I have 3 systems in master-slave setup, the simplified modbus protocol does not work either.

I added in support for Parallel Mode (Master Slave) a while back for the Modbus Integration. It's just untested, I don't see why it wouldn't work though.
I haven't fully implemented each individual Inverter yet, but the whole system should return the totals.

As it is connected to the inverter by the Modbus it seems my only option to integrate it is the local API...

If you can get some Modbus network traffic I can look into supporting that as well in the Modbus Integration.

@fxstein
Copy link

fxstein commented Mar 7, 2023

@wills106 Thank you! I did take a look at that but only had an ESP32 device with RS485 handy, so I played with your parallel protocol in ESPHome, but could never get any of the inverters to respond at all. I did get the occasional heartbeat message off the RS845 bus, so I knew I was on the correct settings. Currently 6000 miles away from the setup, hope to be back in person onsite in April.

@johny-mnemonic
Copy link

As it is connected to the inverter by the Modbus it seems my only option to integrate it is the local API...

If you can get some Modbus network traffic I can look into supporting that as well in the Modbus Integration.

@wills106 The trouble is we would probably need a passive mode for this as the EV charger is connected to the same Modbus connector/cable on the Inverter as is used for the communication with the Modbus Smart meter. It is entirely possible there is no communication between EV charger and Inverter at all and the EV charger is just reading grid load from the Smart meter over Modbus...
In other words it is different Modbus cable than I am using for communication with the Inverter.
I am a Modbus noob, but if I understand it correctly, I would need to switch my Waveshare to passive mode and connect it to the cabling Inverter(and EV charger) is using to get values from Smart meter. That would allow me to get the values from Smart meter directly instead of from the inverter and possibly allow me to communicate with both the Inverter and EV charger and as a bonus I wouldn't even need separate cabling.
Not sure if this is even technically possible though and I already have the extra Modbus cabling between Inverter and Waveshare...

@wills106
Copy link

wills106 commented Mar 8, 2023

need a passive mode

This might do the trick https://github.com/infradom/ha-addon-modbusspy

@wills106
Copy link

It is entirely possible there is no communication between EV charger and Inverter at all and the EV charger is just reading grid load from the Smart meter over Modbus...

There is a Modbus document available for the Charger.

With your installation does the Charger rely solely on the Inverter for Import / Export values?
Or do you also have the CT's hooked up to your Charger?
I'm going to start a discussion on my Github if you want to join in?

@johny-mnemonic
Copy link

With your installation does the Charger rely solely on the Inverter for Import / Export values? Or do you also have the CT's hooked up to your Charger?

On the charger there is only one RJ45 port and (same as on Inverters) you have to choose whether to use that port for CT Clamps or for the Modbus link. So I have both Inverter and Charger linked through common Modbus link that leads to the main grid meter.
As I said no clue whether there is any communication between Charger and Inverter on this link 🤷‍♂️

I'm going to start a discussion on my Github if you want to join in?

I already noticed and added a comment there. Thanks!

@nazar-pc
Copy link
Author

@squishykid do you have progress on this front?
It is getting annoying to manually editing files every time I upgrade Home Assistant to fix Solax integration, hoping for version where I don't need to do that anymore 🙏

@squishykid
Copy link
Owner

@nazar-pc I am working on this at the moment, but the refactor is difficult. It seems like some of the inverters have different mappings and I don't understand why. I.e. your mappings for inverter type 4 do not match https://github.com/squishykid/solax/blob/master/solax/inverters/x1_mini_v34.py.

I am trying to figure out how to resolve this. Perhaps you could send me a copy of the decompiled android app?

Perhaps this library can distinguish between different mappings based on whether the data parameter is required? Or perhaps it's based on whether the firmware version is >3?

@nazar-pc
Copy link
Author

nazar-pc commented Jun 11, 2023

From what I read in the code, it doesn't check the firmware version. Attaching zip file with all the JS files contained in the from official Android application (version 0.4.3).

One confusing thing I found was Type and Information[1] fields. They are both type of inverter and IIRC used interchangeably in some contexts, but not others. I suspect they are not guaranteed to be the same 🤷‍♂️

I'm fairly confident I mapped those correctly according to their source code, but I could have also made a mistake somewhere.

UPD: Uploaded unmodified JS files (some were re-formatted and modified in the previous upload):
solax.js.zip

@squishykid
Copy link
Owner

@nazar-pc perhaps you can help me, the specific case I am trying to distinguish is between these two test cases. They both have the same inverter number in Information[1] fields, but they both decode the data differently. I.e. X1MiniV34 has "Total Feed-in Energy" at index 41. X1 Boost has "Total Export Energy" at 50. They seem like the same field, but are at different indexes. When I look at your docs for inverter type 4, "FeedInEnergy" is between Data[50] and Data[51]`.

How can the solax library determine which decoding to use when Information[1] == 4?

@nazar-pc
Copy link
Author

What does type field say for those two, is it the same as Information[1]? It was the confusing part for me in their codebase.

Also I see the link to a blog post with modified firmware, is there a chance that firmware is incompatible with official app and doesn't follow the rules?

Also this is concerning, I have not seen any handling for type being a string in the app:

vol.Required("type"): vol.All(str, startswith("X1-")),

I'll try to take a look at the latest version of the Android app (0.4.5 vs 0.4.3 that I looked at before) this weekend.

@squishykid
Copy link
Owner

@nazar-pc If you look at the request fields for each of the linked examples, you can see the type fields

@nazar-pc
Copy link
Author

I see, well, I'll have to dive back into JS then to figure it out, will try to find time this weekend

@nazar-pc
Copy link
Author

I have looked at Android app 0.4.5 (latest as of right now, I was previous looking at and attached JS for 0.4.3), no meaningful changes to data handling there.


Looks like #101 also used official app to decode the data.

I have one user also stating that the data conversion must follow new format for X1 Boost in nazar-pc/solax-local-api-docs#1 (note that they are on v3 firmware already, also you can take a sample of their inverter data there).

So far there is no consistency regarding versions or type there. As far as I can see in the code, those inverters should not be working properly with local WiFi connection. If they do I just don't see how.

This is literally from application's sources:

e.inverterType = i["Information"][1];
localStorage.setItem("inverterType", e.inverterType);
e.transportType = i["Information"][9];
e.inverterType >= 8 ? localStorage.setItem("transportType", 1) : localStorage.setItem("transportType", e.transportType);
1 == e.transportType || e.inverterType >= 8 ? e.getPub2(i["Data"]) : e.getPub1(i["Data"]);
localStorage.setItem("dongleModType", i["Information"][10]);
localStorage.setItem("dongleWorkMode", i["Information"][11]);
localStorage.setItem("sn", i["SN"] || i["sn"]);

They write dumb redundant code, but it essentially boils down to this:

inverterType >= 8 ? getPub2(Data) : getPub1(Data)

There are no checks for other fields in there. Would be great to confirm with someone whether they can see proper data in their Android app with local WiFi connection.


One thing I noticed (there are too few data points, but still) is that Information[0] is above 1 for all devices that seem to follow new format (Data2 unique for every model) and everything that is 0.x seems to follow the old format. Try to go with that for now when deciding between Data1/Data2 and see if it passes tests. Though I don't see anywhere in the app that this field is actually used.

@Heronimonimo
Copy link

For anyone interested, I found a way to set the PowerLimit (which is usefull because I have dynamic tariffs which sometimes go negative in spring and summer). Value has is a number between 1 and 100.

The trick to find what to send was using the app in local mode connected to the WiFi broadcasted from the inverter and capturing the packets with PCAPdroid.

Below what I have in a NodeRed function what I send as an HTTP request to the local IP adress of the inverter:

msg.headers = {}
msg.headers = {
    'content-type': 'application/x-www-form-urlencoded',
    'X-Forwarded-For': '5.8.8.8'
};

msg.payload = {}
msg.payload = {
    'optType': 'ActivePowerLimit',
    'ActivePowerLimitValue': '100',
    'pwd':'admin'
};

return msg;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants