Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Empty readings from meter #4

Open
LucaTNT opened this issue Mar 10, 2018 · 40 comments
Open

Empty readings from meter #4

LucaTNT opened this issue Mar 10, 2018 · 40 comments

Comments

@LucaTNT
Copy link

LucaTNT commented Mar 10, 2018

Hi,
I'm not sure if this is the right place to ask this question, but I'll give it a shot anyway.

I have a SolarEdge SE6000 inverter + a WattNode WND-3Y-400-MB meter.
All the data coming from the inverter is correctly shown, while I can only see Model, Option, Firmware version and Serial Number of the meter.
All the other data coming from the meter is 0, it appears that the ModBUS data is empty (see below).

Do you think this could be due to a misconfiguration on the inverter? Any hint would be appreciated, thank you very much.

$VAR1 = {
          'M_AC_Power_C' => -32768,
          'M_AC_Voltage_LL' => -32768,
          'M_Imported' => 0,
          'M_AC_VA_C' => -32768,
          'M_AC_PF_A' => -32768,
          'M_Import_VARh_Q2B' => 0,
          'M_AC_Voltage_AB' => -32768,
          'M_AC_Freq' => -32768,
          'M_AC_VAR_SF' => -32768,
          'M_Exported_VA' => 0,
          'M_Imported_VA_C' => 0,
          'M_AC_Power_SF' => -32768,
          'M_Exported_VA_B' => 0,
          'M_AC_Power' => -32768,
          'M_Import_VARh_Q2' => 0,
          'M_Import_VARh_Q4' => 0,
          'M_Imported_VA' => 0,
          'M_Energy_W_SF' => -32768,
          'M_Events' => 131072,
          'M_Import_VARh_Q1C' => 0,
          'M_Energy_VAR_SF' => -32768,
          'M_AC_Power_B' => -32768,
          'M_Exported' => 0,
          'M_Exported_VA_A' => 0,
          'M_Import_VARh_Q1' => 0,
          'M_Exported_B' => 0,
          'M_AC_Current_A' => -32768,
          'M_AC_Voltage_AN' => -32768,
          'M_AC_Current_B' => -32768,
          'M_AC_Voltage_BC' => -32768,
          'M_AC_PF_C' => -32768,
          'M_AC_Voltage_CA' => -32768,
          'M_AC_Current_SF' => -32768,
          'M_AC_VA_B' => -32768,
          'M_Import_VARh_Q3A' => 0,
          'M_Import_VARh_Q3' => 0,
          'M_AC_Power_A' => -32768,
          'M_AC_Voltage_BN' => -32768,
          'M_Imported_B' => 0,
          'M_AC_Voltage_LN' => -32768,
          'M_Import_VARh_Q4A' => 0,
          'M_Imported_A' => 0,
          'M_Imported_VA_B' => 0,
          'M_AC_Current_C' => -32768,
          'C_SunSpec_DID' => 201,
          'M_AC_VAR_B' => -32768,
          'M_Import_VARh_Q4B' => 0,
          'M_AC_Current' => -32768,
          'M_Imported_VA_A' => 0,
          'M_AC_VA' => -32768,
          'M_Import_VARh_Q1A' => 0,
          'M_Exported_VA_C' => 0,
          'M_AC_PF' => -32768,
          'C_SunSpec_Length' => 105,
          'M_Exported_C' => 0,
          'M_AC_Freq_SF' => -32768,
          'M_Exported_A' => 0,
          'M_Import_VARh_Q2A' => 0,
          'M_AC_VAR_A' => -32768,
          'M_AC_VA_A' => -32768,
          'M_Imported_C' => 0,
          'M_Energy_VA_SF' => -32768,
          'M_Import_VARh_Q4C' => 0,
          'M_AC_Voltage_CN' => -32768,
          'M_Import_VARh_Q3B' => 0,
          'M_AC_PF_SF' => -32768,
          'M_AC_VA_SF' => -32768,
          'M_Import_VARh_Q3C' => 0,
          'M_Import_VARh_Q1B' => 0,
          'M_AC_VAR_C' => -32768,
          'M_AC_PF_B' => -32768,
          'M_AC_VAR' => -32768,
          'M_Import_VARh_Q2C' => 0,
          'M_AC_Voltage_SF' => -32768
        };
@mcmellow
Copy link

Hi,
Did you configured the Solaredge LAN according to the document.
https://www.solaredge.com/sites/default/files/sunspec-implementation-technical-note.pdf
That is how I did it. After that it worked, with some minor details. But they are solved. Hope this might be of help.

@LucaTNT
Copy link
Author

LucaTNT commented Mar 10, 2018

That's the procedure I followed in order to enable ModBus TCP, it's really weird that I'm not getting only part of the data 🤔
I sent an email to SolarEdge's support, if they are able to provide any help I'll be sure to post it here.

Thanks for your reply, though!

@mcmellow
Copy link

What firmware level is the SolarEdge? I had some time out troubles, after a call with SolarEdgw support they updated my firmware and the troubles were solved.

@LucaTNT
Copy link
Author

LucaTNT commented Mar 10, 2018

I’m on 3.2173, which according to the readme of this repo appears to be problematic.
I hope they will provide an update for my inverter as well.

@lloydwatkin
Copy link
Contributor

@LucaTNT I was just about to raise an issue for the same thing, it looks like we have the same type of export meter. Using latest version of this library.

lloyd@xps13:~/Dropbox/code/sunspec-monitor$ ./sunspec-status -v 192.168.1.6 
INVERTER:
             Model: SolarEdge  SE3680
  Firmware version: 3.2016
     Serial Number: ...

            Status: ON (MPPT)

 Power Output (AC):          884 W
  Power Input (DC):          898 W
        Efficiency:        98.50 %
  Total Production:     2711.770 kWh
      Voltage (AC):       238.90 V (49.96 Hz)
      Current (AC):         3.75 A
      Voltage (DC):       380.40 V
      Current (DC):         2.36 A
       Temperature:        33.05 C (heatsink)

