evcc icon indicating copy to clipboard operation
evcc copied to clipboard

Add Modbus service for dynamic parameter reading

Open iseeberg79 opened this issue 3 weeks ago • 7 comments

Adds HTTP service endpoint /api/modbus/params for reading device parameters based on template definitions without creating a full device instance.

Features (fixes #25857):

  • Dynamic register reading using template configurations
  • Automatic scaling and type conversion

Enables auto-filling of device parameters in the configuration UI by reading actual values from the device.

Usage examples:

  • ModbusTCP
  - name: capacity
    type: int
    unit: kWh
    advanced: true
    usages: ["battery"]
    service: modbus/params?uri={host}:{port}&id={id}&address=1068&type=holding&encoding=float32s&scale=0.001&resulttype=int
  • rs485serial
  - name: capacity
    type: float
    unit: kWh
    advanced: true
    usages: ["battery"]
    service: modbus/params?device={device}&baudrate={baudrate}&comset={comset}&id={id}&address=1068&type=holding&encoding=float32s&scale=0.001

iseeberg79 avatar Dec 08 '25 20:12 iseeberg79

Das ist grausig viel Logik. Warum muss der modbus Service etwas vin templates wissen? Die sollten ihm egal sein! Spannend ist allerdings die Modbus Config. Dafür hatte ich keine gute Idee :/

andig avatar Dec 08 '25 21:12 andig

Das ist grausig viel Logik. Warum muss der modbus Service etwas vin templates wissen? Die sollten ihm egal sein! Spannend ist allerdings die Modbus Config. Dafür hatte ich keine gute Idee :/

Die zu ermittelnden Register lädt er aus dem Template zur Laufzeit, nachdem die Modbus-Konfiguration verfügbar ist. Dafür der Name des Templates. Man könnte das Laden des Template ggf. überflüssig machen: statt properties, mehr URL Parameter. Auch deinen Hinweis mit dem modbus-Plugin.. - schau' ich mir an.

Die UI Integration ist etwas schwierig, wg. der Einschränkung mit den dataLists und der Positionierung des clear-Feldes bei Feldern mit Einheit. Das ist noch unschön, überlappt aber so aktuell nicht mehr - die Ausrichtung passt aktuell nicht. Ich stell' das mal hinten an erstmal... - ich hatte zuerst die Werte direkt in die Felder geschrieben, wenn initial - das widerspricht aber der aktuellen Integration/Tests aus demo-service. Die Dropdown's sind schon ganz charmant...

image

iseeberg79 avatar Dec 08 '25 23:12 iseeberg79

Die zu ermittelnden Register lädt er aus dem Template zur Laufzeit,

Die gehören in den Serviceaufruf. HA zeigt wie's geht:

service: homeassistant/entities?uri={uri}&domain=sensor

Die uri wird dynamisch vom UI da rein gebastelt. Ich weiss nur nicht ob/wie wir das für Modbus zum Leben erwecken können.

/cc @naltatis

andig avatar Dec 09 '25 07:12 andig

Ich bin einen Entwurf weiter, klappt soweit auch mit URL Parametern, ist übersichtlicher. Ein Update heute Abend!

Problematisch ist die Parallelität der Browseranfragen und Wiederverwendung des Modbus-Plugins, theoretisch läuft es.

Praktisch blockierte da gerade etwas, was einen weiteren echten Gerätetest braucht.

Der aktuelle Ansatz ist unnötig aufwändig, läuft aber.

iseeberg79 avatar Dec 09 '25 08:12 iseeberg79

Checks gerne ein, ich kann auch testen.

andig avatar Dec 09 '25 08:12 andig

Checks gerne ein, ich kann auch testen.

jetzt aber...

iseeberg79 avatar Dec 09 '25 19:12 iseeberg79

@naltatis having values for optional fields with units would really be useful. I've introduced an idea here - could you have a look? little logic + UI changes ...

There is another thing: it would be great to discuss about serviceDependencyGroups and how to configure/render/use them.

I have prepared another evolution as next step where it is possible to differentiate the required parameters for the protocol type, for modbus it would require to define ((host+port)v(device))id) and use them either, i.e. OR dependencies.

