Skip to content

Commit

Permalink
Add Time of Use grid charging/discharging support and improve Modbus …
Browse files Browse the repository at this point in the history
…performance

 (#129)

* improve: remove locks to improve performance, as this is not needed for TCP
* feat: add missing TOU Grid switches closes #93
  • Loading branch information
Pho3niX90 authored Feb 15, 2025
1 parent 96079dc commit c5eaf81
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 65 deletions.
2 changes: 1 addition & 1 deletion custom_components/solis_modbus/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"issue_tracker": "https://github.com/Pho3niX90/solis_modbus/issues",
"quality_scale": "silver",
"requirements": ["pymodbus>=3.7.4"],
"version": "2.0.0"
"version": "2.1.0"
}
106 changes: 50 additions & 56 deletions custom_components/solis_modbus/modbus_controller.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import asyncio
import logging
import time
from pymodbus.client import AsyncModbusTcpClient
Expand All @@ -11,9 +10,8 @@ class ModbusController:
def __init__(self, host, port=502, poll_interval=15):
self.host = host
self.port = port
self.client: AsyncModbusTcpClient = AsyncModbusTcpClient(self.host, port=self.port, retries=10, timeout=10, reconnect_delay=10)
self.client: AsyncModbusTcpClient = AsyncModbusTcpClient(host=self.host, port=self.port, retries=10, timeout=10, reconnect_delay=10)
self.connect_failures = 0
self._lock = asyncio.Lock()
self._data_received = False
self._poll_interval = poll_interval
self._model = MODEL
Expand All @@ -22,85 +20,81 @@ def __init__(self, host, port=502, poll_interval=15):
self._last_attempt = 0 # Track last connection attempt time

async def connect(self):
async with self._lock:
now = time.monotonic()
if now - self._last_attempt < 1:
return False # Skip execution if called too soon

self._last_attempt = now # Update last attempt time

try:
if self.client.connected:
return True

_LOGGER.debug('connecting')

if not await self.client.connect():
self.connect_failures += 1
fail_msg = f"Failed to connect to Modbus device. Will retry, failures = {self.connect_failures}"
if self.connect_failures > 50:
_LOGGER.warning(fail_msg)
elif self.connect_failures > 30:
_LOGGER.info(fail_msg)
else:
_LOGGER.debug(fail_msg)
return False

else:
self.connect_failures = 0
return True

except ConnectionError as e:
return False # Return False if an exception occurs

async def disconnect(self):
"""Ensure the Modbus TCP connection is active."""
if self.client.connected:
self.client.close()
return True # Already connected

now = time.monotonic()
if now - self._last_attempt < 1:
return False # Prevent excessive reconnections

self._last_attempt = now # Update last attempt time

try:
_LOGGER.debug('Connecting to Modbus TCP...')

if not await self.client.connect():
self.connect_failures += 1
_LOGGER.warning(f"Failed to connect (Attempt {self.connect_failures})")
return False

self.connect_failures = 0 # Reset failure counter
return True

except Exception as e:
_LOGGER.error(f"Connection error: {e}")
return False

async def async_read_input_register(self, register, count=1):
"""Reads an input register asynchronously without locking."""
try:
await self.connect()
async with self._lock:
result = await self.client.read_input_registers(address=register, count=count, slave=1)
_LOGGER.debug(f'register value, register = {register}, result = {result.registers}')
result = await self.client.read_input_registers(address=register, count=count, slave=1)
_LOGGER.debug(f"Register {register}: {result.registers}")
return result.registers
except Exception as e:
_LOGGER.debug(f"Failed to read Modbus holding register: {str(e)}")
_LOGGER.error(f"Failed to read input register {register}: {str(e)}")
return None

