Skip to content

Commit

Permalink
Use async modbus client
Browse files Browse the repository at this point in the history
  • Loading branch information
jvitkauskas committed Dec 20, 2023
1 parent e1e3ef6 commit 999829b
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 113 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Blauberg S21 Python TCP API
# Blauberg S21 Asynchronous Python API
An api allowing control of AC state (temperature, on/off, speed) of an Blauberg S21 device locally over TCP.

## Usage
To initialize:
`client = S21Client("192.168.0.125")`

To load:
`client.poll()`
`await client.poll()`

The following functions are available:
`turn_on()`
Expand Down
2 changes: 1 addition & 1 deletion demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ async def main():

client = S21Client(args.host, args.port)

status = client.poll()
status = await client.poll()

print(repr(status))

Expand Down
96 changes: 48 additions & 48 deletions pybls21/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pyModbusTCP.client import ModbusClient
from pymodbus.client import AsyncModbusTcpClient

from .constants import *
from .exceptions import *
Expand All @@ -25,55 +25,55 @@ class S21Client:
def __init__(self, host: str, port: int = 502):
self.host = host
self.port = port
self.client = ModbusClient(host=self.host, port=self.port, auto_open=False, auto_close=False)
self.client = AsyncModbusTcpClient(host=self.host, port=self.port)
self.device: Optional[ClimateDevice] = None
self.lock = Lock()

def poll(self) -> ClimateDevice:
return self._do_with_connection(self._poll)
async def poll(self) -> ClimateDevice:
return await self._do_with_connection(self._poll)

def turn_on(self) -> None:
self._do_with_connection(self._turn_on)
async def turn_on(self) -> None:
await self._do_with_connection(self._turn_on)

def turn_off(self) -> None:
self._do_with_connection(self._turn_off)
async def turn_off(self) -> None:
await self._do_with_connection(self._turn_off)

def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
self._do_with_connection(lambda: self._set_hvac_mode(hvac_mode))
async def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
await self._do_with_connection(lambda: self._set_hvac_mode(hvac_mode))

def set_fan_mode(self, mode: int) -> None:
self._do_with_connection(lambda: self._set_fan_mode(mode))
async def set_fan_mode(self, mode: int) -> None:
await self._do_with_connection(lambda: self._set_fan_mode(mode))

def set_manual_fan_speed_percent(self, speed_percent: int) -> None:
self._do_with_connection(lambda: self._set_manual_fan_speed_percent(speed_percent))
async def set_manual_fan_speed_percent(self, speed_percent: int) -> None:
await self._do_with_connection(lambda: self._set_manual_fan_speed_percent(speed_percent))

def set_temperature(self, temp_celsius: int) -> None:
self._do_with_connection(lambda: self._set_temperature(temp_celsius))
async def set_temperature(self, temp_celsius: int) -> None:
await self._do_with_connection(lambda: self._set_temperature(temp_celsius))

def reset_filter_change_timer(self) -> None:
self._do_with_connection(self._reset_filter_change_timer)
async def reset_filter_change_timer(self) -> None:
await self._do_with_connection(self._reset_filter_change_timer)

def _do_with_connection(self, func: Callable):
async def _do_with_connection(self, func: Callable):
with self.lock: # Device does not support multiple connections
if not self.client.open():
if not await self.client.connect():
raise Exception("Failed to open connection")

try:
return func()
return await func()
except Exception:
if isinstance(self.device, ClimateDevice):
self.device.available = False
raise
finally:
self.client.close() # Also, long connections break over time and become unusable

def _poll(self) -> ClimateDevice:
if self.client.read_input_registers(IR_DeviceTYPE)[0] != 1:
async def _poll(self) -> ClimateDevice:
if (await self.client.read_input_registers(IR_DeviceTYPE)).registers[0] != 1:
raise UnsupportedDeviceException("Unsupported device (IR_DeviceTYPE != 1)")

coils = self.client.read_coils(0, 4)
holding_registers = self.client.read_holding_registers(0, 45)
input_registers = self.client.read_input_registers(0, 39)
coils = (await self.client.read_coils(0, 4)).bits
holding_registers = (await self.client.read_holding_registers(0, 45)).registers
input_registers = (await self.client.read_input_registers(0, 39)).registers

is_on: bool = coils[CL_POWER]
is_boosting: bool = coils[CL_Boost_MODE]
Expand Down Expand Up @@ -128,36 +128,36 @@ def _poll(self) -> ClimateDevice:

return self.device

def _turn_on(self) -> None:
self.client.write_single_coil(CL_POWER, True)
async def _turn_on(self) -> None:
await self.client.write_coil(CL_POWER, True)

def _turn_off(self) -> None:
self.client.write_single_coil(CL_POWER, False)
async def _turn_off(self) -> None:
await self.client.write_coil(CL_POWER, False)

def _set_hvac_mode(self, hvac_mode: HVACMode) -> None:
async def _set_hvac_mode(self, hvac_mode: HVACMode) -> None:
if hvac_mode == HVACMode.OFF:
self._turn_off()
await self._turn_off()
elif hvac_mode == HVACMode.FAN_ONLY:
self._turn_on()
self.client.write_single_register(HR_OPERATION_MODE, 0)
await self._turn_on()
await self.client.write_register(HR_OPERATION_MODE, 0)
elif hvac_mode == HVACMode.HEAT:
self._turn_on()
self.client.write_single_register(HR_OPERATION_MODE, 1)
await self._turn_on()
await self.client.write_register(HR_OPERATION_MODE, 1)
elif hvac_mode == HVACMode.COOL:
self._turn_on()
self.client.write_single_register(HR_OPERATION_MODE, 2)
await self._turn_on()
await self.client.write_register(HR_OPERATION_MODE, 2)
elif hvac_mode == HVACMode.AUTO:
self._turn_on()
self.client.write_single_register(HR_OPERATION_MODE, 3)
await self._turn_on()
await self.client.write_register(HR_OPERATION_MODE, 3)

def _set_fan_mode(self, mode: int) -> None:
self.client.write_single_register(HR_SPEED_MODE, mode)
async def _set_fan_mode(self, mode: int) -> None:
await self.client.write_register(HR_SPEED_MODE, mode)

def _set_manual_fan_speed_percent(self, speed_percent: int) -> None:
self.client.write_single_register(HR_ManualSPEED, speed_percent)
async def _set_manual_fan_speed_percent(self, speed_percent: int) -> None:
await self.client.write_register(HR_ManualSPEED, speed_percent)

def _set_temperature(self, temp_celsius: int) -> None:
self.client.write_single_register(HR_SetTEMP, temp_celsius)
async def _set_temperature(self, temp_celsius: int) -> None:
await self.client.write_register(HR_SetTEMP, temp_celsius)

def _reset_filter_change_timer(self) -> None:
self.client.write_single_coil(CL_RESET_FILTER_TIMER, True)
async def _reset_filter_change_timer(self) -> None:
await self.client.write_coil(CL_RESET_FILTER_TIMER, True)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pymodbus==3.5.4
pyModbusTCP==0.2.1
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@

setuptools.setup(
name="pybls21",
version="3.0.4",
version="4.0.0",
author="Julius Vitkauskas",
author_email="[email protected]",
description="An api allowing control of AC state (temperature, on/off, speed) of an Blauberg S21 device locally over TCP",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/jvitkauskas/pybls21",
packages=setuptools.find_packages(exclude=["tests"]),
install_requires=['pyModbusTCP>=0.2.1,<1.0'],
install_requires=['pymodbus>=3.5.4,<4.0'],
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
Expand Down
Loading

0 comments on commit 999829b

Please sign in to comment.