METER (#1):
             Model: WattNode WNC-3Y-400-MB
            Option: Export+Import
  Firmware version: 24
     Serial Number: ...

   Exported Energy:        0.000 kWh
   Imported Energy:        0.000 kWh
        Real Power:            0 W
    Apparent Power:            0 VA
      Power Factor:        -0.00
      Voltage (AC):         0.00 V (0.00 Hz)
      Current (AC):         0.00 A

Dropped debug dump here, https://gist.github.com/lloydwatkin/6f99110ba1bbd9828bfa21da1e4128e9

@lloydwatkin
Copy link
Contributor

I've read the meter data using a different library to confirm that this library isn't the issue, and can confirm that is the case (@LucaTNT if you hear anything back about this issue please do let me know - and I'll do the same).

lloyd@xps13:~/Dropbox/code/sunspec-monitor$ modbus read 192.168.1.6  400190 64
400190        105
400191      32768
400192      32768
400193      32768
400194      32768
400195      32768
400196      32768
400197      32768
400198      32768
400199      32768
400200      32768
400201      32768
400202      32768
400203      32768
400204      32768
400205      32768
400206      32768
400207      32768
400208      32768
400209      32768
400210      32768
400211      32768
400212      32768
400213      32768
400214      32768
400215      32768
400216      32768
400217      32768
400218      32768
400219      32768
400220      32768
400221      32768
400222      32768
400223      32768
400224      32768
400225      32768
400226      32768
400227          0
400228          0
400229          0
400230          0
400231          0
400232          0
400233          0
400234          0
400235          0
400236          0
400237          0
400238          0
400239          0
400240          0
400241          0
400242          0
400243      32768
400244          0
400245          0
400246          0
400247          0
400248          0
400249          0
400250          0
400251          0
400252          0
400253          0

@LucaTNT
Copy link
Author

LucaTNT commented Apr 16, 2018

@lloydwatkin I contacted SolarEdge and they remotely updated my inverter, but nothing changed.
I tried to "speak ModBus TCP" directly to the inverter as you did, but I got the same result.
SolarEdge suggested moving my meter to be number 1, while now it is number 2. I have not done that yet, both because I haven't had the time and because I'm afraid to tinker with this kind of expensive toys which I don't fully understand. Maybe I should contact the installer who setup the whole solar system.

@lloydwatkin
Copy link
Contributor

I've emailed the installer today who have past the request to solaredge so will update with feedback.

My meter is number 1 so its not your meter number.

As I paid extra to have the modbus installed (for the very purpose of monitoring inside my own network) I consider this a product defect so will insist its fixed by the installer 👍

@lloydwatkin
Copy link
Contributor

But also, I can see the data in the solaredge portal (because that's how I get import/export/production data) so its obviously has the correct information somewhere.

@LucaTNT
Copy link
Author

LucaTNT commented Apr 17, 2018

@lloydwatkin great, thanks for keeping us posted, Lloyd!
Let me know if I can be of any help 💪

@lloydwatkin
Copy link
Contributor

lloydwatkin commented Apr 17, 2018 via email

@lloydwatkin
Copy link
Contributor

@LucaTNT does your setup also export data to solaredge portal? Mine does. Apparently the wattnode device can either be set up to export data in a fashion suitable for the portal or sunspec, but not both. Meaning you'd need to purchase and fit an additional meter.

I'm chasing this up with my installer since I specifically asked about this at the time and was told I'd be able to do both.

I may experiment and switch my meter setup to see if I can get the data out, but we also like having the portal so I don't want to log data internally exclusively (and Solaredge don't have an API for pushing data).

@LucaTNT
Copy link
Author

LucaTNT commented Apr 27, 2018 via email

@lloydwatkin
Copy link
Contributor

lloydwatkin commented Apr 27, 2018 via email

@deligoz
Copy link

deligoz commented Jun 6, 2018

I am having the same problem, is there any solution to this?

sunspec-monitor$ ./sunspec-status -v 10.1.0.101
INVERTER:
Model: SolarEdge SE7600
Firmware version: 3.2305
Serial Number: xxxxxxxx

        Status: ON (MPPT)

Power Output (AC): 672 W
Power Input (DC): 682 W
Efficiency: 98.50 %
Total Production: 1884.949 kWh
Voltage (AC): 245.40 V (59.99 Hz)
Current (AC): 2.80 A
Voltage (DC): 425.70 V
Current (DC): 1.60 A
Temperature: 32.39 C (heatsink)

METER (#1):
Model: WattNode WNC-3D-240-MB
Option: Export+Import
Firmware version: 24
Serial Number: xxxxxxx

Exported Energy: 0.000 kWh
Imported Energy: 0.000 kWh
Real Power: 0 W
Apparent Power: 0 VA
Power Factor: -0.00
Voltage (AC): 0.00 V (0.00 Hz)
Current (AC): 0.00 A

@lloydwatkin
Copy link
Contributor

@mcmellow are you able to post your output? Specifically I'm interested in what meter you have installed so I can look into that.

@mcmellow
Copy link

mcmellow commented Oct 9, 2018

@mcmellow are you able to post your output? Specifically I'm interested in what meter you have installed so I can look into that.

There is no meter installed in my situation. It is just the inverter

@lloydwatkin
Copy link
Contributor

I'm still in discussions on this. Solar installer is kicking the can down the road and has set me up for a tech call with SolarEdge direct. Looks like you might need a WattNode RWND-3D-240-MB to get the data (based on @tjko's meter in the README).

@mcmellow
Copy link

mcmellow commented Oct 9, 2018

This the output of my inveter sunspec-status -v -m 0 192.168.xxx.xxx

INVERTER:
Model: SolarEdge SE3500
Firmware version: 3.2186
Serial Number: 7312CCFE

        Status: ON (MPPT)

Power Output (AC): 278 W
Power Input (DC): 282 W
Efficiency: 98.50 %
Total Production: 3550.742 kWh
Voltage (AC): 235.30 V (49.99 Hz)
Current (AC): 1.29 A
Voltage (DC): 375.50 V
Current (DC): 0.75 A
Temperature: 44.58 C (heatsink)

I hope this helps

@wraithrmm
Copy link

Wow, I am having this exact same issue.

I have the same WattNode Meter as you and the exact same issue.

I have had SolarEdge update the firmware, etc. and no change. In fact, after they did that a week ago, they then started to ignore my support ticket, which is very annoying.

I'm gonna watch this thread very closely as I to was pretty clear with my installers about what I wanted to do and why, so very annoyed that I've been unable to get the data out for Home Automation stuff.

Just in case it's of interest, here's my output, but I've used all the same scripts as you guys already to confirm through many sources that the data simply seems not to be coming out of the Mete correctly:

`INVERTER:
Model: SolarEdge SE6000
Firmware version: 3.2251
Serial Number: 73163C5B

Power Output (AC): 758 W
Power Input (DC): 769 W
Efficiency: 98.49 %
Total Production: 617.070 kWh
Voltage (AC): 245.30 V (49.98 Hz)
Current (AC): 3.12 A
Voltage (DC): 389.90 V
Current (DC): 1.97 A
Temperature: 28.60 C (heatsink)

METER (#1):
Model: WattNode WND-3Y-400-MB
Option: Export+Import
Firmware version: 25
Serial Number: 4136367

Exported Energy: 0.000 kWh
Imported Energy: 0.000 kWh
Real Power: 0 W
Apparent Power: 0 VA
Power Factor: -0.00
Voltage (AC): 0.00 V (0.00 Hz)
Current (AC): 0.00 A`

@lloydwatkin
Copy link
Contributor

lloydwatkin commented Oct 16, 2018 via email

@wraithrmm
Copy link

UPDATE TO KEEP THIS THREAD LIVE:

The technician at SolarEdge has informed me he cannot deal with the issue and he needs to escalate the problem.

After this I have yet to be contacted with anything.

Currently I'm also getting my installer to apply some pressure from the 'Installer' path and if this fails, I'm going to throw all my toys of of my pram at my installer and insist they remove the defective equipment in place of something that works.

They have admitted several times that they don't understand the feature of the Inverter, which is not acceptable really, as they are the people making the promises before you hand over your money.

@lloydwatkin
Copy link
Contributor

@wraithrmm I had a call from solaredge last week who said they'd follow up later the same day. I've heard nothing back. My situation is essentially the same as yours.

@wraithrmm
Copy link

So.... I had a reply from the Solar Edge people at long last. I read as follows:

"Dear Wraith,
The meter is set to measure import+export. In order to extract TCP the data from it you will either need a data logger or another meter to be configured as Sunspec Protocol, not revenue meter. You can also try to extract the data with the API.
Best Regards
Yordan
SolarEdge Support Team - UK"

The upshot then is that we cannot both send the data to the Solar Edge Portal AND collect said information locally over TCP with only one meter. Which is totally ridiculous, but... a fact none the less.

I've told my installer this and called them out on their general lack of understanding of the products they are selling. I don't expect anything to come of it to be honest but just in case anyone else reads this and is in the market for solar panels, they are called Naked Solar and while very friendly until you pay them their money, their installers are carless, lack attention to detail and clearly make promises they cannot fulfil as they don't understand the hardware they are selling.

Anyhow, all that is beside the point.

I'm not sure what I can do to resolve this atm, without purchasing a second meter, which rather defeats the point of USING it to SAVE ME MONEY in the first place. :-(

I've tried to packet-sniff on a hub, etc. to get the data, but have yet to find anything at all, let alone be able to decode it.

As such, I think this TCP feature is a no-go for me unless I disconnect from the Solar Edge portal completely, which I don't really want to do either.

If anyone finds anything more helpful then I did, please do let us know :-)

@lloydwatkin
Copy link
Contributor

@wraithrmm yes this is the information I had already - just trying to get my installer to realise it (and I've told them this information several times). You can have two meters connected at the same time, which is one bonus (I found the meter on ebay but it was around $240).

Another solution would be to update the API endpoint on the inverter to send data to an internal server and then proxy this through to solaredge. The issue here is that, I assume, you'd lose the ability to receive firmware updates (although I guess you could revert to get updates). Although I seem to remember there being an issue with solaredge adding some certificate to the inverter so you'd need to decode that.

I'm going to continue to push my installer to put in the correct gear. In the meantime I'm planning on getting an openelecmonitor (the cheaper 4 CT box) which pushes data over MQTT and then combine this with calls to the inverter. https://openenergymonitor.com/emontx-v3-electricity-monitoring-transmitter/

@tjko
Copy link
Owner

tjko commented Oct 26, 2018

My setup is two RWND-3D-240-MB meters, 1st meter is configured "Production" (this is inside the DC disconnect box that was included with the SE11400 Inverter), 2nd meter is configured "Export+Import" and has current clamps installed at grid connection point. I have no issues reading data from both meters locally.

After 2nd meter was added, this enabled seeing "Consumption" and "Self Consumption" in the SolarEdge portal automatically.

One thing to try might be to put "Export+Import" meter as 2nd meter, if one doesnt have separate production meter. As it could be that inverter firmware expects first meter to be always a production meter...

@lloydwatkin
Copy link
Contributor

I have success! I've been talking to the following member of staff at solaredge [email protected]. His response today was:

I think I discovered the source of the issue.
Your modbus meter was configured as “Meter 2” out of a list with 3 meter options in the inverter menu.
Although a revenue meter can be configured and work to any of those, sunspec protocol will read Meter 1 and if is blank will not skip to the next one.
Please try again to get meter readings and let me know if it works. We appreciate your patience and feedback on this matter.

....and then...
./sunspec-status -v 192.168.1.189
...resulted in...

INVERTER:
Model: SolarEdge SE3680
Firmware version: 3.2251
Serial Number: 73116AD2

        Status: SLEEPING

Power Output (AC): 0 W
Power Input (DC): 0 W
Efficiency: 0.00 %
Total Production: 5188.547 kWh
Voltage (AC): 246.20 V (50.01 Hz)
Current (AC): 0.00 A
Voltage (DC): 0.00 V
Current (DC): 0.00 A
Temperature: 23.63 C (heatsink)

METER (#1):
Model: WattNode WNC-3Y-400-MB
Option: Export+Import
Firmware version: 24
Serial Number: 4054004

Exported Energy: 3533.793 kWh
Imported Energy: 3868.243 kWh
Real Power: -314 W
Apparent Power: 315 VA
Power Factor: 1.00
Voltage (AC): 246.56 V (50.11 Hz)
Current (AC): 1.20 A

Woo hoo! I hope this is able to help others.

@lloydwatkin
Copy link
Contributor

I can also confirm that data is still being shared with the solaredge portal too.

lloydwatkin added a commit to lloydwatkin/sunspec-monitor that referenced this issue Nov 15, 2018
@lloydwatkin
Copy link
Contributor

Another update for solaredge this morning:

I’m glad this solution has worked.
We are looking to implement this to our next software upgrade, so it will skip meter 1 if is not configured and proceed to the next one on the list.

@wraithrmm
Copy link

wraithrmm commented Nov 16, 2018

OMG YES!

This was it. I placed a support request with SolarEdge and they moved it to meter 1 within 2 hours and then everything just started working!

I cannot BELIEVE how much effort that took and how few people actually know how the inverter is supposed to work in their own company, but am SO glad it's now working (before I spent £100 on some alternate solution).

Thank you so much for sharing this solution.

@LucaTNT
Copy link
Author

LucaTNT commented Nov 16, 2018

So I need to ask SolarEdge to update my inverter and it will automagically work?

Thanks for your help guys!

@lloydwatkin
Copy link
Contributor

@LucaTNT I think so.

@wraithrmm
Copy link

So I’ve managed to get it all hooked up to Hass.io now using its native MODBUS support and the up to the second data is not driving my Immerssion heater :-)

This is my Hass.io copy of the Solar Edge reporting app, that now works even if there is not internet access to the home :-)

https://www.dropbox.com/s/6u9yd8qbjo3dj9k/2018-11-16%2020.09.02.png?dl=0

@timberrrr
Copy link

So I’ve managed to get it all hooked up to Hass.io now using its native MODBUS support and the up to the second data is not driving my Immerssion heater :-)