async def async_read_holding_register(self, register: int, count=1):
async def async_read_holding_register(self, register, count=1):
"""Reads a holding register asynchronously."""
try:
await self.connect()
async with self._lock:
result = await self.client.read_holding_registers(address=register, count=count, slave=1)
_LOGGER.debug(f'holding register value, register = {register}, result = {result.registers}')
result = await self.client.read_holding_registers(address=register, count=count, slave=1)
_LOGGER.debug(f"Holding Register {register}: {result.registers}")
return result.registers
except Exception as e:
_LOGGER.debug(f"Failed to read Modbus holding register: {str(e)}")
_LOGGER.error(f"Failed to read holding register {register}: {str(e)}")
return None

async def async_write_holding_register(self, register: int, value):
async def async_write_holding_register(self, register, value):
"""Writes a single holding register asynchronously."""
try:
await self.connect()
async with self._lock:
result = await self.client.write_register(address=register, value=value, slave=1)
return result
return await self.client.write_register(address=register, value=value, slave=1)
except Exception as e:
_LOGGER.debug(f"Failed to write Modbus holding register ({register}): {str(e)}")
_LOGGER.error(f"Failed to write holding register {register}: {str(e)}")
return None

async def async_write_holding_registers(self, start_register: int, values: list[int]):
async def async_write_holding_registers(self, start_register, values):
"""Writes multiple holding registers asynchronously."""
try:
await self.connect()
async with self._lock:
result = await self.client.write_registers(address=start_register, values=values, slave=1)
return result
return await self.client.write_registers(address=start_register, values=values, slave=1)
except Exception as e:
_LOGGER.debug(
f"Failed to write Modbus holding registers ({start_register}), values = {values}: {str(e)}")
_LOGGER.error(f"Failed to write holding registers {start_register}: {str(e)}")
return None

def disable_connection(self):
self.enabled = False
self.close_connection()

async def enable_connection(self):
self.enabled = True
await self.connect()

def close_connection(self):
"""Closes the Modbus connection."""
self.client.close()

def connected(self):
Expand Down
16 changes: 16 additions & 0 deletions custom_components/solis_modbus/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ async def async_setup_entry(hass, config_entry: ConfigEntry, async_add_devices):
{"type": "SBS", "name": "Solis RC Force Battery Discharge", "on_value": 1},
{"type": "SBS", "name": "Solis RC Force Battery Charge", "on_value": 2}
]
}, {
"register": 43707,
"entities": [
{"type": "SBS", "name": "Solis Grid Time of Use Charging Period 1", "bit_position": 0},
{"type": "SBS", "name": "Solis Grid Time of Use Charging Period 2", "bit_position": 1},
{"type": "SBS", "name": "Solis Grid Time of Use Charging Period 3", "bit_position": 2},
{"type": "SBS", "name": "Solis Grid Time of Use Charging Period 4", "bit_position": 3},
{"type": "SBS", "name": "Solis Grid Time of Use Charging Period 5", "bit_position": 4},
{"type": "SBS", "name": "Solis Grid Time of Use Charging Period 6", "bit_position": 5},
{"type": "SBS", "name": "Solis Grid Time of Use Discharge Period 1", "bit_position": 6},
{"type": "SBS", "name": "Solis Grid Time of Use Discharge Period 2", "bit_position": 7},
{"type": "SBS", "name": "Solis Grid Time of Use Discharge Period 3", "bit_position": 8},
{"type": "SBS", "name": "Solis Grid Time of Use Discharge Period 4", "bit_position": 9},
{"type": "SBS", "name": "Solis Grid Time of Use Discharge Period 5", "bit_position": 10},
{"type": "SBS", "name": "Solis Grid Time of Use Discharge Period 6", "bit_position": 11},
]
}
]

Expand Down
8 changes: 0 additions & 8 deletions custom_components/solis_modbus/time.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
"""
This is a docstring placeholder.
This is where we will describe what this module does
"""
import asyncio
import logging
import datetime
from datetime import time
Expand Down

0 comments on commit c5eaf81

Please sign in to comment.