Ingramz / ecl110

MODBUS protocol for Danfoss ECL Comfort 110
17 stars 8 forks source link

Figure out the purpose of DLG #2

Open Ingramz opened 6 years ago

Ingramz commented 6 years ago

Danfoss Link Gateway or 087H3241 is a companion device for connecting ECL Comfort 110 with Danfoss Link CC. As the name says it acts as a gateway between the RS485 line and 868MHz RF communication.

An application example from http://heating.danfoss.com/PCMPDF/087H9216_VIJMC36K_DLG_ECL110_Link_IG.pdf shows how it should connect, however the indoor temperature sensor S2 is missing, replaced by DLG. Documentation doesn't mention anywhere that S2 should be removed (but should make sense as Danfoss Link temperature sensors should be used. Confirm whether S2 is omitted intentionally or it serves for illustrative purposes only.

http://heating.danfoss.com/PCMPDF/VDKTC902_ECL110_LINK.pdf promises the following:

By system integration of ECL Comfort 110 and Danfoss Link™ via the DLG interface the following is achieved: • Heating is ON whenever there is a demand

Water is only circulated (pump is on) when any of the radiator valves is open/the indoor temperature is below threshold? We can control the pump in manual mode, but that is not possible in comfort mode. My gut feeling tells that Danfoss Link CC somehow provides additional control information to ECL110, but no idea what it is.

• Heating is operated from only one interface

That one interface being Danfoss Link, not useful for us.

• Heating comfort is achieved with the lowest energy consumption

Given that heating is not ON 24/7, same as 1?

• Outdoor temperature is shown in the Danfoss Link™ panel.

Reads outdoor temperature sensor S1 from PNU 11200, we can do that already.


It doesn't hurt to ask Danfoss regarding the working principle either, it shouldn't be a huge secret how the ECL110 controlling part works.


If you happen to own a DLG and rest of the system (mainly Danfoss Link CC), it would be extremely helpful if the communication was sniffed using a USB-to-RS485 adapter ($1 from ebay) and a computer to connect it to. I can provide further instructions if necessary. The process is nondestructive and safe to do.

To sweeten the deal, I can offer a 014G0002 for anyone in return.

blomqvist commented 3 years ago

I recently installed a DLG along with a ECL 110. I can make an effort and see if I can buy a cheap adapter. However, I do need the instructions :)

It does turn off the pump and close the valve for the incoming district heating when there is not temperature demand.

bild

Ingramz commented 3 years ago

Interesting, thank you for posting this.

Could you perhaps provide some information about what DLG does or can do when it is connected to ECL110. Because this is not explained very well on the website at all. Basically what we are interested in is what does it do better than just ECL110 alone.

I can probably provide some further information later tonight if you are interested in monitoring the communication between DLG and ECL110.

blomqvist commented 3 years ago

One key feature for me is the ability to schedule the heating from the same app I use with my other Danfoss thermostats and the floor heating (controller by a Danfoss HC, a second one is to be installed). This will allow me to tune my heating in absurdum. I can set away from home temperature on the go, from anywhere.

Another key feature is, as mentioned, that heat will be delivered when wanted somewhere in the system. This will allow me to have a nice warm floor in my bathrooms even during summer time. This would else require me to adjust the indoor temperature on the ECL 110 to above 23 degrees, which would waste some heat during transfer. It would also require me to upgrade all thermostats in the house to Danfoss Link. I have a garage and a basement room which is not covered by the Link CC (yet...).

The ECL 110 is in auto mode, and the DLG seems to adjust all that is needed in order to maintain the heat. I don't know if the Link system keeps track of an internal heat curve. I've yet to observe the water temperature demanded by the DLG to see if I can map it to any logical step of the heat curve or if it is an arbitrary "this should do" number.

My home consists in total of 10 Living Connect thermostats (I have one more not yet installed), 1 HC with five actuators controlled by 3 Danfoss Icon and the DLG. I will soon wire my HC to controll the shunt pump also.

Ingramz commented 3 years ago

Impressive setup!

So far we know pretty much how to read or change data that you can access through the LCD and buttons on the ECL110, because it was very easy to correlate most of these things with the MODBUS readings. For logging purposes it is also possible to read all of the information about status that appears on the LCD, like temperature readouts and pump/valve status and then store it in a time series database for later analysis.

To me it feels like the feature that we do not know how to imitate yet is notifying ECL110 of the demand. If we figure this out, it would instantly make the device a lot better for anyone also using MODBUS to control the ECL110. Any clue if the demand is represented as a "yes/no" signal or does it correspond to any specific temperature that is demanded?

Thanks for clarifying that you use AUTO mode. I'm using COMFORT, but I really cannot remember if there was any significant difference.

I see that you don't have S2 indoor temperature sensor connected (correct me if I am wrong). If you open the temperature submenu (hold enter when you are on the room temperature line), is there anything in S2 that might be interpreted as demand? Perhaps this already provides any hints on how it works. To me it felt that S2 desired temp. is always the same as the set room temperature in main menu, but let me know if they are different in case of demand.

Lastly, you are probably running version 1.08, correct?


Here is a small outline of the procedure how to start recording data between ECL110 and DLG:

Basically you are looking for an adapter like this: https://www.aliexpress.com/wholesale?trafficChannel=main&d=y&CatId=0&SearchText=usb+to+rs485&ltype=wholesale&SortType=price_asc&page=1

They are about 4-5 EUR on ebay Europe and perhaps something similar on amazon, knowing that ordering from China might turn out more expensive for anyone in Sweden. If you are willing to wait up to two full weeks (based on last package I sent to Sweden a few months ago), I can also send mine once I can find it - haven't used it for a while.

image

All you have to do is extend the A and B wires from the connector on the ECL 110 side to the USB adapter. Since you can just unscrew the two middle wires from connector on the ECL110 side, you can temporarily fit two extension wires between the terminal block and screw it back together.

To record the traffic between the devices, it is possible to use any serial port program (for windows: tera term, putty, mobaxterm etc...) with these settings. But there are also some programs that can decode modbus from a serial port stream directly, which also might be easier to use. Last time I used some .NET modbus library together with a simple 20-line console application to read in values.

