Skip to content

Daily Power Usage / Generation + EG4 Config #3

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

Open
Joannou1 opened this issue Jan 8, 2025 · 12 comments
Open

Daily Power Usage / Generation + EG4 Config #3

Joannou1 opened this issue Jan 8, 2025 · 12 comments

Comments

@Joannou1
Copy link

Joannou1 commented Jan 8, 2025

Just wanted to say thank you for creating this!
I was able to dig into their API's a bit further and I created a way to extract the daily generation / consumption.

It was tricky because their API needs a signature that matches the request + some other stuff.
The code to generate a signature and pull daily data is as follows:

dessmonitor_api_uri_bat_active_power: >-  
  {% set dess_param = "BATTERY_ACTIVE_POWER" %}
  {% set dess_action = "querySPDeviceKeyParameterOneDay" %}
  {% set dess_salt = "1736254948715" %}
  {% set dess_secret = "8d424eb86exxxxxxxxxedde7ac98f086a" %}
  {% set dess_token = "0058f23badac72018153bc5f7fcxxxxxxxxxxxxd737318c4638c985d" %}
  {% set dess_pn = "Q123456789" %}
  {% set dess_sn = "Q12345678912345678" %}

  {% set current_date = (as_timestamp(now())) | timestamp_custom("%Y-%m-%d", True) %}
  {% set signature = sha1(dess_salt + dess_secret + dess_token + "&action=" + dess_action + "&source=1&pn=" + dess_pn + "&sn=" + dess_sn + "&devcode=2376&devaddr=1&i18n=en_US&parameter=" + dess_param + "&chartStatus=false&date=" + current_date) %}

  {% set baseURL = "https://web.dessmonitor.com/public/?sign=" + signature + "&salt=" + dess_salt + "&token=" + dess_token + "&action=" + dess_action + "&source=1&pn=" + dess_pn + "&sn=" + dess_sn + "&devcode=2376&devaddr=1&i18n=en_US&parameter=" + dess_param + "&chartStatus=false&date=" + current_date %}
  {{ baseURL }}

Spook Required for SHA1

dess_param - Query parameter. Other options are: PV_OUTPUT_POWER and LOAD_ACTIVE_POWER
dess_action - The daily query.
dess_salt - The current timestamp, but it looks like it can be any number. It doesn't appear that it needs to change either.

dess_secret - This one is tricky. I'm not actually sure how to decode the one store in cookies. It seems like base64, but it doesn't decode to the string that I have above. I used Chrome devtools to override their JS and print out the secret.
Can be found on line 28065 in their app.js. Search for , s = g["a"].state.user.secret

dess_token - Your token
dess_pn - Part number?
dess_sn - Serial number?

There's also devcode and devaddr, which I'm unsure what they do, but leaving as they were in the request.

I was able to put the above code in secrets.yaml and use it as the resource template.

  - platform: rest
    scan_interval: 120
    name: Battery Charge
    resource_template: !secret dessmonitor_api_uri_bat_active_power
    method: GET
    json_attributes_path: "$.dat"
    json_attributes:
      - detail
    value_template: "OK"

Now to actually use the data, I had to take their data intervals, get an average, convert to watt hours then sum up.
Since their intervals aren't exactly 5 minutes, sometimes 5 minutes and change, I had to create a time difference and get a hourly fractional to make 5 minute watt hour measurements.