This is my Hass.io copy of the Solar Edge reporting app, that now works even if there is not internet access to the home :-)

https://www.dropbox.com/s/6u9yd8qbjo3dj9k/2018-11-16%2020.09.02.png?dl=0

Hi, I'm really interested to know your hass.io configuration, the dropbox link is dead. I have got the modbus component configured but can't read the correct sensor info into from the registers.

@wraithrmm
Copy link

wraithrmm commented Jan 25, 2019

I used this modbus config to get the values originally, which kinda worked a bit:

  - platform: modbus
    registers:
    - name: SolarGenerationRaw
      unit_of_measurement: kW
      slave: 1
      register: 40083
      data_type: uint
      scan_interval: 2
    - name: SolarGenerationSF
      register: 40084
      slave: 1
      count: 1
      data_type: int
      scan_interval: 2
    - name: EnergyExportRaw
      slave: 1
      register: 40206
      count: 1
      data_type: int
      scan_interval: 2
    - name: EnergyExportSF
      slave: 1
      register: 40210
      count: 1
      data_type: int
      scan_interval: 2

But I ran into all sorts of issues with the modbus system as it queries for each of these values in separate calls, which often resulted in things being out of sync and created allot of lag due to multiple calls to collect the ENTIRE modbus block, but then only use one register from it :-/

One of the biggest issues was it not having the correct scale factors as they would get out of sync occasionally, causing insane spikes in the reporting.

As such, I ended up creating my very own component for use with this, which solves all of these problems and now I have fully replicated the Solar Edge UI in Hasss.io using the new Lovelace image card and the reporting fugures from the modbus. Here's the code, you should put it into a file called "/config/custom_components/modbus1.py"

"""
Support for Modbus.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/modbus/
"""
import logging
import threading

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
    EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
    CONF_HOST, CONF_METHOD, CONF_PORT, CONF_TYPE, CONF_TIMEOUT, ATTR_STATE)

DOMAIN = 'modbus1'

