hultenvp / solis-sensor

HomeAssistant integration for the SolisCloud PV Monitoring portal via SolisCloud API
Apache License 2.0
191 stars 42 forks source link

Soliscloud API support #18

Closed hawkeye100 closed 2 years ago

hawkeye100 commented 2 years ago

As far as I can tell, I have followed the instructions - files downloaded and put in the correct place on my HA install including the config in configuration.yaml. Checked via File Editor as well as Samba share. HA is 2021.10.6 on Virtualbox vdi. [Other changes on HA are OK including adding integrations from UI].

When I check the config, I get the following:

Platform error sensor.solis - Integration 'solis' not found.

The full log shows:

File "/usr/src/homeassistant/homeassistant/components/websocket_api/commands.py", line 185, in handle_call_service await hass.services.async_call( File "/usr/src/homeassistant/homeassistant/core.py", line 1491, in async_call task.result() File "/usr/src/homeassistant/homeassistant/core.py", line 1526, in _execute_service await handler.job.target(service_call) File "/usr/src/homeassistant/homeassistant/components/hassio/init.py", line 585, in async_handle_core_service raise HomeAssistantError( homeassistant.exceptions.HomeAssistantError: The system cannot restart because the configuration is not valid: Platform error sensor.solis - Integration 'solis' not found.

I am afraid this does not mean much to me as a HA newbie (and rusty programmer).

I have created a new account in m.ginlong.com and checked it logs on OK to web interface. Those credentials are in the config. (this is because Solis moved my main account to soliscloud.com recently)

Any help (in layman's terms) would be much appreciated. Thanks

hultenvp commented 2 years ago

Hi @hawkeye100,

It looks like HA cannot find the solis integration. Please (double) check the following:

Your configuration.yaml should have a block like this, use "check configuration" in your config menu to check if it's correct:

  - platform: solis
    name: "Solis"
    portal_domain: "m.ginlong.com"
    portal_username: "your_user_name" 
    portal_password: "your password"
    portal_plant_id: "your_plant_id"
    inverter_serial: "your_inverter_serial"
    sensors:
      temperature:
      dcinputvoltagepv1:
      dcinputcurrentpv1:
      acoutputvoltage3:
      acoutputcurrent3:
      actualpower:
      energylastmonth:
      energytoday:
      energythismonth:
      energythisyear:
      energytotal:

If that all doesn't help then please provide a anonymized copy of your configuration.yaml and your custom_components layout.

hawkeye100 commented 2 years ago

Thanks for the prompt reply. Overnight I wondered about where in the process the restart should be. So I removed the info from the configuration.yaml. The config check was then OK and so I restarted from the UI. Then I added it back. This time the following error was thrown:

Invalid config for [sensor.solis]: expected int for dictionary value @ data['portal_plant_id']. Got '10F45A'. (See ?, line ?).``

The UI then prevented a restart.

The error looks like a data type issue - expecting integer when appearing to get a string (as it is in quotes) - though the data itself looks like hex and might need converting first? (I could be way off here)

Wondering if the same will apply to the inverter serial.

We are definitely closer, but any further help will be welcome. TIA

hultenvp commented 2 years ago

Can you confirm that on m.ginlong.com, tab "plant info" your plant ID is '10F45A'? If so then you're the first one where the plantID is not base10 numerical. I always assumed it was a number in normal digits.

hawkeye100 commented 2 years ago

So this is interesting.

  1. Earlier this year Solis/ginlong moved my (main) account from m.ginlong.com to soliscloud.com
  2. I could not log back in to m.gonlong.com once it was moved over so I set up a new account to see if it still picked up my data, in order to use your code to gain access to our PV data in our HA.
  3. The 'plant ID' on the inverter page in soliscloud is 10F45A. I used this to set up a new account in m.ginlong.com and after a while it picked up the data OK on the m.ginlong.com website. (how long they will support m.ginlong.com I do not know, if they are moving over to soliscloud.com - but this will hopefully give me something in the meantime)
  4. Having checked back to m.ginlong.com to find the plant ID there, it is a different number (likely integer as no digit beyond 9). It is not even the decimal equivalent of 10F45A as far as I can see, having converted it back to check.
  5. So I have put that 'new' m.ginlong.com number into the configuration.yaml, and lo and behold the configuration is now valid and I can see the solis entities on the entity list. (now to access them to do something useful!)
  6. Conclusion: I think the 'Plant ID' shown on the inverter page in soliscloud.com may actually be the user ID, or linked to that - who knows. I have looked at every combination of user and plant ID on both sites and converted them both ways and nothing matches, so they must have some kind of index/lookup to match the IDs using the logger ID (and/or the inverter ID) which is the same - and which appears to be key to identifying the incoming data.

Short story resolution in case it helps others:

  1. Restart HA after adding the files and before adding the settings to configuration.yaml
  2. Double check the plant ID from m.ginlong.com, especially if you also have an account at soliscloud.com
  3. Restart again after adding settings to configuration.yaml (the double restart may not be necessary if the numbers are OK, but it may be worth trying if you have any similar issue)

Thanks for your help and patience in getting us this far. (as always, sometimes the conversation just helps clarify the problem and suggest possible solutions)

hultenvp commented 2 years ago

Thanks for your elaborate answer, very useful. I'm working on integrating the solis integration into HACS. This requires some editing of the documentation anyhow. Ideal moment to include your findings in the documentation.

Have fun with the Solis integration and Home Assistant.

hawkeye100 commented 2 years ago

I think my issue most likely arose because I had to go back and re-create a plant in m.ginlong.com (after solis migrated my plant to soliscloud.com), thus ending up with different plant ID numbers - it may be the soliscloud plant ID is the same as the original m.ginlong.com plant (but will never know)

I forgot to say that I also placed the file custom_components.json in the config folder (same level as configuration.yaml) - I don't know if that helped or not (the contents suggested it might be relevant)

Wondering if your code might also work with soliscloud.com (as that is also solarman), with an adjustment to handle hex plant IDs.

Not sure if solis are going to deprecate the m.ginlong.com site eventually; it is interesting our data is processed by both systems - guessing soliscloud is getting it from a common collection point within their ecosystem. Solis also migrated my plant unilaterally, without notice. Took me a few days to regain access.

(exporting data from soliscloud inverter page includes more headings than that from m.ginlong.com - again, guessing some of this may only be supplied by some logger/inverter combinations. I can send a comparison list if it helps, though you may already know this; I am interested in a couple of items in m.ginlong.com not on your sensor list, but need to check if they are calculated rather than sent from the PV system - will post separate question if I get stuck)

Thanks again for guiding me through this.

hultenvp commented 2 years ago

Hey @hawkeye100 ,

I had a look into SolisCloud. Did not dare to login as I was afraid it would start migrating and blocking me from using ginlong, but googled around what the iea behind Soliscloud is. From what I understand it is indeed the successor of the ginlong service, but they'll continue to support ginlong for 5 years to come.

That will give the community some time to figure out how to scrape the Soliscloud portal or access its backend. For now I'll wait for https://github.com/dkruyt/ginlong-scraper.

FYI: I removed custom_components.json. Was legacy, no longer needed.

Cheers

hultenvp commented 2 years ago

I saw that issue, yes. Scraping the soliscloud frontend can indeed also be done, but is a completely different solution. I'd prefer if someone figures out how to interact with the service backend, so that we get similar JSON as for the ginlong backend. Much easier and much more reliable. If you scrape the frontend then language, UI changes, popups etc all can cause issues. Very difficult to get robust.

Cheers, Peter

hawkeye100 commented 2 years ago

I have the soliscloud API document if that is of interest (it seems it is available to anyone who has an account) - if quicker than you asking, I can send via email (or does github have a PM feature?)

(yes - soliscloud is more complex/comprehensive than m.ginlong.com - I should imagine screen/page scraping would not be easy)

LucidityCrash commented 2 years ago

@hawkeye100 I've just got my system installed and am using soliscloud I'd love to get the API document and endpoints if you'd be willing to share

hultenvp commented 2 years ago

@hawkeye100 , @LucidityCrash : Oww, I missed your offer. Yes please, if you can share? You can attach to this issue (or a new one) Alternatively PM me at peter.vanHulten[at]gmx.net

I'm currently rewriting the integration to decouple the API from the actual integration. This would make it easier to also support Soliscloud.

LucidityCrash commented 2 years ago

I found the API Doc (attached) but there are 3 key bits of info missing .... the actual URL and the KeyID and KeySecret - all of which the doc says to contact support to obtain .... KeyID and KeySecret look to be used instead of username and password. SolisCloud API 1.0.8.pdf

hawkeye100 commented 2 years ago

Hi - just catching up. I wasn't sure the doc had any commercially sensitive info in it but on checking again it seems OK. Yes it needs API and key - they supplied this reasonably promptly by email when requested. (I'll find their email if that would help)

Decoupling might well be useful - I am also thinking of using a Raspberry pi (with LED matrix) to display some key stats (without HA - it is a very old Pi so quite limited in RAM and CPU but OK with some python) - wondering if it would be feasible to run the code (adapted) in some way.

hultenvp commented 2 years ago

Thanks for the API documentation, it looks feasible. It's my intent to migrate the ginlong portal 2API to PyPI. The same could be done for the soliscloud API and then any project, being your rPI or my HomeAssistant integration can use it.

I'm looking for help to get this done. Got some time, but implementation of the soliscloud API and debugging it will take some time.

@hawkeye100 : From your experience, could I use soliscloud and the ginlong portal 2 (m.ginlong.com) at the same time or can one only use one of the two?

I'll reopen the issue and rename it

hawkeye100 commented 2 years ago

@hawkeye100 : From your experience, could I use soliscloud and the ginlong portal 2 (m.ginlong.com) at the same time or can one only use one of the two?

I am successfully accessing both portals (as websites/platform) using different credentials for each, though pointing at the same inverter ID. Then also using the same ginlong credentials for your integration. They all appear to show the same data.

Happy to try some testing, ideally if it can be done alongside the existing working integration e.g. in a separate Lovelace dashboard perhaps rather than spinning up a separate HA as mine is running on an already stretched Win10 home host (in Virtual Box).

hultenvp commented 2 years ago

Hi @hawkeye100 & @LucidityCrash I just kind of finished the rewrite of my code. It's currently under test and available in https://github.com/hultenvp/solis-sensor/tree/48_Rewrite_api_code_and_introduce_mypy_supported_typing/custom_components/solis Portal code is now much better isolated and easier now to set up a different API in parallel. We could start writing a Soliscloud API, similar tot the Ginlong API in my new code and first test it in isolation before integrating it. I assume the most difficult step is getting working code that can access some part of the API. Extending that afterwards to full support will be less complicated. Proposal would be to start from commandline, just some test code that sets up the connection and reads some data.

LucidityCrash commented 2 years ago

I've made some progress with python testing app.

import hashlib
from hashlib import sha1
import hmac
import base64
from datetime import datetime
from datetime import timezone
import requests
import json

KeyId = "xxxxx"
secretKey = b'xxxxx'

VERB="POST"
now = datetime.now(timezone.utc)
Date = now.strftime("%a, %d %b %Y %H:%M:%S GMT")

#Body = '{"userId":"xxxxx"}'
#Body = '{"id":"xxxxx"}'
#Body = '{"stationId":"xxxxxx"}'
Body='{"id":"xxxxx","sn":"xxxxx"}'
#Body='{"userName":"xxxxx","userType":0}'

Content_MD5 = base64.b64encode(hashlib.md5(Body.encode('utf-8')).digest()).decode('utf-8')
Content_Type = "application/json"

#CanonicalizedResource = "/v1/api/userStationList"
#CanonicalizedResource = "/v1/api/stationDetail"
#CanonicalizedResource = "/v1/api/inveterList"
CanonicalizedResource = "/v1/api/inveterDetail"
#CanonicalizedResource = "/v1/api/addUser"

encryptStr = (VERB + "\n"
    + Content_MD5 + "\n"
    + Content_Type + "\n"
    + Date + "\n"
    + CanonicalizedResource)

h = hmac.new(secretKey, msg=encryptStr.encode('utf-8'), digestmod=hashlib.sha1)

Sign = base64.b64encode(h.digest())

Authorization = "API " + KeyId + ":" + Sign.decode('utf-8')

requestStr = (VERB + " " + CanonicalizedResource + "\n"
    + "Content-MD5: " + Content_MD5 + "\n"
    + "Content-Type: " + Content_Type + "\n"
    + "Date: " + Date + "\n"
    + "Authorization: "+ Authorization + "\n"
    + "Body:" + Body)

header = { "Content-MD5":Content_MD5,
            "Content-Type":Content_Type,
            "Date":Date,
            "Authorization":Authorization
            }
header1 = { "Content-Type":Content_Type,
            "Date":Date,
            "Authorization":Authorization
            }

print (requestStr)

url = 'https://www.soliscloud.com:13333'
req = url + CanonicalizedResource
x = requests.post(req, data=Body, headers=header)
print ("")
print(json.dumps(x.json(),indent=4, sort_keys=True))

In order to find your userId use the Body='{"userName":"xxxxx","userType":0}' where username is your account username and the CanonicalizedResource = "/v1/api/addUser", it wont add the user but it will return the userId.

Note: I would be very hesitant posting ANY id's here I'm not sure how well the user auth seperates data I used the wrong userId with the userStationList endpoint and it returns data (all zeros but that may just be because the userid doesn't exist)

hultenvp commented 2 years ago

Wow you already made quite some progress! Indeed, let's not post any id's here.

Some questions about your code. Maybe it's in the API docs, but did not yet have time to look into those.

Really interested in the output of that inveterDetails call. Perhaps I can make a start with the parser.

LucidityCrash commented 2 years ago

did a little edit ... the Body and CanonicalizedResource variables are now in the same order they need to be used in. ie if you want to use /v1/api/userStationList then the Body is {"userId":"xxxxx"}

  • The ID in your body, is that the same as userID?

id and userId are not the same ... and it also depends on the endpoint. userId is used with /v1/api/userStationList and is the numerical representation of the user account. id is used with /v1/api/stationDetail and is a unique id for a station associated with your account. when id and sn is used with /v1/api/inveterDetail id is then the unique id of the inverter. (got by sending the stationId to /v1/api/inveterList)

  • the canonicalizedResource refers to endpoint inveterDetails (without an 'R' ). Is that a mistake in their implementation?

Sorry not sure what you mean by this.

The output is specified in the Doc ... it is just output as JSON key value pairs. An example output from my code is (I'm sorting the Keys alphabetically ... they don't naturally come out in this order) :

{
    "code": "0",
    "data": {
        "acOutputType": 1,
        "apparentPower": 0.0,
        "apparentPowerStr": "VA",
        "batteryCapacitySoc": 17.0,
        "batteryChargeEnergy": 0,
        "batteryChargeEnergyStr": "kWh",
        "batteryChargingCurrent": 25.0,
        "batteryChargingCurrentStr": "A",
        "batteryDischargeEnergy": 0,
        "batteryDischargeEnergyStr": "kWh",
        "batteryDischargeLimiting": 25.0,
        "batteryDischargeLimitingStr": "A",
        "batteryFailureInformation01": "0",
        "batteryFailureInformation02": "0",
        "batteryHealthSoh": 100.0,
        "batteryMonthChargeEnergy": 0,
        "batteryMonthChargeEnergyStr": "kWh",
        "batteryMonthDischargeEnergy": 0,
        "batteryMonthDischargeEnergyStr": "kWh",
        "batteryPower": -0.073,
        "batteryPowerBms": 0,
        "batteryPowerBmsStr": "kW",
        "batteryPowerFu": 73.0,
        "batteryPowerPec": "1",
        "batteryPowerStr": "kW",
        "batteryPowerZheng": 0,
        "batteryTodayChargeEnergy": 1.5,
        "batteryTodayChargeEnergyStr": "kWh",
        "batteryTodayDischargeEnergy": 2.1,
        "batteryTodayDischargeEnergyStr": "kWh",
        "batteryTotalChargeEnergy": 30.0,
        "batteryTotalChargeEnergyStr": "kWh",
        "batteryTotalDischargeEnergy": 64.0,
        "batteryTotalDischargeEnergyStr": "kWh",
        "batteryType": 0,
        "batteryVoltage": 48.93,
        "batteryVoltageStr": "V",
        "batteryYearChargeEnergy": 0,
        "batteryYearChargeEnergyStr": "kWh",
        "batteryYearDischargeEnergy": 0,
        "batteryYearDischargeEnergyStr": "kWh",
        "batteryYesterdayChargeEnergy": 2.2,
        "batteryYesterdayChargeEnergyStr": "kWh",
        "batteryYesterdayDischargeEnergy": 2.8,
        "batteryYesterdayDischargeEnergyStr": "kWh",
        "bstteryCurrent": 0.4,
        "bstteryCurrentStr": "A",
        "bypassAcCurrent": 0.0,
        "bypassAcCurrentStr": "A",
        "bypassAcVoltage": 238.8,
        "bypassAcVoltageStr": "V",
        "bypassLoadPower": 0.017,
        "bypassLoadPowerStr": "kW",
        "collectorId": "xxxxx",
        "collectorsn": "xxxxx",
        "currentState": "0",
        "dataTimestamp": "1642448261000",
        "daylight": "0",
        "dcBus": 0.0,
        "dcBusHalf": 0.0,
        "dcBusHalfStr": "V",
        "dcBusStr": "V",
        "dcInputtype": 1,
        "dispersionRate": 0.0,
        "eMonth": 36.0,
        "eMonthStr": "kWh",
        "eToday": 0,
        "eTodayStr": "kWh",
        "eTotal": 54.0,
        "eTotalStr": "kWh",
        "eYear": 36.0,
        "eYearStr": "kWh",
        "epmFailSafe": 0,
        "fac": 49.97,
        "facStr": "Hz",
        "familyLoadPercent": 0,
        "familyLoadPower": 0.796,
        "familyLoadPowerPec": "1",
        "familyLoadPowerStr": "kW",
        "fullHour": 0.8,
        "fullHourStr": "h",
        "gridPurchasedEnergy": 0,
        "gridPurchasedEnergyStr": "kWh",
        "gridPurchasedMonthEnergy": 0,
        "gridPurchasedMonthEnergyStr": "kWh",
        "gridPurchasedTodayEnergy": 8.5,
        "gridPurchasedTodayEnergyStr": "kWh",
        "gridPurchasedTotalEnergy": 491.0,
        "gridPurchasedTotalEnergyStr": "kWh",
        "gridPurchasedYearEnergy": 0,
        "gridPurchasedYearEnergyStr": "kWh",
        "gridPurchasedYesterdayEnergy": 0.0,
        "gridPurchasedYesterdayEnergyStr": "kWh",
        "gridSellEnergy": 0,
        "gridSellEnergyStr": "kWh",
        "gridSellMonthEnergy": 0,
        "gridSellMonthEnergyStr": "kWh",
        "gridSellTodayEnergy": 0.0,
        "gridSellTodayEnergyStr": "kWh",
        "gridSellTotalEnergy": 199.0,
        "gridSellTotalEnergyStr": "kWh",
        "gridSellYearEnergy": 0,
        "gridSellYearEnergyStr": "kWh",
        "gridSellYesterdayEnergy": 0.0,
        "gridSellYesterdayEnergyStr": "kWh",
        "homeLoadEnergy": 0,
        "homeLoadEnergyStr": "kWh",
        "homeLoadTodayEnergy": 12.7,
        "homeLoadTodayEnergyStr": "kWh",
        "homeLoadTotalEnergy": 577.0,
        "homeLoadTotalEnergyStr": "kWh",
        "homeLoadYesterdayEnergy": 18.9,
        "homeLoadYesterdayEnergyStr": "kWh",
        "iAc1": 0.1,
        "iAc1Str": "A",
        "iAc2": 0.0,
        "iAc2Str": "A",
        "iAc3": 0.0,
        "iAc3Str": "A",
        "iPv1": 0,
        "iPv10": 0,
        "iPv10Str": "A",
        "iPv11": 0,
        "iPv11Str": "A",
        "iPv12": 0,
        "iPv12Str": "A",
        "iPv13": 0,
        "iPv13Str": "A",
        "iPv14": 0,
        "iPv14Str": "A",
        "iPv15": 0,
        "iPv15Str": "A",
        "iPv16": 0,
        "iPv16Str": "A",
        "iPv17": 0,
        "iPv17Str": "A",
        "iPv18": 0,
        "iPv18Str": "A",
        "iPv19": 0,
        "iPv19Str": "A",
        "iPv1Str": "A",
        "iPv2": 0,
        "iPv20": 0,
        "iPv20Str": "A",
        "iPv21": 0,
        "iPv21Str": "A",
        "iPv22": 0,
        "iPv22Str": "A",
        "iPv23": 0,
        "iPv23Str": "A",
        "iPv24": 0,
        "iPv24Str": "A",
        "iPv25": 0,
        "iPv25Str": "A",
        "iPv26": 0,
        "iPv26Str": "A",
        "iPv27": 0,
        "iPv27Str": "A",
        "iPv28": 0,
        "iPv28Str": "A",
        "iPv29": 0,
        "iPv29Str": "A",
        "iPv2Str": "A",
        "iPv3": 0,
        "iPv30": 0,
        "iPv30Str": "A",
        "iPv31": 0,
        "iPv31Str": "A",
        "iPv32": 0,
        "iPv32Str": "A",
        "iPv3Str": "A",
        "iPv4": 0,
        "iPv4Str": "A",
        "iPv5": 0,
        "iPv5Str": "A",
        "iPv6": 0,
        "iPv6Str": "A",
        "iPv7": 0,
        "iPv7Str": "A",
        "iPv8": 0,
        "iPv8Str": "A",
        "iPv9": 0,
        "iPv9Str": "A",
        "id": "xxxxx",
        "insulationResistance": 0,
        "inverterMeterModel": 5,
        "inverterTemperature": 15.1,
        "inverterTemperatureUnit": "\u2103",
        "model": "F2",
        "nationalStandardstr": "G98",
        "oneSelf": 0.0,
        "pac": 0.0,
        "pacPec": "1",
        "pacStr": "kW",
        "pepm": 0.0,
        "pepmSet": 0.0,
        "pepmSetStr": "kW",
        "pepmStr": "kW",
        "porwerPercent": 0.0,
        "pow1": 0,
        "pow10": 0,
        "pow10Str": "W",
        "pow11": 0,
        "pow11Str": "W",
        "pow12": 0,
        "pow12Str": "W",
        "pow13": 0,
        "pow13Str": "W",
        "pow14": 0,
        "pow14Str": "W",
        "pow15": 0,
        "pow15Str": "W",
        "pow16": 0,
        "pow16Str": "W",
        "pow17": 0,
        "pow17Str": "W",
        "pow18": 0,
        "pow18Str": "W",
        "pow19": 0,
        "pow19Str": "W",
        "pow1Str": "W",
        "pow2": 0,
        "pow20": 0,
        "pow20Str": "W",
        "pow21": 0,
        "pow21Str": "W",
        "pow22": 0,
        "pow22Str": "W",
        "pow23": 0,
        "pow23Str": "W",
        "pow24": 0,
        "pow24Str": "W",
        "pow25": 0,
        "pow25Str": "W",
        "pow26": 0,
        "pow26Str": "W",
        "pow27": 0,
        "pow27Str": "W",
        "pow28": 0,
        "pow28Str": "W",
        "pow29": 0,
        "pow29Str": "W",
        "pow2Str": "W",
        "pow3": 0,
        "pow30": 0,
        "pow30Str": "W",
        "pow31": 0,
        "pow31Str": "W",
        "pow32": 0,
        "pow32Str": "W",
        "pow3Str": "W",
        "pow4": 0,
        "pow4Str": "W",
        "pow5": 0,
        "pow5Str": "W",
        "pow6": 0,
        "pow6Str": "W",
        "pow7": 0,
        "pow7Str": "W",
        "pow8": 0,
        "pow8Str": "W",
        "pow9": 0,
        "pow9Str": "W",
        "power": 4.6,
        "powerFactor": 1.0,
        "powerPec": "1",
        "powerStr": "kWp",
        "productModel": "F2",
        "psum": -0.829,
        "psumCal": -0.829,
        "psumCalPec": "1",
        "psumCalStr": "kW",
        "psumStr": "kW",
        "reactivePower": 0.0,
        "reactivePowerStr": "Var",
        "rs485ComAddr": "101",
        "sirRealtime": 0,
        "sn": "xxxxx",
        "sno": "xxxxx",
        "socChargingSet": 0,
        "socDischargeSet": 0,
        "state": 1,
        "stationId": "xxxxx",
        "stationName": "xxxxx",
        "storageBatteryCurrent": -1.5,
        "storageBatteryCurrentStr": "A",
        "storageBatteryVoltage": 49.2,
        "storageBatteryVoltageStr": "V",
        "tag": "YingZhen",
        "temp": 95.0,
        "tempName": "inverter internal operating ambient temperature",
        "timeStr": "2022-01-17 19:37:41",
        "timeZone": 0.0,
        "uAc1": 238.6,
        "uAc1Str": "V",
        "uAc2": 0.0,
        "uAc2Str": "V",
        "uAc3": 0.0,
        "uAc3Str": "V",
        "uInitGnd": 0,
        "uInitGndStr": "V",
        "uPv1": 0,
        "uPv10": 0,
        "uPv10Str": "V",
        "uPv11": 0,
        "uPv11Str": "V",
        "uPv12": 0,
        "uPv12Str": "V",
        "uPv13": 0,
        "uPv13Str": "V",
        "uPv14": 0,
        "uPv14Str": "V",
        "uPv15": 0,
        "uPv15Str": "V",
        "uPv16": 0,
        "uPv16Str": "V",
        "uPv17": 0,
        "uPv17Str": "V",
        "uPv18": 0,
        "uPv18Str": "V",
        "uPv19": 0,
        "uPv19Str": "V",
        "uPv1Str": "V",
        "uPv2": 0,
        "uPv20": 0,
        "uPv20Str": "V",
        "uPv21": 0,
        "uPv21Str": "V",
        "uPv22": 0,
        "uPv22Str": "V",
        "uPv23": 0,
        "uPv23Str": "V",
        "uPv24": 0,
        "uPv24Str": "V",
        "uPv25": 0,
        "uPv25Str": "V",
        "uPv26": 0,
        "uPv26Str": "V",
        "uPv27": 0,
        "uPv27Str": "V",
        "uPv28": 0,
        "uPv28Str": "V",
        "uPv29": 0,
        "uPv29Str": "V",
        "uPv2Str": "V",
        "uPv3": 0,
        "uPv30": 0,
        "uPv30Str": "V",
        "uPv31": 0,
        "uPv31Str": "V",
        "uPv32": 0,
        "uPv32Str": "V",
        "uPv3Str": "V",
        "uPv4": 0,
        "uPv4Str": "V",
        "uPv5": 0,
        "uPv5Str": "V",
        "uPv6": 0,
        "uPv6Str": "V",
        "uPv7": 0,
        "uPv7Str": "V",
        "uPv8": 0,
        "uPv8Str": "V",
        "uPv9": 0,
        "uPv9Str": "V",
        "userId": "xxxxx",
        "version": "xxxxx"
    },
    "msg": "success",
    "success": true
}
LucidityCrash commented 2 years ago

I wanna know why my "gridSellTotalEnergy" = 199.0 ... I've not even had this a month and given its winter I've barely been covering the base usage and sometimes charging the battery never mind actually exporting to the grid 😁

hultenvp commented 2 years ago

Thanks for sharing. I'll start with the parser, hopefully later this week.

I assume you know, but to state the obvious: that gridSellTotalEnergy is not only this year but what the system returned during its lifetime.

the canonicalizedResource refers to endpoint inveterDetails (without an 'R' ). Is that a mistake in their implementation?

Sorry not sure what you mean by this.

There seems to be a typo in the endpoint name: inveterDetails instead of inve_r_terDetails. Since your test code works i guess it's a thing in the API?

LucidityCrash commented 2 years ago

Thanks for sharing. I'll start with the parser, hopefully later this week.

I assume you know, but to state the obvious: that gridSellTotalEnergy is not only this year but what the system returned during its lifetime.

yeah but its lifetime is < 1 month :)

There seems to be a typo in the endpoint name: inveterDetails instead of inve_r_terDetails. Since your test code works i guess it's a thing in the API?

doh I completly missed that 😀 ... I just copy/pasted from the PDF so that is a problem with their API

hultenvp commented 2 years ago

I see indeed that gridSellYearEnergy is 0. Perhaps you are not the real first owner and they forgot to do a factory reset?

hultenvp commented 2 years ago

I've created branch 18_soliscloud_api_support. Feel free to start contributing and creating PR's!

micbox commented 2 years ago

Hi, I'm trying to access SolisCloud API (https://www.soliscloud.com:13333), but can't find how to obtain the KeyID and KeySecret. I am a plant owner and using their web (m.ginlong... and soliscloud), but would like to have more freedom to integrate my PV system to smart home scenarios. API doc says only to contact the technical support, but I can't find the corresponding contacts. Does anyone of you have experience in contacting Ginlong staff for this matter? Would be grateful for any help with obtaining the mentioned keys.

LucidityCrash commented 2 years ago

I guess is dependes on your country ... I contacted euservice@solisinverters.com for another question they created an account for me on a support portal and from there you can create tickets ... one of the options is an API access request, though if you email for api access I guess you wouldn't need to create a ticket 😁 . look at https://www.ginlong.com/global/aftersales.html for the initial contact email address in your region

micbox commented 2 years ago

Thanks a lot! Will try to contact them.

hultenvp commented 2 years ago

I created the code in https://github.com/hultenvp/solis-sensor/tree/18_soliscloud_api_support

sensor:
  - platform: solis
    name: "My Solis Inverter"
    portal_domain: "www.soliscloud.com:1333"
    portal_username: "my_portal_username"
    portal_key_id: "my_portal_key_id"
    portal_secret: "portal secret"
    portal_plant_id: plantId/stationID as string

Don't have secret and keyid myself yet, so if you could debug the code that would be great. De

LucidityCrash commented 2 years ago

I'm getting an error in the logs :

 Traceback (most recent call last):
  File "/config/custom_components/solis/service.py", line 128, in async_update
    if await self._login():
  File "/config/custom_components/solis/service.py", line 76, in _login
    if await self._api.login(async_get_clientsession(self._hass)):
  File "/config/custom_components/solis/soliscloud_api.py", line 185, in login
    result = await self._post_data_json(canonicalized_resource, params)
  File "/config/custom_components/solis/soliscloud_api.py", line 410, in _post_data_json
    header: dict[str, str] = self._prepare_header(params, canonicalized_resource)
  File "/config/custom_components/solis/soliscloud_api.py", line 389, in _prepare_header
    hmac_obj = hmac.new(
  File "/usr/local/lib/python3.9/hmac.py", line 170, in new
    return HMAC(key, msg, digestmod)
  File "/usr/local/lib/python3.9/hmac.py", line 53, in __init__
    raise TypeError("key: expected bytes or bytearray, but got %r" % type(key).__name__)
TypeError: key: expected bytes or bytearray, but got 'str'

Looks like the secret is being passed to hmac as a string not a byte array ... I'm sorry my Python skills are not that good and I'm still trying to work out your code 😁 it looks to me like self.config.secret should be a byte array but I'm not 100% on the syntax you are using.

hultenvp commented 2 years ago

Welp! I thought I had that one covered. I'll have a look

hultenvp commented 2 years ago

I've fixed the string type error.

It should work as follows: portal_secret: 'xxxxxxxxxxx' in configuration.yaml is converted in python to portal_secret = b'xxxxxxxxxxx'

There was also a mixup of secret and key_id, also fixed that.

Looking forward to the next stacktrace ;-)

LucidityCrash commented 2 years ago

https://github.com/hultenvp/solis-sensor/blob/d9dc48ec3ed35918903c36fe0d320124385f4b96/custom_components/solis/sensor.py#L142 - should be : portal_secret: bytes = bytes(config.get(CONF_SECRET), 'utf-8')

Because you are storing the body as real json you need to make json.dumps have exactly the right format else the md5 sum doesn't work right (no spaces after separators)

https://github.com/hultenvp/solis-sensor/blob/d9dc48ec3ed35918903c36fe0d320124385f4b96/custom_components/solis/soliscloud_api.py#L379 - should be : hashlib.md5(json.dumps(body,separators=(",", ":")).encode('utf-8')).digest()

https://github.com/hultenvp/solis-sensor/blob/d9dc48ec3ed35918903c36fe0d320124385f4b96/custom_components/solis/soliscloud_api.py#L418 - probably should be : resp = await self._session.post(url, data=json.dumps(params,separators=(",", ":")), headers=header) NB it is headers not header as the parameter name and I've switched to data=body as a string rather than json ... this is what my test code does ... your way is propper but I wanted to make sure it was as close to my working code as possible.

That said it still doesn't work - Headers look good, Content-MD5 matches my test code, Sign string matches against my test code but I just get a Timeout error from line 416. been shoving in debug statements left and right but I just can't see what is wrong.

hultenvp commented 2 years ago

Thanks for the fixes. I agree moving away from json is best for now. Let's first get this working and then we can make it nice and shiny.

I'm equally confused about the timeouts. If there's something wrong with the signatures or body then I'd expect an HTTP error. Not using aiohttp and using requests would be a temporary solution to try things out, won't work for the long term as sync calls lock up the HA main thread.

Maybe increase timeout for debugging to 30secs or so and see if the server just needs more time to respond. Other things that pop up:

If you know how, could you create a Pull Request for your fixes? I'll integrate them in the branch.

Cheers, Peter

LucidityCrash commented 2 years ago

I increased the timout to 30 seconds already :) Still the same. I also got it to output the full header and body as text which I then injected into my test code so it made exactly the same request (it is the addUser request done as your login function) and it worked fine. If you leave it too long after generating the Auth string the authorization times out and you start getting 403's. Having an error in any part (Header, body, MD5, Auth String etc) always results in some kind of json being returned almost immediately.

I'll try and get a pull request in soon.

LucidityCrash commented 2 years ago

Doh I'm an idiot ... missed a 3 on the port number for the URL !!! Fixed some more issues ... will redo the Pull request

Will check the actual data later.

hultenvp commented 2 years ago

Ah, that explains a lot ;-)

