From 4ba3c7aed90cdc5e5ac7f1def431042394bdfba8 Mon Sep 17 00:00:00 2001 From: IATkachenko Date: Fri, 29 Oct 2021 12:31:51 +0300 Subject: [PATCH 1/6] fix(tion): fix heating detection To be sure that heating is on we should check that: * heater is on (breezer may heat) * out_temp > in_temp (something heat air) * in_temp is more thant 3 degree awy from target temperature (some internal breezer logic may be missed here, it may be not only one condition) Fix #32 --- tion_btle/tion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tion_btle/tion.py b/tion_btle/tion.py index 993e5a8..c6c408e 100644 --- a/tion_btle/tion.py +++ b/tion_btle/tion.py @@ -217,7 +217,7 @@ def __detect_heating_state(self, if heater == "off": self.heating = "off" else: - if in_temp < target_temp and out_temp - target_temp < 3: + if target_temp - in_temp > 3 and out_temp > in_temp: self.heating = "on" else: self.heating = "off" From eb4a424ba2e9330f7f213fb7dff8f7eaab4f385e Mon Sep 17 00:00:00 2001 From: IATkachenko Date: Fri, 29 Oct 2021 12:33:36 +0300 Subject: [PATCH 2/6] test(tion): add unit tests for __detect_heating_state --- tests/unit/__init__.py | 0 tests/unit/tion.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/tion.py diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/tion.py b/tests/unit/tion.py new file mode 100644 index 0000000..3c95d3a --- /dev/null +++ b/tests/unit/tion.py @@ -0,0 +1,37 @@ +import sys +import unittest +import unittest.mock as mock +from unittest.mock import MagicMock +sys.modules['bluepy'] = MagicMock() +sys.modules['bluepy.btle'] = MagicMock() +import tion_btle.tion # noqa: E402 + + +class TionTests(unittest.TestCase): + def setUp(self): + self.patch = mock.patch('tion_btle.tion.tion.__init__', return_value=None) + self.patch.start() + self._tion = tion_btle.tion.tion("") + + def tearDown(self): + self.patch.stop() + + def test___detect_heating_state(self): + # state, in, out, target, heater + _variants = [ + ["on", -2, 21, 20, "on"], + ["on", 15, 21, 20, "on"], + ["off", 15, 21, 16, "on"], + ["off", 15, 21, 20, "off"], + ] + + for expect, in_temp, out_temp, target_temp, heater in _variants: + with self.subTest(expect=expect, + in_temp=in_temp, out_temp=out_temp, target_temp=target_temp, heater=heater): + # call private __detect_heating_state from tion class + self._tion._tion__detect_heating_state(in_temp, out_temp, target_temp, heater) + self.assertEqual(self._tion.heating, expect) + + +if __name__ == '__main__': + unittest.main() From f253205658fcf808819e0dd4755ce8ddffcdb608 Mon Sep 17 00:00:00 2001 From: IATkachenko Date: Fri, 29 Oct 2021 12:38:45 +0300 Subject: [PATCH 3/6] refactor(tion): move updating sate from breezer to own function --- tion_btle/tion.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tion_btle/tion.py b/tion_btle/tion.py index c6c408e..3763af1 100644 --- a/tion_btle/tion.py +++ b/tion_btle/tion.py @@ -222,12 +222,11 @@ def __detect_heating_state(self, else: self.heating = "off" - def get(self, keep_connection: bool = False) -> dict: + def get_state_from_breezer(self, keep_connection: bool = False) -> None: """ - Get current device state + Get current state from breezer :param keep_connection: should we keep connection to device or disconnect after getting data - :return: - dictionary with device state + :return: None """ try: self.connect() @@ -242,6 +241,16 @@ def get(self, keep_connection: bool = False) -> dict: self._decode_response(response) self.__detect_heating_state() + + def get(self, keep_connection: bool = False) -> dict: + """ + Report current breezer state + :param keep_connection: should we keep connection to device or disconnect after getting data + :return: + dictionary with device state + """ + + self.get_state_from_breezer(keep_connection) common = self.__generate_common_json() model_specific_data = self._generate_model_specific_json() From 87834bbcdd43cf1685e3037fef9aebf00ae4911a Mon Sep 17 00:00:00 2001 From: IATkachenko Date: Fri, 29 Oct 2021 13:11:56 +0300 Subject: [PATCH 4/6] feat(tion): allow skipping breezer querying while get() If we don't disconnects from breezer, then nothing could change settings and we may skip getting breezer state while get() Initial state of have_breezer_state is False -- we don't know breezer state. That it goes to True in _get_data_from_breezer if we have data from breezer and may decode it and to False again while connect/disconnect calls. --- tion_btle/tion.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tion_btle/tion.py b/tion_btle/tion.py index 3763af1..8c764fe 100644 --- a/tion_btle/tion.py +++ b/tion_btle/tion.py @@ -242,15 +242,19 @@ def get_state_from_breezer(self, keep_connection: bool = False) -> None: self._decode_response(response) self.__detect_heating_state() - def get(self, keep_connection: bool = False) -> dict: + def get(self, keep_connection: bool = False, skip_update: bool = False) -> dict: """ Report current breezer state + :param skip_update: may we skip requesting data from breezer or not :param keep_connection: should we keep connection to device or disconnect after getting data :return: dictionary with device state """ - - self.get_state_from_breezer(keep_connection) + if skip_update and self.have_breezer_state: + _LOGGER.debug(f"Skipping getting state from breezer because skip_update={skip_update} and " + f"have_breezer_state={self.have_breezer_state}") + else: + self.get_state_from_breezer(keep_connection) common = self.__generate_common_json() model_specific_data = self._generate_model_specific_json() @@ -281,6 +285,7 @@ def set(self, new_settings=None) -> None: encoded_request = self._encode_request(merged_settings) _LOGGER.debug("Will write %s", encoded_request) self._send_request(encoded_request) + # ToDo set internal state according to new_settings finally: self.disconnect() @@ -551,6 +556,7 @@ def connect(self): self.__connections_count = 0 if self.__connections_count == 0: + self.have_breezer_state = False self._connect() self.__connections_count += 1 @@ -559,6 +565,7 @@ def disconnect(self): self.__connections_count -= 1 if self.__connections_count <= 0: self._disconnect() + self.have_breezer_state = False @property @abc.abstractmethod From e2b7646e3db628bb7e60c402fa9e582c5ef6a906 Mon Sep 17 00:00:00 2001 From: IATkachenko Date: Fri, 29 Oct 2021 13:30:12 +0300 Subject: [PATCH 5/6] feat(tion): set internal state based on user request It will allow us to don't make new request to breezer after set to get updated params i.e. use set() get(skip_update=True) sequence to get updated params without connecting to breezer. Also set() set() calls may be done without additional internal get(). --- tion_btle/tion.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tion_btle/tion.py b/tion_btle/tion.py index 8c764fe..926f7cd 100644 --- a/tion_btle/tion.py +++ b/tion_btle/tion.py @@ -260,6 +260,20 @@ def get(self, keep_connection: bool = False, skip_update: bool = False) -> dict: return {**common, **model_specific_data} + def _set_internal_state_from_request(self, request: dict) -> None: + """ + Set internal parameters based on user request + :param request: changed breezer parameter from set request + :return: None + """ + for p in ['fan_speed', 'target_temp', 'heater', 'sound', 'mode', 'state']: + # ToDo: lite have additional parameters to set: "light" and "co2_auto_control", so we should get this + # list from class + try: + setattr(self, p, request[p]) + except KeyError: + pass + def set(self, new_settings=None) -> None: """ Set new breezer state @@ -278,14 +292,14 @@ def set(self, new_settings=None) -> None: try: self.connect() - current_settings = self.get() + current_settings = self.get(skip_update=True) merged_settings = {**current_settings, **new_settings} encoded_request = self._encode_request(merged_settings) _LOGGER.debug("Will write %s", encoded_request) self._send_request(encoded_request) - # ToDo set internal state according to new_settings + self._set_internal_state_from_request(new_settings) finally: self.disconnect() @@ -487,6 +501,10 @@ def heating(self, new_state: str): def mode(self): return self._process_mode(self._mode) + @mode.setter + def mode(self, new_state: str): + self._mode = self._encode_mode(new_state) + @property def model(self) -> str: return self._model From e73497f800fcba6569bdc0c4ba7d78aa1f7b0039 Mon Sep 17 00:00:00 2001 From: IATkachenko Date: Fri, 29 Oct 2021 13:40:49 +0300 Subject: [PATCH 6/6] refactor: rename internal property target_temp to heater_temp We use heater_temp outside, so it is comfortable to use same name for internal property. --- tests/lite.py | 4 ++-- tests/s4.py | 2 +- tests/unit/tion.py | 6 +++--- tion_btle/lite.py | 4 ++-- tion_btle/s3.py | 2 +- tion_btle/s4.py | 2 +- tion_btle/tion.py | 28 ++++++++++++++++++---------- 7 files changed, 28 insertions(+), 20 deletions(-) diff --git a/tests/lite.py b/tests/lite.py index 051839f..98de5cf 100644 --- a/tests/lite.py +++ b/tests/lite.py @@ -23,13 +23,13 @@ _LOGGER.debug("Result is %s " % result) -_LOGGER.info("Initial state: device is %s, light is %s, sound is %s, heater is %s, fan_speed is %d, target_temp is %d", +_LOGGER.info("Initial state: device is %s, light is %s, sound is %s, heater is %s, fan_speed is %d, heater_temp is %d", device.state, device.light, device.sound, device.heater, device.fan_speed, - device.target_temp + device.heater_temp ) diff --git a/tests/s4.py b/tests/s4.py index 86408d2..8a34da2 100644 --- a/tests/s4.py +++ b/tests/s4.py @@ -15,7 +15,7 @@ def read_state(tion: S4): tion.sound, tion.heater, tion.fan_speed, - tion.target_temp) + tion.heater_temp) def pause(interval: int = 10): diff --git a/tests/unit/tion.py b/tests/unit/tion.py index 3c95d3a..e928df5 100644 --- a/tests/unit/tion.py +++ b/tests/unit/tion.py @@ -25,11 +25,11 @@ def test___detect_heating_state(self): ["off", 15, 21, 20, "off"], ] - for expect, in_temp, out_temp, target_temp, heater in _variants: + for expect, in_temp, out_temp, heater_temp, heater in _variants: with self.subTest(expect=expect, - in_temp=in_temp, out_temp=out_temp, target_temp=target_temp, heater=heater): + in_temp=in_temp, out_temp=out_temp, heater_temp=heater_temp, heater=heater): # call private __detect_heating_state from tion class - self._tion._tion__detect_heating_state(in_temp, out_temp, target_temp, heater) + self._tion._tion__detect_heating_state(in_temp, out_temp, heater_temp, heater) self.assertEqual(self._tion.heating, expect) diff --git a/tion_btle/lite.py b/tion_btle/lite.py index a414608..3fee785 100644 --- a/tion_btle/lite.py +++ b/tion_btle/lite.py @@ -78,7 +78,7 @@ def _decode_response(self, response: bytearray): self._have_heater = response[0] >> 7 & 1 self._mode = response[2] - self._target_temp = response[3] + self._heater_temp = response[3] self._fan_speed = response[4] self._in_temp = self.decode_temperature(response[5]) self._out_temp = self.decode_temperature(response[6]) @@ -122,7 +122,7 @@ def encode_state(): return result sb = 0x00 # ?? - tb = 0x02 if (self.target_temp > 0 or self.fan_speed > 0) else 0x01 + tb = 0x02 if (self.heater_temp > 0 or self.fan_speed > 0) else 0x01 lb = [0x60, 0x00] if sb == 0 else [0x00, 0x00] return bytearray( diff --git a/tion_btle/s3.py b/tion_btle/s3.py index 3e2f176..2fbf6c9 100644 --- a/tion_btle/s3.py +++ b/tion_btle/s3.py @@ -97,7 +97,7 @@ def _decode_response(self, response: bytearray): self._mode = int(list("{:02x}".format(response[2]))[0]) self._heater = response[4] & 1 self._state = response[4] >> 1 & 1 - self._target_temp = response[3] + self._heater_temp = response[3] self._sound = response[4] >> 3 & 1 self._out_temp = self.decode_temperature(response[7]) self._in_temp = self.decode_temperature(response[8]) diff --git a/tion_btle/s4.py b/tion_btle/s4.py index ff205e4..a84bdd0 100644 --- a/tion_btle/s4.py +++ b/tion_btle/s4.py @@ -37,7 +37,7 @@ def _decode_response(self, response: bytearray): _LOGGER.debug("Data is %s", bytes(response).hex()) try: self._mode = response[2] - self._target_temp = response[3] + self._heater_temp = response[3] self._fan_speed = response[4] self._in_temp = self.decode_temperature(response[5]) self._out_temp = self.decode_temperature(response[6]) diff --git a/tion_btle/tion.py b/tion_btle/tion.py index 926f7cd..81577f4 100644 --- a/tion_btle/tion.py +++ b/tion_btle/tion.py @@ -108,7 +108,7 @@ def __init__(self, mac: str): # states self._in_temp: int = 0 self._out_temp: int = 0 - self._target_temp: int = 0 + self._heater_temp: int = 0 self._fan_speed: int = 0 self._mode: int = 0 self._state: bool = False @@ -184,7 +184,7 @@ def __generate_common_json(self) -> dict: "mode": self.mode, "out_temp": self.out_temp, "in_temp": self.in_temp, - "heater_temp": self.target_temp, + "heater_temp": self._heater_temp, "fan_speed": self.fan_speed, "filter_remain": self.filter_remain, "time": strftime("%H:%M", localtime()), @@ -195,13 +195,13 @@ def __generate_common_json(self) -> dict: def __detect_heating_state(self, in_temp: int = None, out_temp: int = None, - target_temp: int = None, + heater_temp: int = None, heater: str = None) -> None: """ Tries to guess is heater working right now :param in_temp: air intake temperature :param out_temp: ait outtake temperature - :param target_temp: target temperature for heater + :param heater_temp: target temperature for heater :param heater: heater state :return: None """ @@ -209,15 +209,15 @@ def __detect_heating_state(self, in_temp = self.in_temp if out_temp is None: out_temp = self.out_temp - if target_temp is None: - target_temp = self.target_temp + if heater_temp is None: + heater_temp = self.heater_temp if heater is None: heater = self.heater if heater == "off": self.heating = "off" else: - if target_temp - in_temp > 3 and out_temp > in_temp: + if heater_temp - in_temp > 3 and out_temp > in_temp: self.heating = "on" else: self.heating = "off" @@ -266,7 +266,7 @@ def _set_internal_state_from_request(self, request: dict) -> None: :param request: changed breezer parameter from set request :return: None """ - for p in ['fan_speed', 'target_temp', 'heater', 'sound', 'mode', 'state']: + for p in ['fan_speed', 'heater_temp', 'heater', 'sound', 'mode', 'state']: # ToDo: lite have additional parameters to set: "light" and "co2_auto_control", so we should get this # list from class try: @@ -459,13 +459,21 @@ def heater(self) -> str: def heater(self, new_state: str): self._heater = self._encode_state(new_state) + @property + def heater_temp(self) -> int: + return self._heater_temp + + @heater_temp.setter + def heater_temp(self, new_temp: int): + self._heater_temp = new_temp + @property def target_temp(self) -> int: - return self._target_temp + return self.heater_temp @target_temp.setter def target_temp(self, new_temp: int): - self._target_temp = new_temp + self.heater_temp = new_temp @property def in_temp(self):