REQUIREMENTS = ['pymodbus==2.1.0']

# Type of network
CONF_BAUDRATE = 'baudrate'
CONF_BYTESIZE = 'bytesize'
CONF_STOPBITS = 'stopbits'
CONF_PARITY = 'parity'

SERIAL_SCHEMA = {
    vol.Required(CONF_BAUDRATE): cv.positive_int,
    vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8),
    vol.Required(CONF_METHOD): vol.Any('rtu', 'ascii'),
    vol.Required(CONF_PORT): cv.string,
    vol.Required(CONF_PARITY): vol.Any('E', 'O', 'N'),
    vol.Required(CONF_STOPBITS): vol.Any(1, 2),
    vol.Required(CONF_TYPE): 'serial',
    vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
}

ETHERNET_SCHEMA = {
    vol.Required(CONF_HOST): cv.string,
    vol.Required(CONF_PORT): cv.positive_int,
    vol.Required(CONF_TYPE): vol.Any('tcp', 'udp', 'rtuovertcp'),
    vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
}


CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)
}, extra=vol.ALLOW_EXTRA)


_LOGGER = logging.getLogger(__name__)

SERVICE_WRITE_REGISTER = 'write_register'
SERVICE_WRITE_COIL = 'write_coil'

ATTR_ADDRESS = 'address'
ATTR_UNIT = 'unit'
ATTR_VALUE = 'value'

SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema({
    vol.Required(ATTR_UNIT): cv.positive_int,
    vol.Required(ATTR_ADDRESS): cv.positive_int,
    vol.Required(ATTR_VALUE): vol.All(cv.ensure_list, [cv.positive_int])
})

SERVICE_WRITE_COIL_SCHEMA = vol.Schema({
    vol.Required(ATTR_UNIT): cv.positive_int,
    vol.Required(ATTR_ADDRESS): cv.positive_int,
    vol.Required(ATTR_STATE): cv.boolean
})

HUB = None


def setup(hass, config):
    """Set up Modbus component."""
    # Modbus connection type
    client_type = config[DOMAIN][CONF_TYPE]

    # Connect to Modbus network
    # pylint: disable=import-error

    if client_type == 'serial':
        from pymodbus.client.sync import ModbusSerialClient as ModbusClient
        client = ModbusClient(method=config[DOMAIN][CONF_METHOD],
                              port=config[DOMAIN][CONF_PORT],
                              baudrate=config[DOMAIN][CONF_BAUDRATE],
                              stopbits=config[DOMAIN][CONF_STOPBITS],
                              bytesize=config[DOMAIN][CONF_BYTESIZE],
                              parity=config[DOMAIN][CONF_PARITY],
                              timeout=config[DOMAIN][CONF_TIMEOUT])
    elif client_type == 'rtuovertcp':
        from pymodbus.client.sync import ModbusTcpClient as ModbusClient
        from pymodbus.transaction import ModbusRtuFramer as ModbusFramer
        client = ModbusClient(host=config[DOMAIN][CONF_HOST],
                              port=config[DOMAIN][CONF_PORT],
                              framer=ModbusFramer,
                              timeout=config[DOMAIN][CONF_TIMEOUT])
    elif client_type == 'tcp':
        from pymodbus.client.sync import ModbusTcpClient as ModbusClient
        client = ModbusClient(host=config[DOMAIN][CONF_HOST],
                              port=config[DOMAIN][CONF_PORT],
                              timeout=config[DOMAIN][CONF_TIMEOUT])
    elif client_type == 'udp':
        from pymodbus.client.sync import ModbusUdpClient as ModbusClient
        client = ModbusClient(host=config[DOMAIN][CONF_HOST],
                              port=config[DOMAIN][CONF_PORT],
                              timeout=config[DOMAIN][CONF_TIMEOUT])
    else:
        return False

    global HUB
    HUB = ModbusHub(client)

    def stop_modbus(event):
        """Stop Modbus service."""
        HUB.close()

    def start_modbus(event):
        """Start Modbus service."""
        HUB.connect()
        hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)

        # Register services for modbus
        hass.services.register(
            DOMAIN, SERVICE_WRITE_REGISTER, write_register,
            schema=SERVICE_WRITE_REGISTER_SCHEMA)
        hass.services.register(
            DOMAIN, SERVICE_WRITE_COIL, write_coil,
            schema=SERVICE_WRITE_COIL_SCHEMA)

    def write_register(service):
        """Write modbus registers."""
        unit = int(float(service.data.get(ATTR_UNIT)))
        address = int(float(service.data.get(ATTR_ADDRESS)))
        value = service.data.get(ATTR_VALUE)
        if isinstance(value, list):
            HUB.write_registers(
                unit,
                address,
                [int(float(i)) for i in value])
        else:
            HUB.write_register(
                unit,
                address,
                int(float(value)))

    def write_coil(service):
        """Write modbus coil."""
        unit = service.data.get(ATTR_UNIT)
        address = service.data.get(ATTR_ADDRESS)
        state = service.data.get(ATTR_STATE)
        HUB.write_coil(unit, address, state)

    hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)

    return True


class ModbusHub(object):
    """Thread safe wrapper class for pymodbus."""

    def __init__(self, modbus_client):
        """Initialize the modbus hub."""
        self._client = modbus_client
        self._lock = threading.Lock()

    def close(self):
        """Disconnect client."""
        with self._lock:
            self._client.close()

    def connect(self):
        """Connect client."""
        with self._lock:
            self._client.connect()

    def read_coils(self, unit, address, count):
        """Read coils."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            return self._client.read_coils(
                address,
                count,
                **kwargs)

    def read_input_registers(self, unit, address, count):
        """Read input registers."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            return self._client.read_input_registers(
                address,
                count,
                **kwargs)

    def read_holding_registers(self, unit, address, count):
        """Read holding registers."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            return self._client.read_holding_registers(
                address,
                count,
                **kwargs)

    def write_coil(self, unit, address, value):
        """Write coil."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            self._client.write_coil(
                address,
                value,
                **kwargs)

    def write_register(self, unit, address, value):
        """Write register."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            self._client.write_register(
                address,
                value,
                **kwargs)

    def write_registers(self, unit, address, values):
        """Write registers."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            self._client.write_registers(
                address,
                values,
                **kwargs)

Then another file here "custom_components/sensor/modbus1.py"

"""
Support for Modbus Register sensors.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.modbus/
"""
import logging
import struct
import math
import datetime

import voluptuous as vol

import custom_components.modbus1 as modbus
from homeassistant.const import (
    CONF_NAME, CONF_OFFSET, CONF_UNIT_OF_MEASUREMENT, CONF_SLAVE,
    CONF_STRUCTURE)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers import config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA

_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = ['modbus1']

CONF_COUNT = 'count'
CONF_REVERSE_ORDER = 'reverse_order'
CONF_PRECISION = 'precision'
CONF_REGISTER = 'register'
CONF_REGISTERS = 'registers'
CONF_SCALE = 'scale'
CONF_DATA_TYPE = 'data_type'
CONF_REGISTER_TYPE = 'register_type'

REGISTER_TYPE_HOLDING = 'holding'
REGISTER_TYPE_INPUT = 'input'