Hopefully you see something useful coming in.

EDIT: I mean the missing '3' explains a lot 😄

LucidityCrash commented 2 years ago

EDIT: I mean the missing '3' explains a lot 😄

yep. I tell you something words fail me about the SolisCloud/Ginlong monitoring platform. I keep getting my daily totals not being reset at midnight, which really mucks up the data, the SolisCloud not as much as it just resets to 0 at the next measurement but the m.ginlong one keeps the same daily total for the whole day unless it goes beyond what the max was. And looking at the graphs and exported xls from soliscloud my "Daily Energy from Grid" is always 0 despite the "Total Energy from Grid" continually increasing ... BUT the api is currently returning a real value.

This is what I'm seeing in HA : image image image

Some things don't seem to be coming through that I'd expect - pSum (GRID_TOTAL_POWER) being an obvious example. Also somehow Phase 3 V and I is not being removed but Phase 2 V and I are there just unknown. Other things I've noticed ... familyLoadPower and pSum appear to be in kWh in the returned JSON and need 3 decimal places for the precision.

EDIT: I'll try and do some tweaking later this week and raise PR's as I fix things.

hultenvp commented 2 years ago

I'm pleasantly surprised the rest generally seems to work as intended. Thanks for your work! The configuration table still needs review, there will definitely be mistakes in the table. At some point while trying to match the grid data fields from soliscloud to m.ginlong.m I just gave up. It was just impossible to find the correlation without real measurements to do the comparison, so I just filled in some best guesses with the right units and focus on getting things running first.