Once there is data coming in, the best would be to go through all of the features it has so everything gets recorded for later analysis. Power cycling to see initialization process could be of interest as well.

blomqvist commented 3 years ago

I placed an order for the RS485 stick (and also for two IR readers so I can get real time district heating readings..). I will get back to you when they arrive. The shipping estimates is in the range 30-50 days..

You are correct that I don't have an indoor sensor hooked up. I have one spare that was included with the ECL 110. I can try what you suggested, and also do the same with the indoor sensor connected. I will have to get back to you regarding the software version and also if the demand is binary or some number (by hunch).

I did notice that the DLG has one RJ45. I guess I can use USB to RS485 with RJ45 socket to connect (something like this: https://www.kjell.com/se/produkter/el-verktyg/stromforsorjning/solceller/usb-till-rs485-adapter-for-regulator-p45135 (swedish)).

Ingramz commented 3 years ago

Isn't the RJ45 in use when DLG is connected to ECL110?

blomqvist commented 3 years ago

It's free as a bird! The MODBUS cord uses the same connector as power. Can attach photo later today.

Ingramz commented 3 years ago

Please do :). I thought it would connect to RJ45 on the DLG side like shown in manual:

image

The adapter looks like the one you'd want to use if RJ45 carried the MODBUS connections, but beware that there probably isn't a standard on how to wire RJ45 for MODBUS. So Epever (or whoever made the cable) and Danfoss could have used different pinouts. If you know how and have a RJ45 crimping tool already available, it would be trivial to just rewire the adapter correctly for it to work with DLG instead.

Before buying you can verify that the ECL110 connector B, A (D-, D+ on the pic above) connections are present on the DLG RJ45 by connecting an ethernet cable to the RJ45 port on DLG and then using multimeter to find continuity between the two unconnected sides. Make sure not to do it while the DLG is powered on (disconnect DLG from ECL110 and wall first).

If I'm explaining too basic or obvious stuff, you can let me know. It's hard to gauge over the internet how much you already know about electronics or what kind of tools you already have and know to use.

blomqvist commented 3 years ago

Yeah, one would guess. The connector on the DLG is actually inserted on the right or left hand side (can't remember right now), and looks more like a 6 pin PCI-E connector, albeit smaller. The RJ45 is located in the middle, as by the image.

I will test with an ethernet cable.

I have some knowledge about electronics, but I appreciate the level of your explanations.

blomqvist commented 3 years ago

I have continuity between the first and second pin of the RJ45 male connector when viewed with the safety pin facing up. It would be the first and second from the right on the picture below.

bild I realize now that this is not a very good picture. On the right we have a USB mini connector.

I think I have software version 1.08. All serial numbers and such seems to point to 1.08 as they start with the number 108.

When I toggle vacation mode and home mode, the DLG sends 8˚ and 21˚ respectively as target temperature.

Ingramz commented 3 years ago

I meant to write during the weekend, but was occupied with other things.

The picture is good enough with the descriptions you provided, thank you for that.

Vacation and home mode seem fairly straightforward, it's basically what you can do by hand from menu manually too.

Also great to hear that you are running 1.08, there was a way to see this from ECL110 menu too, either when it booted up or from one of the about menus somewhere deeper.

I'll try to find or write the software to log the data in the meanwhile, Also I feel like I should re-read the manual in english, so that nothing gets lost in translation.

plysdyret commented 3 years ago

• Heating comfort is achieved with the lowest energy consumption

Given that heating is not ON 24/7, same as 1?

I bought a DLG some years ago and talked to Danfoss afterwards since the only difference I noticed was the ability to see the outdoor temperature from the ECL in the Link app. After a little digging they told me that the DLG also enabled the Link to control the temperature setting on the ECL based on the highest room temperature requested by the thermostats in the system. E.g. I want 20 degrees in my bedroom and 22 degrees in the living room, so if the living room is requesting heat the ECL goes to 22 and if only the bedroom is requesting heat the ECL goes to 20 (allegedly).

If you happen to own a DLG and rest of the system (mainly Danfoss Link CC), it would be extremely helpful if the communication was sniffed using a USB-to-RS485 adapter ($1 from ebay) and a computer to connect it to. I can provide further instructions if necessary. The process is nondestructive and safe to do.

I have pretty much the same setup as Niklas and I've also just ordered an RS485 USB stick. I'm mainly interested in reading temperature data, that should be possible even with the DLG connected, right? Or will I only be able to sniff whatever the DLG/Link communicates?

Ingramz commented 3 years ago

Thank you for the input, definitely a welcome observation, which makes sense.

As for your question whether the USB dongle and DLG can communicate to ECL over the same bus - we don't really know for sure if that causes any sort of conflicts with DLG, but there is very little reason to believe it would be an issue (because why else they would be using RS-485 then). So in short, yes, but we need to confirm it to be absolutely sure.

Requesting sensor data that ECL is aware of (S1-S4 readings) is well supported and as a rule of thumb, anything you can see or do using the LCD display and buttons of ECL, you should be able to do using the dongle as well.

plysdyret commented 3 years ago

I got the thing wired up, I think, but unsure how to proceed.

20210107_204059

I've connected with minicom but only getting garbage:

Capture_garbage

Any suggestions on how to proceed or what I may have done wrong?

Ingramz commented 3 years ago

You probably didn't do anything wrong as far as connections go.

Modbus RTU is a binary protocol, so it doesn't output anything human readable to the serial terminal. So it is actually alright if it produces garbage output in minicom.

One thing you can do is turn on hex mode for your minicom (-H). This way you will see bytes instead of garbage, but you will probably still not understand anything without reading on how modbus messages frames are constructed. They are actually quite simple, but for a first-timer perhaps maybe a bit tricky to grasp without help. If you are interested however, there are articles like https://www.modbustools.com/modbus.html which help you to better understand the protocol.

There are also programs which can read and format the Modbus RTU output from serial port directly, but I don't know which ones are easy to install or use on a pi. If you know programming, then you can also try using any of the modbus libraries available (python and node.js ones are probably one of the easiest to get up and running), many of which come with examples how to use the library. There are some nice graphical applications made for windows, but it has been long since I have used one.

I can look into this during the weekend, if I can't find any good programs, I'll just write my own and share it along with the instructions how to get it running. I meant to do this earlier, but did not have the time for it then.

plysdyret commented 3 years ago

Thanks, it looks better with -H, so hopefully the wiring is good at least =)