DATA_TYPE_INT = 'int'
DATA_TYPE_UINT = 'uint'
DATA_TYPE_FLOAT = 'float'
DATA_TYPE_CUSTOM = 'custom'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_REGISTERS): [{
        vol.Required(CONF_NAME): cv.string,
        vol.Required(CONF_REGISTER): cv.positive_int,
        vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING):
            vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]),
        vol.Optional(CONF_COUNT, default=1): cv.positive_int,
        vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean,
        vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
        vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
        vol.Optional(CONF_SCALE, default=1): vol.Coerce(float),
        vol.Optional(CONF_SLAVE): cv.positive_int,
        vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT):
            vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT,
                    DATA_TYPE_CUSTOM]),
        vol.Optional(CONF_STRUCTURE): cv.string,
        vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string
    }]
})


def setup_platform(hass, config, add_devices, discovery_info=None):
    """Set up the Modbus sensors."""
    sensors = []
    data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'}}
    data_types[DATA_TYPE_UINT] = {1: 'H', 2: 'I', 4: 'Q'}
    data_types[DATA_TYPE_FLOAT] = {1: 'e', 2: 'f', 4: 'd'}

    for register in config.get(CONF_REGISTERS):
        structure = '>i'
        if register.get(CONF_DATA_TYPE) != DATA_TYPE_CUSTOM:
            try:
                structure = '>{}'.format(data_types[
                    register.get(CONF_DATA_TYPE)][register.get(CONF_COUNT)])
            except KeyError:
                _LOGGER.error("Unable to detect data type for %s sensor, "
                              "try a custom type.", register.get(CONF_NAME))
                continue
        else:
            structure = register.get(CONF_STRUCTURE)

        try:
            size = struct.calcsize(structure)
        except struct.error as err:
            _LOGGER.error(
                "Error in sensor %s structure: %s",
                register.get(CONF_NAME), err)
            continue

        if register.get(CONF_COUNT) * 2 != size:
            _LOGGER.error(
                "Structure size (%d bytes) mismatch registers count "
                "(%d words)", size, register.get(CONF_COUNT))
            continue

        sensors.append(ModbusGenerationRegisterSensor(
            "SolarEdgeGeneration",
            register.get(CONF_SLAVE),
            register.get(CONF_REGISTER),
            register.get(CONF_REGISTER_TYPE),
            register.get(CONF_UNIT_OF_MEASUREMENT),
            register.get(CONF_COUNT),
            register.get(CONF_REVERSE_ORDER),
            register.get(CONF_SCALE),
            register.get(CONF_OFFSET),
            structure,
            register.get(CONF_PRECISION),
            hass))

        sensors.append(ModbusExportRegisterSensor(
            "SolarEdgeExport",
            register.get(CONF_SLAVE),
            register.get(CONF_REGISTER),
            register.get(CONF_REGISTER_TYPE),
            register.get(CONF_UNIT_OF_MEASUREMENT),
            register.get(CONF_COUNT),
            register.get(CONF_REVERSE_ORDER),
            register.get(CONF_SCALE),
            register.get(CONF_OFFSET),
            structure,
            register.get(CONF_PRECISION),
            hass))

    if not sensors:
        return False
    add_devices(sensors)


class ModbusGenerationRegisterSensor(Entity):
    """Modbus register sensor."""

    def __init__(self, name, slave, register, register_type,
                 unit_of_measurement, count, reverse_order, scale, offset,
                 structure, precision, hass):
        """Initialize the modbus register sensor."""
        self._name = name
        self._slave = int(slave) if slave else None
        self._register = int(register)
        self._register_type = register_type
        self._unit_of_measurement = "kW"
        self._reverse_order = reverse_order
        self._scale = scale
        self._offset = offset
        self._precision = precision
        self._structure = structure
        self._value = None
        self._hass = hass

    @property
    def state(self):
        """Return the state of the sensor."""
        return self._value

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement."""
        return "kW"

    def querymodbus(self, register, count):
        if self._register_type == REGISTER_TYPE_INPUT:
            result = modbus.HUB.read_input_registers(
                self._slave,
                register,
                count)
        else:
            result = modbus.HUB.read_holding_registers(
                self._slave,
                register,
                count)
        return result
        
    def getdatafrommodbus(self, register, count):
        """
        _LOGGER.error("Started to fetch %s registers from %s from Modbus at %s", count, register, datetime.datetime.now())
        """
        result = self.querymodbus(register, count)
        try:
            registers = result.registers
            if self._reverse_order:
                registers.reverse()
        except AttributeError:
            try:
                _LOGGER.error("No response from modbus slave %s, register %s (Trying again)", self._slave, register)
                result = self.querymodbus(register, count)
                registers = result.registers
                if self._reverse_order:
                    registers.reverse()
            except AttributeError:
                _LOGGER.error("No response from modbus slave %s, register %s", self._slave, register)
        """
        _LOGGER.error("Finished fetching %s registers from %s from Modbus at %s", count, register, datetime.datetime.now())
        """
        return registers

    def extractGenerationValue(self):
        registers = self.getdatafrommodbus(40083, 2)
        byte_string = b''.join(
            [x.to_bytes(2, byteorder='big') for x in { registers[0] }]
        )
        val = struct.unpack('>H', byte_string)[0]
        byte_string = b''.join(
            [x.to_bytes(2, byteorder='big') for x in { registers[1] }]
        )
        sf = struct.unpack('>h', byte_string)[0]
        value = format(
            (math.pow(10, sf) * val + self._offset) / 1000, '.{}f'.format(self._precision))
        """
        _LOGGER.error("SolarEdgeGeneration calculated as %s based on the values V %s & SF %s as S %s", value, val, sf, self._structure)
        """
        return value

    def update(self):
        """Update the state of the sensor."""
        self._value = self.extractGenerationValue()
        return

class ModbusExportRegisterSensor(Entity):
    """Modbus register sensor."""

    def __init__(self, name, slave, register, register_type,
                 unit_of_measurement, count, reverse_order, scale, offset,
                 structure, precision, hass):
        """Initialize the modbus register sensor."""
        self._name = name
        self._slave = int(slave) if slave else None
        self._register = int(register)
        self._register_type = register_type
        self._unit_of_measurement = unit_of_measurement
        self._count = 125
        self._reverse_order = reverse_order
        self._scale = scale
        self._offset = offset
        self._precision = precision
        self._structure = structure
        self._value = None
        self._hass = hass

    @property
    def state(self):
        """Return the state of the sensor."""
        return self._value

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement."""
        return "kW"

    def querymodbus(self, register, count):
        if self._register_type == REGISTER_TYPE_INPUT:
            result = modbus.HUB.read_input_registers(
                self._slave,
                register,
                count)
        else:
            result = modbus.HUB.read_holding_registers(
                self._slave,
                register,
                count)
        return result
        
    def getdatafrommodbus(self, register, count):
        """
        _LOGGER.error("Started to fetch %s registers from %s from Modbus at %s", count, register, datetime.datetime.now())
        """
        result = self.querymodbus(register, count)
        try:
            registers = result.registers
            if self._reverse_order:
                registers.reverse()
        except AttributeError:
            try:
                _LOGGER.error("No response from modbus slave %s, register %s (Trying again)", self._slave, register)
                result = self.querymodbus(register, count)
                registers = result.registers
                if self._reverse_order:
                    registers.reverse()
            except AttributeError:
                _LOGGER.error("No response from modbus slave %s, register %s", self._slave, register)
        """
        _LOGGER.error("Finished fetching %s registers from %s from Modbus at %s", count, register, datetime.datetime.now())
        """
        return registers

    def extractExportValue(self):
        registers = self.getdatafrommodbus(40206, 5)
        byte_string = b''.join(
            [x.to_bytes(2, byteorder='big') for x in { registers[0] }]
        )
        val = struct.unpack('>h', byte_string)[0]
        byte_string = b''.join(
            [x.to_bytes(2, byteorder='big') for x in { registers[4] }]
        )
        sf = struct.unpack('>h', byte_string)[0]
        value = format(
            (math.pow(10, sf) * val + self._offset) / 1000, '.{}f'.format(self._precision))
        """
        _LOGGER.error("SolarEdgeExport calculated as %s based on the values V %s & SF %s as S %s", value, val, sf, self._structure)
        """
        return value

    def update(self):
        """Update the state of the sensor."""
        self._value = self.extractExportValue()
        return