If you'd like to have a look how it could look like the next step, as I don't want to make things more complex at the moment here - see my branch feature/service-dependency-groups-http

iseeberg79 avatar Dec 10 '25 19:12 iseeberg79

@iseeberg79 @naltatis we need to trigger the service only if all required fields are present. We're already (sort of) doing this for authorisation (see Viessmann):

auth:
  type: viessmann
  params: [clientid, redirecturi, gateway_serial]

This already includes all params in the request without building a specific url. I'm wondering if we can re-use this magic for Modbus like this:

  - name: modbus
    choice: ["rs485", "tcpip"]
    baudrate: 9600
    id: 1
  - name: minsoc
    advanced: true
    service:
        uri: modbus/read?address=1&type&uint32
        params:
            - [id, host, port, rtu]
            - [device, baudrate, comset]

Where params would be a list of sets. A complete set would be appended to the uri as query parameters. We need this to allow both Serial and Ethernet connections over Modbus. If we do not want to express this I'd be happy to have it hard-coded somewhere, maybe depending on the service url (if it starts with modbus/ it must depend on the modbus connection type).

Wdyt?

andig avatar Dec 21 '25 12:12 andig

A complete set would be appended to the uri as query parameters. We need this to allow both Serial and Ethernet connections over Modbus

I think it's important to allow flexibility here. As URI is required for TCP it could get build by http or https, host+port - because templates are using it differently it is important to combine protocol+host+port or to add just host or host+port and use it as appropriate dependend on the UI fields. Example: URI is build by UI fields host+port making them required as dependency build as a singe URL parameter

iseeberg79 avatar Dec 21 '25 14:12 iseeberg79

I think it's important to allow flexibility here. As URI is required for TCP it could get build by http or https, host+port - because templates are using it differently it is important to combine protocol+host+port or to add just host or host+port and use it as appropriate dependend on the

Modbus doesn't have a scheme. Instead of uri the group could contain host, port. Port may just have a default value. If existing implementations use this differently we could fix them.

andig avatar Dec 21 '25 15:12 andig

About the service dependency groups I was thinking about:

  1. case (no dependencies, actual):
  - name: capacity
    type: float
    unit: kWh
    advanced: true 
    usages: ["battery"]
    service: modbus/read?uri={host}:{port}&id={id}&address=1068&type=holding&encoding=float32s&scale=0.001
  1. case (dependencies)
  - name: capacity
    advanced: true 
    service: modbus/read?uri={host}:{port}&device={device}&baudrate={baudrate}&comset={comset}&id={id}&address=1068&type=holding&encoding=float32s&scale=0.001
    servicedependencies:
      - [host, port, id] 
      - [device, id]

We could assume a http-service looking like this - I personally like URI getting reused:

- name: minsoc
    advanced: true
    service: http/params?uri=http://{host}/rest/channel/{battery}/_PropertyMinsoc&authtype=basic&username=x&password={password}&jq=.value

As an alternative implementation we could think about:

Modbus:

  - name: capacity
    service:
      endpoint: modbus/read
      params:
        uri: "{host}:{port}" 
        id: "{id}"
        address: 1068
        type: holding
        encoding: float32s
        scale: 0.001
      dependencies:
        - [host, port, id]
        - [device, baudrate, comset, id]

HTTP: (idea about extending the implementation by a http service)

  - name: minsoc
    service:
      endpoint: http/read
      params:
        uri: "http://{host}/rest/channel/{battery}/_PropertyMinsoc"
        authtype: basic
        username: x
        password: "{password}"
        jq: ".value"
      dependencies:
        - [host, battery, password]

Of course there are mixes possible?

iseeberg79 avatar Dec 21 '25 15:12 iseeberg79

uri={host}:{port}

That's ugly since it will create a non-empty parameter in the device case. But can be worked around in the backend. Makes sense that parameters still need to be specified in the uri for this purpose.