I also tried the following program but that fail with either a timeout or (not so often) some CRC error. Very easy to install and would work well for my use (pull data with cron and punt it towards influxdb) but won't help with figuring out what the DLG does.

https://github.com/favalex/modbus-cli

" root@flowerpi3:~/modbus-cli/modbus_cli# modbus -v --baud 19200 --parity=e --stop-bits=1 /dev/ttyUSB0 11200 Parsed 0 registers definitions from 1 files → < 01 03 2b c0 00 01 8d d2 > Traceback (most recent call last): File "/usr/local/bin/modbus", line 4, in import('pkg_resources').run_script('modbus-cli==0.1.4rc2', 'modbus') File "/usr/lib/python3/dist-packages/pkg_resources/init.py", line 666, in run_script self.require(requires)[0].run_script(script_name, ns) File "/usr/lib/python3/dist-packages/pkg_resources/init.py", line 1446, in run_script exec(code, namespace, namespace) File "/usr/local/lib/python3.7/dist-packages/modbus_cli-0.1.4rc2-py3.7.egg/EGG-INFO/scripts/modbus", line 61, in main() File "/usr/local/lib/python3.7/dist-packages/modbus_cli-0.1.4rc2-py3.7.egg/EGG-INFO/scripts/modbus", line 58, in main connect_to_device(args).perform_accesses(parse_accesses(args.access, definitions), definitions).close() File "/usr/local/lib/python3.7/dist-packages/modbus_cli-0.1.4rc2-py3.7.egg/modbus_cli/modbus_rtu.py", line 58, in perform_accesses access.perform(self) File "/usr/local/lib/python3.7/dist-packages/modbus_cli-0.1.4rc2-py3.7.egg/modbus_cli/access.py", line 108, in perform self.read_registers_receive(modbus) File "/usr/local/lib/python3.7/dist-packages/modbus_cli-0.1.4rc2-py3.7.egg/modbus_cli/access.py", line 139, in read_registers_receive words = modbus.receive(self.request) File "/usr/local/lib/python3.7/dist-packages/modbus_cli-0.1.4rc2-py3.7.egg/modbus_cli/modbus_rtu.py", line 33, in receive raise RuntimeError('timeout') RuntimeError: timeout "

Sounds good =)

plysdyret commented 3 years ago

Tried another library (pymodbus) and same thing, no reply and hitting timeout. Also, with minicom, it takes like a minute before I get any data.

I also bought a connector from aviborg, might try that to cut out the DLG just to see if that changes anything.

Ingramz commented 3 years ago

Check your wiring, could be that your A and B are swapped on the USB adapter. Red should go to B and brown should go to A. Guessing the backside of your adapter looks like this? https://ae01.alicdn.com/kf/H70f7b7e5ba6b4f53952605b8fc7d40c0j.jpg

Another thing you can maybe post here is some output in hex from the minicom, it should be fairly easy to detect modbus traffic, if it is being received correctly.

I'll also leave http://rapidscada.net/modbus/ModbusParser.aspx here, which is quite helpful at recoding frames from hex, although it requires some trial and error to find the start of the frame and then paste correct length of it.

plysdyret commented 3 years ago

Check your wiring, could be that your A and B are swapped on the USB adapter. Red should go to B and brown should go to A. Guessing the backside of your adapter looks like this? https://ae01.alicdn.com/kf/H70f7b7e5ba6b4f53952605b8fc7d40c0j.jpg

That's the one, yes. I think it's wired ok:

20210108_091515 20210108_091436

Another thing you can maybe post here is some output in hex from the minicom, it should be fairly easy to detect modbus traffic, if it is being received correctly.

05 03 07 da 00 01 a5 01 05 03 02 06 7e ca 04 05 03 2b c0 00 01 8c 56 05 03 02 00 1d 89 8d 05 03 10 68 00 01 00 92 05 03 02 00 02 c8 45 05 03 2b c2 00 01 2d 96 05 03 02 02 49 89 12 05 03 2b c3 00 01 7c 56 05 03 02 00 e8 49 ca 05 03 2b 15 00 01 9d ae 05 03 02 00 2e c9 98 05 03 0f a1 00 01 d7 78 05 03 02 00 01 88 44 05 03 2b ab 00 01 fd 8a 05 03 02 00 16 c8 4a 05 03 2b ac 00 01 4c 4b 05 03 02 00 05 89 87 05 03 2b 45 00 01 9d bf 05 03 02 00 12 c9 89 05 03 2b dd 00 01 1c 50 05 03 02 02 41 88 d4

I'll also leave http://rapidscada.net/modbus/ModbusParser.aspx here, which is quite helpful at recoding frames from hex, although it requires some trial and error to find the start of the frame and then paste correct length of it.

Hmm, if I paste my hex into that I get the following error:

" Data package CRC error. Actual CRC is 88 D4. Expected CRC is EF 89. "

Ingramz commented 3 years ago

It actually looks correct. One thing you need to mind is that when pasting commands there, you need to know if your frame is a request or a response and then only paste the correct length of it.

I haven't gone through entire stream yet, but the beginning seems alright:

Request: 05 03 07 da 00 01 a5 01
Response: 05 03 02 06 7e ca 04

Notice that response is 1 byte shorter than the request. Next one is again a request and takes 8 bytes...