Then finally the config I have for this is as follows:

sensor:
  - platform: modbus1
    scan_interval: 2
    registers:
    - name: SolarEdgeGeneration
      unit_of_measurement: kW
      slave: 1
      register: 40083
      data_type: uint
      scan_interval: 2
      precision: 2

Note: Please don't shoot me for the bad implementation. It's not the best code and is based on what I could find from the original modbus code as I'm not a very confident coder in python. But it works and so you are welcome to give it a go if you like. :-)

@timberrrr
Copy link

Note: Please don't shoot me for the bad implementation. It's not the best code and is based on what I could find from the original modbus code as I'm not a very confident coder in python. But it works and so you are welcome to give it a go if you like. :-)

Wow, this is great and works perfectly!! I had exactly the issues you described using the inbuilt modbus component. I can now reliably use the modbus data to make load control decisions in seconds... rather than waiting minutes due to the solaredge api restrictions. Also, I was never a fan of the semonitor.py solution... I tried for ages to retrieve my inverter key using rs232/485/wireshark but had no luck. If you still want per-panel data I guess there is no alternative, but for anyone with a modbus meter on a solaredge installation this is a far simpler and elegant solution to get realtime production/consumption data without breaking the existing monitoring.

Thanks for your work on this!

@wraithrmm
Copy link

wraithrmm commented Jan 30, 2019

OMG yes, I know what you mean, I spent DAYS trying to sniff the packets, etc. Didn't work at all.

I have to be honest, I really was NOT expecting it to work for you from my code (you know how these things usually go). :-p But you are very welcome and I hope others can also benefit from it to. :-) I love being able to have my heating etc all responding and use my excess power generation. Saves me allot of money and all told (aside from allot of effort on my part) this has saved me £800 for the Immersion Heater Power Diversion Unit :-p

And just for reference, here's a re-share of the Hass.io UI I put in place:

https://www.dropbox.com/s/y1rq39z17g6wj1i/2019-01-30%2009.53.17.png?dl=0

It's identical to the SolarEdge one, but way more responsive.

@wraithrmm
Copy link

Hi people!

Just a heads-up.

I updated to Home Assistant 0.87.1 the other day and everything stopped working! :-(

After a few hours of crying into my pillow, I man'ed up and sorted it out.

Turns out there is a dependance that is a bit old in the code, look for ['pymodbus==1.3.1'].

This needs to be updated as that version of the pymodbus thing is no longer supported.

Simply change this to ['pymodbus==2.1.0'] and then restart Home Assistant and it all works again. YAY!

Just for all future people that come across this, I have already edited my original post with this code in it. :-)

@timberrrr
Copy link

timberrrr commented Apr 24, 2019

@wraithrmm The recent changes to home assistant custom component file locations have broken the modbus custom component code you posted earlier, it looks like everything now needs to reside under custom_components/modbus1. Have you had any luck getting this to work on newer versions of home assistant?

@tomdk
Copy link

tomdk commented Jan 15, 2020

I used this modbus config to get the values originally, which kinda worked a bit:

  - platform: modbus
    registers:
    - name: SolarGenerationRaw
      unit_of_measurement: kW
      slave: 1
      register: 40083
      data_type: uint
      scan_interval: 2
    - name: SolarGenerationSF
      register: 40084
      slave: 1
      count: 1
      data_type: int
      scan_interval: 2
    - name: EnergyExportRaw
      slave: 1
      register: 40206
      count: 1
      data_type: int
      scan_interval: 2
    - name: EnergyExportSF
      slave: 1
      register: 40210
      count: 1
      data_type: int
      scan_interval: 2

But I ran into all sorts of issues with the modbus system as it queries for each of these values in separate calls, which often resulted in things being out of sync and created allot of lag due to multiple calls to collect the ENTIRE modbus block, but then only use one register from it :-/

One of the biggest issues was it not having the correct scale factors as they would get out of sync occasionally, causing insane spikes in the reporting.

As such, I ended up creating my very own component for use with this, which solves all of these problems and now I have fully replicated the Solar Edge UI in Hasss.io using the new Lovelace image card and the reporting fugures from the modbus. Here's the code, you should put it into a file called "/config/custom_components/modbus1.py"

"""
Support for Modbus.

For more details about this component, please refer to the documentation at
https://home-assistant.io/components/modbus/
"""
import logging
import threading

import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
    EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
    CONF_HOST, CONF_METHOD, CONF_PORT, CONF_TYPE, CONF_TIMEOUT, ATTR_STATE)

DOMAIN = 'modbus1'

REQUIREMENTS = ['pymodbus==2.1.0']

# Type of network
CONF_BAUDRATE = 'baudrate'
CONF_BYTESIZE = 'bytesize'
CONF_STOPBITS = 'stopbits'
CONF_PARITY = 'parity'

SERIAL_SCHEMA = {
    vol.Required(CONF_BAUDRATE): cv.positive_int,
    vol.Required(CONF_BYTESIZE): vol.Any(5, 6, 7, 8),
    vol.Required(CONF_METHOD): vol.Any('rtu', 'ascii'),
    vol.Required(CONF_PORT): cv.string,
    vol.Required(CONF_PARITY): vol.Any('E', 'O', 'N'),
    vol.Required(CONF_STOPBITS): vol.Any(1, 2),
    vol.Required(CONF_TYPE): 'serial',
    vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
}

ETHERNET_SCHEMA = {
    vol.Required(CONF_HOST): cv.string,
    vol.Required(CONF_PORT): cv.positive_int,
    vol.Required(CONF_TYPE): vol.Any('tcp', 'udp', 'rtuovertcp'),
    vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
}


CONFIG_SCHEMA = vol.Schema({
    DOMAIN: vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)
}, extra=vol.ALLOW_EXTRA)


_LOGGER = logging.getLogger(__name__)

SERVICE_WRITE_REGISTER = 'write_register'
SERVICE_WRITE_COIL = 'write_coil'

ATTR_ADDRESS = 'address'
ATTR_UNIT = 'unit'
ATTR_VALUE = 'value'

SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema({
    vol.Required(ATTR_UNIT): cv.positive_int,
    vol.Required(ATTR_ADDRESS): cv.positive_int,
    vol.Required(ATTR_VALUE): vol.All(cv.ensure_list, [cv.positive_int])
})

SERVICE_WRITE_COIL_SCHEMA = vol.Schema({
    vol.Required(ATTR_UNIT): cv.positive_int,
    vol.Required(ATTR_ADDRESS): cv.positive_int,
    vol.Required(ATTR_STATE): cv.boolean
})

HUB = None


