Skip to content

Commit d4ba614

Browse files
committed
Personal fork: metadata, changelog, simulated-inverter test
1 parent 16a4c06 commit d4ba614

4 files changed

Lines changed: 89 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Changelog (personal fork)
2+
3+
## 0.1.3 (personal fork of upstream 0.1.1)
4+
- Fix: settings (battery currents, SOC limits, work mode, switches, TOU) were never
5+
applied. Deye hybrids reject Modbus FC06; all writes now use FC16.
6+
- Fix: pymodbus >= 3.10 renamed `slave=` to `device_id=`; the unit id is now passed
7+
correctly for any pymodbus version.
8+
- Fix: writes share the polling lock; freshly written values are held for 30 s so a
9+
stale poll does not flip the UI back.
10+
- Fix: failed writes raise an error in HA and are logged.
11+
- Fix: address offset was applied twice on reads; RTU-over-TCP client construction.
12+
- New: `PV Power` sensor = PV1 + PV2 + PV3 + PV4 power.
13+
- New: `compute: sum` + `sources:` supported in register maps for derived sensors.

‎README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Deye Modbus for Home Assistant
22

3+
> **Personal fork** of [Developer089/deye-modbus-ha](https://github.com/Developer089/deye-modbus-ha).
4+
> Changes: settings writes fixed (FC16 instead of FC06, pymodbus 3.10+ unit id),
5+
> write errors shown in HA, and a total **PV Power** sensor. See `CHANGELOG.md`.
6+
> The integration domain is still `deye_modbus`, so existing entity IDs are kept.
7+
> Remove the upstream HACS install before using this one.
8+
9+
310
Local Home Assistant integration for **Deye three-phase hybrid inverters** (SUN-\*K-SG04LP3 / SG05LP3 family, including the SUN-12K-SG05LP3). It talks Modbus TCP directly to the inverter's WiFi/LAN logger or an RS485-to-TCP gateway — no cloud, no SolarMAN account required.
411

512
[![HACS Custom](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://hacs.xyz/)

‎custom_components/deye_modbus/manifest.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"domain": "deye_modbus",
3-
"name": "Deye Modbus (Hybrid Inverter)",
3+
"name": "Deye Modbus (Hybrid Inverter) - personal fork",
44
"codeowners": [
55
"@Developer089"
66
],
@@ -16,5 +16,5 @@
1616
"requirements": [
1717
"pymodbus>=3.5.0"
1818
],
19-
"version": "0.1.1"
19+
"version": "0.1.3"
2020
}

‎tests/sim_inverter_test.py‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Simulated-inverter test (no Home Assistant needed).
2+
3+
Starts a local Modbus server that, like a Deye, rejects FC06, then checks that
4+
reads, FC16 writes, read-back and the PV Power sum work.
5+
Run from repo root: pip install pymodbus pyyaml && python tests/sim_inverter_test.py
6+
"""
7+
import os, sys, types, asyncio, logging, yaml
8+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9+
logging.basicConfig(level=logging.WARNING)
10+
# --- minimal homeassistant stubs
11+
ha=types.ModuleType("homeassistant"); sys.modules["homeassistant"]=ha
12+
core=types.ModuleType("homeassistant.core"); core.HomeAssistant=object; sys.modules["homeassistant.core"]=core
13+
h=types.ModuleType("homeassistant.helpers"); sys.modules["homeassistant.helpers"]=h
14+
uc=types.ModuleType("homeassistant.helpers.update_coordinator")
15+
class UpdateFailed(Exception): pass
16+
class DataUpdateCoordinator:
17+
def __class_getitem__(cls, i): return cls
18+
def __init__(self, hass, logger, name, update_interval): self.data=None
19+
async def async_request_refresh(self): self.data = await self._async_update_data()
20+
def async_set_updated_data(self, d): self.data=d
21+
uc.UpdateFailed=UpdateFailed; uc.DataUpdateCoordinator=DataUpdateCoordinator
22+
sys.modules["homeassistant.helpers.update_coordinator"]=uc
23+
sys.path.insert(0,REPO + "/custom_components/deye_modbus")
24+
import importlib.util
25+
spec=importlib.util.spec_from_file_location("coord",REPO + "/custom_components/deye_modbus/coordinator.py")
26+
coord=importlib.util.module_from_spec(spec); sys.modules["coord"]=coord; spec.loader.exec_module(coord)
27+
28+
from pymodbus.server import StartAsyncTcpServer
29+
from pymodbus.datastore import ModbusServerContext, ModbusDeviceContext
30+
from pymodbus.datastore import ModbusSparseDataBlock
31+
from pymodbus.pdu import ExceptionResponse
32+
33+
class DeyeBlock(ModbusSparseDataBlock): pass
34+
vals={i:0 for i in range(0,1000)}
35+
vals.update({108:100,117:20,672:1200,673:850,674:0,675:300,588:55})
36+
block=DeyeBlock(vals)
37+
dev=ModbusDeviceContext(hr=block)
38+
ctx=ModbusServerContext(devices={1:dev},single=False)
39+
40+
async def main():
41+
import pymodbus.pdu.register_message as rm
42+
# emulate Deye: FC06 not supported
43+
pass
44+
async def reject(self, context, device_id): return ExceptionResponse(self.function_code, 1)
45+
rm.WriteSingleRegisterRequest.datastore_update=reject
46+
task=asyncio.create_task(StartAsyncTcpServer(context=ctx,address=("127.0.0.1",15020)))
47+
await asyncio.sleep(1)
48+
m=yaml.safe_load(open(REPO + "/custom_components/deye_modbus/maps/sun_3ph_hybrid.yaml"))
49+
initmod=open(REPO + "/custom_components/deye_modbus/__init__.py").read()
50+
ns={}
51+
src=initmod[initmod.index("_REGISTER_FIELDS"):initmod.index("async def async_setup_entry")]
52+
exec(src,ns)
53+
sensors,controls=ns["_auto_inject_readbacks"](m["sensors"],m["controls"])
54+
regs=[coord.RegisterDef(**{k:v for k,v in r.items() if k in ns["_REGISTER_FIELDS"]}) for r in sensors]
55+
c=coord.DeyeModbusCoordinator(None,"127.0.0.1",15020,1,regs,15)
56+
await c.async_request_refresh()
57+
d=c.data
58+
print("PV power:",d["pv_power"],"| max charge rb:",d["rb_batt_max_charge_current"],"| low soc rb:",d["rb_batt_low_soc"])
59+
# old-style FC06 write would fail here; new write uses FC16
60+
ok=await c.write_single_register(108,60); ok2=await c.write_single_register(117,35)
61+
cl=await c._ensure_client(); r=await cl.read_holding_registers(108,count=10,device_id=1)
62+
print("writes ok:",ok,ok2,"| device now 108,117:",r.registers[0],r.registers[9])
63+
print("after refresh rb:",c.data["rb_batt_max_charge_current"],c.data["rb_batt_low_soc"])
64+
# FC06 directly, to prove emulation rejects it
65+
cl=await c._ensure_client(); rr=await cl.write_register(108,50,device_id=1); print("raw FC06 isError:",rr.isError())
66+
await c.async_close(); task.cancel()
67+
asyncio.run(main())

0 commit comments

Comments
 (0)