Edit: it is already quite interesting, for some reason it requested PNU (address) 2010, which we know nothing about yet (https://github.com/Ingramz/ecl110/blob/master/README.md) other that it can be requested.

plysdyret commented 3 years ago

Ah, that's something atleast. It would be nice to be able to actively poll the registers I want but perhaps what I need can be sniffed from the DLG traffic if all else fails. Still odd none of the libraries worked, hopefully I just missed some essential detail.

I happen to have an open ticket with Danfoss on a zwave repeater and asked them about reading modbus on the ECL 110 and if the RJ45 port on the DLG could be used for anything in that regard. They claim it's not possible to get anything from modbus on the ECL (heh) and that the RJ45 port is only used when the DLG is used as an CCM module for Danfoss Air units.

Ingramz commented 3 years ago

It depends on how tolerant or considerate the DLG is when it comes to taking turns with other devices on the bus. The very next request/response is for S1 reading and it's likely you can indeed sniff everything you need at worst.

plysdyret commented 3 years ago

So request for S1 (11200)?

Capture_request

But also S2 (11201)?

Response:

Capture_response

29 should be 2.9 which is increased to 3.0 in the app 5 mins later:

Screenshot_20210108-095022_Danfoss Link

Ingramz commented 3 years ago

I'm not sure what is meant exactly by Physical/Logical representation here, but I think the way it is documented in README is all based on the "Physical" value. But indeed, since it is in 0.1C increments, it's 2.9C, which seems correct based on your confirmation.

plysdyret commented 3 years ago

Confirmed by checking an S3 modbus value against the readout on the actual unit. Pretty cool, then I just need a parser of some sort.

plysdyret commented 3 years ago

To further both your and my purpose I think it would be cool with a little python daemon that listens on the serial port and parses the stream into requests and responses. Those could then be persisted in whatever form you would like for DLG discovery and I could probably manage to tack on some MQTT code and punt the relevant values off to OpenHAB/InfluxDB.

Somewhat like https://github.com/ThomDietrich/miflora-mqtt-daemon which I'm using so the missus remembers to water her plants.

Do you have time to hack up a little daemon to parse and persist?

plysdyret commented 3 years ago

This works btw, getting a nice stream of bytes:

import serial

ser = serial.Serial(
    port='/dev/ttyUSB0',
    baudrate=19200,
    parity=serial.PARITY_EVEN,
    stopbits=serial.STOPBITS_ONE,
    bytesize=serial.EIGHTBITS,
)
ser.flushInput()

while True:
    try:
         print(ser.read().hex())
    except:
        print("Keyboard Interrupt")
        break
root@flowerpi3:~# python3 read.py
05
03
07
da
00
01
a5
01
05
03
02
06
7e
ca
04
05
03
2b
c0
00
01
8c
56
05
03
02
00
27
09
9e
05
03
10
68
00
01
00
92
05
03
02
00
02
c8
45
05
03
2b
c2
00
01

It takes like a minut after starting before I'm getting any data.

Ingramz commented 3 years ago

It looks like my understanding of modbus was incorrect and wikipedia was quite clear about pointing it out:

On Modbus RTU, Modbus ASCII and Modbus Plus (which are all RS-485 single cable multi-drop networks), only the node assigned as the Master may initiate a command. All other devices are slaves and respond to requests and commands.

https://en.wikipedia.org/wiki/Modbus#Communications_and_devices

In our case the ECL is a slave device and typically the USB adapter and DLG are master devices.

The request-response cycle is rather straightforward too, send a request, wait, expect a response for your request within a certain time frame.

So people from Danfoss actually are right that you cannot connect more than one "master" to the same bus that ask readings from slave device(s). The reason why becomes clear as soon as you try to request information from a slave device using two masters at the same time - neither master knows how to take turns with the other. There are clever ways around this, but most of them require additional hardware, for instance splitting the bus using two USB adapters (one acting as a master and other as a slave) and then relaying or buffering the messages between them. But in many cases this is not worth the trouble.

However as established earlier, we can still monitor the bus without interfering with the ECL-DLG communications. I'll look into it now.

plysdyret commented 3 years ago

I concur, I read as much as well.

I made some monkey code to get the values I want into OpenHAB:

import serial
from datetime import datetime
import os

# Connect serial
ser = serial.Serial(
    port='/dev/ttyUSB0',
    baudrate=19200,
    parity=serial.PARITY_EVEN,
    stopbits=serial.STOPBITS_ONE,
    bytesize=serial.EIGHTBITS,
)
ser.flushInput()

bb=''
tmp=''
address=''
payload=''

while True:

    try:
        b = ser.read()
        h = b.hex()

        if h == '03':
            if tmp == '05 ':
#              print(h, end=' ', flush=True)

               r = bb.replace(" ", "")

               if len(r) == 16: # request
                   address = r[4:8]
               elif len(r) == 14: # reponse
                   payload = r[6:10]

                   if int(address, 16) == 11200:
                       temp_int = int(payload,16)
                       temp_float = float(temp_int) / 10

                       now = datetime.now()
                       current_time = now.strftime("%H:%M:%S")

                       print(current_time + ' Outside temperature: ' + str(temp_float))
                       os.system('mosquitto_pub -h openhab.localdomain -t ECL/11200 -m ' + str(temp_float))

               bb=tmp+h+ ' '
               tmp=''
            else:
              bb+=h+' '
        elif h == '05':
            tmp+=h + ' '
        else:
            bb+=tmp+h + ' '
            tmp=''

    except Exception as e:
        print(e)
        break

I'm sure parsing can be made a lot smarter but so far so good.

Ingramz commented 3 years ago

I was looking at libaries like pymodbus so I could rely on their parsing, but none of them really support sniffing the bus and only deal with the easy part of just detecting either requests or responses.

So I figured as well it is easier to just parse the entire thing ourselves, like you did with that code. I wrote a prototype in javascript, but might consider writing it to python if that is preferred. It doesn't have any serial input or database output added to it yet, but in javascript these are fairly trivial to add.

It operates on a per-byte level and attempts to construct first valid modbus frames from the stream of bytes, by asserting a minimum length of required bytes and then looking for shortest frame with valid CRC. Bytes that cannot be used to construct modbus frames are kept track of, but eventually discarded.

After that if two consecutive frames have been detected, it assumes the first one is request and other one is response, then looks that they belong to the same slave and function and finally compares the lengths of request and response messages to best of its abilities. If a valid pair is found, it records the match and then tries to parse the addresses and data specific to the function of these frames.

There are some corner cases where this approach would not work for other devices, but given that ECL/DLG communicate using one function only (it seems) where the frames are already almost as short as they can be, then it is hard to construct something that gets detected incorrectly.

The code I have for now is here:

```js const data = "05 03 07 da 00 01 a5 01 05 03 02 06 7e ca 04 05 03 2b c0 00 01 8c 56 05 03 02 00 1d 89 8d 05 03 10 68 00 01 00 92 05 03 02 00 02 c8 45 05 03 2b c2 00 01 2d 96 05 03 02 02 49 89 12 05 03 2b c3 00 01 7c 56 05 03 02 00 e8 49 ca 05 03 2b 15 00 01 9d ae 05 03 02 00 2e c9 98 05 03 0f a1 00 01 d7 78 05 03 02 00 01 88 44 05 03 2b ab 00 01 fd 8a 05 03 02 00 16 c8 4a 05 03 2b ac 00 01 4c 4b 05 03 02 00 05 89 87 05 03 2b 45 00 01 9d bf 05 03 02 00 12 c9 89 05 03 2b dd 00 01 1c 50 05 03 02 02 41 88 d4".split(' ') function try_parse(buffer) { for (let offset = 0; offset + 4 < buffer.length; offset++) { const address = buffer[offset + 0] const fn = buffer[offset + 1] const dataOffset = offset + 2 for (let dataLength = 0; dataOffset + dataLength + 2 - 1 < buffer.length; dataLength++) { const databytes = buffer.slice(dataOffset, dataOffset + dataLength) const crcbytes = buffer.slice(dataOffset + dataLength, dataOffset + dataLength + 2) if (parseInt(crcbytes[1] + crcbytes[0], 16) === CRC([address, fn, ...databytes].map(x => parseInt(x, 16)))) { return { offset, bytes: [address, fn, ...databytes, ...crcbytes] } } } } return false } function try_parse2(frames) { for (let offset = 0; offset + 1 < frames.length; offset++) { const request = frames[offset + 0] const response = frames[offset + 1] if (request.type !== 'FRAME' || response.type !== 'FRAME') { continue } if (request.bytes[0] !== response.bytes[0] && request.bytes[1] !== response.bytes[1]) { continue } const functionCode = request.bytes[1] switch (functionCode) { case '03': // Read holding registers if (request.bytes.length !== 1 + 1 + 2 + 2 + 2) { continue } const numRegisters = parseInt(request.bytes[4] + request.bytes[5], 16) const startAddress = parseInt(request.bytes[2] + request.bytes[3], 16) if (response.bytes.length !== 1 + 1 + 1 + numRegisters * 2 + 2) { continue } if (parseInt(response.bytes[2], 16) !== response.bytes.length - (1 + 1 + 1 + 2)) { continue } const registerData = {} for (let i = 0; i < numRegisters; i++) { registerData[startAddress + i] = parseInt(response.bytes[3 + i] + response.bytes[3 + i + 1], 16) } const readings = getReadings(registerData) return { offset: offset, request: request.bytes, response: response.bytes, meta: { functionCode: functionCode, slaveAddress: request.bytes[0], registers: registerData, readings: readings } } } } return false } function getReadings(registerData) { const result = {} if ('11200' in registerData) { result.S1 = registerData['11200'] / 10 } if ('11201' in registerData) { result.S2 = registerData['11201'] / 10 } if ('11202' in registerData) { result.S3 = registerData['11202'] / 10 } if ('11203' in registerData) { result.S4 = registerData['11203'] / 10 } return result } // https://gist.github.com/TooTallNate/946745 var POLY = 0xA001; var SEED = 0xFFFF; function CRC(buffer) { var crc = SEED; for (var i=0; i>=1; if (carry) crc ^= POLY; } return crc; } let incomingBuffer = [] let parsedPackets = [] let framePairs = [] for (let i = 0; i < data.length; i++) { incomingBuffer.push(data[i]) const result = try_parse(incomingBuffer) if (result) { const discarded = incomingBuffer.slice(0, result.offset) if (discarded.length) { parsedPackets.push({ type: 'DISCARD', bytes: discarded }) } parsedPackets.push({ type: 'FRAME', bytes: result.bytes }) incomingBuffer = incomingBuffer.slice(result.offset + result.bytes.length) } const fResult = try_parse2(parsedPackets) if (fResult) { const discarded = parsedPackets.slice(0, fResult.offset) if (discarded.length) { discarded.forEach(x => framePairs.push(x)) } framePairs.push({ type: 'FRAME_PAIR', request: fResult.request.join(''), response: fResult.response.join(''), meta: fResult.meta }) parsedPackets = parsedPackets.slice(fResult.offset + 2) } } console.log(JSON.stringify(parsedPackets, null, 2)) console.log(JSON.stringify(incomingBuffer, null, 2)) console.log(JSON.stringify(framePairs, null, 2)) ```

And the output looks like this:

```json [] [] [ { "type": "FRAME_PAIR", "request": "050307da0001a501", "response": "050302067eca04", "meta": { "functionCode": "03", "slaveAddress": "05", "registers": { "2010": 1662 }, "readings": {} } }, { "type": "FRAME_PAIR", "request": "05032bc000018c56", "response": "050302001d898d", "meta": { "functionCode": "03", "slaveAddress": "05", "registers": { "11200": 29 }, "readings": { "S1": 2.9 } } }, { "type": "FRAME_PAIR", "request": "0503106800010092", "response": "0503020002c845", "meta": { "functionCode": "03", "slaveAddress": "05", "registers": { "4200": 2 }, "readings": {} } }, { "type": "FRAME_PAIR", "request": "05032bc200012d96", "response": "05030202498912", "meta": { "functionCode": "03", "slaveAddress": "05", "registers": { "11202": 585 }, "readings": { "S3": 58.5 } } }, { "type": "FRAME_PAIR", "request": "05032bc300017c56", "response": "05030200e849ca", "meta": { "functionCode": "03", "slaveAddress": "05", "registers": { "11203": 232 }, "readings": { "S4": 23.2 } } }, { "type": "FRAME_PAIR", "request": "05032b1500019dae", "response": "050302002ec998", "meta": { "functionCode": "03", "slaveAddress": "05", "registers": { "11029": 46 }, "readings": {} } }, { "type": "FRAME_PAIR", "request": "05030fa10001d778", "response": "05030200018844", "meta": { "functionCode": "03", "slaveAddress": "05", "registers": { "4001": 1 }, "readings": {} } }, { "type": "FRAME_PAIR", "request": "05032bab0001fd8a", "response": "0503020016c84a", "meta": { "functionCode": "03", "slaveAddress": "05", "registers": { "11179": 22 }, "readings": {} } }, { "type": "FRAME_PAIR", "request": "05032bac00014c4b", "response": "05030200058987", "meta": { "functionCode": "03", "slaveAddress": "05", "registers": { "11180": 5 }, "readings": {} } }, { "type": "FRAME_PAIR", "request": "05032b4500019dbf", "response": "0503020012c989", "meta": { "functionCode": "03", "slaveAddress": "05", "registers": { "11077": 18 }, "readings": {} } }, { "type": "FRAME_PAIR", "request": "05032bdd00011c50", "response": "050302024188d4", "meta": { "functionCode": "03", "slaveAddress": "05", "registers": { "11229": 577 }, "readings": {} } } ] ```
plysdyret commented 3 years ago

So I figured as well it is easier to just parse the entire thing ourselves, like you did with that code. I wrote a prototype in javascript, but might consider writing it to python if that is preferred. It doesn't have any serial input or database output added to it yet, but in javascript these are fairly trivial to add.

Cool. Javascript is fine. I never used it but it looks easy enough to run on Linux.

There are some corner cases where this approach would not work for other devices, but given that ECL/DLG communicate using one function only (it seems) where the frames are already almost as short as they can be, then it is hard to construct something that gets detected incorrectly.

Sounds good.

Mvh.

Torkil

plysdyret commented 3 years ago

Raw data for a couple of hours.

bytes.txt

Ingramz commented 3 years ago

Thanks for the raw data. I converted it to a excel spreadsheet, but noticed it only reads a couple of registers periodically. Not in exact same order, but all registers should be covered here:

2010 - returns 1662, I think that's the ECL110 model number
11200 - S1 readout
4200 - Desired control mode (AUTO/COMFORT/SETBACK/STANDBY)
11202 - S3 readout
11203 - S4 readout
11029 - Limit (return temp. limitation)
4001 - Pump status (on/off)
11179 - Desired room temperature (unsure)
11180 - Desired temperature in manual mode (unsure)
11077 - P1 heat T (heat demand)
11229 - S3 desired readout

10-01-2021-raw-data.xlsx

Now my concern is that does it ever write anything to the ECL110? We probably need more data to find that out, but I'm somewhat disappointed by the results from a couple of hours 😕.

I'll add the missing parts to the script and write some instructions how to get it running. If you want to prepare your pi, try to get node.js installed (version 10 or greater). Make sure both node -v and npm -v work afterward. Raspbian should have them installable via apt, but you can also install it by adding a separate repository from nodesource.

Ingramz commented 3 years ago

Might not be pretty, but it should work: https://gist.github.com/Ingramz/c2ad0d1b835564f31a102d27e9068e09

You can download both files as zip or save them individually. Place them in some directory and then from shell write: npm install and it should install everything required to run the program.

Configure the four variables at the top of the file modbus.js:

Then npm start

I'm not much of an expert on influxdb and this is probably far from using it optimally, but it should do things well enough that we both can use the data.

If you need any modifications to it to better suit your needs or are experiencing crashes, let me know.

plysdyret commented 3 years ago

Thanks for the raw data. I converted it to a excel spreadsheet, but noticed it only reads a couple of registers periodically. Not in exact same order, but all registers should be covered here:

2010 - returns 1662, I think that's the ECL110 model number
11200 - S1 readout
4200 - Desired control mode (AUTO/COMFORT/SETBACK/STANDBY)
11202 - S3 readout
11203 - S4 readout
11029 - Limit (return temp. limitation)
4001 - Pump status (on/off)
11179 - Desired room temperature (unsure)
11180 - Desired temperature in manual mode (unsure)
11077 - P1 heat T (heat demand)
11229 - S3 desired readout

Now my concern is that does it ever write anything to the ECL110? We probably need more data to find that out, but I'm somewhat disappointed by the results from a couple of hours 😕.

No other values seen so far since yesterday, so not terribly exciting =( I will leave the raw dump running until tonight so we have 24h of data.

Thanks for the code, I'll see if I can find time Tuesday evening to plug it instead of my own.

Something not terribly related but perhaps you have a suggestion? The temperature sensor for the ECL seems to be reporting a temperature that's 2-4 degrees C too high. Danfoss claims that can't be and the heating company says a +/- 2 degree difference is to be expected. The only option they suggest is to replace the thing but there is no guarantee another sensor will be more accurate.

For influx I could massage the data easily but it annoys me that the temperature in the Danfoss app is inaccurate, especially with temperatures around 0 (ice or no ice).

Ingramz commented 3 years ago

I didn't really understand the problem you are having with the temperature sensors. Maybe you can explain in greater detail how did you get that 2-4C difference.

The Pt1000 sensors connected to ECL110 are rather reliable, but not immune from laws of physics. If the cable for the sensor is too long or of bad quality, it will introduce an offset towards the positive side of the scale due to the resistance.

Let's say a thick ethernet cable wire of 24awg made of copper has a resistance approximately 0.084 ohms per meter. https://www.cirris.com/learning-center/calculators/133-wire-resistance-calculator-table

Pt1000 sensor at between 0-1 degrees seems to have 3.9 ohms difference, so to influence the reading about one degree, you would need 3.9 / 0.084 = 46,43 meter run of such wire. Since there are two wires which are connected along with sensor in series, the effective distance of the cable going to the sensor is half of that - so closer to 23 meters. https://www.sterlingsensors.co.uk/pt1000-resistance-table

Most installations have the critical temperature sensors close enough to the controller, like approximately 3 meters away at most, so somewhere close to 0.1C difference to the actual, which is within margin of error for most other temperature sensors anyway.

I am not saying that there's something wrong with your wiring, but that in most cases the sensors should produce a good-enough result. Of course rest of the system is as important to determining an accurate temperature reading, such as how the sensor is installed on the surface you are measuring (sensor that measures directly the water is much quicker to react than one that is clamped to the pipe with 2 layers of plastic insulation). But there are many things to try and measure (along with using common sense) if you want to find where potentially a fault is.

I know one specific case where the outdoor sensor was reporting way higher temperatures. Issue was caused by the low angle of sun in winter managing to directly shine on the outdoor sensor, which heated up the box in which the sensor was placed. Moving the sensor in a permanent shade fixed it, but probably introducing some holes in the sealed box could have worked as well.

plysdyret commented 3 years ago

Thanks a lot for taking the time to help me with this =)

I didn't really understand the problem you are having with the temperature sensors. Maybe you can explain in greater detail how did you get that 2-4C difference.

That is from comparing (eyeballing) the Link app temperature with various outdoors thermometers over the years and from comparing with weather data from the national meterological institute.

The Pt1000 sensors connected to ECL110 are rather reliable, but not immune from laws of physics. If the cable for the sensor is too long or of bad quality, it will introduce an offset towards the positive side of the scale due to the resistance.

Let's say a thick ethernet cable wire of 24awg made of copper has a resistance approximately 0.084 ohms per meter. https://www.cirris.com/learning-center/calculators/133-wire-resistance-calculator-table

Pt1000 sensor at between 0-1 degrees seems to have 3.9 ohms difference, so to influence the reading about one degree, you would need 3.9 / 0.084 = 46,43 meter run of such wire. Since there are two wires which are connected along with sensor in series, the effective distance of the cable going to the sensor is half of that - so closer to 23 meters. https://www.sterlingsensors.co.uk/pt1000-resistance-table

I looked at charts like that also and the +/- 2 degrees from the heating company sounds crazy inaccurate.

Most installations have the critical temperature sensors close enough to the controller, like approximately 3 meters away at most, so somewhere close to 0.1C difference to the actual, which is within margin of error for most other temperature sensors anyway.

Same here, 2m to the wall and something like 60cm foundation thickness.

I am not saying that there's something wrong with your wiring, but that in most cases the sensors should produce a good-enough result. Of course rest of the system is as important to determining an accurate temperature reading, such as how the sensor is installed on the surface you are measuring (sensor that measures directly the water is much quicker to react than one that is clamped to the pipe with 2 layers of plastic insulation). But there are many things to try and measure (along with using common sense) if you want to find where potentially a fault is.

I guess the main problem right now is I don't have something to compare with which I trust 100% The weather data is from 10 km away and my own thermometers are not industry grade. Would you happen to know of some USB device to which I could attach a PT1000 sensor?

I know one specific case where the outdoor sensor was reporting way higher temperatures. Issue was caused by the low angle of sun in winter managing to directly shine on the outdoor sensor, which heated up the box in which the sensor was placed. Moving the sensor in a permanent shade fixed it, but probably introducing some holes in the sealed box could have worked as well.

My sensor is on the north side of the house out of the sun so that shouldn't affect it outside of the early hours. I guess I could try putting a ventilated box over it.

Ingramz commented 3 years ago

I wouldn't advise placing a box around it if you don't need it. At least here most of the days right now are cloudy anyway, so direct direct sunlight is not an issue most days. Unless it is constantly sunny there.

Do you have a multimeter? You can measure the resistance at the end of the cable on ECL110 side while it is disconnected from the ECL110, look the temperature up from table and see if the result differs drastically from what ECL110 reports.

plysdyret commented 3 years ago

Do you have a multimeter? You can measure the resistance at the end of the cable on ECL110 side while it is disconnected from the ECL110, look the temperature up from table and see if the result differs drastically from what ECL110 reports.

Good idea, I'll get one next time I go to a discount shop.

Here's the continued datadump after ~24 hours:

bytes.txt

Ingramz commented 3 years ago

Still the same 11 registers being queried over and over again.

I'm also seeing that the pump is constantly on. Perhaps it doesn't bother changing anything while the pump is running.

I might also need to tweak the program a little. There are a couple of events where it constructs a frame from too few bytes, but despite this, it's mostly still usable.

> select * from data where data !~ /FRAME_PAIR/
name: data
time          data
----          ----
1610382108904 {"type":"FRAME","bytes":["05","03","2b","c3","00","01","7c","56"]}
1610382108905 {"type":"FRAME","bytes":["05","03","02","00","f1","88"]}
1610382108906 {"type":"DISCARD","bytes":["00"]}
1610382123334 {"type":"FRAME","bytes":["05","03","2b","c3","00","01","7c","56"]}
1610382123335 {"type":"FRAME","bytes":["05","03","02","01","30","48"]}
1610382123336 {"type":"DISCARD","bytes":["00"]}
1610382127046 {"type":"FRAME","bytes":["05","03","2b","c3","00","01","7c","56"]}
1610382127047 {"type":"FRAME","bytes":["05","03","02","00","f1","88"]}
1610382127048 {"type":"DISCARD","bytes":["00"]}
1610382128134 {"type":"FRAME","bytes":["05","03","2b","c3","00","01","7c","56"]}
1610382128135 {"type":"FRAME","bytes":["05","03","02","00","f1","88"]}
1610382128136 {"type":"DISCARD","bytes":["00"]}
1610382128822 {"type":"FRAME","bytes":["05","03","2b","c3","00","01","7c","56"]}
1610382128823 {"type":"FRAME","bytes":["05","03","02","00","f1","88"]}
1610382128824 {"type":"DISCARD","bytes":["00"]}
1610382131846 {"type":"FRAME","bytes":["05","03","2b","c3","00","01","7c","56"]}
1610382131847 {"type":"FRAME","bytes":["05","03","02","00","f1","88"]}
1610382131848 {"type":"DISCARD","bytes":["00"]}

Notice how 00 gets left out from the last frame, because it doesn't change the CRC of the byte sequence before. So I guess I need to parse the data in a slightly different sequence. 6 times in 24 hours is not much though.

Ingramz commented 3 years ago

Also may I ask, what are you using to dump the data? Minicom that sends data directly to a file?

plysdyret commented 3 years ago

Also may I ask, what are you using to dump the data? Minicom that sends data directly to a file?

Been running this since yesterday:

import serial
from datetime import datetime
import os

# Connect serial
ser = serial.Serial(
    port='/dev/ttyUSB0',
    baudrate=19200,
    parity=serial.PARITY_EVEN,
    stopbits=serial.STOPBITS_ONE,
    bytesize=serial.EIGHTBITS,
)
ser.flushInput()

bb=''
tmp=''
address=''
payload=''

f = open("/tmp/bytes.txt", "a")

while True:

    try:
        b = ser.read()
        h = b.hex()
        f.write(str(h) + ' ')
        f.flush()

        if h == '03':
            if tmp == '05 ':
#              print(h, end=' ', flush=True)

               r = bb.replace(" ", "")

               if len(r) == 16: # request
                   address = r[4:8]
               elif len(r) == 14: # reponse
                   payload = r[6:10]
                   temp_int = int(payload,16)
                   temp_float = float(temp_int) / 10
                   now = datetime.now()
                   current_time = now.strftime("%H:%M:%S")

                   print(current_time + ' ' + str(int(address, 16)) + ' ' +  str(temp_float))
                   os.system('mosquitto_pub -h openhab.localdomain -t ECL/' + str(int(address, 16)) + ' -m ' + str(temp_float))

               bb=tmp+h+ ' '
               tmp=''
            else:
              bb+=h+' '
        elif h == '05':
            tmp+=h + ' '
        else:
            bb+=tmp+h + ' '
            tmp=''

    except Exception as e:
        print(e)
        break
plysdyret commented 3 years ago

Still the same 11 registers being queried over and over again.

I'm also seeing that the pump is constantly on. Perhaps it doesn't bother changing anything while the pump is running.

I have a couple rooms that are pretty much constantly calling for heat. I'll try going to vacation mode.

Notice how 00 gets left out from the last frame, because it doesn't change the CRC of the byte sequence before. So I guess I need to parse the data in a slightly different sequence. 6 times in 24 hours is not much though.

Agree, that is good enough for the purpose.

plysdyret commented 3 years ago

So I tried setting the Link to vacation mode. No new fields as far as I can tell but the following values changed:

4200 - Desired control mode (AUTO/COMFORT/SETBACK/STANDBY) -> changed from 2 to 3. On the unit it changed from COMFORT to REDUCER 11179 - Desired room temperature (unsure) -> dropped to 15 from 22 so this value is probably the highest temperature requested by a room. I have 22 in a few rooms and vacation mode = 15. 11229 - S3 desired readout -> dropped to 30 from ~54

Pump stayed on.

bytes.txt

Same data as before with new bits at the end.

Ingramz commented 3 years ago

It wrote to these registers using function 6:

11179 -> 15

1610386677617 {"type":"FRAME","bytes":["05","06","2b","ab","00","0f","b0","4e"]}
1610386677618 {"type":"FRAME","bytes":["05","06","2b","ab","00","0f","b0","4e"]}

4200 -> 3
1610386677681 {"type":"FRAME","bytes":["05","06","10","68","00","03","4d","53"]}
1610386677682 {"type":"FRAME","bytes":["05","06","10","68","00","03","4d","53"]}

11179 -> 22
1610386677921 {"type":"FRAME","bytes":["05","06","2b","ab","00","16","71","84"]}
1610386677922 {"type":"FRAME","bytes":["05","06","2b","ab","00","16","71","84"]}

4200 -> 2
1610386677985 {"type":"FRAME","bytes":["05","06","10","68","00","02","8c","93"]}
1610386677986 {"type":"FRAME","bytes":["05","06","10","68","00","02","8c","93"]}

I'll have to add support for detecting pairs of these as well.

plysdyret commented 3 years ago

I'll have to add support for detecting pairs of these as well.

Here's a continued data file. Temperatures went below 0 and that caused a rollover on the S1 value.

bytes.txt

Ingramz commented 3 years ago

Hah, indeed I think this wasn't handled. I'll look into it after work. Thanks again for the data, makes it easier to test.

plysdyret commented 3 years ago

A long shot but have you looked at interpreting the zwave traffic between the link and the termostats pr chance? I got an SDR sniffer up and running and found a project to convert to zwave frames but have been unable to make further progress. It should be possible to extract temperature reponses from thermostats to the controller.

frames.log

Ingramz commented 3 years ago

No, because I only had one device, but interpreting requires typically two devices communicating with eachother. It makes no sense if there's only one that sits quiet.

You can give it a try however. If you know what should be contained inside the frames, you can start mapping out the locations of the data you are certain about.

For example this person also tried sniffing frames almost 3 years ago: https://community.openhab.org/t/danfoss-living-connect-new-proprietary-z-wave-binding/34263/16

I can't see where the 19.97 is in 0x00021028120C054720202021030020102011030F031703280318032F0330057F0800049D013700004901C06599DA

however, 24.22 can be found inside the other frame, which is 2422 in hex: 0976 0x0002100A0D0D0001000409761C9A655E

I have no idea what any of the other bytes correspond to.

So you can use similar logic to do educated guesses regarding what you are looking for. The easiest ones usually are ones that you can change on demand, because then only that one specific part should change.

plysdyret commented 3 years ago

I actually commented at the end of the same thread 🙂

The same guy provided another example further down the thread so I've started mapping by setting room temperatures to something unique.

Thanks

On Sat, 16 Jan 2021, 14:39 Indrek Ardel, notifications@github.com wrote:

No, because I only had one device, but interpreting requires typically two devices communicating with eachother. It makes no sense if there's only one that sits quiet.

You can give it a try however. If you know what should be contained inside the frames, you can start mapping out the locations of the data you are certain about.

For example this person also tried sniffing frames almost 3 years ago:

https://community.openhab.org/t/danfoss-living-connect-new-proprietary-z-wave-binding/34263/16

I can't see where the 19.97 is in 0x00021028120C054720202021030020102011030F031703280318032F0330057F0800049D013700004901C06599DA

however, 24.22 can be found inside the other frame, which is 2422 in hex: 0976 0x0002100A0D0D0001000409761C9A655E

I have no idea what any of the other bytes correspond to.

So you can use similar logic to do educated guesses regarding what you are looking for. The easiest ones usually are ones that you can change on demand, because then only that one specific part should change.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/Ingramz/ecl110/issues/2#issuecomment-761564814, or unsubscribe https://github.com/notifications/unsubscribe-auth/AMBD23NLVNYTTZ2GBXEWIV3S2GJH3ANCNFSM4EGJLKVQ .