On the topic of removing phases: On the ginlong site all 3 phases are always reported, but 2 are always 0.0 if only one phase is used. That's not necessarily Phase 1, in my case it is phase 3. Not sure if Soliscloud does the same. I found at least a pointer that indicates how many phases are in use, but no indication of which one, so for now I stuck to checking if all are reporting 0.0 and if so, removing them from the results.

On the topics of "unexpected behaviour": In the ginlong API I also have some code in place to correct the wrong reporting of energy today. There were still some race conditions, which I believe I've tackled now. I'll release those fixes today and merge them up to the development branch for Soliscloud we are using. The solution is generic and should also work for Soliscloud, if needed.

I also found one issue with power unit of measurement changing from W to kW if the solar panels are producing a lot. May need to introduce some unit checking and correction somehow as HA sensors expect static unit of measurement. Created a ticket for that.

Regarding the daily energy from grid I have no clue, looks like that's a bug on their side. For the energy dashboard you could also use Total energy, assuming you do not lose precision (ginlong does). The dashboard just expects an increasing value so it can calculate delta's. Periodic resets in that value are allowed.

I'm also on discord if you need help: Peter van Hulten#8923

LucidityCrash commented 2 years ago

Just an update I removed my config, and deleted all the sensors then re-added my config ... all the red arrow'd sensors no longer show up - I'd used the old style config specifying the sensors initially, and then realised that they wern't needed any more. Yay me for not reading the docs 😁

