diff --git a/demo.py b/demo.py index ba9eb94..d2dce7a 100644 --- a/demo.py +++ b/demo.py @@ -12,7 +12,9 @@ def help(): print("syntax: demo.py [options]") print("options:") print(" --host ... network address of your HVAC device") - print(" --port [hvac_port] ... optional TCP port if device is behind the proxy") + print( + " --port [hvac_port] ... optional TCP port if device is behind the proxy" + ) print() print("examples:") print(" demo.py --host 192.168.0.125 --port 502") diff --git a/pybls21/client.py b/pybls21/client.py index b0c730e..22ea4cf 100644 --- a/pybls21/client.py +++ b/pybls21/client.py @@ -1,20 +1,26 @@ +from threading import Lock +from typing import Callable, List, Optional + from pymodbus.client import AsyncModbusTcpClient from .constants import * from .exceptions import * -from .models import ClimateDevice, ClimateEntityFeature, HVACAction, HVACMode, TEMP_CELSIUS - -from typing import Callable, List, Optional -from threading import Lock +from .models import ( + TEMP_CELSIUS, + ClimateDevice, + ClimateEntityFeature, + HVACAction, + HVACMode, +) def _parse_firmware_version(firmware_info: List[int]) -> str: - major, minor = firmware_info[0].to_bytes(2, 'big') + major, minor = firmware_info[0].to_bytes(2, "big") - day, month = firmware_info[1].to_bytes(2, 'big') + day, month = firmware_info[1].to_bytes(2, "big") year: int = firmware_info[2] - return f'{major}.{minor} ({year}-{month:02d}-{day:02d})' + return f"{major}.{minor} ({year}-{month:02d}-{day:02d})" class S21Client: @@ -41,7 +47,9 @@ async def set_fan_mode(self, mode: int) -> None: await self._do_with_connection(lambda: self._set_fan_mode(mode)) 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)) + await self._do_with_connection( + lambda: self._set_manual_fan_speed_percent(speed_percent) + ) async def set_temperature(self, temp_celsius: int) -> None: await self._do_with_connection(lambda: self._set_temperature(temp_celsius)) @@ -81,14 +89,16 @@ async def _poll(self) -> ClimateDevice: current_fan_level: int = holding_registers[HR_SPEED_MODE] # 255 - manual temp_before_heating_x10: int = input_registers[IR_CurTEMP_SuAirIn] temp_after_heating_x10: int = input_registers[IR_CurTEMP_SuAirOut] - firmware_info: List[int] = input_registers[IR_VerMAIN_FMW_start:IR_VerMAIN_FMW_end+1] + firmware_info: List[int] = input_registers[ + IR_VerMAIN_FMW_start : IR_VerMAIN_FMW_end + 1 + ] operation_mode: int = holding_registers[HR_OPERATION_MODE] manual_fan_speed_percent: int = holding_registers[HR_ManualSPEED] self.device = ClimateDevice( available=True, name="Blauberg S21", - unique_id=f'S21_{self.host}_{self.port}', + unique_id=f"S21_{self.host}_{self.port}", temperature_unit=TEMP_CELSIUS, # Seems like no Fahrenheit option is available precision=1, current_temperature=temp_after_heating_x10 / 10, @@ -97,20 +107,41 @@ async def _poll(self) -> ClimateDevice: min_temp=15, max_temp=30, current_humidity=None if current_humidity == 0 else current_humidity, - hvac_mode=HVACMode.OFF if not is_on - else HVACMode.FAN_ONLY if operation_mode == 0 - else HVACMode.HEAT if operation_mode == 1 - else HVACMode.COOL if operation_mode == 2 + hvac_mode=HVACMode.OFF + if not is_on + else HVACMode.FAN_ONLY + if operation_mode == 0 + else HVACMode.HEAT + if operation_mode == 1 + else HVACMode.COOL + if operation_mode == 2 else HVACMode.AUTO, - hvac_action=HVACAction.OFF if not is_on - else HVACAction.FAN if operation_mode == 0 - else HVACAction.HEATING if (operation_mode == 1 or (temp_before_heating_x10 < temp_after_heating_x10)) - else HVACAction.COOLING if (operation_mode == 2 or (temp_before_heating_x10 > temp_after_heating_x10)) + hvac_action=HVACAction.OFF + if not is_on + else HVACAction.FAN + if operation_mode == 0 + else HVACAction.HEATING + if ( + operation_mode == 1 + or (temp_before_heating_x10 < temp_after_heating_x10) + ) + else HVACAction.COOLING + if ( + operation_mode == 2 + or (temp_before_heating_x10 > temp_after_heating_x10) + ) else HVACAction.IDLE, - hvac_modes=[HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.AUTO, HVACMode.FAN_ONLY], + hvac_modes=[ + HVACMode.OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.AUTO, + HVACMode.FAN_ONLY, + ], fan_mode=current_fan_level, fan_modes=[x + 1 for x in range(max_fan_level)] + [255], - supported_features=ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE, + supported_features=ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE, manufacturer="Blauberg", model="S21", sw_version=_parse_firmware_version(firmware_info), diff --git a/pybls21/models.py b/pybls21/models.py index d0c9eb4..845ad68 100644 --- a/pybls21/models.py +++ b/pybls21/models.py @@ -1,7 +1,5 @@ -from typing import List, NamedTuple, Optional - from enum import Enum - +from typing import List, NamedTuple, Optional TEMP_CELSIUS: str = "°C" diff --git a/setup.py b/setup.py index 16ffb59..e43f26b 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="pybls21", - version="4.0.0", + version="4.0.1", author="Julius Vitkauskas", author_email="zadintuvas@gmail.com", description="An api allowing control of AC state (temperature, on/off, speed) of an Blauberg S21 device locally over TCP", @@ -13,11 +13,11 @@ long_description_content_type="text/markdown", url="https://github.com/jvitkauskas/pybls21", packages=setuptools.find_packages(exclude=["tests"]), - install_requires=['pymodbus>=3.5.4,<4.0'], + install_requires=["pymodbus>=3.5.4,<4.0"], classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], - python_requires='>=3.7', + python_requires=">=3.7", ) diff --git a/tests/test_client.py b/tests/test_client.py index b839b16..aef6b11 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,17 +1,19 @@ import unittest -from pyModbusTCP.server import ModbusServer, DataBank +from pyModbusTCP.server import DataBank, ModbusServer from pybls21.client import S21Client from pybls21.constants import * from pybls21.exceptions import * -from pybls21.models import HVACAction, HVACMode, ClimateDevice, ClimateEntityFeature +from pybls21.models import ClimateDevice, ClimateEntityFeature, HVACAction, HVACMode class TestClient(unittest.IsolatedAsyncioTestCase): @classmethod def setUpClass(cls): - cls.server = ModbusServer(host='localhost', port=5502, no_block=True, data_bank=TestDataBank()) + cls.server = ModbusServer( + host="localhost", port=5502, no_block=True, data_bank=TestDataBank() + ) cls.server.start() @classmethod @@ -41,39 +43,51 @@ async def test_poll(self): self.server.data_bank.set_input_registers(IR_ALARM, [2]) self.server.data_bank.set_input_registers(IR_CurTEMP_SuAirIn, [108]) self.server.data_bank.set_input_registers(IR_CurTEMP_SuAirOut, [192]) - self.server.data_bank.set_input_registers(IR_VerMAIN_FMW_start, [36, 2053, 2019]) + self.server.data_bank.set_input_registers( + IR_VerMAIN_FMW_start, [36, 2053, 2019] + ) client = S21Client(host=self.server.host, port=self.server.port) device = await client.poll() - self.assertEqual(device, ClimateDevice( - available=True, - name="Blauberg S21", - unique_id=f'S21_{self.server.host}_{self.server.port}', - temperature_unit="°C", - precision=1, - current_temperature=19.2, - target_temperature=15, - target_temperature_step=1, - min_temp=15, - max_temp=30, - current_humidity=None, - hvac_mode=HVACMode.FAN_ONLY, - hvac_action=HVACAction.FAN, - hvac_modes=[HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.AUTO, HVACMode.FAN_ONLY], - fan_mode=2, - fan_modes=[1, 2, 3, 255], - supported_features=ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE, - manufacturer="Blauberg", - model="S21", - sw_version="0.36 (2019-05-08)", - is_boosting=False, - current_intake_temperature=10.8, - manual_fan_speed_percent=100, - max_fan_level=3, - filter_state=3, - alarm_state=2 - )) + self.assertEqual( + device, + ClimateDevice( + available=True, + name="Blauberg S21", + unique_id=f"S21_{self.server.host}_{self.server.port}", + temperature_unit="°C", + precision=1, + current_temperature=19.2, + target_temperature=15, + target_temperature_step=1, + min_temp=15, + max_temp=30, + current_humidity=None, + hvac_mode=HVACMode.FAN_ONLY, + hvac_action=HVACAction.FAN, + hvac_modes=[ + HVACMode.OFF, + HVACMode.HEAT, + HVACMode.COOL, + HVACMode.AUTO, + HVACMode.FAN_ONLY, + ], + fan_mode=2, + fan_modes=[1, 2, 3, 255], + supported_features=ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE, + manufacturer="Blauberg", + model="S21", + sw_version="0.36 (2019-05-08)", + is_boosting=False, + current_intake_temperature=10.8, + manual_fan_speed_percent=100, + max_fan_level=3, + filter_state=3, + alarm_state=2, + ), + ) async def test_poll_when_device_is_off(self): self.server.data_bank.set_coils(CL_POWER, [False]) @@ -152,7 +166,9 @@ async def test_poll_when_auto_mode_is_set_and_output_temperature_is_lower(self): self.assertEqual(device.hvac_mode, HVACMode.AUTO) self.assertEqual(device.hvac_action, HVACAction.COOLING) - async def test_poll_when_auto_mode_is_set_and_in_temperature_matches_out_temperature(self): + async def test_poll_when_auto_mode_is_set_and_in_temperature_matches_out_temperature( + self, + ): self.server.data_bank.set_holding_registers(HR_OPERATION_MODE, [3]) self.server.data_bank.set_input_registers(IR_CurTEMP_SuAirIn, [10]) self.server.data_bank.set_input_registers(IR_CurTEMP_SuAirOut, [10]) @@ -163,7 +179,9 @@ async def test_poll_when_auto_mode_is_set_and_in_temperature_matches_out_tempera self.assertEqual(device.hvac_mode, HVACMode.AUTO) self.assertEqual(device.hvac_action, HVACAction.IDLE) - async def test_poll_when_auto_mode_is_set_and_in_temperature_is_cooler_than_out_temperature(self): + async def test_poll_when_auto_mode_is_set_and_in_temperature_is_cooler_than_out_temperature( + self, + ): self.server.data_bank.set_holding_registers(HR_OPERATION_MODE, [3]) self.server.data_bank.set_input_registers(IR_CurTEMP_SuAirIn, [5]) self.server.data_bank.set_input_registers(IR_CurTEMP_SuAirOut, [10]) @@ -174,7 +192,9 @@ async def test_poll_when_auto_mode_is_set_and_in_temperature_is_cooler_than_out_ self.assertEqual(device.hvac_mode, HVACMode.AUTO) self.assertEqual(device.hvac_action, HVACAction.HEATING) - async def test_poll_when_auto_mode_is_set_and_in_temperature_is_hotter_than_out_temperature(self): + async def test_poll_when_auto_mode_is_set_and_in_temperature_is_hotter_than_out_temperature( + self, + ): self.server.data_bank.set_holding_registers(HR_OPERATION_MODE, [3]) self.server.data_bank.set_input_registers(IR_CurTEMP_SuAirIn, [10]) self.server.data_bank.set_input_registers(IR_CurTEMP_SuAirOut, [5]) @@ -315,7 +335,9 @@ class TestDataBank(DataBank): __test__ = False def __init__(self): - super().__init__(coils_size=25, d_inputs_size=72, h_regs_size=182, i_regs_size=51) + super().__init__( + coils_size=25, d_inputs_size=72, h_regs_size=182, i_regs_size=51 + ) self.reset() def reset(self):