Skip to content

Commit

Permalink
Feature/speedup (Pho3niX90#48)
Browse files Browse the repository at this point in the history
* - bump version

* - bump version

* Refactor update methods and adjust poll intervals

Changed the 'async_update' methods to just 'update' in sensor.py, number.py, and switch.py files. Additionally, the poll intervals have been adjusted. These modifications aim to enhance the efficiency and timing of data updates.

* Update async operations in sensor.py

The async operations for updating sensor entities have been refactored and encapsulated in a new function `get_modbus_updates`. This improves the asynchronous handling of updates, making the code more efficient and organized.

* Switch to async Modbus operations for responsiveness

Refactored Modbus reading methods across several components to use asynchronous operations. This allows the event loop to manage other processes to keep the system responsive, particularly during heavy IO operations.

* Update version and clean error handling in Modbus operations

The version number in the system has been updated from 1.4.3 to 1.4.4 in both manifest.json and const.py. Additionally, error handling for modbus operations within modbus_controller.py has been updated to streamline failure responses, eliminating unnecessary error check steps and focusing on exception-based error handling.

fixes Pho3niX90#43 and Pho3niX90#27

* Switch Modbus operations to asynchronous version

Switched all write operations in Modbus component to their asynchronous counterparts, starting from calling the asynchronous version of write methods in modbus_controller.py, propagated through other components like time.py, sensor.py, and more. This change aims to boost performance by leveraging Python's native async features. Logging statements unrelated to debugging were also removed to clean the code.

* Replace synchronous Modbus operations with asynchronous

The Modbus operations have been replaced with their asynchronous counterparts in modbus_controller.py, number.py, and time.py. The switch to asynchronous operations aims to improve performance by effectively utilizing Python's in-built asynchronous features. Logging messages unrelated to debugging have been removed for a cleaner codebase.

* Implement asynchronous Modbus operations

Switched synchronous Modbus functions to their asynchronous variants to leverage Python's asyncio capabilities and improve performance. Debugging-specific logging messages have been removed to tidy up the codebase. Asynchronous operations were adopted in files like modbus_controller.py, number.py, and time.py.
  • Loading branch information
Pho3niX90 authored Apr 4, 2024
1 parent a98b2de commit da00d3c
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 43 deletions.
5 changes: 4 additions & 1 deletion custom_components/solis_modbus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def service_write_holding_register(call: ServiceCall):
controller = hass.data[DOMAIN][CONTROLLER]
# Perform the logic to write to the holding register using register_address and value_to_write
# ...
controller.write_holding_register(address, value)
asyncio.create_task(controller.write_holding_register(address, value))

hass.services.async_register(
DOMAIN, "solis_write_holding_register", service_write_holding_register, schema=SCHEME_HOLDING_REGISTER
Expand All @@ -61,6 +61,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
port = entry.data.get("port", 502)

hass.data[DOMAIN][CONTROLLER] = ModbusController(host, port)
controller = hass.data[DOMAIN][CONTROLLER]
if not controller.connected():
await controller.connect()

_LOGGER.debug(f'config entry host = {host}, post = {port}')

Expand Down
2 changes: 1 addition & 1 deletion custom_components/solis_modbus/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ async def _validate_config(self, user_input):
modbus_controller = ModbusController(user_input["host"], user_input.get("port", 502))
try:
await modbus_controller.connect()
await modbus_controller.read_input_register(33093)
await modbus_controller.async_read_input_register(33093)
return True
except ConnectionError:
return False
Expand Down
2 changes: 1 addition & 1 deletion custom_components/solis_modbus/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
DOMAIN = "solis_modbus"
CONTROLLER = "modbus_controller"
VERSION = "1.4.4"
VERSION = "1.4.5"
POLL_INTERVAL_SECONDS = 15
MANUFACTURER = "Solis"
MODEL = "S6"
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.5.4"],
"version": "1.4.4"
"version": "1.4.5"
}
44 changes: 29 additions & 15 deletions custom_components/solis_modbus/modbus_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,49 +18,63 @@ def __init__(self, host, port=502):
async def connect(self):
_LOGGER.debug('connecting')
try:
async with self._lock:
if not await self.client.connect():
self.connect_failures += 1
raise _LOGGER.warning(f"Failed to connect to Modbus device. Will retry, failures = {self.connect_failures}")
else:
self.connect_failures = 0
if self.client.connected:
return True

if not await self.client.connect():
self.connect_failures += 1
raise _LOGGER.warning(f"Failed to connect to Modbus device. Will retry, failures = {self.connect_failures}")
else:
self.connect_failures = 0
return True

except Exception as e:
raise _LOGGER.error(f"Failed to connect to Modbus device. Will retry")

async def async_read_input_register(self, register, count=1):
try:
await self.connect()
async with self._lock:
result = await self.client.read_input_registers(register, count, slave=1)
_LOGGER.debug(f'register value, register = {register}, result = {result.registers}')
return result.registers
except ModbusIOException as e:
raise ValueError(f"Failed to read Modbus register: {str(e)}")
except Exception as e:
raise _LOGGER.error(f"Failed to read Modbus register: {str(e)}")

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

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

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

def write_holding_registers(self, start_register: int, values: list[int]):
try:
result = self.client.write_registers(start_register, values, slave=1)
return result
except Exception as e:
raise _LOGGER.error(f"Failed to write Modbus holding registers ({start_register}), values = {values}: {str(e)}")

def close_connection(self):
self.client.close()
Expand Down
12 changes: 5 additions & 7 deletions custom_components/solis_modbus/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,18 +127,16 @@ async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
_LOGGER.debug(f"async_added_to_hass {self._attr_name}, {self.entity_id}, {self.unique_id}")

def update(self):
async def async_update(self):
"""Update Modbus data periodically."""
controller = self._hass.data[DOMAIN][CONTROLLER]
self._attr_available = True

value: float = self._hass.data[DOMAIN]['values'][str(self._register)]

if value == 0:
_LOGGER.debug(f'got 0 for register {self._register}, forcing update')
value = controller.async_read_holding_register(self._register)[0]

_LOGGER.debug(f'Update number entity with value = {value / self._multiplier}')
if value == 0 and controller.connected():
register_value = await controller.async_read_holding_register(self._register)
value = register_value[0] if register_value else value

self._attr_native_value = round(value / self._multiplier)

Expand All @@ -158,6 +156,6 @@ def set_native_value(self, value):
if self._attr_native_value == value:
return

self._modbus_controller.async_write_holding_register(self._register, round(value * self._multiplier))
asyncio.create_task(self._modbus_controller.async_write_holding_register(self._register, round(value * self._multiplier)))
self._attr_native_value = value
self.schedule_update_ha_state()
14 changes: 11 additions & 3 deletions custom_components/solis_modbus/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ def extract_serial_number(values):
return ''.join([hex_to_ascii(hex_value) for hex_value in values])


def clock_drift_test(controller, hours, minutes, seconds):
async def clock_drift_test(hass, controller, hours, minutes, seconds):
# Get the current time
current_time = datetime.now()

Expand All @@ -757,8 +757,16 @@ def clock_drift_test(controller, hours, minutes, seconds):
d_seconds = r_seconds - seconds
total_drift = (d_hours * 60 * 60) + (d_minutes * 60) + d_seconds

drift_counter = hass.data[DOMAIN].get('drift_counter', 0)

if abs(total_drift) > 5:
controller.async_write_holding_registers(43003, [current_time.hour, current_time.minute, current_time.second])
"""this is to make sure that we do not accidentally roll back the time, resetting all stats"""
if drift_counter > 5:
await controller.write_holding_registers(43003, [current_time.hour, current_time.minute, current_time.second])
else:
hass.data[DOMAIN]['drift_counter'] = drift_counter + 1
else:
hass.data[DOMAIN]['drift_counter'] = 0


class SolisDerivedSensor(RestoreSensor, SensorEntity):
Expand Down Expand Up @@ -899,7 +907,7 @@ def update(self):
hours = self._hass.data[DOMAIN]['values'][str(int(self._register[0]) - 2)]
minutes = self._hass.data[DOMAIN]['values'][str(int(self._register[0]) - 1)]
seconds = self._hass.data[DOMAIN]['values'][self._register[0]]
clock_drift_test(self._modbus_controller, hours, minutes, seconds)
self.hass.create_task(clock_drift_test(self._hass, self._modbus_controller, hours, minutes, seconds))

if len(self._register) == 1 and self._register[0] in ('33001', '33002', '33003'):
n_value = hex(round(get_value(self)))[2:]
Expand Down
5 changes: 3 additions & 2 deletions custom_components/solis_modbus/switch.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import logging
from datetime import timedelta
from typing import List, Any
Expand Down Expand Up @@ -106,8 +107,8 @@ def set_register_bit(self, value):
_LOGGER.debug(
f"Attempting bit {self._bit_position} to {value} in register {self._read_register}. New value for register {new_register_value}")
# we only want to write when values has changed. After, we read the register again to make sure it applied.
if current_register_value != new_register_value:
controller.async_write_holding_register(self._write_register, new_register_value)
if current_register_value != new_register_value and controller.connected():
self._hass.create_task(controller.async_write_holding_register(self._write_register, new_register_value))
self._hass.data[DOMAIN]['values'][str(self._read_register)] = new_register_value

self._attr_is_on = value
Expand Down
22 changes: 10 additions & 12 deletions custom_components/solis_modbus/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
This is where we will describe what this module does
"""
import asyncio
import logging
from datetime import time
from datetime import timedelta
Expand Down Expand Up @@ -63,10 +64,9 @@ async def async_setup_entry(hass, config_entry: ConfigEntry, async_add_devices):
async_add_devices(timeEntities, True)

@callback
def async_update(now):
async def async_update(now):
"""Update Modbus data periodically."""
for entity in hass.data[DOMAIN]["time_entities"]:
entity.update()
await asyncio.gather(*[entity.async_update() for entity in hass.data[DOMAIN]["time_entities"]])
# Schedule the update function to run every X seconds

async_track_time_interval(hass, async_update, timedelta(seconds=POLL_INTERVAL_SECONDS * 5))
Expand Down Expand Up @@ -103,23 +103,21 @@ async def async_added_to_hass(self) -> None:
await super().async_added_to_hass()
_LOGGER.debug(f"async_added_to_hass {self._attr_name}, {self.entity_id}, {self.unique_id}")

def update(self):
async def async_update(self):
"""Update Modbus data periodically."""
controller = self._hass.data[DOMAIN][CONTROLLER]
self._attr_available = True

hour = self._hass.data[DOMAIN]['values'][str(self._register)]
minute = self._hass.data[DOMAIN]['values'][str(self._register + 1)]

if hour == 0 or minute == 0:
new_vals = controller.async_read_holding_register(self._register, count=2)
hour = new_vals[0]
minute = new_vals[1]
if (hour == 0 or minute == 0) and controller.connected():
new_vals = await controller.async_read_holding_register(self._register, count=2)
hour = new_vals[0] if new_vals else None
minute = new_vals[1] if new_vals else None

_LOGGER.debug(f'Update time entity with hour = {hour}, minute = {minute}')

self._attr_native_value = time(hour=hour, minute=minute)
# self.async_write_ha_state()
if hour is not None:
self._attr_native_value = time(hour=hour, minute=minute)

@property
def device_info(self):
Expand Down

0 comments on commit da00d3c

Please sign in to comment.