Or maybe just unused ones.

andig avatar Dec 21 '25 15:12 andig

uri={host}:{port}

That's ugly since it will create a non-empty parameter in the device case. But can be worked around in the backend. Makes sense that parameters still need to be specified in the uri for this purpose.

Or maybe just unused ones.

That's correct but currently have no idea how to enable it without empty parameters. It's not that complex and proven to work as expected in a branch, as well to drop unused parameters. Needs a small UI adjustment for cases where the UI today does not empty fields on protocol switch (modbus tcp <--> serial) where device and host should get cleared when switching.

iseeberg79 avatar Dec 21 '25 15:12 iseeberg79

The style of having one line containing URL, Placeholder and statics is really simple. Alternatively I'd love to see something like below. The backend should only use parameters of the dependency group:

- name: maxacpower
    type: int
    unit: W
    usages: ["pv"]
    service: modbus/read
      params:
        uri: "{host}:{port}"
        id: "{id}"
        address: 531
        type: holding
        encoding: uint16

or

  - name: maxacpower
    service: modbus/read
      params:
        uri: "{host}:{port}"
        device: "{device}" 
        baudrate: "{baudrate}"
        comset: "{comset}"
        id: "{id}"
        address: 1069
        type: holding
        encoding: float32s
      dependencies:
        - [host, port, id]
        - [device, baudrate, comset, id]

or

  - name: maxacpower
    service: 
      endpoint: modbus/read
      params:
        uri: "{host}:{port}"
        device: "{device}" 
        baudrate: "{baudrate}"
        comset: "{comset}"
        id: "{id}"
        address: 1069
        type: holding
        encoding: float32s
      dependencies:
        - [host, port, id]
        - [device, baudrate, comset, id]

It's has a clearer structure and intendation. Very close to your initial. Wdyt? Which approach should we go for?

iseeberg79 avatar Dec 21 '25 23:12 iseeberg79

  - name: minsoc
    advanced: true
    service:
        uri: modbus/read?address=1&type=uint32
        params:
            - [id, host, port, rtu]
            - [device, baudrate, comset]

or

  - name: minsoc
    advanced: true
    service:
      uri: modbus/read?uri={host}:{port}&device={device}&baudrate={baudrate}&comset={comset}&id={id}
      address: 1069
      type: holding
      encoding: float32s
      dependencies:
        - [host, port, id]
        - [device, baudrate, comset, id]

or

  - name: maxacpower
    service: 
      endpoint: modbus/read
      params:
        uri: "{host}:{port}"
        device: "{device}" 
        baudrate: "{baudrate}"
        comset: "{comset}"
        id: "{id}"
        address: 1069
        type: holding
        encoding: float32s
      dependencies:
        - [host, port, id]
        - [device, baudrate, comset, id]

or

- name: maxacpower
  service:
    endpoint: modbus/read
    params:
      uri: "{host}:{port}"
      address: 1069
      type: holding
      encoding: float32s
    dependencies:
      - [host, port, id]
      - [device, baudrate, comset, id]

which one to choose? I'd like to go for 3rd option as it gives full flexibility and clear structure. 2nd option has unnecessary redundancy in it's URI.

Thinking about the last added 4th option would be even better but is adding some "magic" as it's adding unused parameters from dependencies as simple parameters if not yet used.

iseeberg79 avatar Dec 22 '25 06:12 iseeberg79

@andig I'd like to push the commits for the service dependencies as suggested above, going for:

- name: maxacpower
  service:
    endpoint: modbus/read
    params:
      uri: "{host}:{port}"
      address: 1069
      type: holding
      encoding: float32s
    dependencies:
      - [host, port, id]
      - [device, baudrate, comset, id]

@naltatis not going to disrupt changes from your side, ok?

iseeberg79 avatar Dec 23 '25 10:12 iseeberg79

I've prepared some UI tests as well, but skipped here initially common got added because of some functions I'd like to re-use for a http service later