For now trying to work out why pSum (GRID_TOTAL_POWER) is not showing as a sensor when it is in the returned JSON.

hultenvp commented 2 years ago

Hey @LucidityCrash,

Is there something I can help with? Got some time at hand. Is pSum now the only sensor not working yet and is the rest working fine or is there more still to do?

LucidityCrash commented 2 years ago

no psum works fine now ... I think everything looks good. The monthly energy purchased and yearly energy purchased are always 0 (from the backend not a code error) ... but in the exported data from the website there is only daily and total from grid so while those exist in the API I'm not sure that they would ever be non Zero.

Personaly I've got an issue with the daily energy exported to grid being always being 0 but again that is a SolisCloud backend issue and I have a ticket open with them that they seem to be ignoring. That said for the Daily Purchased from Grid my XML shows 0 while my API returns the right value. Looks like they are having some major backend issues at the moment.

Without more people using it and getting feedback its as good as it is gonna get right now - ie it works for me :)

hultenvp commented 2 years ago

I'd propose to launch it as beta then. I'll attach a discussion where we can mention the known limitations.

Thanks a lot for the effort you put in, much appreciated.

Cheers

hultenvp commented 2 years ago

Merged to main. Let's open new issues for whatever we need to fix more.

Kaya2017 commented 2 years ago