- sensor:
    - name: "Daily Battery Discharge Power"
      state: >
        {% set dates = state_attr('sensor.trailer_battery_charge','detail') %}
        {% set ns = namespace(prev_power=0) %}
        {% set ns = namespace(prev_time=0) %}
        {% set ns = namespace(total_power_discharge=0) %}

        {% for i in range(0, dates | count, 1) %}
            {% set curr_time = dates[i]['ts'] %}
            {% set curr_power = dates[i]['val'] | float %}
            
            {#{curr_time + " / " + (curr_power | string)}#}

            {% if i > 0 %}
              {% set pow_diff = ((ns.prev_power + curr_power) / 2) %}
              {% set time_diff = ((curr_time | as_datetime).astimezone() - (ns.prev_time | as_datetime).astimezone()).total_seconds() %}

              {#
              Prev Power: {{ns.prev_power}}
              Curr Time: {{(curr_time | as_datetime).astimezone()}}
              Prev Time: {{ns.prev_time | as_datetime}}
              Power Diff: {{pow_diff}}
              Time Diff: {{time_diff}}
              #}
             

              {% if pow_diff < 0 %}
                {% set ns.total_power_discharge = ns.total_power_discharge + ((pow_diff * (time_diff / 3600) * 1000)) %}
                {#
                Total Power: {{ns.total_power_discharge}} Wh
                #}
              {% endif %}
            {% else %}
              {% set ns.total_power_discharge = ns.total_power_discharge %}
            {% endif %}
            
            {% set ns.prev_time = curr_time %}
            {% set ns.prev_power = curr_power %}
        {% endfor %}
        
        {{ ns.total_power_discharge }}

      unit_of_measurement: "W"
      device_class: "energy"
      state_class: "total_increasing"

{% if pow_diff < 0 %} can be replaced with {% if pow_diff > 0 %} to count charge.

This is my first time writing any substantial code for Home Assistant, so apologies if it's not as nice as it could or should be :p

The end result are a bunch of nice daily readings that can be hooked up to the energy flow card!
image
image

I'm not confident enough to submit it as a PR, so I figure I'd leave the code here if you or anyone wants to integrate it a bit better than I did.

@Joannou1
Copy link
Author

Joannou1 commented Jan 8, 2025

It's also worth noting that I'm using an EG4 3000EHV-48
I've updated the template to the following:

      
    - name: "Grid Voltage"
      state: >
        {% set gd_items = state_attr('sensor.trailer_inverter_data', 'gd_') %}
        {{ (gd_items | selectattr('id', 'equalto', 'gd_eybond_read_15') | map(attribute='val') | first) | float }}
      unit_of_measurement: "V"
      device_class: "voltage"
      state_class: "measurement"

    - name: "Grid Frequency"
      state: >
        {% set gd_items = state_attr('sensor.trailer_inverter_data', 'gd_') %}
        {{ (gd_items | selectattr('id', 'equalto', 'gd_eybond_read_16') | map(attribute='val') | first) | float }}
      unit_of_measurement: "Hz"
      device_class: "frequency"
      state_class: "measurement"

    - name: "Grid Power"
      state: >
        {% set gd_items = state_attr('sensor.trailer_inverter_data', 'gd_') %}
        {% set bt_items = state_attr('sensor.trailer_inverter_data', 'bt_') %}
        {% set bc_items = state_attr('sensor.trailer_inverter_data', 'bc_') %}
        {% set pv_items = state_attr('sensor.trailer_inverter_data', 'pv_') %}
        {% set grid_voltage = (gd_items | selectattr('id', 'equalto', 'gd_eybond_read_15') | map(attribute='val') | first) | float %}
        {% set charging_current = (pv_items | selectattr('id', 'equalto', 'pv_eybond_read_46') | map(attribute='val') | first) | float %}
        {% set battery_voltage = (bt_items | selectattr('id', 'equalto', 'bt_eybond_read_28') | map(attribute='val') | first) | float %}
        {% set load_power = (bc_items | selectattr('id', 'equalto', 'bc_load_active_power') | map(attribute='val') | first) | float %}
        
        {% if grid_voltage > 0 %}
          {{ (charging_current * battery_voltage) + load_power }}
        {% else %}
          0
        {% endif %}
      unit_of_measurement: "W"
      device_class: "energy"
      state_class: "measurement"

    - name: "PV1 Voltage"
      state: >
        {% set pv_items = state_attr('sensor.trailer_inverter_data', 'pv_') %}
        {{ (pv_items | selectattr('id', 'equalto', 'pv_eybond_read_32') | map(attribute='val') | first) | float }}
      unit_of_measurement: "V"
      device_class: "voltage"
      state_class: "measurement"

    - name: "PV1 Current"
      state: >
        {% set pv_items = state_attr('sensor.trailer_inverter_data', 'pv_') %}
        {{ (pv_items | selectattr('id', 'equalto', 'pv_eybond_read_33') | map(attribute='val') | first) | float }}
      unit_of_measurement: "A"
      device_class: "current"
      state_class: "measurement"

    - name: "PV1 Power"
      state: >
        {% set pv_items = state_attr('sensor.trailer_inverter_data', 'pv_') %}
        {{ (pv_items | selectattr('id', 'equalto', 'pv_output_power') | map(attribute='val') | first) | float }}
      unit_of_measurement: "W"
      device_class: "energy"
      state_class: "measurement"

    - name: "Battery Voltage"
      state: >
        {% set bt_items = state_attr('sensor.trailer_inverter_data', 'bt_') %}
        {{ (bt_items | selectattr('id', 'equalto', 'bt_eybond_read_28') | map(attribute='val') | first) | float }}
      unit_of_measurement: "V"
      device_class: "voltage"
      state_class: "measurement"

    - name: "Battery Power"
      state: >
        {% set bt_items = state_attr('sensor.trailer_inverter_data', 'bt_') %}
        {% set battery_current = (bt_items | selectattr('id', 'equalto', 'bt_eybond_read_29') | map(attribute='val') | first) | float %}
        {% set battery_voltage = (bt_items | selectattr('id', 'equalto', 'bt_eybond_read_28') | map(attribute='val') | first) | float %}
        {{ (battery_current * battery_voltage) | float }}
      unit_of_measurement: "W"
      device_class: "energy"
      state_class: "measurement"

    - name: "Battery SOC"
      state: >
        {% set bt_items = state_attr('sensor.trailer_battery_data', 'parameter') %}

        {{ (bt_items | selectattr('par', 'equalto', 'bt_battery_capacity') | map(attribute='val') | first) | float }}
      unit_of_measurement: "%"
      device_class: "battery"
      state_class: "measurement"

    - name: "Battery Discharge Current"
      state: >
        {% set bt_items = state_attr('sensor.trailer_inverter_data', 'bt_') %}
        {% set battery_current = (bt_items | selectattr('id', 'equalto', 'bt_eybond_read_29') | map(attribute='val') | first) | float %}
        
        {% if battery_current < 0 %}
          {{ battery_current }}
        {% else %}
          0
        {% endif %}
      unit_of_measurement: "A"
      device_class: "current"
      state_class: "measurement"

    - name: "Battery Charging Current"
      state: >
        {% set bt_items = state_attr('sensor.trailer_inverter_data', 'bt_') %}
        {% set battery_current = (bt_items | selectattr('id', 'equalto', 'bt_eybond_read_29') | map(attribute='val') | first) | float %}
        
        {% if battery_current > 0 %}
          {{ battery_current }}
        {% else %}
          0
        {% endif %}
      unit_of_measurement: "A"
      device_class: "current"
      state_class: "measurement"

    - name: "Battery Current"
      state: >
        {% set bt_items = state_attr('sensor.trailer_inverter_data', 'bt_') %}

        {{ (bt_items | selectattr('id', 'equalto', 'bt_eybond_read_29') | map(attribute='val') | first) | float }}

      unit_of_measurement: "A"
      device_class: "current"
      state_class: "measurement"

    - name: "Battery Current Direction"
      state: >
        {% set bt_items = state_attr('sensor.trailer_inverter_data', 'bt_') %}
        {% set battery_current = (bt_items | selectattr('id', 'equalto', 'bt_eybond_read_29') | map(attribute='val') | first) | float %}
        {{ 1 if battery_current > 0 else 0 }}

    - name: "Load Output Voltage"
      state: >
        {% set bc_items = state_attr('sensor.trailer_inverter_data', 'bc_') %}
        {{ (bc_items | selectattr('id', 'equalto', 'bc_eybond_read_23') | map(attribute='val') | first) | float }}
      unit_of_measurement: "V"
      device_class: "voltage"
      state_class: "measurement"

    - name: "Load Power"
      state: >
        {% set bc_items = state_attr('sensor.trailer_inverter_data', 'bc_') %}
        {{ (bc_items | selectattr('id', 'equalto', 'bc_load_active_power') | map(attribute='val') | first) | float }}
      unit_of_measurement: "W"
      device_class: "energy"
      state_class: "measurement"

@Joannou1 Joannou1 changed the title Daily Power Usage / Generation Daily Power Usage / Generation + EG4 Config Jan 8, 2025
@SilverFire
Copy link
Owner

Hi!

This is my first time writing any substantial code for Home Assistant, so apologies if it's not as nice as it could or should be :p

This integration is the first HA-related coding experience for me too. You've done a great job! Thanks for your research and input!

I've added the EG4 template, but still trying to figure out, how to include a secret generator in the README to keep it simple and usable. Tried to add a code block generator, but it needs adjustments: https://jsfiddle.net/zw9qjcxh/8/

@Joannou1
Copy link
Author

Very nice! That looks like the perfect utility to translate URL's to a usable config.
I'm also noticing, the same as #4, the secret will expire. It seems the signature isn't what matters. it's the secret.
I wont be around for a week, but I'd like to try to dig into how the secret is defined, and see if it can be extracted from the response headers from normal requests.
If it requires re-authenticating with user/pass, then may need to figure out their auth endpoint to programmatically generate the secret regularly.

@amedyukh
Copy link

amedyukh commented Jan 31, 2025

Hi all,
I have found api documentation for dessmonitor: https://api.dessmonitor.com
An easy way to get actual secret/tocken for your account is:

  1. open www.dessmonitor.com in browser
  2. press F12 to open Developer Tools
  3. login with your account
  4. in Developer Tools filter requests by authSource and copy the URL. It loos like:
    https://web.dessmonitor.com/public/?sign=ea92095677a62f966f63d926b23xxxxxxxxxxxxx&salt=1738317995400&action=authSource&usr=your_login&source=1&company-key=bnrl_frRFjEz8Mkn
    It seems it will be able to login with the URL until password is changed.
    Output for this URL:
    {
    "err": 0,
    "desc": "ERR_NONE",
    "dat": {
    "secret": "8e3b4b9db7240c4c75fa30b21cc6exxxxxxxxxxx",
    "expire": 604800, //expiration time in minutes, 7 days
    "token": "7cbcf67052808424b70699d7d292d8dec6f61b282eaea4fexxxxxxxxxxxxxxxx",
    "role": 0,
    "usr": "your_login",
    "uid": 1234567
    }
    }
  5. then received "secret" and "token" can be used in requests to dessmonitor.com to get inverter data:
    action = "&action=querySPDeviceLastData&source=1&devcode=6416&pn=Q0033520000000&devaddr=5&sn=Q0033520734000000000&i18n=en_US"
    sign = SHA-1(salt + secret + token + action)
    salt = it can be a constant value, used for "sign" generation, e.g.1738311227846

Final request:
"https://web.dessmonitor.com/public/"+"?sign=" + sign + "&salt=" + salt + "&token=" + token + action;

@DooDesch
Copy link

DooDesch commented Feb 7, 2025

Hello 👋

I dipped down the rabbit hole and got it to work.
My code is far from being good but it works.

I've used pyscript, installed via HACS, to achieve my result. If you don't want to use pyscript/python, this approach might not work for you.

/pyscript/apps/dessmonitor.py

import time
import hashlib
import requests

@time_trigger("startup")
def init_dessmonitor():
    update_auth()
    update_inverter()

# Auth every hour
@time_trigger("cron(0 * * * *)")
def update_auth():
    if not pyscript.app_config:
        log.error("[DessMonitor] Keine App-Konfiguration in pyscript.app_config gefunden!")
        return

    config = pyscript.app_config[0]
    usr = config.get("usr", "default_user")
    pwd = config.get("pwd", "default_pwd")
    company_key = config.get("company_key", "default_company_key")

    salt = str(int(time.time() * 1000))
    pwd_sha1 = hashlib.sha1(pwd.encode("utf-8")).hexdigest()
    action = "&action=authSource&usr={}&source=1&company-key={}".format(usr, company_key)
    sign_str = salt + pwd_sha1 + action
    sign = hashlib.sha1(sign_str.encode("utf-8")).hexdigest()
    url = f"https://web.dessmonitor.com/public/?sign={sign}&salt={salt}{action}"
    log.info(f"[DessMonitor] Auth URL: {url}")

    try:
        response = task.executor(
            requests.get,
            url,
            timeout=10,
            headers={"User-Agent": "Mozilla/5.0 (HomeAssistant pyscript)"},
            verify=False
        )
        if response is None:
            raise Exception("task.executor returned None.")
        response.raise_for_status()
        data = response.json().get("dat", {})
        token = data.get("token", "")
        secret_value = data.get("secret", "")
        if token and secret_value:
            state.set("sensor.dessmonitor_token", token)
            state.set("sensor.dessmonitor_secret", secret_value)
            log.info("[DessMonitor] Auth erfolgreich – Token und Secret aktualisiert")
        else:
            log.error("[DessMonitor] Auth-Antwort enthält nicht die erwarteten Daten")
    except Exception as e:
        log.error(f"[DessMonitor] Fehler bei der Auth-Anfrage: {e}")

# Refresh data every minute
@time_trigger("cron(* * * * *)")
def update_inverter():
    token = state.get("sensor.dessmonitor_token")
    secret_value = state.get("sensor.dessmonitor_secret")
    if not token or not secret_value:
        log.error("[DessMonitor] Token oder Secret nicht verfügbar – überspringe update_inverter")
        return

    salt = str(int(time.time() * 1000))
    action = ("&action=querySPDeviceLastData&source=1&devcode=6416"
              "&pn=YOURPNHERE&devaddr=5&sn=YOURSNHERE&i18n=en_US") # Add your PN & SN here
    sign_str = salt + secret_value + token + action
    sign = hashlib.sha1(sign_str.encode("utf-8")).hexdigest()
    url = f"http://api.dessmonitor.com/public/?sign={sign}&salt={salt}&token={token}{action}"
    log.info(f"[DessMonitor] Inverter Data URL: {url}")

    try:
        response = task.executor(
            requests.get,
            url,
            timeout=10,
            headers={"User-Agent": "Mozilla/5.0 (HomeAssistant pyscript)"},
            verify=False
        )
        if response is None:
            raise Exception("task.executor returned None.")
        response.raise_for_status()
        data = response.json()
        inverter_attrs = data.get("dat", {}).get("pars", {})
        state.set("sensor.inverter_data", "OK", inverter_attrs)
        log.info("[DessMonitor] Inverter Daten aktualisiert")
    except Exception as e:
        log.error(f"[DessMonitor] Fehler bei der Inverter-Daten-Anfrage: {e}")

/configuration.yaml

pyscript:
  allow_all_imports: true
  hass_is_global: true
  apps:
    dessmonitor:
      - usr: !secret dessmonitor_usr
        pwd: !secret dessmonitor_pwd
        company_key: !secret dessmonitor_company_key

/secrets.yaml

dessmonitor_usr: YourUsername
dessmonitor_pwd: YourPassword
dessmonitor_company_key: YourCompanyKey # (obtainable via F12 Developer Tools)

It's important for pyscript that the script is in an apps folder - pyscript/apps/your_file.py

@dea9
Copy link

dea9 commented Feb 7, 2025

Hello everyone and thank you for your work on this. I'm a retired programmer from years ago, but got this up and running with all your code. I have two questions because I am also using an EG4 3000EHV-48:

  1. The dessmonitor api does not return bt_battery_capacity in the bt_ array from the uri for me. All other parameters are present.

Do any of you have the same issue?

The battery is connected to the inverter and reporting data to the cloud as the battery charge % is visible on the main page of the SmartESS app and the dessmonitor website.

I thought I might have the format of the uri wrong, but I have double checked it against the docs. Here is my redacted dessmonitor_api_uri:

https://web.dessmonitor.com/public/?sign=[sign]&salt=[salt]&token=[token]&action=querySPDeviceLastData&source=1&devcode=2376&pn=Q2721295335039&devaddr=1&sn=[sn]&i18n=en_US

There is, however, a different uri that returns a different subset of the data with the battery capacity included:

https://web.dessmonitor.com/public/?sign=[sign]&salt=[salt]&token=[token]&action=queryDeviceParsEs&source=1&devcode=2376&pn=Q2721295335039&devaddr=1&sn=[sn]&i18n=en_US

Is it possible my configuration is different from everyone else's, or there is a configuration setting I need to include to get SOC included in the recommended uri output?

  1. I noticed that your code above uses sensor.trailer_inverter_data, but my parameters come through as sensor.inverter_data when using the code provided on the main page for this restful.

I'm not sure if that is an artifact from testing or a prefix set elsewhere in the code, but it means that this example is not purely copy-and-paste for amateurs like me. I just removed the "trailer_" everywhere I found it, and the parameters came in as expected.

If this is the wrong place for this discussion, please let me know. I stopped programming before Github existed! Thank you.

@sbhowan09
Copy link

Thank you for this it is most appreciated. I am new at homeassistant, based on your use of the python script where would you define the sensor for the inverter data and how would it be defined?

Thanks

@Joannou1
Copy link
Author

Joannou1 commented Mar 8, 2025

Just wanted to say that I got the auth api working without the use of additional python code. Thank you @DooDesch!

There's a weird bug where accessing state attributes from the auth return sensor would cause HA / Spook to throw an undefined SHA5 function exception and would prevent the template from loading.
(Spook Issue #909)
It sounds like some of these functions from spook may get added to the official HA version? (Frenck is the lead HA engineer)

This may still happen with the below code.
Your DESS sensors will be unavailable for the duration set in the sensor refresh, so 2 minutes. It'll continue to try again after that period and work.

(Salt can be an arbitrary time or number. It doesn't seem important to use a changing number.)
Added to secrets.yaml:

dessmonitor_api_auth: >-  
  {% set dess_action = "authSource" %}
  {% set dess_salt = "1741226063614" %}
  {% set dess_user = "xxx" %}
  {% set dess_pass = "xxx" %}
  {% set dess_company_key = "xxx" %}

  {% set signature = sha1(dess_salt + sha1(dess_pass) + "&action=" + dess_action + "&usr=" + dess_user + "&source=1&company-key=" + dess_company_key) %}

  {% set baseURL = "https://api.dessmonitor.com/public/?sign=" + signature + "&salt=" + dess_salt + "&action=" + dess_action + "&usr=" + dess_user + "&source=1&company-key=" + dess_company_key %}
  {{ baseURL }}

You'd want to use this for every other API function token / secret

  {% set dess_secret = states('sensor.dess_secret') %}
  {% set dess_token = states('sensor.dess_token') %}

Added to template.yaml:

- sensor:
      - name: "Dess Token"
        state: >
            {{ state_attr('sensor.dess_auth', 'token') }}
- sensor:
      - name: "Dess Secret"
        state: >
            {{ state_attr('sensor.dess_auth', 'secret') }}

Added to configuration.yaml:

sensor:
  - platform: rest
    scan_interval: 120
    name: Dess Auth
    resource_template: !secret dessmonitor_api_auth
    method: GET
    json_attributes_path: "$.dat"
    json_attributes:
      - secret
      - token
    value_template: "OK"

It is also worth noting - I never got the authSource call when I logged in because I was logging in with the QR code.

@sbhowan09
Copy link

Thanks for this, will you be able to update the setup guide incorporating the above details as I still cannot get it to work in HA.

@scoleri
Copy link

scoleri commented Mar 17, 2025

thanks guys - you helped make my stupid little script.
https://github.com/scoleri/EG43000Data

@Zenyk-L
Copy link

Zenyk-L commented Apr 18, 2025

Looks like at last week inverter data from web.dessmonitor.com became unavailable very often. Each 3-5 minute. Does some one have the same issue??? How to solve it?

@amedyukh
Copy link

Yes, have the same issue. I noticed the site dessmonitor.com has an performance issue now, e.g. when login or refresh the page.
The most likely increasing timeout in restful sensor will help. By default it is 10 seconds.

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

8 participants