iseeberg79 avatar Dec 23 '25 11:12 iseeberg79

That's ugly since it will create a non-empty parameter in the device case. But can be worked around in the backend. Makes sense that parameters still need to be specified in the uri for this purpose. Or maybe just unused ones.

I'd prefer not mixing explicit param definition with placeholder substitution like we see with uri here. Do we need this extra flexibility?

  service:
    endpoint: modbus/read
    params:
-     uri: "{host}:{port}"
      address: 1069
      type: holding
      encoding: float32s
    dependencies:
      - [host, port, id]
      - [device, baudrate, comset, id]

I like the idea of having fixed params and dynamic dependencies, but keeping them as distinct concepts. Then questions like missing values or how to handle deps that are also placeholders would disappear.

If we really need the flexibility (uri) another, maybe simpler, solution could be to stick to the existing logic of url substitution and service being a string but allowing multiples. Could look like this:

  service:
    - modbus/read?address=1069&type=holding&encoding=float32s&id={id}&uri={host}:{port}
    - modbus/read?address=1069&type=holding&encoding=float32s&id={id}&device={device}&baudrate={baudrate}&comset={comset}

Then the first server string where all placeholders can be filled would be used. Drawback with this solution is that we'd have to repeat static values.

naltatis avatar Dec 24 '25 10:12 naltatis

I think the flexibility is required because of URI might getting build by different fields. Specially having the extension for http-templates in mind we have to deal with host plus port, additional protocol, URL parts or host:port in one field.

This approach does support any UI field combination which is relevant looking at modbus and http templates.

I really like this approach but see more UI complexity as well. URL style is fine, too - see initial description, but one line + deps I'd prefer.

iseeberg79 avatar Dec 24 '25 10:12 iseeberg79

Then the first server string where all placeholders can be filled would be used. Drawback with this solution is that we'd have to repeat static values

I like the idea. Maybe at a later stage we'll need to add optional parameters (?). @naltatis one thing that's a striking is that the auth service is done differently by specifying the actual parameters. Does it make sense to do both in the same way?

andig avatar Dec 24 '25 10:12 andig

one thing that's a striking is that the auth service is done differently by specifying the actual parameters. Does it make sense to do both in the same way?

We have to things at play here:

a) being able to remap or concat params from user input (host:port) b) supporting alternative param combinations (modbus tcp / serial)

In the auth case we have none of there requirements. There it's fine to 1:1 pass user fields to the service endpoint. However we could also expand this in the future to support remapping input field to param (a) if we really need it:

auth:
  type: demo
  params: 
    - server
    - method

as well as interpolation

auth:
  type: demo
  params: 
    server: "{protocol}{host}"
    method: "{method}"

But coming back to the actual topic here let's look at b). Do we really have the use-case for these alternative combinations outside of modbus? Modbus configuration is a special candidate in many places of evcc (docs, config ui, templates ,..). Maybe it would also be fine to introduce a dedicated modbus (meta)-param and keep the logic of "required params for tcp/serial" in UI.

Idea:

 service: modbus/read?address=1069&type=holding&foo={foo}&encoding=float32s&{modbus}

Where {modbus} will be expanded to host=..&port=..&id=.. or device=..&baudrate=..&comset=..id=.. depending on user input by the UI.

We could write the above example also in endpoint + params structure, but this would the primarily be a readability difference.

naltatis avatar Dec 24 '25 11:12 naltatis

Maybe it would also be fine to introduce a dedicated modbus (meta)-param and keep the logic of "required params for tcp/serial" in UI.

Like it! Advantage: much less copy/paste since we'll need this pattern quite a lot!

We could write the above example also in endpoint + params structure, but this would the primarily be a readability difference.

Yagni? Can still do that later on.

andig avatar Dec 24 '25 12:12 andig

Modbus configuration is a special candidate

Agreed on this. Also identified modbus and http as candidates for this use case. Only modbus has this serial or tcp selection.

iseeberg79 avatar Dec 24 '25 12:12 iseeberg79