Hi hultenvp, i have a problem with the Plant ID "XXXXXX" i have just 6 digits, its the same error like https://github.com/hultenvp/solis-sensor/issues/18#issuecomment-948517171

i had installed with HACS and the directories in the custom_components are right.

hope someone can help me

hultenvp commented 2 years ago

Hi @Kaya2017,

Are you using the current m.ginlong.com portal or are you using the new soliscloud.com portal? If you use the latter you should look for the stationId, not plantId.

Cheers

Kaya2017 commented 2 years ago

I just use the Soliscloud Dashboard. I have the inverter about 3 months. And on the Soliscloud Dashboard i just find my plantID. Example 11F4S6

I did search on the complete Dashboard Sites and SiteCoding to find any ID. Maybe i am blind of to much looking

hultenvp commented 2 years ago

Have a look at this discussion: https://github.com/hultenvp/solis-sensor/discussions/71#discussioncomment-2281489

Might be that that's the problem. If not then please open a discussion there, under beta: soliscloud. Others, who have been working on the support can then respond and help you, they do not see your comments here in this closed issue and I do not have soliscloud running myself.

snigehere commented 2 years ago

I have been following this as i currently use m.g and considering moving to solis cloud (the android app i have uses solis cloud).

I checked in solis cloud and see my "solis inverter ID" is hex format. I searched and in reports i added the "plant id" to the report and it was the same so guess they renamed it so i will have the same issue as commented.

LucidityCrash commented 2 years ago

ok my system had a bit of a glitch for the last 3 days and wasn't picking anything up, but I've just restarted it and everything is working fine. I'm guessing you have the config wrong

jonla76 commented 2 years ago

Hi @hultenvp ,

Thank you so much for this and for helping us all out. I am really hoping to get the battery SOC into home assistant. Are you taking donations?

I think I am having the same problem with the thing I am calling my PlantID having letters. I agree that it might be hex.

"Are you using the current m.ginlong.com portal or are you using the new soliscloud.com portal? If you use the latter you should look for the stationId, not plantId."

Can you please tell me where the stationid can be found or what else it might show up as?

Thank you so much.