def setup(hass, config):
    """Set up Modbus component."""
    # Modbus connection type
    client_type = config[DOMAIN][CONF_TYPE]

    # Connect to Modbus network
    # pylint: disable=import-error

    if client_type == 'serial':
        from pymodbus.client.sync import ModbusSerialClient as ModbusClient
        client = ModbusClient(method=config[DOMAIN][CONF_METHOD],
                              port=config[DOMAIN][CONF_PORT],
                              baudrate=config[DOMAIN][CONF_BAUDRATE],
                              stopbits=config[DOMAIN][CONF_STOPBITS],
                              bytesize=config[DOMAIN][CONF_BYTESIZE],
                              parity=config[DOMAIN][CONF_PARITY],
                              timeout=config[DOMAIN][CONF_TIMEOUT])
    elif client_type == 'rtuovertcp':
        from pymodbus.client.sync import ModbusTcpClient as ModbusClient
        from pymodbus.transaction import ModbusRtuFramer as ModbusFramer
        client = ModbusClient(host=config[DOMAIN][CONF_HOST],
                              port=config[DOMAIN][CONF_PORT],
                              framer=ModbusFramer,
                              timeout=config[DOMAIN][CONF_TIMEOUT])
    elif client_type == 'tcp':
        from pymodbus.client.sync import ModbusTcpClient as ModbusClient
        client = ModbusClient(host=config[DOMAIN][CONF_HOST],
                              port=config[DOMAIN][CONF_PORT],
                              timeout=config[DOMAIN][CONF_TIMEOUT])
    elif client_type == 'udp':
        from pymodbus.client.sync import ModbusUdpClient as ModbusClient
        client = ModbusClient(host=config[DOMAIN][CONF_HOST],
                              port=config[DOMAIN][CONF_PORT],
                              timeout=config[DOMAIN][CONF_TIMEOUT])
    else:
        return False

    global HUB
    HUB = ModbusHub(client)

    def stop_modbus(event):
        """Stop Modbus service."""
        HUB.close()

    def start_modbus(event):
        """Start Modbus service."""
        HUB.connect()
        hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus)

        # Register services for modbus
        hass.services.register(
            DOMAIN, SERVICE_WRITE_REGISTER, write_register,
            schema=SERVICE_WRITE_REGISTER_SCHEMA)
        hass.services.register(
            DOMAIN, SERVICE_WRITE_COIL, write_coil,
            schema=SERVICE_WRITE_COIL_SCHEMA)

    def write_register(service):
        """Write modbus registers."""
        unit = int(float(service.data.get(ATTR_UNIT)))
        address = int(float(service.data.get(ATTR_ADDRESS)))
        value = service.data.get(ATTR_VALUE)
        if isinstance(value, list):
            HUB.write_registers(
                unit,
                address,
                [int(float(i)) for i in value])
        else:
            HUB.write_register(
                unit,
                address,
                int(float(value)))

    def write_coil(service):
        """Write modbus coil."""
        unit = service.data.get(ATTR_UNIT)
        address = service.data.get(ATTR_ADDRESS)
        state = service.data.get(ATTR_STATE)
        HUB.write_coil(unit, address, state)

    hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)

    return True


class ModbusHub(object):
    """Thread safe wrapper class for pymodbus."""

    def __init__(self, modbus_client):
        """Initialize the modbus hub."""
        self._client = modbus_client
        self._lock = threading.Lock()

    def close(self):
        """Disconnect client."""
        with self._lock:
            self._client.close()

    def connect(self):
        """Connect client."""
        with self._lock:
            self._client.connect()

    def read_coils(self, unit, address, count):
        """Read coils."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            return self._client.read_coils(
                address,
                count,
                **kwargs)

    def read_input_registers(self, unit, address, count):
        """Read input registers."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            return self._client.read_input_registers(
                address,
                count,
                **kwargs)

    def read_holding_registers(self, unit, address, count):
        """Read holding registers."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            return self._client.read_holding_registers(
                address,
                count,
                **kwargs)

    def write_coil(self, unit, address, value):
        """Write coil."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            self._client.write_coil(
                address,
                value,
                **kwargs)

    def write_register(self, unit, address, value):
        """Write register."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            self._client.write_register(
                address,
                value,
                **kwargs)

    def write_registers(self, unit, address, values):
        """Write registers."""
        with self._lock:
            kwargs = {'unit': unit} if unit else {}
            self._client.write_registers(
                address,
                values,
                **kwargs)

Then another file here "custom_components/sensor/modbus1.py"

"""
Support for Modbus Register sensors.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.modbus/
"""
import logging
import struct
import math
import datetime

import voluptuous as vol

import custom_components.modbus1 as modbus
from homeassistant.const import (
    CONF_NAME, CONF_OFFSET, CONF_UNIT_OF_MEASUREMENT, CONF_SLAVE,
    CONF_STRUCTURE)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers import config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA

_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = ['modbus1']

CONF_COUNT = 'count'
CONF_REVERSE_ORDER = 'reverse_order'
CONF_PRECISION = 'precision'
CONF_REGISTER = 'register'
CONF_REGISTERS = 'registers'
CONF_SCALE = 'scale'
CONF_DATA_TYPE = 'data_type'
CONF_REGISTER_TYPE = 'register_type'

REGISTER_TYPE_HOLDING = 'holding'
REGISTER_TYPE_INPUT = 'input'

DATA_TYPE_INT = 'int'
DATA_TYPE_UINT = 'uint'
DATA_TYPE_FLOAT = 'float'
DATA_TYPE_CUSTOM = 'custom'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Required(CONF_REGISTERS): [{
        vol.Required(CONF_NAME): cv.string,
        vol.Required(CONF_REGISTER): cv.positive_int,
        vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING):
            vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]),
        vol.Optional(CONF_COUNT, default=1): cv.positive_int,
        vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean,
        vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
        vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
        vol.Optional(CONF_SCALE, default=1): vol.Coerce(float),
        vol.Optional(CONF_SLAVE): cv.positive_int,
        vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT):
            vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT,
                    DATA_TYPE_CUSTOM]),
        vol.Optional(CONF_STRUCTURE): cv.string,
        vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string
    }]
})


def setup_platform(hass, config, add_devices, discovery_info=None):
    """Set up the Modbus sensors."""
    sensors = []
    data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'}}
    data_types[DATA_TYPE_UINT] = {1: 'H', 2: 'I', 4: 'Q'}
    data_types[DATA_TYPE_FLOAT] = {1: 'e', 2: 'f', 4: 'd'}

    for register in config.get(CONF_REGISTERS):
        structure = '>i'
        if register.get(CONF_DATA_TYPE) != DATA_TYPE_CUSTOM:
            try:
                structure = '>{}'.format(data_types[
                    register.get(CONF_DATA_TYPE)][register.get(CONF_COUNT)])
            except KeyError:
                _LOGGER.error("Unable to detect data type for %s sensor, "
                              "try a custom type.", register.get(CONF_NAME))
                continue
        else:
            structure = register.get(CONF_STRUCTURE)

        try:
            size = struct.calcsize(structure)
        except struct.error as err:
            _LOGGER.error(
                "Error in sensor %s structure: %s",
                register.get(CONF_NAME), err)
            continue

        if register.get(CONF_COUNT) * 2 != size:
            _LOGGER.error(
                "Structure size (%d bytes) mismatch registers count "
                "(%d words)", size, register.get(CONF_COUNT))
            continue

        sensors.append(ModbusGenerationRegisterSensor(
            "SolarEdgeGeneration",
            register.get(CONF_SLAVE),
            register.get(CONF_REGISTER),
            register.get(CONF_REGISTER_TYPE),
            register.get(CONF_UNIT_OF_MEASUREMENT),
            register.get(CONF_COUNT),
            register.get(CONF_REVERSE_ORDER),
            register.get(CONF_SCALE),
            register.get(CONF_OFFSET),
            structure,
            register.get(CONF_PRECISION),
            hass))

        sensors.append(ModbusExportRegisterSensor(
            "SolarEdgeExport",
            register.get(CONF_SLAVE),
            register.get(CONF_REGISTER),
            register.get(CONF_REGISTER_TYPE),
            register.get(CONF_UNIT_OF_MEASUREMENT),
            register.get(CONF_COUNT),
            register.get(CONF_REVERSE_ORDER),
            register.get(CONF_SCALE),
            register.get(CONF_OFFSET),
            structure,
            register.get(CONF_PRECISION),
            hass))

    if not sensors:
        return False
    add_devices(sensors)


class ModbusGenerationRegisterSensor(Entity):
    """Modbus register sensor."""

    def __init__(self, name, slave, register, register_type,
                 unit_of_measurement, count, reverse_order, scale, offset,
                 structure, precision, hass):
        """Initialize the modbus register sensor."""
        self._name = name
        self._slave = int(slave) if slave else None
        self._register = int(register)
        self._register_type = register_type
        self._unit_of_measurement = "kW"
        self._reverse_order = reverse_order
        self._scale = scale
        self._offset = offset
        self._precision = precision
        self._structure = structure
        self._value = None
        self._hass = hass

    @property
    def state(self):
        """Return the state of the sensor."""
        return self._value

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement."""
        return "kW"

    def querymodbus(self, register, count):
        if self._register_type == REGISTER_TYPE_INPUT:
            result = modbus.HUB.read_input_registers(
                self._slave,
                register,
                count)
        else:
            result = modbus.HUB.read_holding_registers(
                self._slave,
                register,
                count)
        return result
        
    def getdatafrommodbus(self, register, count):
        """
        _LOGGER.error("Started to fetch %s registers from %s from Modbus at %s", count, register, datetime.datetime.now())
        """
        result = self.querymodbus(register, count)
        try:
            registers = result.registers
            if self._reverse_order:
                registers.reverse()
        except AttributeError:
            try:
                _LOGGER.error("No response from modbus slave %s, register %s (Trying again)", self._slave, register)
                result = self.querymodbus(register, count)
                registers = result.registers
                if self._reverse_order:
                    registers.reverse()
            except AttributeError:
                _LOGGER.error("No response from modbus slave %s, register %s", self._slave, register)
        """
        _LOGGER.error("Finished fetching %s registers from %s from Modbus at %s", count, register, datetime.datetime.now())
        """
        return registers

    def extractGenerationValue(self):
        registers = self.getdatafrommodbus(40083, 2)
        byte_string = b''.join(
            [x.to_bytes(2, byteorder='big') for x in { registers[0] }]
        )
        val = struct.unpack('>H', byte_string)[0]
        byte_string = b''.join(
            [x.to_bytes(2, byteorder='big') for x in { registers[1] }]
        )
        sf = struct.unpack('>h', byte_string)[0]
        value = format(
            (math.pow(10, sf) * val + self._offset) / 1000, '.{}f'.format(self._precision))
        """
        _LOGGER.error("SolarEdgeGeneration calculated as %s based on the values V %s & SF %s as S %s", value, val, sf, self._structure)
        """
        return value

    def update(self):
        """Update the state of the sensor."""
        self._value = self.extractGenerationValue()
        return

class ModbusExportRegisterSensor(Entity):
    """Modbus register sensor."""

    def __init__(self, name, slave, register, register_type,
                 unit_of_measurement, count, reverse_order, scale, offset,
                 structure, precision, hass):
        """Initialize the modbus register sensor."""
        self._name = name
        self._slave = int(slave) if slave else None
        self._register = int(register)
        self._register_type = register_type
        self._unit_of_measurement = unit_of_measurement
        self._count = 125
        self._reverse_order = reverse_order
        self._scale = scale
        self._offset = offset
        self._precision = precision
        self._structure = structure
        self._value = None
        self._hass = hass

    @property
    def state(self):
        """Return the state of the sensor."""
        return self._value

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement."""
        return "kW"

    def querymodbus(self, register, count):
        if self._register_type == REGISTER_TYPE_INPUT:
            result = modbus.HUB.read_input_registers(
                self._slave,
                register,
                count)
        else:
            result = modbus.HUB.read_holding_registers(
                self._slave,
                register,
                count)
        return result
        
    def getdatafrommodbus(self, register, count):
        """
        _LOGGER.error("Started to fetch %s registers from %s from Modbus at %s", count, register, datetime.datetime.now())
        """
        result = self.querymodbus(register, count)
        try:
            registers = result.registers
            if self._reverse_order:
                registers.reverse()
        except AttributeError:
            try:
                _LOGGER.error("No response from modbus slave %s, register %s (Trying again)", self._slave, register)
                result = self.querymodbus(register, count)
                registers = result.registers
                if self._reverse_order:
                    registers.reverse()
            except AttributeError:
                _LOGGER.error("No response from modbus slave %s, register %s", self._slave, register)
        """
        _LOGGER.error("Finished fetching %s registers from %s from Modbus at %s", count, register, datetime.datetime.now())
        """
        return registers

    def extractExportValue(self):
        registers = self.getdatafrommodbus(40206, 5)
        byte_string = b''.join(
            [x.to_bytes(2, byteorder='big') for x in { registers[0] }]
        )
        val = struct.unpack('>h', byte_string)[0]
        byte_string = b''.join(
            [x.to_bytes(2, byteorder='big') for x in { registers[4] }]
        )
        sf = struct.unpack('>h', byte_string)[0]
        value = format(
            (math.pow(10, sf) * val + self._offset) / 1000, '.{}f'.format(self._precision))
        """
        _LOGGER.error("SolarEdgeExport calculated as %s based on the values V %s & SF %s as S %s", value, val, sf, self._structure)
        """
        return value

    def update(self):
        """Update the state of the sensor."""
        self._value = self.extractExportValue()
        return

Then finally the config I have for this is as follows:

sensor:
  - platform: modbus1
    scan_interval: 2
    registers:
    - name: SolarEdgeGeneration
      unit_of_measurement: kW
      slave: 1
      register: 40083
      data_type: uint
      scan_interval: 2
      precision: 2

Note: Please don't shoot me for the bad implementation. It's not the best code and is based on what I could find from the original modbus code as I'm not a very confident coder in python. But it works and so you are welcome to give it a go if you like. :-)

Very interesting. I am running against the same issue with using the modbus implementation as is, being scale factor and readings out of sync. I now end up with spikes because I get a duplicate reading before the switch of scale factor. Could you explain in some kind of psuedo language how you resolved it?

I am reading the modbus via loxone and this writes down every value. I am then monitoring the measurement from loxone with node red and writing it to a mysql database. The only solution I have for now is post process checking the value before and after to detect if it's a spike and if yes, delete the record. I would however like to move this somewhere before saving it to the database but I have no idea how to check if the SF and value are "in sync".

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